From b2b1d5a8f518c9f659794cd976e811eeed94af23 Mon Sep 17 00:00:00 2001 From: Nejc Bernot Date: Wed, 3 Aug 2016 15:31:25 +0200 Subject: [PATCH] Initial commit of WOPI integration --- Gemfile | 2 + Gemfile.lock | 1 + app/controllers/wopi_controller.rb | 130 ++++++++++++++++++++++++++ app/models/asset.rb | 18 ++++ app/models/user.rb | 30 +++++- app/models/wopi_action.rb | 12 +++ app/models/wopi_app.rb | 8 ++ app/models/wopi_discovery.rb | 6 ++ app/utilities/wopi_util.rb | 67 +++++++++++++ config/routes.rb | 12 +++ db/migrate/20160728145000_add_wopi.rb | 51 ++++++++++ db/schema.rb | 38 +++++++- 12 files changed, 370 insertions(+), 5 deletions(-) create mode 100644 app/controllers/wopi_controller.rb create mode 100644 app/models/wopi_action.rb create mode 100644 app/models/wopi_app.rb create mode 100644 app/models/wopi_discovery.rb create mode 100644 app/utilities/wopi_util.rb create mode 100644 db/migrate/20160728145000_add_wopi.rb diff --git a/Gemfile b/Gemfile index 0d3e8260b..8c5a5d5ba 100644 --- a/Gemfile +++ b/Gemfile @@ -57,6 +57,8 @@ gem 'delayed_job_active_record' gem 'devise-async' gem 'ruby-graphviz', '~> 1.2' # Graphviz for rails +gem 'nokogiri' # XML parser + group :development, :test do gem 'byebug' gem 'web-console', '~> 2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 32f598656..35f1f7c91 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -321,6 +321,7 @@ DEPENDENCIES minitest-reporters (~> 1.1) momentjs-rails (>= 2.9.0) nested_form_fields + nokogiri paperclip (~> 4.3) pg puma diff --git a/app/controllers/wopi_controller.rb b/app/controllers/wopi_controller.rb new file mode 100644 index 000000000..d18ec7562 --- /dev/null +++ b/app/controllers/wopi_controller.rb @@ -0,0 +1,130 @@ +class WopiController < ActionController::Base + before_action :authenticate_user_from_token!, :load_vars + #before_action :verify_proof! + + def get_file + Rails.logger.warn "get_file called" + #Only used for checkfileinfo + check_file_info + end + + def get_file_contents + Rails.logger.warn "get_file_contents called" + #Only used for getfile + get_file + + end + + def post_file + Rails.logger.warn "post_file called" + override = request.headers["X-WOPI-Override"] + case override + when "PUT_RELATIVE" + put_relative + when "LOCK" + lock + when "UNLOCK" + unlock + when "REFRESH_LOCK" + refresh_lock + else + render :nothing => true, :status => 404 and return + end + end + + def post_file_contents + Rails.logger.warn "post_file_contents called" + #Only used for putfile + put_file + end + + def check_file_info + msg = { :BaseFileName => @asset.file_file_name, + :OwnerId => @asset.created_by_id.to_s, + :Size => @asset.file_file_size, + :UserId => @user.id, + :Version => @asset.version, + :SupportsExtendedLockLength => true, + :SupportsGetLock => true, + :SupportsLocks => true, + :SupportsUpdate => true, + #Setting all users to business until we figure out which should NOT be business + :LicenseCheckForEditIsEnabled => true, + :UserFriendlyName => @user.name, + #TODO Check user permisisons + :ReadOnly => false, + :UserCanWrite => true, + #TODO decide what to put here + :CloseUrl => "", + :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 + end + + + + def load_vars + @asset = Asset.find_by_id(params[:id]) + if @asset.nil? + render :nothing => true, :status => 404 and return + else + Rails.logger.warn "Found asset with id: #{params[:id]}, (id: #{@asset.id})" + step_assoc = @asset.step + result_assoc = @asset.result + @assoc = step_assoc if not step_assoc.nil? + @assoc = result_assoc if not result_assoc.nil? + + if @assoc.class == Step + @protocol = @asset.step.protocol + else + @my_module = @assoc.my_module + end + end + end + + private + def authenticate_user_from_token! + wopi_token = params[:access_token] + if wopi_token.nil? + Rails.logger.warn "nil wopi token" + render :nothing => true, :status => 401 and return + end + + @user = User.find_by_valid_wopi_token(wopi_token) + if @user.nil? + Rails.logger.warn "no user with this token found" + render :nothing => true, :status => 401 and return + end + + #TODO check if the user can do anything with the file + end + + def verify_proof! + begin + token = params[:access_token] + timestamp = request.headers["X-WOPI-TimeStamp"] + url = request.original_url + + token_length = [token.length].pack('N').bytes + timestamp_bytes = [timestamp.to_i].pack('Q').bytes.reverse + timestamp_length = [timestamp_bytes.length].pack('N').bytes + url_length = [url.length].pack('N').bytes + + proof = token_length + token.bytes + url_length + url.bytes + timestamp_length + timestamp_bytes + + Rails.logger.warn "PROOF: #{proof}" + + xml_doc = Nokogiri::XML("Alf") + rescue + Rails.logger.warn "proof verification failed" + render :nothing => true, :status => 401 and return + end + + + end + +end \ No newline at end of file diff --git a/app/models/asset.rb b/app/models/asset.rb index d7436ed69..838739ff2 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -2,6 +2,7 @@ class Asset < ActiveRecord::Base include SearchableModel include DatabaseHelper include Encryptor + include WopiUtil require 'tempfile' @@ -295,6 +296,23 @@ class Asset < ActiveRecord::Base cache end + + def get_action_path(user,action) + 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 + 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}" + else + return nil + end + 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 f6ab98e29..f2953592c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -252,12 +252,40 @@ class User < ActiveRecord::Base .uniq end - protected + def self.find_by_valid_wopi_token(token) + Rails.logger.warn "Searching by token #{token}" + user = User.where("wopi_token = ?", token).first + return user + end + + def token_valid + if !self.wopi_token.nil? and (self.wopi_token_ttl==0 or self.wopi_token_ttl > Time.now.to_i) + return true + else + return false + end + end + + def get_wopi_token + 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) + self.wopi_token_ttl = Time.now.to_i + 60*60*24 + self.save + Rails.logger.warn("Generating new token #{self.wopi_token}") + end + Rails.logger.warn("Returning token #{self.wopi_token}") + self.wopi_token + end + +protected def time_zone_check if time_zone.nil? or ActiveSupport::TimeZone.new(time_zone).nil? errors.add(:time_zone) end end + + end diff --git a/app/models/wopi_action.rb b/app/models/wopi_action.rb new file mode 100644 index 000000000..3c9efa432 --- /dev/null +++ b/app/models/wopi_action.rb @@ -0,0 +1,12 @@ +class WopiAction < ActiveRecord::Base + + belongs_to :wopi_app, :foreign_key => 'wopi_app_id', :class_name => 'WopiApp' + validates :action,:extension,:urlsrc,:wopi_app, presence: true + + + def self.find_action(extension,activity) + WopiAction.distinct + .where("extension = ? and action = ?",extension,activity).first + end + +end \ No newline at end of file diff --git a/app/models/wopi_app.rb b/app/models/wopi_app.rb new file mode 100644 index 000000000..19f8d227f --- /dev/null +++ b/app/models/wopi_app.rb @@ -0,0 +1,8 @@ +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 + + validates :name, :icon, :wopi_discovery, presence: true + +end \ No newline at end of file diff --git a/app/models/wopi_discovery.rb b/app/models/wopi_discovery.rb new file mode 100644 index 000000000..9b72793a5 --- /dev/null +++ b/app/models/wopi_discovery.rb @@ -0,0 +1,6 @@ +class WopiDiscovery < ActiveRecord::Base + + has_many :wopi_apps, class_name: 'WopiApp', foreign_key: 'wopi_discovery_id', :dependent => :delete_all + 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 new file mode 100644 index 000000000..ad16dd00d --- /dev/null +++ b/app/utilities/wopi_util.rb @@ -0,0 +1,67 @@ +module WopiUtil + require 'open-uri' + + + DISCOVERY_TTL = 60*60*24 + DISCOVERY_TTL.freeze + + def get_action(extension, activity) + discovery = WopiDiscovery.first + if discovery.nil? || discovery.expires < Time.now.to_i + initializeDiscovery(discovery) + end + + action = WopiAction.find_action(extension,activity) + + end + + private + # Currently only saves Excel, Word and PowerPoint view and edit actions + def initializeDiscovery(discovery) + begin + Rails.logger.warn "Initializing discovery" + unless discovery.nil? + discovery.destroy + end + + @doc = Nokogiri::XML(open(ENV["WOPI_DISCOVERY_URL"])) + + discovery = WopiDiscovery.new + discovery.expires = Time.now.to_i + DISCOVERY_TTL + key = @doc.xpath("//proof-key") + discovery.proof_key_mod = key.xpath("@modulus").first.value + discovery.proof_key_exp = key.xpath("@exponent").first.value + discovery.proof_key_old_mod = key.xpath("@oldmodulus").first.value + discovery.proof_key_old_exp = key.xpath("@oldexponent").first.value + discovery.save! + + @doc.xpath("//app").each do |app| + app_name = app.xpath("@name").first.value + if ["Excel","Word","PowerPoint","WopiTest"].include?(app_name) + wopi_app = WopiApp.new + wopi_app.name = app.xpath("@name").first.value + wopi_app.icon = app.xpath("@favIconUrl").first.value + wopi_app.wopi_discovery_id=discovery.id + wopi_app.save! + app.xpath("action").each do |action| + name = action.xpath("@name").first.value + if ["view","edit","wopitest"].include?(name) + wopi_action = WopiAction.new + wopi_action.action = name + wopi_action.extension = action.xpath("@ext").first.value + wopi_action.urlsrc = action.xpath("@urlsrc").first.value + wopi_action.wopi_app_id = wopi_app.id + wopi_action.save! + end + end + end + end + rescue + Rails.logger.warn "Initialization failed" + WopiDiscovery.first.destroy + end + + end + + +end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index b45b67444..bfa77a62e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -32,6 +32,7 @@ Rails.application.routes.draw do get "users/settings/user_organizations/:user_organization_id/destroy_html", to: "users/settings#destroy_user_organization_html", as: "destroy_user_organization_html" delete "users/settings/user_organizations/:user_organization_id", to: "users/settings#destroy_user_organization", as: "destroy_user_organization" + resources :organizations, only: [] do resources :samples, only: [:new, :create] resources :sample_types, only: [:new, :create] @@ -256,10 +257,21 @@ Rails.application.routes.draw do get "files/:id/present", to: "assets#file_present", as: "file_present_asset" get "files/:id/download", to: "assets#download", as: "download_asset" get "files/:id/preview", to: "assets#preview", as: "preview_asset" + get "files/:id/view", to: "assets#view", as: "view_asset" + get "files/:id/edit", to: "assets#edit", as: "edit_asset" post 'asset_signature' => 'assets#signature' devise_scope :user do get 'avatar/:id/:style' => 'users/registrations#avatar', as: 'avatar' post 'avatar_signature' => 'users/registrations#signature' 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" + + end diff --git a/db/migrate/20160728145000_add_wopi.rb b/db/migrate/20160728145000_add_wopi.rb new file mode 100644 index 000000000..471dc1872 --- /dev/null +++ b/db/migrate/20160728145000_add_wopi.rb @@ -0,0 +1,51 @@ +class AddWopi< ActiveRecord::Migration + + def up + add_column :users, :wopi_token, :string + add_column :users, :wopi_token_ttl, :integer + + add_column :assets, :lock, :string, :limit => 1024 + add_column :assets, :lock_ttl, :integer + add_column :assets, :version, :integer + + create_table :wopi_discoveries do |t| + t.integer :expires, null: false + t.string :proof_key_mod, null: false + t.string :proof_key_exp, null: false + t.string :proof_key_old_mod, null: false + t.string :proof_key_old_exp, null: false + end + + create_table :wopi_apps do |t| + t.string :name, null: false + t.string :icon, null: false + t.integer :wopi_discovery_id, null: false + end + + create_table :wopi_actions do |t| + t.string :action, null: false + t.string :extension, null: false + t.string :urlsrc, null: false + t.integer :wopi_app_id, null: false + end + + add_foreign_key :wopi_actions, :wopi_apps, column: :wopi_app_id + add_foreign_key :wopi_apps, :wopi_discoveries, column: :wopi_discovery_id + + add_index :wopi_actions, [:extension,:action] + + end + + def down + remove_column :users, :wopi_token + remove_column :users, :wopi_token_ttl + + remove_column :assets, :lock + remove_column :assets, :lock_ttl + remove_column :assets, :version + + drop_table :wopi_actions + drop_table :wopi_apps + drop_table :wopi_discoveries + end +end diff --git a/db/schema.rb b/db/schema.rb index 4bb3282f7..64c4c79c4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -46,16 +46,19 @@ ActiveRecord::Schema.define(version: 20160809074757) do add_index "asset_text_data", ["data_vector"], name: "index_asset_text_data_on_data_vector", using: :gin create_table "assets", force: :cascade do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "file_file_name" t.string "file_content_type" t.integer "file_file_size" t.datetime "file_updated_at" t.integer "created_by_id" t.integer "last_modified_by_id" - t.integer "estimated_size", default: 0, null: false - t.boolean "file_present", default: false, null: false + t.integer "estimated_size", default: 0, null: false + t.boolean "file_present", default: false, null: false + t.string "lock", limit: 1024 + t.integer "lock_ttl" + t.integer "version" end add_index "assets", ["created_at"], name: "index_assets_on_created_at", using: :btree @@ -637,6 +640,8 @@ ActiveRecord::Schema.define(version: 20160809074757) do t.string "invited_by_type" t.integer "invitations_count", default: 0 t.integer "tutorial_status", default: 0, null: false + t.string "wopi_token" + t.integer "wopi_token_ttl" end add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree @@ -646,6 +651,29 @@ ActiveRecord::Schema.define(version: 20160809074757) do add_index "users", ["invited_by_id"], name: "index_users_on_invited_by_id", using: :btree add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree + create_table "wopi_actions", force: :cascade do |t| + t.string "action", null: false + t.string "extension", null: false + t.string "urlsrc", null: false + t.integer "wopi_app_id", null: false + end + + add_index "wopi_actions", ["extension", "action"], name: "index_wopi_actions_on_extension_and_action", using: :btree + + create_table "wopi_apps", force: :cascade do |t| + t.string "name", null: false + t.string "icon", null: false + t.integer "wopi_discovery_id", null: false + end + + create_table "wopi_discoveries", force: :cascade do |t| + t.integer "expires", null: false + t.string "proof_key_mod", null: false + t.string "proof_key_exp", null: false + t.string "proof_key_old_mod", null: false + t.string "proof_key_old_exp", null: false + end + add_foreign_key "activities", "my_modules" add_foreign_key "activities", "projects" add_foreign_key "activities", "users" @@ -764,4 +792,6 @@ ActiveRecord::Schema.define(version: 20160809074757) do add_foreign_key "user_projects", "projects" add_foreign_key "user_projects", "users" add_foreign_key "user_projects", "users", column: "assigned_by_id" + add_foreign_key "wopi_actions", "wopi_apps" + add_foreign_key "wopi_apps", "wopi_discoveries" end