From 1b260785ec646ed8d8f2f97ba69393df27173d5d Mon Sep 17 00:00:00 2001 From: Nejc Bernot Date: Wed, 10 Aug 2016 17:49:25 +0200 Subject: [PATCH] First working version of office integration Only works on scinote-preview --- app/controllers/assets_controller.rb | 12 ++ app/controllers/wopi_controller.rb | 185 ++++++++++++++++++++++++-- app/models/asset.rb | 81 ++++++++++- app/models/user.rb | 1 + app/models/wopi_app.rb | 2 +- app/models/wopi_discovery.rb | 2 +- app/utilities/wopi_util.rb | 5 +- app/views/assets/edit.erb | 64 +++++++++ app/views/assets/view.erb | 64 +++++++++ app/views/steps/_step.html.erb | 8 ++ config/application.rb | 8 +- config/routes.rb | 9 +- db/migrate/20160728145000_add_wopi.rb | 2 +- db/schema.rb | 2 +- 14 files changed, 417 insertions(+), 28 deletions(-) create mode 100644 app/views/assets/edit.erb create mode 100644 app/views/assets/view.erb diff --git a/app/controllers/assets_controller.rb b/app/controllers/assets_controller.rb index 7a7dd169a..640d07ebe 100644 --- a/app/controllers/assets_controller.rb +++ b/app/controllers/assets_controller.rb @@ -65,6 +65,18 @@ class AssetsController < ApplicationController end end + def edit + @action_url = @asset.get_action_url(current_user,"edit",false) + @token = current_user.get_wopi_token + @ttl = (current_user.wopi_token_ttl*1000).to_s + end + + def view + @action_url = @asset.get_action_url(current_user,"view",false) + @token = current_user.get_wopi_token + @ttl = (current_user.wopi_token_ttl*1000).to_s + end + private def load_vars diff --git a/app/controllers/wopi_controller.rb b/app/controllers/wopi_controller.rb index d18ec7562..eb0c95fa6 100644 --- a/app/controllers/wopi_controller.rb +++ b/app/controllers/wopi_controller.rb @@ -1,28 +1,35 @@ class WopiController < ActionController::Base - before_action :authenticate_user_from_token!, :load_vars + before_action :load_vars,:authenticate_user_from_token! #before_action :verify_proof! - def get_file + def get_file_endpoint Rails.logger.warn "get_file called" #Only used for checkfileinfo check_file_info end - def get_file_contents + def get_file_contents_endpoint Rails.logger.warn "get_file_contents called" #Only used for getfile get_file end - def post_file + def post_file_endpoint Rails.logger.warn "post_file called" override = request.headers["X-WOPI-Override"] case override + when "GET_LOCK" + get_lock when "PUT_RELATIVE" put_relative when "LOCK" - lock + old_lock = request.headers["X-WOPI-OldLock"] + if old_lock.nil? + lock + else + unlock_and_relock + end when "UNLOCK" unlock when "REFRESH_LOCK" @@ -32,13 +39,14 @@ class WopiController < ActionController::Base end end - def post_file_contents + def post_file_contents_endpoint Rails.logger.warn "post_file_contents called" #Only used for putfile put_file end def check_file_info + Rails.logger.warn "Check file info started" msg = { :BaseFileName => @asset.file_file_name, :OwnerId => @asset.created_by_id.to_s, :Size => @asset.file_file_size, @@ -53,18 +61,175 @@ class WopiController < ActionController::Base :UserFriendlyName => @user.name, #TODO Check user permisisons :ReadOnly => false, + :UserCanNotWriteRelative => true, :UserCanWrite => true, #TODO decide what to put here - :CloseUrl => "", + :CloseUrl => "https://scinote-preview.herokuapp.com", :DownloadUrl => url_for(controller: 'assets',action: 'download',id: @asset.id), :HostEditUrl => url_for(controller: 'assets',action: 'edit',id: @asset.id), :HostViewUrl => url_for(controller: 'assets',action: 'view',id: @asset.id) #TODO breadcrumbs? #:FileExtension } - render json:msg + response.headers['X-WOPI-HostEndpoint'] = ENV["WOPI_ENDPOINT_URL"] + response.headers['X-WOPI-MachineName'] = ENV["WOPI_ENDPOINT_URL"] + response.headers['X-WOPI-ServerVersion'] = APP_VERSION + render json:msg and return + end + def get_file + Rails.logger.warn "getting file" + response.headers["X-WOPI-ItemVersion"] = @asset.version + response.body = Paperclip.io_adapters.for(@asset.file).read + send_data response.body, disposition: "inline", :content_type => 'text/plain' + end + + def put_relative + Rails.logger.warn "put relative" + render :nothing => true, :status => 501 and return + end + + def lock + Rails.logger.warn "lock" + lock = request.headers["X-WOPI-Lock"] + if lock.nil? || lock.blank? + render :nothing => true, :status => 400 and return + end + @asset.with_lock do + if @asset.is_locked + if @asset.lock == lock + @asset.refresh_lock + response.headers["X-WOPI-ItemVersion"] = @asset.version + render :nothing => true, :status => 200 and return + else + response.headers["X-WOPI-Lock"] = @asset.lock + render :nothing => true, :status => 409 and return + end + else + @asset.lock_asset(lock) + response.headers["X-WOPI-ItemVersion"] = @asset.version + render :nothing => true, :status => 200 and return + end + end + end + + def unlock_and_relock + Rails.logger.warn "lock and relock" + lock = request.headers["X-WOPI-Lock"] + old_lock = request.headers["X-WOPI-OldLock"] + if lock.nil? || lock.blank? || old_lock.blank? + render :nothing => true, :status => 400 and return + end + @asset.with_lock do + if @asset.is_locked + if @asset.lock == old_lock + @asset.unlock + @asset.lock_asset(lock) + response.headers["X-WOPI-ItemVersion"] = @asset.version + render :nothing => true, :status => 200 and return + else + response.headers["X-WOPI-Lock"] = @asset.lock + render :nothing => true, :status => 409 and return + end + else + response.headers["X-WOPI-Lock"] = "" + render :nothing => true, :status => 409 and return + end + end + end + + def unlock + Rails.logger.warn "unlock" + lock = request.headers["X-WOPI-Lock"] + if lock.nil? || lock.blank? + render :nothing => true, :status => 400 and return + end + @asset.with_lock do + if @asset.is_locked + Rails.logger.warn "Current asset lock: #{@asset.lock}, unlocking lock #{lock}" + if @asset.lock == lock + @asset.unlock + response.headers["X-WOPI-ItemVersion"] = @asset.version + render :nothing => true, :status => 200 and return + else + response.headers["X-WOPI-Lock"] = @asset.lock + render :nothing => true, :status => 409 and return + end + else + Rails.logger.warn "Tried to unlock non-locked file" + response.headers["X-WOPI-Lock"] = "" + render :nothing => true, :status => 409 and return + end + end + end + + def refresh_lock + Rails.logger.warn "refresh lock" + lock = request.headers["X-WOPI-Lock"] + if lock.nil? || lock.blank? + render :nothing => true, :status => 400 and return + end + @asset.with_lock do + if @asset.is_locked + if @asset.lock == lock + @asset.refresh_lock + response.headers["X-WOPI-ItemVersion"] = @asset.version + response.headers["X-WOPI-ItemVersion"] = @asset.version + render :nothing => true, :status => 200 and return + else + response.headers["X-WOPI-Lock"] = @asset.lock + render :nothing => true, :status => 409 and return + end + else + response.headers["X-WOPI-Lock"] = "" + render :nothing => true, :status => 409 and return + end + end + end + + def get_lock + Rails.logger.warn "get lock" + @asset.with_lock do + if @asset.is_locked + response.headers["X-WOPI-Lock"] = @asset.lock + render :nothing => true, :status => 200 and return + else + response.headers["X-WOPI-Lock"] = "" + render :nothing => true, :status => 200 and return + end + end + end + # TODO When should we extract file text? + def put_file + Rails.logger.warn "put file" + @asset.with_lock do + lock = request.headers["X-WOPI-Lock"] + if @asset.is_locked + if @asset.lock == lock + Rails.logger.warn "replacing file" + @asset.update_contents(request.body) + response.headers["X-WOPI-ItemVersion"] = @asset.version + render :nothing => true, :status => 200 and return + else + Rails.logger.warn "wrong lock used to try and modify file" + response.headers["X-WOPI-Lock"] = @asset.lock + render :nothing => true, :status => 409 and return + end + else + if !@asset.file_file_size.nil? and @asset.file_file_size==0 + Rails.logger.warn "initializing empty file" + @asset.update_contents(request.body) + response.headers["X-WOPI-ItemVersion"] = @asset.version + render :nothing => true, :status => 200 and return + else + Rails.logger.warn "trying to modify unlocked file" + response.headers["X-WOPI-Lock"] = "" + render :nothing => true, :status => 409 and return + end + end + end + end def load_vars @@ -72,7 +237,7 @@ class WopiController < ActionController::Base if @asset.nil? render :nothing => true, :status => 404 and return else - Rails.logger.warn "Found asset with id: #{params[:id]}, (id: #{@asset.id})" + Rails.logger.warn "Found asset" step_assoc = @asset.step result_assoc = @asset.result @assoc = step_assoc if not step_assoc.nil? @@ -99,6 +264,7 @@ class WopiController < ActionController::Base Rails.logger.warn "no user with this token found" render :nothing => true, :status => 401 and return end + Rails.logger.warn "user found by token" #TODO check if the user can do anything with the file end @@ -118,7 +284,6 @@ class WopiController < ActionController::Base Rails.logger.warn "PROOF: #{proof}" - xml_doc = Nokogiri::XML("Alf") rescue Rails.logger.warn "proof verification failed" render :nothing => true, :status => 401 and return diff --git a/app/models/asset.rb b/app/models/asset.rb index 838739ff2..44eae8002 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -5,6 +5,8 @@ class Asset < ActiveRecord::Base include WopiUtil require 'tempfile' + # Lock duration set to 30 minutes + LOCK_DURATION = 60*30 # Paperclip validation has_attached_file :file, { @@ -296,23 +298,88 @@ class Asset < ActiveRecord::Base cache end + def can_perform_action(action) + file_ext = file_file_name.split(".").last + action = get_action(file_ext,action) + if action.nil? + return false + end + true + end - def get_action_path(user,action) + + def get_action_url(user,action,with_tokens = true) file_ext = file_file_name.split(".").last action = get_action(file_ext,action) if !action.nil? - edit_url = action.urlsrc - edit_url = edit_url.gsub(//, "IsLicensedUser=1&") - edit_url = edit_url.gsub(//, "IsLicensedUser=1") - edit_url = edit_url.gsub(/<.*?=.*?>/, "") - #This does not work yet - provides path instead of absolute url + action_url = action.urlsrc + action_url = action_url.gsub(//, "IsLicensedUser=0&") + action_url = action_url.gsub(//, "IsLicensedUser=0") + action_url = action_url.gsub(/<.*?=.*?>/, "") + rest_url = Rails.application.routes.url_helpers.wopi_rest_endpoint_url(host: ENV["WOPI_ENDPOINT_URL"],id: id) - edit_url = edit_url + "WOPISrc=#{rest_url}&access_token=#{user.get_wopi_token}&access_token_ttl=#{user.wopi_token_ttl.to_s}" + action_url = action_url + "WOPISrc=#{rest_url}" + if with_tokens + action_url = action_url + "&access_token=#{user.get_wopi_token}&access_token_ttl=#{(user.wopi_token_ttl*1000).to_s}" + else + action_url + end else return nil end end + #is_locked, lock_asset and refresh_lock rely on the asset being locked in the database to prevent race conditions + def is_locked + if lock.nil? + return false + else + return true + end + end + + def lock_asset(lock_string) + self.lock = lock_string + self.lock_ttl = Time.now.to_i + LOCK_DURATION + delay(queue: :assets, run_at: LOCK_DURATION.seconds.from_now).unlock_expired + self.save! + end + + def refresh_lock + self.lock_ttl = Time.now.to_i + LOCK_DURATION + delay(queue: :assets, run_at: LOCK_DURATION.seconds.from_now).unlock_expired + self.save! + end + + def unlock + self.lock = nil + self.lock_ttl = nil + self.save! + end + + def unlock_expired + self.with_lock do + if !self.lock_ttl.nil? and self.lock_ttl>= Time.now.to_i + self.lock = nil + self.lock_ttl = nil + self.save! + end + end + end + + def update_contents(new_file) + new_file.class.class_eval { attr_accessor :original_filename } + new_file.original_filename = self.file_file_name + self.file = new_file + if self.version.nil? + self.version = 1 + else + self.version = self.version + 1 + end + self.save + end + + protected # Checks if attachments is an image (in post processing imagemagick will diff --git a/app/models/user.rb b/app/models/user.rb index f2953592c..b3d1dc20b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -270,6 +270,7 @@ class User < ActiveRecord::Base unless token_valid # if current token is not valid generate a new one with a one day TTL self.wopi_token = Devise.friendly_token(20) + # WOPI uses millisecond TTLs self.wopi_token_ttl = Time.now.to_i + 60*60*24 self.save Rails.logger.warn("Generating new token #{self.wopi_token}") diff --git a/app/models/wopi_app.rb b/app/models/wopi_app.rb index 19f8d227f..3150a03df 100644 --- a/app/models/wopi_app.rb +++ b/app/models/wopi_app.rb @@ -1,7 +1,7 @@ class WopiApp < ActiveRecord::Base belongs_to :wopi_discovery, :foreign_key => 'wopi_discovery_id', :class_name => 'WopiDiscovery' - has_many :wopi_actions, class_name: 'WopiAction', foreign_key: 'wopi_app_id', :dependent => :delete_all + has_many :wopi_actions, class_name: 'WopiAction', foreign_key: 'wopi_app_id', :dependent => :destroy validates :name, :icon, :wopi_discovery, presence: true diff --git a/app/models/wopi_discovery.rb b/app/models/wopi_discovery.rb index 9b72793a5..47f6f9d43 100644 --- a/app/models/wopi_discovery.rb +++ b/app/models/wopi_discovery.rb @@ -1,6 +1,6 @@ class WopiDiscovery < ActiveRecord::Base - has_many :wopi_apps, class_name: 'WopiApp', foreign_key: 'wopi_discovery_id', :dependent => :delete_all + has_many :wopi_apps, class_name: 'WopiApp', foreign_key: 'wopi_discovery_id', :dependent => :destroy validates :expires, :proof_key_mod, :proof_key_exp, :proof_key_old_mod, :proof_key_old_exp, presence: true end diff --git a/app/utilities/wopi_util.rb b/app/utilities/wopi_util.rb index ad16dd00d..4ffe19950 100644 --- a/app/utilities/wopi_util.rb +++ b/app/utilities/wopi_util.rb @@ -58,7 +58,10 @@ module WopiUtil end rescue Rails.logger.warn "Initialization failed" - WopiDiscovery.first.destroy + discovery = WopiDiscovery.first + unless discovery.nil? + discovery.destroy + end end end diff --git a/app/views/assets/edit.erb b/app/views/assets/edit.erb new file mode 100644 index 000000000..41fa8eaf5 --- /dev/null +++ b/app/views/assets/edit.erb @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + +
+ method="post"> + type="hidden"/> + type="hidden"/> +
+ + + + + + + \ No newline at end of file diff --git a/app/views/assets/view.erb b/app/views/assets/view.erb new file mode 100644 index 000000000..41fa8eaf5 --- /dev/null +++ b/app/views/assets/view.erb @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + +
+ method="post"> + type="hidden"/> + type="hidden"/> +
+ + + + + + + \ No newline at end of file diff --git a/app/views/steps/_step.html.erb b/app/views/steps/_step.html.erb index b360b722b..95be5bcf5 100644 --- a/app/views/steps/_step.html.erb +++ b/app/views/steps/_step.html.erb @@ -72,6 +72,14 @@ <%= link_to download_asset_path(asset), data: {no_turbolink: true, id: true, status: "asset-present"} do %> <%= image_tag preview_asset_path(asset) if asset.is_image? %>

