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