Initial commit of WOPI integration

This commit is contained in:
Nejc Bernot 2016-08-03 15:31:25 +02:00 committed by Jure Grabnar
parent 908f85f6c0
commit b2b1d5a8f5
12 changed files with 370 additions and 5 deletions

View file

@ -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'

View file

@ -321,6 +321,7 @@ DEPENDENCIES
minitest-reporters (~> 1.1)
momentjs-rails (>= 2.9.0)
nested_form_fields
nokogiri
paperclip (~> 4.3)
pg
puma

View file

@ -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("<root><aliens><alien><name>Alf</name></alien></aliens></root>")
rescue
Rails.logger.warn "proof verification failed"
render :nothing => true, :status => 401 and return
end
end
end

View file

@ -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=BUSINESS_USER&>/, "IsLicensedUser=1&")
edit_url = edit_url.gsub(/<IsLicensedUser=BUSINESS_USER>/, "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

View file

@ -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

12
app/models/wopi_action.rb Normal file
View file

@ -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

8
app/models/wopi_app.rb Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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