<%= truncate(asset.file_file_name, length: 50) %>

+ <% if asset.can_perform_action("view") %> + <%= link_to "View", view_asset_url(id: asset) %> + <% end %> + <% if asset.can_perform_action("edit") %> + <%= link_to "Edit", edit_asset_url(id: asset) %> + <% end %> + <% else %> + <%= asset_loading_span(asset) %> <% end %> <% else %> <%= asset_loading_span(asset) %> diff --git a/config/application.rb b/config/application.rb index 605efe45d..a95eb3598 100644 --- a/config/application.rb +++ b/config/application.rb @@ -30,7 +30,13 @@ module Scinote "[#{datetime}] #{severity}: #{msg}\n" end + config.action_dispatch.default_headers = { + 'X-WOPI-Lock' => "", + 'Random-header' => "with value", + 'Random-non-special-header' => "a" + } + # Paperclip spoof checking - Paperclip.options[:content_type_mappings] = {:csv => "text/plain"} + Paperclip.options[:content_type_mappings] = {:csv => "text/plain", wopitest: ['text/plain', 'inode/x-empty'] } end end diff --git a/config/routes.rb b/config/routes.rb index bfa77a62e..47c65e092 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -267,11 +267,10 @@ Rails.application.routes.draw do end # Office integration - get "wopi/files/:id/contents", to: "wopi#get_file_contents" - post "wopi/files/:id/contents", to: "wopi#post_file_contents" - - get "wopi/files/:id", to: "wopi#get_file", as: 'wopi_rest_endpoint' - post "wopi/files/:id", to: "wopi#post_file" + get "wopi/files/:id/contents", to: "wopi#get_file_contents_endpoint" + post "wopi/files/:id/contents", to: "wopi#post_file_contents_endpoint" + get "wopi/files/:id", to: "wopi#get_file_endpoint", as: 'wopi_rest_endpoint' + post "wopi/files/:id", to: "wopi#post_file_endpoint" end diff --git a/db/migrate/20160728145000_add_wopi.rb b/db/migrate/20160728145000_add_wopi.rb index 471dc1872..01c7fc70b 100644 --- a/db/migrate/20160728145000_add_wopi.rb +++ b/db/migrate/20160728145000_add_wopi.rb @@ -6,7 +6,7 @@ class AddWopi< ActiveRecord::Migration add_column :assets, :lock, :string, :limit => 1024 add_column :assets, :lock_ttl, :integer - add_column :assets, :version, :integer + add_column :assets, :version, :integer, default: 1 create_table :wopi_discoveries do |t| t.integer :expires, null: false diff --git a/db/schema.rb b/db/schema.rb index 64c4c79c4..3151280b9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -58,7 +58,7 @@ ActiveRecord::Schema.define(version: 20160809074757) do t.boolean "file_present", default: false, null: false t.string "lock", limit: 1024 t.integer "lock_ttl" - t.integer "version" + t.integer "version", default: 1 end add_index "assets", ["created_at"], name: "index_assets_on_created_at", using: :btree