Merge pull request #338 from biosistemika/office_integration

Office integration
This commit is contained in:
Zmago Devetak 2017-03-13 14:19:03 +01:00 committed by GitHub
commit 210519b506
47 changed files with 2239 additions and 663 deletions

View file

@ -68,9 +68,10 @@ gem 'tinymce-rails' # Rich text editor
gem 'base62' # Used for smart annotations
gem 'nokogiri' # XML parser
group :development, :test do
gem 'byebug'
gem 'web-console', '~> 2.0'
gem 'better_errors'
gem 'binding_of_caller'
gem 'awesome_print'

View file

@ -320,11 +320,6 @@ GEM
unicode-display_width (1.1.0)
warden (1.2.6)
rack (>= 1.0)
web-console (2.2.1)
activemodel (>= 4.0)
binding_of_caller (>= 0.7.2)
railties (>= 4.0)
sprockets-rails (>= 2.0, < 4.0)
wicked_pdf (1.0.3)
wkhtmltopdf-heroku (2.12.3.0)
yomu (0.2.4)
@ -403,7 +398,6 @@ DEPENDENCIES
tzinfo-data
uglifier (>= 1.3.0)
underscore-rails
web-console (~> 2.0)
wicked_pdf
wkhtmltopdf-heroku
yomu

View file

@ -33,6 +33,10 @@ function setupAssetsLoading() {
data['preview-url'] + "'><p>" +
data.filename + '</p></a>'
);
} else if(data.type === "wopi") {
$el.html(data['wopi-file-name'] +
data['wopi-view'] +
data['wopi-edit']);
} else {
$el.html(
"<a href='" + data['download-url'] + "'><p>" +

View file

@ -491,6 +491,8 @@ function initImport() {
if (data.status === "ok") {
// Simply reload page
location.reload();
} else if (data.status === 'locked') {
alert(I18n.t("my_modules.protocols.load_from_file_error_locked"));
} else {
if (data.status === 'size_too_large') {
alert('<%= I18n.t('my_modules.protocols.load_from_file_size_error',

View file

@ -67,7 +67,14 @@ function formAjaxResultAsset($form) {
Comments.initialize();
})
.on("ajax:error", function(e, data) {
$form.renderFormErrors("result", data.errors, true, e);
// This check is here only because of remotipart bug, which returns
// HTML instead of JSON, go figure
var errors = '';
if (data.errors)
errors = data.errors;
else
errors = data.responseJSON.errors;
$form.renderFormErrors("result", errors, true, e);
});
}

View file

@ -1,7 +1,40 @@
class AssetsController < ApplicationController
include WopiUtil
# include ActionView::Helpers
include ActionView::Helpers::AssetTagHelper
include ActionView::Helpers::TextHelper
include ActionView::Helpers::UrlHelper
include ActionView::Context
include InputSanitizeHelper
include FileIconsHelper
include WopiHelper
before_action :load_vars
before_action :check_read_permission, except: :file_present
before_action :load_vars, except: :signature
before_action :check_read_permission, except: [:signature, :file_present]
before_action :check_edit_permission, only: :edit
# Validates asset and then generates S3 upload posts, because
# otherwise untracked files could be uploaded to S3
def signature
respond_to do |format|
format.json {
asset = Asset.new(asset_params)
if asset.valid?
posts = generate_upload_posts asset
render json: {
posts: posts
}
else
render json: {
status: 'error',
errors: asset.errors
}, status: :bad_request
end
}
end
end
def file_present
respond_to do |format|
@ -26,7 +59,10 @@ class AssetsController < ApplicationController
length:
Constants::FILENAME_TRUNCATION_LENGTH),
'download-url' => download_asset_path(@asset),
'type' => (@asset.is_image? ? 'image' : 'file')
'type' => asset_data_type(@asset),
'wopi-file-name' => wopi_asset_file_name(@asset),
'wopi-edit' => (wopi_asset_edit_button(@asset) if wopi_file?(@asset)),
'wopi-view' => (wopi_asset_view_button(@asset) if wopi_file?(@asset))
}, status: 200
end
end
@ -59,20 +95,39 @@ class AssetsController < ApplicationController
end
end
def edit
@action_url = append_wd_params(@asset
.get_action_url(current_user, 'edit', false))
@favicon_url = @asset.favicon_url('edit')
tkn = current_user.get_wopi_token
@token = tkn.token
@ttl = (tkn.ttl * 1000).to_s
create_wopi_file_activity(current_user, true)
render layout: false
end
def view
@action_url = append_wd_params(@asset
.get_action_url(current_user, 'view', false))
@favicon_url = @asset.favicon_url('view')
tkn = current_user.get_wopi_token
@token = tkn.token
@ttl = (tkn.ttl * 1000).to_s
render layout: false
end
private
def load_vars
@asset = Asset.find_by_id(params[:id])
unless @asset
render_404
end
render_404 unless @asset
step_assoc = @asset.step
result_assoc = @asset.result
@assoc = step_assoc if not step_assoc.nil?
@assoc = result_assoc if not result_assoc.nil?
@assoc = step_assoc unless step_assoc.nil?
@assoc = result_assoc unless result_assoc.nil?
if @assoc.class == Step
@protocol = @asset.step.protocol
@ -93,9 +148,75 @@ class AssetsController < ApplicationController
end
end
def check_edit_permission
if @assoc.class == Step
unless can_edit_step_in_protocol(@protocol)
render_403 and return
end
elsif @assoc.class == Result
unless can_edit_result_asset_in_module(@my_module)
render_403 and return
end
end
end
def generate_upload_posts(asset)
posts = []
s3_post = S3_BUCKET.presigned_post(
key: asset.file.path[1..-1],
success_action_status: '201',
acl: 'private',
storage_class: "STANDARD",
content_length_range: 1..Constants::FILE_MAX_SIZE_MB.megabytes,
content_type: asset.file_content_type
)
posts.push({
url: s3_post.url,
fields: s3_post.fields
})
condition = %r{^image/#{Regexp.union(Constants::WHITELISTED_IMAGE_TYPES)}}
if condition === asset.file_content_type
asset.file.options[:styles].each do |style, option|
s3_post = S3_BUCKET.presigned_post(
key: asset.file.path(style)[1..-1],
success_action_status: '201',
acl: 'public-read',
storage_class: "REDUCED_REDUNDANCY",
content_length_range: 1..Constants::FILE_MAX_SIZE_MB.megabytes,
content_type: asset.file_content_type
)
posts.push({
url: s3_post.url,
fields: s3_post.fields,
style_option: option,
mime_type: asset.file_content_type
})
end
end
posts
end
def append_wd_params(url)
wd_params = ''
params.keys.select { |i| i[/^wd.*/] }.each do |wd|
next if wd == 'wdPreviousSession' || wd == 'wdPreviousCorrelation'
wd_params += "&#{wd}=#{params[wd]}"
end
url + wd_params
end
def asset_params
params.permit(
:file
)
end
def asset_data_type(asset)
return 'wopi' if wopi_file?(asset)
return 'image' if asset.is_image?
'file'
end
end

View file

@ -339,147 +339,177 @@ class ProtocolsController < ApplicationController
def revert
respond_to do |format|
transaction_error = false
Protocol.transaction do
begin
# Revert is basically update from parent
@protocol.update_from_parent(current_user)
rescue Exception
transaction_error = true
raise ActiveRecord:: Rollback
if @protocol.can_destroy?
transaction_error = false
Protocol.transaction do
begin
# Revert is basically update from parent
@protocol.update_from_parent(current_user)
rescue Exception
transaction_error = true
raise ActiveRecord:: Rollback
end
end
end
if transaction_error
# Bad request error
format.json {
render json: {
message: t("my_modules.protocols.revert_error")
},
status: :bad_request
}
if transaction_error
# Bad request error
format.json do
render json: {
message: t('my_modules.protocols.revert_error')
},
status: :bad_request
end
else
# Everything good, display flash & render 200
flash[:success] = t(
'my_modules.protocols.revert_flash'
)
flash.keep(:success)
format.json { render json: {}, status: :ok }
end
else
# Everything good, display flash & render 200
flash[:success] = t(
"my_modules.protocols.revert_flash",
)
flash.keep(:success)
format.json { render json: {}, status: :ok }
format.json do
render json: {
message: t('my_modules.protocols.revert_error_locked')
}, status: :bad_request
end
end
end
end
def update_parent
respond_to do |format|
transaction_error = false
Protocol.transaction do
begin
@protocol.update_parent(current_user)
rescue Exception
transaction_error = true
raise ActiveRecord:: Rollback
if @protocol.parent.can_destroy?
transaction_error = false
Protocol.transaction do
begin
@protocol.update_parent(current_user)
rescue Exception
transaction_error = true
raise ActiveRecord:: Rollback
end
end
end
if transaction_error
# Bad request error
format.json {
render json: {
message: t("my_modules.protocols.update_parent_error")
},
status: :bad_request
}
else
# Everything good, record activity, display flash & render 200
Activity.create(
type_of: :revert_protocol,
project: @protocol.my_module.experiment.project,
my_module: @protocol.my_module,
user: current_user,
message: I18n.t(
'activities.revert_protocol',
user: current_user.full_name,
protocol: @protocol.name
if transaction_error
# Bad request error
format.json {
render json: {
message: t("my_modules.protocols.update_parent_error")
},
status: :bad_request
}
else
# Everything good, record activity, display flash & render 200
Activity.create(
type_of: :revert_protocol,
project: @protocol.my_module.experiment.project,
my_module: @protocol.my_module,
user: current_user,
message: I18n.t(
'activities.revert_protocol',
user: current_user.full_name,
protocol: @protocol.name
)
)
)
flash[:success] = t(
"my_modules.protocols.update_parent_flash",
)
flash.keep(:success)
format.json { render json: {}, status: :ok }
flash[:success] = t(
"my_modules.protocols.update_parent_flash",
)
flash.keep(:success)
format.json { render json: {}, status: :ok }
end
else
format.json do
render json: {
message: t('my_modules.protocols.update_parent_error_locked')
}, status: :bad_request
end
end
end
end
def update_from_parent
respond_to do |format|
transaction_error = false
Protocol.transaction do
begin
@protocol.update_from_parent(current_user)
rescue Exception
transaction_error = true
raise ActiveRecord:: Rollback
if @protocol.can_destroy?
transaction_error = false
Protocol.transaction do
begin
@protocol.update_from_parent(current_user)
rescue Exception
transaction_error = true
raise ActiveRecord:: Rollback
end
end
end
if transaction_error
# Bad request error
format.json {
render json: {
message: t("my_modules.protocols.update_from_parent_error")
},
status: :bad_request
}
if transaction_error
# Bad request error
format.json {
render json: {
message: t("my_modules.protocols.update_from_parent_error")
},
status: :bad_request
}
else
# Everything good, display flash & render 200
flash[:success] = t(
"my_modules.protocols.update_from_parent_flash",
)
flash.keep(:success)
format.json { render json: {}, status: :ok }
end
else
# Everything good, display flash & render 200
flash[:success] = t(
"my_modules.protocols.update_from_parent_flash",
)
flash.keep(:success)
format.json { render json: {}, status: :ok }
format.json do
render json: {
message: t('my_modules.protocols.update_from_parent_error_locked')
}, status: :bad_request
end
end
end
end
def load_from_repository
respond_to do |format|
transaction_error = false
Protocol.transaction do
begin
@protocol.load_from_repository(@source, current_user)
rescue Exception
transaction_error = true
raise ActiveRecord:: Rollback
if @protocol.can_destroy?
transaction_error = false
Protocol.transaction do
begin
@protocol.load_from_repository(@source, current_user)
rescue Exception
transaction_error = true
raise ActiveRecord:: Rollback
end
end
end
if transaction_error
# Bad request error
format.json {
render json: {
message: t("my_modules.protocols.load_from_repository_error")
},
status: :bad_request
}
else
# Everything good, record activity, display flash & render 200
Activity.create(
type_of: :load_protocol_from_repository,
project: @protocol.my_module.experiment.project,
my_module: @protocol.my_module,
user: current_user,
message: I18n.t(
'activities.load_protocol_from_repository',
user: current_user.full_name,
protocol: @source.name
if transaction_error
# Bad request error
format.json do
render json: {
message: t('my_modules.protocols.load_from_repository_error')
},
status: :bad_request
end
else
# Everything good, record activity, display flash & render 200
Activity.create(
type_of: :load_protocol_from_repository,
project: @protocol.my_module.experiment.project,
my_module: @protocol.my_module,
user: current_user,
message: I18n.t(
'activities.load_protocol_from_repository',
user: current_user.full_name,
protocol: @source.name
)
)
)
flash[:success] = t(
"my_modules.protocols.load_from_repository_flash",
)
flash.keep(:success)
format.json { render json: {}, status: :ok }
flash[:success] = t('my_modules.protocols.load_from_repository_flash')
flash.keep(:success)
format.json { render json: {}, status: :ok }
end
else
format.json do
render json: {
message: t('my_modules.protocols.load_from_repository_error_locked')
}, status: :bad_request
end
end
end
end
@ -487,40 +517,46 @@ class ProtocolsController < ApplicationController
def load_from_file
# This is actually very similar to import
respond_to do |format|
transaction_error = false
Protocol.transaction do
begin
import_into_existing(@protocol, @protocol_json, current_user)
rescue Exception
transaction_error = true
raise ActiveRecord:: Rollback
if @protocol.can_destroy?
transaction_error = false
Protocol.transaction do
begin
import_into_existing(@protocol, @protocol_json, current_user)
rescue Exception
transaction_error = true
raise ActiveRecord:: Rollback
end
end
end
if transaction_error
format.json {
render json: { status: :error }, status: :bad_request
}
else
# Everything good, record activity, display flash & render 200
Activity.create(
type_of: :load_protocol_from_file,
project: @protocol.my_module.experiment.project,
my_module: @protocol.my_module,
user: current_user,
message: I18n.t(
'activities.load_protocol_from_file',
user: current_user.full_name,
protocol: @protocol_json[:name]
if transaction_error
format.json {
render json: { status: :error }, status: :bad_request
}
else
# Everything good, record activity, display flash & render 200
Activity.create(
type_of: :load_protocol_from_file,
project: @protocol.my_module.experiment.project,
my_module: @protocol.my_module,
user: current_user,
message: I18n.t(
'activities.load_protocol_from_file',
user: current_user.full_name,
protocol: @protocol_json[:name]
)
)
)
flash[:success] = t(
"my_modules.protocols.load_from_file_flash",
)
flash.keep(:success)
format.json {
render json: { status: :ok }, status: :ok
}
flash[:success] = t(
'my_modules.protocols.load_from_file_flash',
)
flash.keep(:success)
format.json {
render json: { status: :ok }, status: :ok
}
end
else
format.json do
render json: { status: :locked }, status: :bad_request
end
end
end
end

View file

@ -55,26 +55,26 @@ class ResultAssetsController < ApplicationController
)
)
format.html {
format.html do
flash[:success] = t(
"result_assets.create.success_flash",
module: @my_module.name)
redirect_to results_my_module_path(@my_module)
}
format.json {
end
format.json do
render json: {
status: 'ok',
html: render_to_string({
partial: "my_modules/result.html.erb", locals: {result: @result}
})
html: render_to_string(
partial: 'my_modules/result.html.erb', locals: { result: @result }
)
}, status: :ok
}
end
else
# This response is sent as 200 OK due to IE security error when
# accessing iframe content.
format.json {
render json: {status: 'error', errors: @result.errors}
}
format.json do
render json: { status: 'error', errors: @result.errors }
end
end
end
end
@ -92,6 +92,7 @@ class ResultAssetsController < ApplicationController
def update
update_params = result_params
previous_size = @result.space_taken
previous_asset = @result.asset
if update_params.key? :asset_attributes
asset = Asset.find_by_id(update_params[:asset_attributes][:id])
@ -106,6 +107,16 @@ class ResultAssetsController < ApplicationController
module: @my_module.name)
if @result.archived_changed?(from: false, to: true)
if previous_asset.locked?
respond_to do |format|
format.html {
flash[:error] = t('result_assets.archive.error_flash')
redirect_to results_my_module_path(@my_module)
return
}
end
end
saved = @result.archive(current_user)
success_flash = t("result_assets.archive.success_flash",
module: @my_module.name)
@ -125,6 +136,19 @@ class ResultAssetsController < ApplicationController
elsif @result.archived_changed?(from: true, to: false)
render_403
else
if previous_asset.locked?
@result.errors.add(:asset_attributes,
t('result_assets.edit.locked_file_error'))
respond_to do |format|
format.json do
render json: {
status: 'error',
errors: @result.errors
}, status: :bad_request
return
end
end
end
# Asset (file) and/or name has been changed
saved = @result.save

View file

@ -185,29 +185,38 @@ class StepsController < ApplicationController
end
def destroy
# Update position on other steps of this module
@protocol.steps.where("position > ?", @step.position).each do |step|
step.position = step.position - 1
step.save
if @step.can_destroy?
# Update position on other steps of this module
@protocol.steps.where('position > ?', @step.position).each do |step|
step.position = step.position - 1
step.save
end
# Calculate space taken by this step
team = @protocol.team
previous_size = @step.space_taken
# Destroy the step
@step.destroy(current_user)
# Release space taken by the step
team.release_space(previous_size)
team.save
# Update protocol timestamp
update_protocol_ts(@step)
flash[:success] = t(
'protocols.steps.destroy.success_flash',
step: (@step.position + 1).to_s
)
else
flash[:error] = t(
'protocols.steps.destroy.error_flash',
step: (@step.position + 1).to_s
)
end
# Calculate space taken by this step
team = @protocol.team
previous_size = @step.space_taken
# Destroy the step
@step.destroy(current_user)
# Release space taken by the step
team.release_space(previous_size)
team.save
# Update protocol timestamp
update_protocol_ts(@step)
flash[:success] = t(
"protocols.steps.destroy.success_flash",
step: (@step.position + 1).to_s)
if @protocol.in_module?
redirect_to protocols_my_module_path(@step.my_module)
else
@ -537,7 +546,12 @@ class StepsController < ApplicationController
for pos, attrs in params[key] do
if attrs[:_destroy] == '1'
if attrs[:id].present?
attr_params[pos] = { id: attrs[:id], _destroy: '1' }
asset = Asset.find_by_id(attrs[:id])
if asset.try(&:locked?)
asset.errors.add(:base, 'This file is locked.')
else
attr_params[pos] = { id: attrs[:id], _destroy: '1' }
end
end
params[key].delete(pos)
elsif has_destroy_params(params[key][pos])

View file

@ -0,0 +1,339 @@
class WopiController < ActionController::Base
include WopiUtil
include PermissionHelper
before_action :load_vars, :authenticate_user_from_token!
before_action :verify_proof!
# Only used for checkfileinfo
def file_get_endpoint
check_file_info
end
def file_contents_get_endpoint
# get_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 post_file_endpoint
override = request.headers['X-WOPI-Override']
case override
when 'GET_LOCK'
get_lock
when 'PUT_RELATIVE'
put_relative
when 'LOCK'
old_lock = request.headers['X-WOPI-OldLock']
if old_lock.nil?
lock
else
unlock_and_relock
end
when 'UNLOCK'
unlock
when 'REFRESH_LOCK'
refresh_lock
when 'GET_SHARE_URL'
render nothing: :true, status: 501 and return
else
render nothing: :true, status: 404 and return
end
end
# Only used for putfile
def file_contents_post_endpoint
logger.warn 'WOPI: post_file_contents called'
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.to_s,
Version: @asset.version.to_s,
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,
UserCanWrite: @can_write,
UserCanNotWriteRelative: true,
CloseUrl: @close_url,
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),
BreadcrumbBrandName: @breadcrumb_brand_name,
BreadcrumbBrandUrl: @breadcrumb_brand_url,
BreadcrumbFolderName: @breadcrumb_folder_name,
BreadcrumbFolderUrl: @breadcrumb_folder_url
}
response.headers['X-WOPI-HostEndpoint'] = ENV['WOPI_ENDPOINT_URL']
response.headers['X-WOPI-MachineName'] = ENV['WOPI_ENDPOINT_URL']
response.headers['X-WOPI-ServerVersion'] = Constants::APP_VERSION
render json: msg and return
end
def put_relative
render nothing: :true, status: 501 and return
end
def lock
lock = request.headers['X-WOPI-Lock']
logger.warn 'WOPI: lock; ' + lock.to_s
render nothing: :true, status: 404 and return if lock.nil? || lock.blank?
@asset.with_lock do
if @asset.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
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.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
lock = request.headers['X-WOPI-Lock']
render nothing: :true, status: 400 and return if lock.nil? || lock.blank?
@asset.with_lock do
if @asset.locked?
logger.warn "WOPI: current asset lock: #{@asset.lock},
unlocking lock #{lock}"
if @asset.lock == lock
@asset.unlock
@asset.post_process_file # Space is already taken in put_file
create_wopi_file_activity(@user, false)
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
logger.warn 'WOPI: tried to unlock non-locked file'
response.headers['X-WOPI-Lock'] = ' '
render nothing: :true, status: 409 and return
end
end
end
def refresh_lock
lock = request.headers['X-WOPI-Lock']
render nothing: :true, status: 400 and return if lock.nil? || lock.blank?
@asset.with_lock do
if @asset.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
@asset.with_lock do
if @asset.locked?
response.headers['X-WOPI-Lock'] = @asset.lock
else
response.headers['X-WOPI-Lock'] = ' '
end
render nothing: :true, status: 200 and return
end
end
def put_file
@asset.with_lock do
lock = request.headers['X-WOPI-Lock']
if @asset.locked?
if @asset.lock == lock
logger.warn 'WOPI: replacing file'
@organization.release_space(@asset.estimated_size)
@asset.update_contents(request.body)
@asset.last_modified_by = @user
@asset.save
@organization.take_space(@asset.estimated_size)
@organization.save
@protocol.update(updated_at: Time.now) if @protocol
response.headers['X-WOPI-ItemVersion'] = @asset.version
render nothing: :true, status: 200 and return
else
logger.warn 'WOPI: wrong lock used to try and modify file'
response.headers['X-WOPI-Lock'] = @asset.lock
render nothing: :true, status: 409 and return
end
elsif !@asset.file_file_size.nil? && @asset.file_file_size.zero?
logger.warn 'WOPI: initializing empty file'
@organization.release_space(@asset.estimated_size)
@asset.update_contents(request.body)
@asset.last_modified_by = @user
@asset.save
@organization.save
response.headers['X-WOPI-ItemVersion'] = @asset.version
render nothing: :true, status: 200 and return
else
logger.warn 'WOPI: trying to modify unlocked file'
response.headers['X-WOPI-Lock'] = ' '
render nothing: :true, status: 409 and return
end
end
end
def load_vars
@asset = Asset.find_by_id(params[:id])
if @asset.nil?
render nothing: :true, status: 404 and return
else
logger.warn 'Found asset: ' + @asset.id.to_s
step_assoc = @asset.step
result_assoc = @asset.result
@assoc = step_assoc unless step_assoc.nil?
@assoc = result_assoc unless result_assoc.nil?
if @assoc.class == Step
@protocol = @asset.step.protocol
@organization = @protocol.organization
else
@my_module = @assoc.my_module
@organization = @my_module.experiment.project.organization
end
end
end
private
def authenticate_user_from_token!
wopi_token = params[:access_token]
if wopi_token.nil?
logger.warn 'WOPI: nil wopi token'
render nothing: :true, status: 401 and return
end
@user = User.find_by_valid_wopi_token(wopi_token)
if @user.nil?
logger.warn 'WOPI: no user with this token found'
render nothing: :true, status: 401 and return
end
logger.warn 'WOPI: user found by token ' + wopi_token +
' ID: ' + @user.id.to_s
# This is what we get for settings permission methods with
# current_user
@current_user = @user
if @assoc.class == Step
@can_read = can_view_steps_in_protocol(@protocol)
@can_write = can_edit_step_in_protocol(@protocol)
if @protocol.in_module?
@close_url = protocols_my_module_path(@protocol.my_module,
only_path: false)
project = @protocol.my_module.experiment.project
@breadcrumb_brand_name = project.name
@breadcrumb_brand_url = project_path(project, only_path: false)
@breadcrumb_folder_name = @protocol.my_module.name
else
@close_url = protocols_path(only_path: false)
@breadcrump_brand_name = 'Projects'
@breadcrumb_brand_url = root_path(only_path: false)
@breadcrumb_folder_name = 'Protocol managament'
end
@breadcrumb_folder_url = @close_url
else
@can_read = can_view_or_download_result_assets(@my_module)
@can_write = can_edit_result_asset_in_module(@my_module)
@close_url = results_my_module_path(@my_module, only_path: false)
@breadcrumb_brand_name = @my_module.experiment.project.name
@breadcrumb_brand_url = project_path(@my_module.experiment.project,
only_path: false)
@breadcrumb_folder_name = @my_module.name
@breadcrumb_folder_url = @close_url
end
render nothing: :true, status: 404 and return unless @can_read
end
def verify_proof!
token = params[:access_token].encode('utf-8')
timestamp = request.headers['X-WOPI-TimeStamp'].to_i
signed_proof = request.headers['X-WOPI-Proof']
signed_proof_old = request.headers['X-WOPI-ProofOld']
url = request.original_url.upcase.encode('utf-8')
if convert_to_unix_timestamp(timestamp) + 20.minutes >= Time.now
if current_wopi_discovery.verify_proof(token, timestamp, signed_proof,
signed_proof_old, url)
logger.warn 'WOPI: proof verification: successful'
else
logger.warn 'WOPI: proof verification: not verified'
render nothing: :true, status: 500 and return
end
else
logger.warn 'WOPI: proof verification: timestamp too old; ' +
timestamp.to_s
render nothing: :true, status: 500 and return
end
rescue => e
logger.warn 'WOPI: proof verification: failed; ' + e.message
render nothing: :true, status: 500 and return
end
end

View file

@ -0,0 +1,60 @@
module FileIconsHelper
def wopi_file?(asset)
file_ext = asset.file_file_name.split('.').last
%w(csv ods xls xlsb xlsm xlsx odp pot potm potx pps ppsm ppsx ppt pptm pptx doc docm docx dot dotm dotx odt rtf).include?(file_ext)
end
# For showing next to file
def file_extension_icon(asset)
file_ext = asset.file_file_name.split('.').last
if %w(doc docm docx dot dotm dotx odt rtf).include?(file_ext)
image_link = 'office/Word-docx_20x20x32.png'
elsif %w(csv ods xls xlsb xlsm xlsx).include?(file_ext)
image_link = 'office/Excel-xlsx_20x20x32.png'
elsif %w(odp pot potm potx pps ppsm ppsx ppt pptm pptx).include?(file_ext)
image_link = 'office/PowerPoint-pptx_20x20x32.png'
end
if image_link
image_tag image_link
else
''
end
end
# For showing in view/edit buttons (WOPI)
def file_application_icon(asset)
file_ext = asset.file_file_name.split('.').last
if %w(doc docm docx dot dotm dotx odt rtf).include?(file_ext)
image_link = 'office/Word-color_16x16x32.png'
elsif %w(csv ods xls xlsb xlsm xlsx).include?(file_ext)
image_link = 'office/Excel-color_16x16x32.png'
elsif %w(odp pot potm potx pps ppsm ppsx ppt pptm pptx).include?(file_ext)
image_link = 'office/PowerPoint-Color_16x16x32.png'
end
if image_link
image_tag image_link
else
''
end
end
# Shows correct WOPI application text (Word Online/Excel ..)
def wopi_button_text(asset, action)
file_ext = asset.file_file_name.split('.').last
if %w(doc docm docx dot dotm dotx odt rtf).include?(file_ext)
app = 'Word Online'
elsif %w(csv ods xls xlsb xlsm xlsx).include?(file_ext)
app = 'Excel Online'
elsif %w(odp pot potm potx pps ppsm ppsx ppt pptm pptx).include?(file_ext)
app = 'PowerPoint Online'
end
if action == 'view'
t('result_assets.wopi_open_file', app: app)
elsif action == 'edit'
t('result_assets.wopi_edit_file', app: app)
end
end
end

View file

@ -39,6 +39,14 @@ module ResultsHelper
end
end
def result_unlocked?(result)
if result.is_asset
!result.asset.locked?
else
true
end
end
def result_path_of_type(result)
if result.is_asset
result_asset_path(result.result_asset)

View file

@ -0,0 +1,62 @@
module WopiHelper
def wopi_result_view_file_button(result)
if result.asset.can_perform_action('view')
link_to view_asset_url(id: result.asset),
class: 'btn btn-default btn-sm',
target: '_blank',
style: 'display: inline-block' do
"#{file_application_icon(
result.asset
)} #{wopi_button_text(result.asset, 'view')}".html_safe
end
end
end
def wopi_result_edit_file_button(result)
if can_edit_result_asset_in_module(result.my_module) &&
result.asset.can_perform_action('edit')
link_to edit_asset_url(id: result.asset),
class: 'btn btn-default btn-sm',
target: '_blank',
style: 'display: inline-block' do
"#{file_application_icon(
result.asset
)} #{wopi_button_text(result.asset, 'edit')}".html_safe
end
end
end
def wopi_asset_view_button(asset)
if asset.can_perform_action('view')
link_to view_asset_url(id: asset),
class: 'btn btn-default btn-sm',
target: '_blank',
style: 'display: inline-block' do
"#{file_application_icon(asset)} #{wopi_button_text(asset, 'view')}"
.html_safe
end
end
end
def wopi_asset_edit_button(asset)
if asset.can_perform_action('edit')
link_to edit_asset_url(id: asset),
class: 'btn btn-default btn-sm',
target: '_blank',
style: 'display: inline-block' do
"#{file_application_icon(
asset
)} #{wopi_button_text(asset, 'edit')}".html_safe
end
end
end
def wopi_asset_file_name(asset)
html = '<p style="display: inline-block">'
html += "#{file_extension_icon(asset)}&nbsp;"
html += truncate(asset.file_file_name,
length: Constants::FILENAME_TRUNCATION_LENGTH)
html += '&nbsp;</p>'
sanitize_input(html, %w(img))
end
end

View file

@ -45,6 +45,8 @@ class Activity < ActiveRecord::Base
:edit_result_comment,
:delete_result_comment,
:destroy_result,
:start_edit_wopi_file,
:unlock_wopi_file,
:load_protocol_from_file,
:load_protocol_from_repository,
:revert_protocol,

View file

@ -2,8 +2,11 @@ class Asset < ActiveRecord::Base
include SearchableModel
include DatabaseHelper
include Encryptor
include WopiUtil
require 'tempfile'
# Lock duration set to 30 minutes
LOCK_DURATION = 60*30
# Paperclip validation
has_attached_file :file,
@ -305,9 +308,12 @@ class Asset < ActiveRecord::Base
end
end
# Preserving attachments (on client-side) between failed validations (only usable for small/few files!)
# Needs to be called before save method and view needs to have :file_content and :file_info hidden field
# If file is an image, it can be viewed on front-end using @preview_cached with image_tag tag
# Preserving attachments (on client-side) between failed validations
# (only usable for small/few files!).
# Needs to be called before save method and view needs to have
# :file_content and :file_info hidden field.
# If file is an image, it can be viewed on front-end
# using @preview_cached with image_tag tag.
def preserve(file_data)
if file_data[:file_content].present?
restore_cached(file_data[:file_content], file_data[:file_info])
@ -315,6 +321,109 @@ class Asset < ActiveRecord::Base
cache
end
def can_perform_action(action)
if ENV['WOPI_ENABLED'] == 'true'
file_ext = file_file_name.split('.').last
if file_ext == 'wopitest' &&
(!ENV['WOPI_TEST_ENABLED'] || ENV['WOPI_TEST_ENABLED'] == 'false')
return false
end
action = get_action(file_ext, action)
return false if action.nil?
true
else
false
end
end
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?
action_url = action.urlsrc
action_url = action_url.gsub(/<IsLicensedUser=BUSINESS_USER&>/,
'IsLicensedUser=0&')
action_url = action_url.gsub(/<IsLicensedUser=BUSINESS_USER>/,
'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
)
action_url += "WOPISrc=#{rest_url}"
if with_tokens
token = user.get_wopi_token
action_url + "&access_token=#{token.token}"\
"&access_token_ttl=#{(token.ttl * 1000)}"
else
action_url
end
else
return nil
end
end
def favicon_url(action)
file_ext = file_file_name.split('.').last
action = get_action(file_ext, action)
action.wopi_app.icon if action.try(:wopi_app)
end
# locked?, lock_asset and refresh_lock rely on the asset
# being locked in the database to prevent race conditions
def locked?
!lock.nil?
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
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
save!
end
def unlock
self.lock = nil
self.lock_ttl = nil
save!
end
def unlock_expired
with_lock do
if !lock_ttl.nil? && lock_ttl >= Time.now.to_i
self.lock = nil
self.lock_ttl = nil
save!
end
end
end
def update_contents(new_file)
new_file.class.class_eval { attr_accessor :original_filename }
new_file.original_filename = file_file_name
self.file = new_file
self.version = version.nil? ? 1 : version + 1
save
end
protected
# Checks if attachments is an image (in post processing imagemagick will
# generate styles)
def allow_styles_on_images
if !(file.content_type =~ %r{^(image|(x-)?application)/(x-png|pjpeg|jpeg|jpg|png|gif)$})
return false
end
end
private
def filter_paperclip_errors

View file

@ -417,6 +417,8 @@ class Protocol < ActiveRecord::Base
end
def archive(user)
return nil unless can_destroy?
# Don't update "updated_at" timestamp
self.record_timestamps = false
@ -637,6 +639,10 @@ class Protocol < ActiveRecord::Base
self.reload
end
def can_destroy?
steps.map(&:can_destroy?).all?
end
private
def deep_clone(clone, current_user)

View file

@ -83,6 +83,10 @@ class Step < ActiveRecord::Base
super()
end
def can_destroy?
!assets.map(&:locked?).any?
end
def my_module
protocol.present? ? protocol.my_module : nil
end

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

@ -0,0 +1,8 @@
class Token < ActiveRecord::Base
validates :token, presence: true
validates :ttl, presence: true
belongs_to :user, foreign_key: 'user_id', class_name: 'User', inverse_of: :tokens
end

View file

@ -145,6 +145,12 @@ class User < ActiveRecord::Base
class_name: 'Table',
foreign_key: 'last_modified_by_id'
has_many :created_tags, class_name: 'Tag', foreign_key: 'created_by_id'
has_many :tokens,
class_name: 'Token',
foreign_key: 'user_id',
inverse_of: :user
has_many :modified_tags,
class_name: 'Tag',
foreign_key: 'last_modified_by_id'
@ -169,6 +175,7 @@ class User < ActiveRecord::Base
class_name: 'Protocol',
foreign_key: 'restored_by_id',
inverse_of: :restored_by
has_many :user_notifications, inverse_of: :user
has_many :notifications, through: :user_notifications
@ -211,8 +218,8 @@ class User < ActiveRecord::Base
end
result
.where_attributes_like([:full_name, :email], query)
.distinct
.where_attributes_like([:full_name, :email], query)
.distinct
end
def empty_avatar(name, size)
@ -312,6 +319,33 @@ class User < ActiveRecord::Base
.uniq
end
def self.find_by_valid_wopi_token(token)
Rails.logger.warn "WOPI: searching by token #{token}"
User
.joins('LEFT OUTER JOIN tokens ON user_id = users.id')
.where(tokens: { token: token })
.where('tokens.ttl = 0 OR tokens.ttl > ?', Time.now.to_i)
.first
end
def get_wopi_token
# WOPI does not have a good way to request a new token,
# so a new token should be provided each time this is called,
# while keeping any old tokens as long as they have not yet expired
tokens = Token.where(user_id: id).distinct
tokens.each do |token|
token.delete if token.ttl < Time.now.to_i
end
token_string = "#{Devise.friendly_token(20)}-#{id}"
# WOPI uses millisecond TTLs
ttl = (Time.now + 1.day).to_i
wopi_token = Token.create(token: token_string, ttl: ttl, user_id: id)
Rails.logger.warn("WOPI: generating new token #{wopi_token.token}")
wopi_token
end
def teams_ids
teams.pluck(:id)
end

View file

@ -0,0 +1,9 @@
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

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

@ -0,0 +1,10 @@
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: :destroy
validates :name, :icon, :wopi_discovery, presence: true
end

View file

@ -0,0 +1,53 @@
class WopiDiscovery < ActiveRecord::Base
require 'base64'
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
# Verifies if proof from headers, X-WOPI-Proof/X-WOPI-OldProof was encrypted
# with this discovery public key (two key possible old/new)
def verify_proof(token, timestamp, signed_proof, signed_proof_old, 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
expected_proof = token_length + token.bytes +
url_length + url.upcase.bytes +
timestamp_length + timestamp_bytes
key = generate_key(proof_key_mod, proof_key_exp)
old_key = generate_key(proof_key_old_mod, proof_key_old_exp)
# Try all possible combiniations
try_verification(expected_proof, signed_proof, key) ||
try_verification(expected_proof, signed_proof_old, key) ||
try_verification(expected_proof, signed_proof, old_key)
end
# Generates a public key from given modulus and exponent
def generate_key(modulus, exponent)
mod = Base64.decode64(modulus).unpack('H*').first.to_i(16)
exp = Base64.decode64(exponent).unpack('H*').first.to_i(16)
seq = OpenSSL::ASN1::Sequence.new([OpenSSL::ASN1::Integer.new(mod),
OpenSSL::ASN1::Integer.new(exp)])
OpenSSL::PKey::RSA.new(seq.to_der)
end
# Verify if decrypting signed_proof with public_key equals to expected_proof
def try_verification(expected_proof, signed_proof_b64, public_key)
signed_proof = Base64.decode64(signed_proof_b64)
public_key.verify(OpenSSL::Digest::SHA256.new, signed_proof,
expected_proof.pack('c*'))
end
end

View file

@ -0,0 +1,25 @@
class UserSubdomain
def self.matches?(request)
if ENV['USER_SUBDOMAIN']
return (request.subdomain.present? &&
request.subdomain == ENV['USER_SUBDOMAIN'])
else
return true
end
end
end
class WopiSubdomain
def self.matches?(request)
if ENV['WOPI_ENABLED'] == 'true'
if ENV['WOPI_SUBDOMAIN']
return (request.subdomain.present? &&
request.subdomain == ENV['WOPI_SUBDOMAIN'])
else
return true
end
else
return false
end
end
end

112
app/utilities/wopi_util.rb Normal file
View file

@ -0,0 +1,112 @@
module WopiUtil
require 'open-uri'
# Used for timestamp
UNIX_EPOCH_IN_CLR_TICKS = 621355968000000000
CLR_TICKS_PER_SECOND = 10000000
DISCOVERY_TTL = 1.days
DISCOVERY_TTL.freeze
# For more explanation see this:
# http://stackoverflow.com/questions/11888053/
# convert-net-datetime-ticks-property-to-date-in-objective-c
def convert_to_unix_timestamp(timestamp)
Time.at((timestamp - UNIX_EPOCH_IN_CLR_TICKS) / CLR_TICKS_PER_SECOND)
end
def get_action(extension, activity)
current_wopi_discovery
WopiAction.find_action(extension, activity)
end
def current_wopi_discovery
discovery = WopiDiscovery.first
return discovery if discovery && discovery.expires >= Time.now.to_i
initialize_discovery(discovery)
end
private
# Currently only saves Excel, Word and PowerPoint view and edit actions
def initialize_discovery(discovery)
Rails.logger.warn 'Initializing discovery'
discovery.destroy if discovery
@doc = Nokogiri::XML(Kernel.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
next unless %w(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
next unless %w(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
discovery
rescue => e
Rails.logger.warn 'WOPI: initialization failed: ' + e.message
e.backtrace.each { |line| Rails.logger.error line }
discovery = WopiDiscovery.first
discovery.destroy if discovery
end
def create_wopi_file_activity(current_user, started_editing)
if @assoc.class == Step
activity = Activity.new(
type_of: :start_edit_wopi_file,
user: current_user,
message: t(
started_editing ? 'activities.start_edit_wopi_file_step' :
'activities.unlock_wopi_file_step',
user: current_user.full_name,
file: @asset.file_file_name,
step: @asset.step.position + 1,
step_name: @asset.step.name
)
)
if @protocol.in_module?
activity.my_module = @protocol.my_module
activity.project = @protocol.my_module.experiment.project
end
activity.save
else
Activity.create(
type_of: :start_edit_wopi_file,
user: current_user,
project: @my_module.experiment.project,
my_module: @my_module,
message: t(
started_editing ? 'activities.start_edit_wopi_file_result' :
'activities.unlock_wopi_file_result',
user: current_user.full_name,
file: @asset.file_file_name,
result: @asset.result.name
)
)
end
end
end

60
app/views/assets/edit.erb Normal file
View file

@ -0,0 +1,60 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<!-- Enable IE Standards mode -->
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title><%= t('assets.head_title.edit', file_name: @asset.file_file_name) %></title>
<meta name="description" content="">
<meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<link rel="shortcut icon" href='<%= @favicon_url %>' />
<style type="text/css">
body {
margin: 0;
padding: 0;
overflow:hidden;
-ms-content-zooming: none;
}
#office_frame {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
border: none;
display: block;
}
</style>
</head>
<body>
<form id='office_form' name='office_form' target='office_frame'
action='<%= @action_url %>' method='post'>
<input name='access_token' value='<%= @token %>' type='hidden'/>
<input name='access_token_ttl' value='<%= @ttl %>' type='hidden'/>
</form>
<span id="frameholder"></span>
<script type="text/javascript">
var frameholder = document.getElementById('frameholder');
var office_frame = document.createElement('iframe');
office_frame.name = 'office_frame';
office_frame.id ='office_frame';
// The title should be set for accessibility
office_frame.title = 'Office Online Frame';
// This attribute allows true fullscreen mode in slideshow view
// when using PowerPoint Online's 'view' action.
office_frame.setAttribute('allowfullscreen', 'true');
frameholder.appendChild(office_frame);
document.getElementById('office_form').submit();
</script>
</body>
</html>

61
app/views/assets/view.erb Normal file
View file

@ -0,0 +1,61 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<!-- Enable IE Standards mode -->
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title><%= t('assets.head_title.view', file_name: @asset.file_file_name) %></title>
<meta name="description" content="">
<meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<link rel="shortcut icon" href='<%= @favicon_url %>' />
<style type="text/css">
body {
margin: 0;
padding: 0;
overflow:hidden;
-ms-content-zooming: none;
}
#office_frame {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
border: none;
display: block;
}
</style>
</head>
<body>
<form id='office_form' name='office_form' target='office_frame'
action='<%= @action_url %>' method='post'>
<input name='access_token' value='<%= @token %>' type='hidden'/>
<input name='access_token_ttl' value='<%= @ttl %>' type='hidden'/>
</form>
<span id="frameholder"></span>
<script type="text/javascript">
var frameholder = document.getElementById('frameholder');
var office_frame = document.createElement('iframe');
office_frame.name = 'office_frame';
office_frame.id ='office_frame';
// The title should be set for accessibility
office_frame.title = 'Office Online Frame';
// This attribute allows true fullscreen mode in slideshow view
// when using PowerPoint Online's 'view' action.
office_frame.setAttribute('allowfullscreen', 'true');
frameholder.appendChild(office_frame);
document.getElementById('office_form').submit();
</script>
</body>
</html>

View file

@ -18,7 +18,7 @@
<span class="glyphicon glyphicon-edit"></span>
</a>
<% end %>
<% if can_archive_result(result) and not result.archived %>
<% if can_archive_result(result) && !result.archived && result_unlocked?(result) %>
<a href="#" class="btn btn-link form-submit-link" data-submit-form="result-archive-form-<%= result.id %>" data-confirm-form="<%= t('my_modules.results.archive_confirm') %>" title="<%= t'my_modules.results.options.archive_title' %>">
<span class="glyphicon glyphicon-briefcase"></span>
</a>

View file

@ -3,7 +3,10 @@
<%= f.text_field :name, style: "margin-top: 10px;" %><br />
<%= f.fields_for :asset do |ff| %>
<span><strong><%=t "result_assets.edit.uploaded_asset" %></strong></span>
<p style="margin: 10px;"><%= ff.object.file_file_name %></p>
<p style="margin: 10px;">
<%= file_extension_icon(ff.object) %>
<%= ff.object.file_file_name %>
</p>
<%= ff.file_field :file %>
<% end %>
<hr>

View file

@ -17,19 +17,15 @@
<% end %>
<% else %>
<%= link_to download_asset_path(result.asset), data: {no_turbolink: true} do %>
<p><%= truncate(result.asset.file_file_name,
length: Constants::FILENAME_TRUNCATION_LENGTH) %></p>
<% if wopi_file?(result.asset) %>
<%= wopi_asset_file_name(result.asset) %>
<% else %>
<p><%= truncate(result.asset.file_file_name,
length: Constants::FILENAME_TRUNCATION_LENGTH) %></p>
<% end %>
<% end %>
<%= wopi_result_view_file_button(result) %>
<%= wopi_result_edit_file_button(result) %>
<% end %>
<% end %>
<% else %>
<% if result.asset.file.processing? %>
<span data-status='asset-loading'
data-present-url='<%= file_present_asset_path(result.asset) %>'>
<%= image_tag 'medium/processing.gif' %>
</span>
<% else %>
<%= image_tag result.asset.url(:medium) if result.asset.is_image? %>
<p><%= result.asset.file_file_name %></p>
<% end %>
<% end %>

View file

@ -3,7 +3,11 @@
<% if asset.is_image? %>
<span class="glyphicon glyphicon-picture"></span>
<% else %>
<span class="glyphicon glyphicon-file"></span>
<% if wopi_file?(asset) %>
<%= file_extension_icon(asset) %>
<% else %>
<span class="glyphicon glyphicon-file"></span>
<% end %>
<% end %>
<%= render partial: "search/results/partials/asset_text.html.erb", locals: { asset: asset, query: search_query } %>
</h5>

View file

@ -3,8 +3,10 @@
<span class="glyphicon glyphicon-file"></span>
<%= t("protocols.steps.new.asset_panel_title") %>
<div class="pull-right">
<%= ff.remove_nested_fields_link do %>
<span class="glyphicon glyphicon-remove"></span>
<% unless ff.object.file.exists? && ff.object.locked? %>
<%= ff.remove_nested_fields_link do %>
<span class="glyphicon glyphicon-remove"></span>
<% end %>
<% end %>
</div>
</div>
@ -17,15 +19,29 @@
id: "modal_link#{ff.object.id}",
data: {no_turbolink: true, id: true, status: "asset-present",
description: "#{step.position + 1}. #{truncate(step.name, length: Constants::FILENAME_TRUNCATION_LENGTH)}"} do %>
<%= image_tag ff.object.url(:medium), data: {'preview-url': large_image_url_asset_path(ff.object)} %>
<p><%= truncate(ff.object.file_file_name,
length: Constants::FILENAME_TRUNCATION_LENGTH) %></p>
<% if ff.object.is_image? %>
<%= image_tag ff.object.url(:medium), data: {'preview-url': large_image_url_asset_path(ff.object)} %>
<p><%= truncate(ff.object.file_file_name,
length: Constants::FILENAME_TRUNCATION_LENGTH) %></p>
<% else %>
<p>
<%= file_extension_icon(ff.object) %>
<%= ff.object.file_file_name %>
</p>
<% end %>
<% end %>
<% else %>
<%= link_to download_asset_path(ff.object), data: {no_turbolink: true} do %>
<%= image_tag ff.object.url(:medium) if ff.object.is_image? %>
<p><%= truncate(ff.object.file_file_name,
length: Constants::FILENAME_TRUNCATION_LENGTH) %></p>
<% if ff.object.is_image? %>
<%= image_tag ff.object.url(:medium) %>
<p><%= truncate(ff.object.file_file_name,
length: Constants::FILENAME_TRUNCATION_LENGTH) %></p>
<% else %>
<p>
<%= file_extension_icon(ff.object) %>
<%= ff.object.file_file_name %>
</p>
<% end %>
<% end %>
<% end %>
<% else %>

View file

@ -6,27 +6,44 @@
<div class="panel-heading">
<div class="panel-options pull-right">
<% if can_reorder_step_in_protocol(@protocol) %>
<a data-action="move-step" class="btn btn-link" href="<%= move_up_step_path(step, format: :json) %>" title="<%= t("protocols.steps.options.up_arrow_title") %>" data-remote="true">
<a data-action="move-step"
class="btn btn-link"
href="<%= move_up_step_path(step, format: :json) %>"
title="<%= t("protocols.steps.options.up_arrow_title") %>"
data-remote="true">
<span class="glyphicon glyphicon-arrow-up"></span></a>
<a data-action="move-step" class="btn btn-link" href="<%= move_down_step_path(step, format: :json) %>" title="<%= t("protocols.steps.options.down_arrow_title") %>" data-remote="true">
<a data-action="move-step"
class="btn btn-link"
href="<%= move_down_step_path(step, format: :json) %>"
title="<%= t("protocols.steps.options.down_arrow_title") %>"
data-remote="true">
<span class="glyphicon glyphicon-arrow-down"></a>
<% end %>
<% if can_edit_step_in_protocol(@protocol) %>
<a data-action="edit-step" class="btn btn-link" title="<%= t("protocols.steps.options.edit_title") %>" href="<%= edit_step_path(step, format: :json) %>" data-remote="true">
<a data-action="edit-step"
class="btn btn-link"
title="<%= t("protocols.steps.options.edit_title") %>"
href="<%= edit_step_path(step, format: :json) %>"
data-remote="true">
<span class="glyphicon glyphicon-edit">
</a>
<% end %>
<% if can_delete_step_in_protocol(@protocol) %>
<% if can_delete_step_in_protocol(@protocol) && step.can_destroy? %>
<%= link_to(step_path(step), title: t("protocols.steps.options.delete_title"), method: "delete", class: "btn btn-link",
data: {action: "delete-step", confirm: t("protocols.steps.destroy.confirm", step: step.name)}) do %>
<span class="glyphicon glyphicon-trash">
<% end %>
<% end %>
</div>
<a class="step-panel-collapse-link" href="#step-panel-<%= step.id %>" data-toggle="collapse" data-remote="true">
<a class="step-panel-collapse-link"
href="#step-panel-<%= step.id %>"
data-toggle="collapse"
data-remote="true">
<span class="glyphicon glyphicon-collapse-down collapse-step-icon"></span>
<strong><%= step.name %></strong> |
<span><%= sanitize_input t("protocols.steps.published_on", timestamp: l(step.created_at, format: :full), user: h(step.user.full_name)) %></span>
<span><%= sanitize_input t('protocols.steps.published_on',
timestamp: l(step.created_at, format: :full),
user: h(step.user.full_name)) %></span>
</a>
</div>
<div class="panel-collapse collapse" id="step-panel-<%= step.id %>" role="tabpanel">
@ -34,7 +51,7 @@
<div class="row">
<div class="col-xs-12">
<% if strip_tags(step.description).blank? %>
<em><%= t("protocols.steps.no_description") %></em>
<em><%= t('protocols.steps.no_description') %></em>
<% else %>
<div class="ql-editor">
<%= custom_auto_link(step.description, false) %>
@ -86,10 +103,7 @@
length: Constants::FILENAME_TRUNCATION_LENGTH) %></p>
<% end %>
<% else %>
<%= link_to download_asset_path(asset), data: {no_turbolink: true} do %>
<p><%= truncate(asset.file_file_name,
length: Constants::FILENAME_TRUNCATION_LENGTH) %></p>
<% end %>
<%= render partial: 'steps/wopi_controlls.html.erb', locals: { asset: asset } %>
<% end %>
<% end %>
<% else %>
@ -129,9 +143,14 @@
<div class="checkbox" <%= @protocol.in_module? ? "data-action=check-item" : "" %>>
<label>
<% if @protocol.in_module? %>
<input type="checkbox" value="" data-link-url="<%=checklistitem_state_step_path(step) %>" data-id="<%= checklist_item.id %>" <%= "checked" if checklist_item.checked? %> />
<input type="checkbox"
value=""
data-link-url="<%=checklistitem_state_step_path(step) %>"
data-id="<%= checklist_item.id %>" <%= "checked" if checklist_item.checked? %> />
<% else %>
<input type="checkbox" value="" disabled="disabled" />
<input type="checkbox"
value=""
disabled="disabled" />
<% end %>
<%= custom_auto_link(checklist_item.text) %>
</label>
@ -144,11 +163,15 @@
<% if @protocol.in_module? %>
<% if !step.completed? and can_complete_step_in_protocol(@protocol) %>
<div data-action="complete-step" class="pull-right" data-link-url="<%= toggle_step_state_step_path(step)%>">
<div data-action="complete-step"
class="pull-right"
data-link-url="<%= toggle_step_state_step_path(step)%>">
<button class="btn btn-primary"><%= t("protocols.steps.options.complete_title") %></button>
</div>
<% elsif step.completed? and can_uncomplete_step_in_protocol(@protocol) %>
<div data-action="uncomplete-step" class="pull-right" data-link-url="<%= toggle_step_state_step_path(step)%>">
<div data-action="uncomplete-step"
class="pull-right"
data-link-url="<%= toggle_step_state_step_path(step)%>">
<button class="btn btn-default"><%= t("protocols.steps.options.uncomplete_title") %></button>
</div>
<% end %>

View file

@ -0,0 +1,17 @@
<%= 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? %>
<% if wopi_file?(asset) %>
<%= wopi_asset_file_name(asset) %>
<% else %>
<p><%= truncate(asset.file_file_name,
length: Constants::FILENAME_TRUNCATION_LENGTH) %></p>
<% end %>
<% end %>
<%= wopi_asset_view_button(asset) %>
<% if can_edit_step_in_protocol(@protocol) %>
<%= wopi_asset_edit_button(asset) %>
<% end %>

View file

@ -31,6 +31,9 @@ module Scinote
end
# 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

View file

@ -175,10 +175,16 @@ module Paperclip
# Types taken from: http://filext.com/faq/office_mime_types.php and
# https://www.openoffice.org/framework/documentation/mimetypes/mimetypes.html
#
# Generic application
(Set[content_type, content_types_from_name].subset? Set.new %w(
text/rtf
application/rtf
)) ||
# Word processor application
(Set[content_type, content_types_from_name].subset? Set.new %w(
application/vnd.ms-office
application/msword
application/msword-template
application/vnd.openxmlformats-officedocument.wordprocessingml.document
application/vnd.openxmlformats-officedocument.wordprocessingml.template
application/vnd.ms-word.document.macroEnabled.12
@ -229,6 +235,7 @@ module Paperclip
application/vnd.stardivision.impress
application/vnd.stardivision.impress-packed
application/x-starimpress
text/x-gettext-translation-template
)) ||
# Graphics application
(Set[content_type, content_types_from_name].subset? Set.new %w(

View file

@ -526,12 +526,16 @@ en:
unlink_error: "Failed to unlink protocol."
revert_flash: "Protocol was successfully reverted to protocol version."
revert_error: "Failed to revert protocol."
revert_error_locked: "Failed to revert protocol. One or more files in the protocol are currently being edited."
update_parent_flash: "Protocol in repository was successfully updated with the version from the task."
update_parent_error: "Failed to update repository version of the protocol."
update_parent_error_locked: "Failed to update repository version of the protocol. One or more files in the protocol are currently being edited."
update_from_parent_flash: "Version in the repository was successfully updated."
update_from_parent_error: "Failed to update the protocol with the version in the repository."
update_from_parent_error_locked: "Failed to update the protocol with the version in the repository. One or more files in the protocol are currently being edited."
load_from_repository_flash: "Successfully loaded the protocol from the repository."
load_from_repository_error: "Failed to load the protocol from the repository."
load_from_repository_error_locked: "Failed to load the protocol from the repository. One or more files in the protocol are currently being edited."
load_from_repository_modal:
title: "Load protocol from repository"
text: "Choose the protocol to be loaded to the task."
@ -563,6 +567,7 @@ en:
confirm: "Copy to repository"
load_from_file_flash: "Successfully loaded the protocol from the file."
load_from_file_error: "Failed to load the protocol from file."
load_from_file_error_locked: "Failed to load the protocol from file. One or more files are currently being edited."
load_from_file_size_error: "Failed to load the protocol from file. Limit is %{size}Mb."
results:
head_title: "%{project} | %{module} | Results"
@ -787,14 +792,18 @@ en:
title: "Edit result from task %{module}"
uploaded_asset: "Uploaded file"
update: "Update file result"
locked_file_error: 'This file is being edited by someone else.'
create:
success_flash: "Successfully added file result to task <strong>%{module}</strong>"
update:
success_flash: "Successfully updated file result in task <strong>%{module}</strong>"
archive:
success_flash: "Successfully archived file result in task <strong>%{module}</strong>"
error_flash: "Couldn't archive file result. Someone is editing that file."
destroy:
success_flash: "File result successfully deleted."
wopi_open_file: "Open in %{app}"
wopi_edit_file: "Edit in %{app}"
result_tables:
new:
@ -1025,6 +1034,41 @@ en:
delete_step_comment: "<i>%{user}</i> deleted comment on Step %{step} <strong>%{step_name}</strong>."
edit_result_comment: "<i>%{user}</i> edited comment on result <strong>%{result}</strong>."
delete_result_comment: "<i>%{user}</i> deleted comment on result <strong>%{result}</strong>."
start_edit_wopi_file_step: "<i>%{user}</i> started editing File %{file} on Step %{step} <strong>%{step_name}</strong>."
start_edit_wopi_file_result: "<i>%{user}</i> started editing File %{file} on Result <strong>%{result}</strong>."
unlock_wopi_file_step: "<i>%{user}</i> closed File %{file} for editing on Step %{step} <strong>%{step_name}</strong>."
unlock_wopi_file_result: "<i>%{user}</i> started editing File %{file} on Result <strong>%{result}</strong>."
user_my_modules:
new:
head_title: "%{project} | %{module} | Add user"
title: "Add user to task %{module}"
create: "Add user to task"
no_users_available: "All users of the current project all already added to this task."
assign_user: "Add user"
back_button: "Back to task"
create:
success_flash: "Successfully added user %{user} to task <strong>%{module}</strong>."
error_flash: "User %{user} could not be added to task <strong>%{module}</strong>."
destroy:
success_flash: "Successfully removed user %{user} from task <strong>%{module}</strong>."
error_flash: "User %{user} could not be removed from task <strong>%{module}</strong>."
step_comments:
new:
head_title: "%{project} | %{module} | Add comment to step"
title: "Add comment to step %{step}"
create: "Add comment"
create:
success_flash: "Successfully added comment to step <strong>%{step}</strong>."
result_comments:
new:
head_title: "%{project} | %{module} | Add comment to result"
title: "Add comment to result"
create: "Add comment"
create:
success_flash: "Successfully added comment to result."
load_protocol_from_file: "<i>%{user}</i> loaded protocol <strong>%{protocol}</strong> from file."
load_protocol_from_repository: "<i>%{user}</i> loaded protocol <strong>%{protocol}</strong> from repository."
revert_protocol: "<i>%{user}</i> reverted protocol <strong>%{protocol}</strong> from repository."
@ -1379,6 +1423,7 @@ en:
destroy:
confirm: "Are you sure you want to delete step %{step}?"
success_flash: "Step %{step} successfully deleted."
error_flash: "Step %{step} couldn't be deleted. One or more files are locked."
edit:
head_title: "Edit protocol"
no_keywords: "No keywords"
@ -1475,6 +1520,11 @@ en:
unassign_user_from_team: "<i>%{unassigned_user}</i> was removed from team <strong>%{team}</strong> by <i>%{unassigned_by_user}</i>."
task_completed: "%{user} completed task %{module}. %{date} | Project: %{project} | Experiment: %{experiment}"
assets:
head_title:
edit: "sciNote | %{file_name} | Edit"
view: "sciNote | %{file_name} | View"
atwho:
no_results: "No results found"
users:
@ -1489,6 +1539,7 @@ en:
archived: "(archived)"
deleted: "(deleted)"
popover_html: "<span class='silver'>Team:</span>&nbsp;%{team} <br> <span class='silver'>Role:</span>&nbsp;%{role} <br> <span class='silver'>Joined:</span>&nbsp;%{time}"
# This section contains general words that can be used in any parts of
# application.

View file

@ -1,394 +1,408 @@
Rails.application.routes.draw do
devise_for :users, controllers: { registrations: 'users/registrations',
sessions: 'users/sessions',
invitations: 'users/invitations',
confirmations: 'users/confirmations' }
require 'subdomain'
constraints UserSubdomain do
devise_for :users, controllers: { registrations: 'users/registrations',
sessions: 'users/sessions',
invitations: 'users/invitations',
confirmations: 'users/confirmations' }
root 'projects#index'
root 'projects#index'
# Save sample table state
post '/state_save/:team_id/:user_id',
to: 'user_samples#save_samples_table_status',
as: 'save_samples_table_status',
defaults: { format: 'json' }
# Save sample table state
post '/state_save/:team_id/:user_id',
to: 'user_samples#save_samples_table_status',
as: 'save_samples_table_status',
defaults: { format: 'json' }
post '/state_load/:team_id/:user_id',
to: 'user_samples#load_samples_table_status',
as: 'load_samples_table_status',
defaults: { format: 'json' }
post '/state_load/:team_id/:user_id',
to: 'user_samples#load_samples_table_status',
as: 'load_samples_table_status',
defaults: { format: 'json' }
resources :activities, only: [:index]
resources :activities, only: [:index]
get 'forbidden', to: 'application#forbidden', as: 'forbidden'
get 'not_found', to: 'application#not_found', as: 'not_found'
get 'forbidden', to: 'application#forbidden', as: 'forbidden'
get 'not_found', to: 'application#not_found', as: 'not_found'
# Settings
get 'users/settings/account/preferences',
to: 'users/settings/account/preferences#index',
as: 'preferences'
put 'users/settings/account/preferences',
to: 'users/settings/account/preferences#update',
as: 'update_preferences'
get 'users/settings/account/preferences/tutorial',
to: 'users/settings/account/preferences#tutorial',
as: 'tutorial'
post 'users/settings/account/preferences/reset_tutorial/',
to: 'users/settings/account/preferences#reset_tutorial',
as: 'reset_tutorial'
post 'users/settings/account/preferences/notifications_settings',
to: 'users/settings/account/preferences#notifications_settings',
as: 'notifications_settings',
defaults: { format: 'json' }
# Settings
get 'users/settings/account/preferences',
to: 'users/settings/account/preferences#index',
as: 'preferences'
put 'users/settings/account/preferences',
to: 'users/settings/account/preferences#update',
as: 'update_preferences'
get 'users/settings/account/preferences/tutorial',
to: 'users/settings/account/preferences#tutorial',
as: 'tutorial'
post 'users/settings/account/preferences/reset_tutorial/',
to: 'users/settings/account/preferences#reset_tutorial',
as: 'reset_tutorial'
post 'users/settings/account/preferences/notifications_settings',
to: 'users/settings/account/preferences#notifications_settings',
as: 'notifications_settings',
defaults: { format: 'json' }
# Change user's current team
post 'users/settings/user_current_team',
to: 'users/settings#user_current_team',
as: 'user_current_team'
# Change user's current team
post 'users/settings/user_current_team',
to: 'users/settings#user_current_team',
as: 'user_current_team'
get 'users/settings/teams',
to: 'users/settings/teams#index',
as: 'teams'
post 'users/settings/teams/datatable',
to: 'users/settings/teams#datatable',
as: 'teams_datatable'
get 'users/settings/teams/new',
to: 'users/settings/teams#new',
as: 'new_team'
post 'users/settings/teams',
to: 'users/settings/teams#create',
as: 'create_team'
get 'users/settings/teams/:id',
to: 'users/settings/teams#show',
as: 'team'
post 'users/settings/teams/:id/users_datatable',
to: 'users/settings/teams#users_datatable',
as: 'team_users_datatable'
get 'users/settings/teams/:id/name_html',
to: 'users/settings/teams#name_html',
as: 'team_name'
get 'users/settings/teams/:id/description_html',
to: 'users/settings/teams#description_html',
as: 'team_description'
put 'users/settings/teams/:id',
to: 'users/settings/teams#update',
as: 'update_team'
delete 'users/settings/teams/:id',
to: 'users/settings/teams#destroy',
as: 'destroy_team'
get 'users/settings/teams',
to: 'users/settings/teams#index',
as: 'teams'
post 'users/settings/teams/datatable',
to: 'users/settings/teams#datatable',
as: 'teams_datatable'
get 'users/settings/teams/new',
to: 'users/settings/teams#new',
as: 'new_team'
post 'users/settings/teams',
to: 'users/settings/teams#create',
as: 'create_team'
get 'users/settings/teams/:id',
to: 'users/settings/teams#show',
as: 'team'
post 'users/settings/teams/:id/users_datatable',
to: 'users/settings/teams#users_datatable',
as: 'team_users_datatable'
get 'users/settings/teams/:id/name_html',
to: 'users/settings/teams#name_html',
as: 'team_name'
get 'users/settings/teams/:id/description_html',
to: 'users/settings/teams#description_html',
as: 'team_description'
put 'users/settings/teams/:id',
to: 'users/settings/teams#update',
as: 'update_team'
delete 'users/settings/teams/:id',
to: 'users/settings/teams#destroy',
as: 'destroy_team'
put 'users/settings/user_teams/:id',
to: 'users/settings/user_teams#update',
as: 'update_user_team'
get 'users/settings/user_teams/:id/leave_html',
to: 'users/settings/user_teams#leave_html',
as: 'leave_user_team_html'
get 'users/settings/user_teams/:id/destroy_html',
to: 'users/settings/user_teams#destroy_html',
as: 'destroy_user_team_html'
delete 'users/settings/user_teams/:id',
to: 'users/settings/user_teams#destroy',
as: 'destroy_user_team'
put 'users/settings/user_teams/:id',
to: 'users/settings/user_teams#update',
as: 'update_user_team'
get 'users/settings/user_teams/:id/leave_html',
to: 'users/settings/user_teams#leave_html',
as: 'leave_user_team_html'
get 'users/settings/user_teams/:id/destroy_html',
to: 'users/settings/user_teams#destroy_html',
as: 'destroy_user_team_html'
delete 'users/settings/user_teams/:id',
to: 'users/settings/user_teams#destroy',
as: 'destroy_user_team'
# Invite users
devise_scope :user do
post '/invite',
to: 'users/invitations#invite_users',
as: 'invite_users'
end
# Notifications
get 'users/:id/recent_notifications',
to: 'user_notifications#recent_notifications',
as: 'recent_notifications',
defaults: { format: 'json' }
get 'users/:id/unseen_notification',
to: 'user_notifications#unseen_notification',
as: 'unseen_notification',
defaults: { format: 'json' }
get 'users/notifications',
to: 'user_notifications#index',
as: 'notifications'
resources :teams do
resources :samples, only: [:new, :create]
resources :sample_types, except: [:show, :new] do
get 'sample_type_element', to: 'sample_types#sample_type_element'
get 'destroy_confirmation', to: 'sample_types#destroy_confirmation'
# Invite users
devise_scope :user do
post '/invite',
to: 'users/invitations#invite_users',
as: 'invite_users'
end
resources :sample_groups, except: [:show, :new] do
get 'sample_group_element', to: 'sample_groups#sample_group_element'
get 'destroy_confirmation', to: 'sample_groups#destroy_confirmation'
end
resources :custom_fields, only: [:create, :update, :destroy] do
get 'destroy_html'
end
member do
post 'parse_sheet'
post 'import_samples'
post 'export_samples'
# Used for atwho (smart annotations)
get 'atwho_users', to: 'at_who#users'
get 'atwho_samples', to: 'at_who#samples'
get 'atwho_projects', to: 'at_who#projects'
get 'atwho_experiments', to: 'at_who#experiments'
get 'atwho_my_modules', to: 'at_who#my_modules'
get 'atwho_menu_items', to: 'at_who#menu_items'
end
match '*path',
to: 'teams#routing_error',
via: [:get, :post, :put, :patch]
end
get 'projects/archive', to: 'projects#archive', as: 'projects_archive'
# Notifications
get 'users/:id/recent_notifications',
to: 'user_notifications#recent_notifications',
as: 'recent_notifications',
defaults: { format: 'json' }
resources :projects, except: [:new, :destroy] do
resources :user_projects, path: '/users',
only: [:create, :index, :update, :destroy]
resources :project_comments,
path: '/comments',
only: [:create, :index, :edit, :update, :destroy]
# Activities popup (JSON) for individual project in projects index,
# as well as all activities page for single project (HTML)
resources :project_activities, path: '/activities', only: [:index]
resources :tags, only: [:create, :update, :destroy]
resources :reports,
path: '/reports',
only: [:index, :new, :create, :edit, :update] do
collection do
# The posts following here should in theory be gets,
# but are posts because of parameters payload
post 'generate', to: 'reports#generate'
get 'new/', to: 'reports#new'
get 'new/project_contents_modal',
to: 'reports#project_contents_modal',
as: :project_contents_modal
post 'new/project_contents',
to: 'reports#project_contents',
as: :project_contents
get 'new/experiment_contents_modal',
to: 'reports#experiment_contents_modal',
as: :experiment_contents_modal
post 'new/experiment_contents',
to: 'reports#experiment_contents',
as: :experiment_contents
get 'new/module_contents_modal',
to: 'reports#module_contents_modal',
as: :module_contents_modal
post 'new/module_contents',
to: 'reports#module_contents',
as: :module_contents
get 'new/step_contents_modal',
to: 'reports#step_contents_modal',
as: :step_contents_modal
post 'new/step_contents',
to: 'reports#step_contents',
as: :step_contents
get 'new/result_contents_modal',
to: 'reports#result_contents_modal',
as: :result_contents_modal
post 'new/result_contents',
to: 'reports#result_contents',
as: :result_contents
post '_save',
to: 'reports#save_modal',
as: :save_modal
post 'destroy', as: :destroy # Destroy multiple entries at once
get 'users/:id/unseen_notification',
to: 'user_notifications#unseen_notification',
as: 'unseen_notification',
defaults: { format: 'json' }
get 'users/notifications',
to: 'user_notifications#index',
as: 'notifications'
resources :teams do
resources :samples, only: [:new, :create]
resources :sample_types, except: [:show, :new] do
get 'sample_type_element', to: 'sample_types#sample_type_element'
get 'destroy_confirmation', to: 'sample_types#destroy_confirmation'
end
resources :sample_groups, except: [:show, :new] do
get 'sample_group_element', to: 'sample_groups#sample_group_element'
get 'destroy_confirmation', to: 'sample_groups#destroy_confirmation'
end
resources :custom_fields, only: [:create, :update, :destroy] do
get 'destroy_html'
end
member do
post 'parse_sheet'
post 'import_samples'
post 'export_samples'
# Used for atwho (smart annotations)
get 'atwho_users', to: 'at_who#users'
get 'atwho_samples', to: 'at_who#samples'
get 'atwho_projects', to: 'at_who#projects'
get 'atwho_experiments', to: 'at_who#experiments'
get 'atwho_my_modules', to: 'at_who#my_modules'
get 'atwho_menu_items', to: 'at_who#menu_items'
end
match '*path',
to: 'teams#routing_error',
via: [:get, :post, :put, :patch]
end
get 'projects/archive', to: 'projects#archive', as: 'projects_archive'
resources :projects, except: [:new, :destroy] do
resources :user_projects, path: '/users',
only: [:create, :index, :update, :destroy]
resources :project_comments,
path: '/comments',
only: [:create, :index, :edit, :update, :destroy]
# Activities popup (JSON) for individual project in projects index,
# as well as all activities page for single project (HTML)
resources :project_activities, path: '/activities', only: [:index]
resources :tags, only: [:create, :update, :destroy]
resources :reports,
path: '/reports',
only: [:index, :new, :create, :edit, :update] do
collection do
# The posts following here should in theory be gets,
# but are posts because of parameters payload
post 'generate', to: 'reports#generate'
get 'new/', to: 'reports#new'
get 'new/project_contents_modal',
to: 'reports#project_contents_modal',
as: :project_contents_modal
post 'new/project_contents',
to: 'reports#project_contents',
as: :project_contents
get 'new/experiment_contents_modal',
to: 'reports#experiment_contents_modal',
as: :experiment_contents_modal
post 'new/experiment_contents',
to: 'reports#experiment_contents',
as: :experiment_contents
get 'new/module_contents_modal',
to: 'reports#module_contents_modal',
as: :module_contents_modal
post 'new/module_contents',
to: 'reports#module_contents',
as: :module_contents
get 'new/step_contents_modal',
to: 'reports#step_contents_modal',
as: :step_contents_modal
post 'new/step_contents',
to: 'reports#step_contents',
as: :step_contents
get 'new/result_contents_modal',
to: 'reports#result_contents_modal',
as: :result_contents_modal
post 'new/result_contents',
to: 'reports#result_contents',
as: :result_contents
post '_save',
to: 'reports#save_modal',
as: :save_modal
post 'destroy', as: :destroy # Destroy multiple entries at once
end
end
resources :experiments,
only: [:new, :create, :edit, :update],
defaults: { format: 'json' }
member do
# Notifications popup for individual project in projects index
get 'notifications'
get 'samples' # Samples for single project
# Renders sample datatable for single project (ajax action)
post 'samples_index'
get 'experiment_archive' # Experiment archive for single project
post :delete_samples,
constraints: CommitParamRouting.new(
ProjectsController::DELETE_SAMPLES
),
action: :delete_samples
end
# This route is defined outside of member block
# to preserve original :project_id parameter in URL.
get 'users/edit', to: 'user_projects#index_edit'
end
resources :experiments do
member do
get 'canvas' # Overview/structure for single experiment
# AJAX-loaded canvas edit mode (from canvas)
get 'canvas/edit', to: 'canvas#edit'
get 'canvas/full_zoom', to: 'canvas#full_zoom' # AJAX-loaded canvas zoom
# AJAX-loaded canvas zoom
get 'canvas/medium_zoom', to: 'canvas#medium_zoom'
get 'canvas/small_zoom', to: 'canvas#small_zoom' # AJAX-loaded canvas zoom
post 'canvas', to: 'canvas#update' # Save updated canvas action
get 'module_archive' # Module archive for single experiment
get 'archive' # archive experiment
get 'clone_modal' # return modal with clone options
post 'clone' # clone experiment
get 'move_modal' # return modal with move options
post 'move' # move experiment
get 'samples' # Samples for single project
get 'updated_img' # Checks if the workflow image is updated
get 'fetch_workflow_img' # Get udated workflow img
# Renders sample datatable for single project (ajax action)
post 'samples_index'
post :delete_samples,
constraints: CommitParamRouting.new(
ExperimentsController::DELETE_SAMPLES
),
action: :delete_samples
end
end
resources :experiments,
only: [:new, :create, :edit, :update],
defaults: { format: 'json' }
member do
# Notifications popup for individual project in projects index
get 'notifications'
get 'samples' # Samples for single project
# Renders sample datatable for single project (ajax action)
post 'samples_index'
get 'experiment_archive' # Experiment archive for single project
post :delete_samples,
constraints: CommitParamRouting.new(
ProjectsController::DELETE_SAMPLES
),
action: :delete_samples
# Show action is a popup (JSON) for individual module in full-zoom canvas,
# as well as 'module info' page for single module (HTML)
resources :my_modules, path: '/modules', only: [:show, :update] do
resources :my_module_tags, path: '/tags', only: [:index, :create, :destroy]
resources :user_my_modules, path: '/users',
only: [:index, :create, :destroy]
resources :my_module_comments,
path: '/comments',
only: [:index, :create, :edit, :update, :destroy]
resources :sample_my_modules, path: '/samples_index', only: [:index]
resources :result_texts, only: [:new, :create]
resources :result_assets, only: [:new, :create]
resources :result_tables, only: [:new, :create]
member do
# AJAX popup accessed from full-zoom canvas for single module,
# as well as full activities view (HTML) for single module
get 'description'
get 'activities'
get 'activities_tab' # Activities in tab view for single module
get 'due_date'
get 'protocols' # Protocols view for single module
get 'results' # Results view for single module
get 'samples' # Samples view for single module
get 'archive' # Archive view for single module
post 'toggle_task_state'
# Renders sample datatable for single module (ajax action)
post 'samples_index'
post :assign_samples,
constraints: CommitParamRouting.new(
MyModulesController::ASSIGN_SAMPLES
),
action: :assign_samples
post :assign_samples,
constraints: CommitParamRouting.new(
MyModulesController::UNASSIGN_SAMPLES
),
action: :unassign_samples
post :assign_samples,
constraints: CommitParamRouting.new(
MyModulesController::DELETE_SAMPLES
),
action: :delete_samples
end
# Those routes are defined outside of member block
# to preserve original id parameters in URL.
get 'tags/edit', to: 'my_module_tags#index_edit'
get 'users/edit', to: 'user_my_modules#index_edit'
end
# This route is defined outside of member block
# to preserve original :project_id parameter in URL.
get 'users/edit', to: 'user_projects#index_edit'
end
resources :steps, only: [:edit, :update, :destroy, :show] do
resources :step_comments,
path: '/comments',
only: [:create, :index, :edit, :update, :destroy]
member do
post 'checklistitem_state'
post 'toggle_step_state'
get 'move_down'
get 'move_up'
end
end
resources :experiments do
member do
get 'canvas' # Overview/structure for single experiment
# AJAX-loaded canvas edit mode (from canvas)
get 'canvas/edit', to: 'canvas#edit'
get 'canvas/full_zoom', to: 'canvas#full_zoom' # AJAX-loaded canvas zoom
# AJAX-loaded canvas zoom
get 'canvas/medium_zoom', to: 'canvas#medium_zoom'
get 'canvas/small_zoom', to: 'canvas#small_zoom' # AJAX-loaded canvas zoom
post 'canvas', to: 'canvas#update' # Save updated canvas action
get 'module_archive' # Module archive for single experiment
get 'archive' # archive experiment
get 'clone_modal' # return modal with clone options
post 'clone' # clone experiment
get 'move_modal' # return modal with move options
post 'move' # move experiment
get 'samples' # Samples for single project
get 'updated_img' # Checks if the workflow image is updated
get 'fetch_workflow_img' # Get udated workflow img
# Renders sample datatable for single project (ajax action)
post 'samples_index'
post :delete_samples,
constraints: CommitParamRouting.new(
ExperimentsController::DELETE_SAMPLES
),
action: :delete_samples
resources :results, only: [:update, :destroy] do
resources :result_comments,
path: '/comments',
only: [:create, :index, :edit, :update, :destroy]
end
resources :samples, only: [:edit, :update, :destroy]
get 'samples/:id', to: 'samples#show'
resources :result_texts, only: [:edit, :update, :destroy]
get 'result_texts/:id/download' => 'result_texts#download',
as: :result_text_download
resources :result_assets, only: [:edit, :update, :destroy]
get 'result_assets/:id/download' => 'result_assets#download',
as: :result_asset_download
resources :result_tables, only: [:edit, :update, :destroy]
get 'result_tables/:id/download' => 'result_tables#download',
as: :result_table_download
resources :protocols, only: [:index, :edit, :create] do
resources :steps, only: [:new, :create]
member do
get 'linked_children', to: 'protocols#linked_children'
post 'linked_children_datatable',
to: 'protocols#linked_children_datatable'
get 'preview', to: 'protocols#preview'
patch 'metadata', to: 'protocols#update_metadata'
patch 'keywords', to: 'protocols#update_keywords'
post 'clone', to: 'protocols#clone'
get 'unlink_modal', to: 'protocols#unlink_modal'
post 'unlink', to: 'protocols#unlink'
get 'revert_modal', to: 'protocols#revert_modal'
post 'revert', to: 'protocols#revert'
get 'update_parent_modal', to: 'protocols#update_parent_modal'
post 'update_parent', to: 'protocols#update_parent'
get 'update_from_parent_modal', to: 'protocols#update_from_parent_modal'
post 'update_from_parent', to: 'protocols#update_from_parent'
post 'load_from_repository_datatable',
to: 'protocols#load_from_repository_datatable'
get 'load_from_repository_modal',
to: 'protocols#load_from_repository_modal'
post 'load_from_repository', to: 'protocols#load_from_repository'
post 'load_from_file', to: 'protocols#load_from_file'
get 'copy_to_repository_modal', to: 'protocols#copy_to_repository_modal'
post 'copy_to_repository', to: 'protocols#copy_to_repository'
get 'protocol_status_bar', to: 'protocols#protocol_status_bar'
get 'updated_at_label', to: 'protocols#updated_at_label'
get 'edit_name_modal', to: 'protocols#edit_name_modal'
get 'edit_keywords_modal', to: 'protocols#edit_keywords_modal'
get 'edit_authors_modal', to: 'protocols#edit_authors_modal'
get 'edit_description_modal', to: 'protocols#edit_description_modal'
end
collection do
get 'create_new_modal', to: 'protocols#create_new_modal'
post 'datatable', to: 'protocols#datatable'
post 'make_private', to: 'protocols#make_private'
post 'publish', to: 'protocols#publish'
post 'archive', to: 'protocols#archive'
post 'restore', to: 'protocols#restore'
post 'import', to: 'protocols#import'
get 'export', to: 'protocols#export'
end
end
get 'search' => 'search#index'
get 'search/new' => 'search#new', as: :new_search
# We cannot use 'resources :assets' because assets is a reserved route
# in Rails (assets pipeline) and causes funky behavior
get 'files/:id/present', to: 'assets#file_present', as: 'file_present_asset'
get 'files/:id/large_url',
to: 'assets#large_image_url',
as: 'large_image_url_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'
get 'users/auth_token_sign_in' => 'users/sessions#auth_token_create'
end
end
# Show action is a popup (JSON) for individual module in full-zoom canvas,
# as well as 'module info' page for single module (HTML)
resources :my_modules, path: '/modules', only: [:show, :update] do
resources :my_module_tags, path: '/tags', only: [:index, :create, :destroy]
resources :user_my_modules, path: '/users',
only: [:index, :create, :destroy]
resources :my_module_comments,
path: '/comments',
only: [:index, :create, :edit, :update, :destroy]
resources :sample_my_modules, path: '/samples_index', only: [:index]
resources :result_texts, only: [:new, :create]
resources :result_assets, only: [:new, :create]
resources :result_tables, only: [:new, :create]
member do
# AJAX popup accessed from full-zoom canvas for single module,
# as well as full activities view (HTML) for single module
get 'description'
get 'activities'
get 'activities_tab' # Activities in tab view for single module
get 'due_date'
get 'protocols' # Protocols view for single module
get 'results' # Results view for single module
get 'samples' # Samples view for single module
get 'archive' # Archive view for single module
post 'toggle_task_state'
# Renders sample datatable for single module (ajax action)
post 'samples_index'
post :assign_samples,
constraints: CommitParamRouting.new(
MyModulesController::ASSIGN_SAMPLES
),
action: :assign_samples
post :assign_samples,
constraints: CommitParamRouting.new(
MyModulesController::UNASSIGN_SAMPLES
),
action: :unassign_samples
post :assign_samples,
constraints: CommitParamRouting.new(
MyModulesController::DELETE_SAMPLES
),
action: :delete_samples
end
# Those routes are defined outside of member block
# to preserve original id parameters in URL.
get 'tags/edit', to: 'my_module_tags#index_edit'
get 'users/edit', to: 'user_my_modules#index_edit'
end
resources :steps, only: [:edit, :update, :destroy, :show] do
resources :step_comments,
path: '/comments',
only: [:create, :index, :edit, :update, :destroy]
member do
post 'checklistitem_state'
post 'toggle_step_state'
get 'move_down'
get 'move_up'
end
end
resources :results, only: [:update, :destroy] do
resources :result_comments,
path: '/comments',
only: [:create, :index, :edit, :update, :destroy]
end
resources :samples, only: [:edit, :update, :destroy]
get 'samples/:id', to: 'samples#show'
resources :result_texts, only: [:edit, :update, :destroy]
get 'result_texts/:id/download' => 'result_texts#download',
as: :result_text_download
resources :result_assets, only: [:edit, :update, :destroy]
get 'result_assets/:id/download' => 'result_assets#download',
as: :result_asset_download
resources :result_tables, only: [:edit, :update, :destroy]
get 'result_tables/:id/download' => 'result_tables#download',
as: :result_table_download
resources :protocols, only: [:index, :edit, :create] do
resources :steps, only: [:new, :create]
member do
get 'linked_children', to: 'protocols#linked_children'
post 'linked_children_datatable',
to: 'protocols#linked_children_datatable'
get 'preview', to: 'protocols#preview'
patch 'metadata', to: 'protocols#update_metadata'
patch 'keywords', to: 'protocols#update_keywords'
post 'clone', to: 'protocols#clone'
get 'unlink_modal', to: 'protocols#unlink_modal'
post 'unlink', to: 'protocols#unlink'
get 'revert_modal', to: 'protocols#revert_modal'
post 'revert', to: 'protocols#revert'
get 'update_parent_modal', to: 'protocols#update_parent_modal'
post 'update_parent', to: 'protocols#update_parent'
get 'update_from_parent_modal', to: 'protocols#update_from_parent_modal'
post 'update_from_parent', to: 'protocols#update_from_parent'
post 'load_from_repository_datatable',
to: 'protocols#load_from_repository_datatable'
get 'load_from_repository_modal',
to: 'protocols#load_from_repository_modal'
post 'load_from_repository', to: 'protocols#load_from_repository'
post 'load_from_file', to: 'protocols#load_from_file'
get 'copy_to_repository_modal', to: 'protocols#copy_to_repository_modal'
post 'copy_to_repository', to: 'protocols#copy_to_repository'
get 'protocol_status_bar', to: 'protocols#protocol_status_bar'
get 'updated_at_label', to: 'protocols#updated_at_label'
get 'edit_name_modal', to: 'protocols#edit_name_modal'
get 'edit_keywords_modal', to: 'protocols#edit_keywords_modal'
get 'edit_authors_modal', to: 'protocols#edit_authors_modal'
get 'edit_description_modal', to: 'protocols#edit_description_modal'
end
collection do
get 'create_new_modal', to: 'protocols#create_new_modal'
post 'datatable', to: 'protocols#datatable'
post 'make_private', to: 'protocols#make_private'
post 'publish', to: 'protocols#publish'
post 'archive', to: 'protocols#archive'
post 'restore', to: 'protocols#restore'
post 'import', to: 'protocols#import'
get 'export', to: 'protocols#export'
end
end
get 'search' => 'search#index'
get 'search/new' => 'search#new', as: :new_search
# We cannot use 'resources :assets' because assets is a reserved route
# in Rails (assets pipeline) and causes funky behavior
get 'files/:id/present', to: 'assets#file_present', as: 'file_present_asset'
get 'files/:id/large_url',
to: 'assets#large_image_url',
as: 'large_image_url_asset'
get 'files/:id/download', to: 'assets#download', as: 'download_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'
get 'users/auth_token_sign_in' => 'users/sessions#auth_token_create'
constraints WopiSubdomain do
# Office integration
get 'wopi/files/:id/contents', to: 'wopi#file_contents_get_endpoint'
post 'wopi/files/:id/contents', to: 'wopi#file_contents_post_endpoint'
get 'wopi/files/:id', to: 'wopi#file_get_endpoint', as: 'wopi_rest_endpoint'
post 'wopi/files/:id', to: 'wopi#post_file_endpoint'
end
end

View file

@ -0,0 +1,51 @@
class AddWopi < ActiveRecord::Migration
def up
add_column :assets, :lock, :string, limit: 1024
add_column :assets, :lock_ttl, :integer
add_column :assets, :version, :integer, default: 1
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
create_table :tokens do |t|
t.string :token, null: false
t.integer :ttl, null: false
t.integer :user_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_foreign_key :tokens, :users, column: :user_id
add_index :wopi_actions, [:extension, :action]
end
def down
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
drop_table :tokens
end
end

View file

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170124135736) do
ActiveRecord::Schema.define(version: 20170306121855) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -46,16 +46,19 @@ ActiveRecord::Schema.define(version: 20170124135736) 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", default: 1
t.boolean "file_processing"
end
@ -97,8 +100,11 @@ ActiveRecord::Schema.define(version: 20170124135736) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "last_modified_by_id"
t.string "type"
t.integer "associated_id"
end
add_index "comments", ["associated_id"], name: "index_comments_on_associated_id", using: :btree
add_index "comments", ["created_at"], name: "index_comments_on_created_at", using: :btree
add_index "comments", ["last_modified_by_id"], name: "index_comments_on_last_modified_by_id", using: :btree
add_index "comments", ["user_id"], name: "index_comments_on_user_id", using: :btree
@ -169,13 +175,6 @@ ActiveRecord::Schema.define(version: 20170124135736) do
t.string "message", null: false
end
create_table "my_module_comments", force: :cascade do |t|
t.integer "my_module_id", null: false
t.integer "comment_id", null: false
end
add_index "my_module_comments", ["my_module_id", "comment_id"], name: "index_my_module_comments_on_my_module_id_and_comment_id", using: :btree
create_table "my_module_groups", force: :cascade do |t|
t.string "name", null: false
t.datetime "created_at", null: false
@ -199,24 +198,26 @@ ActiveRecord::Schema.define(version: 20170124135736) do
add_index "my_module_tags", ["tag_id"], name: "index_my_module_tags_on_tag_id", using: :btree
create_table "my_modules", force: :cascade do |t|
t.string "name", null: false
t.string "name", null: false
t.datetime "due_date"
t.string "description"
t.integer "x", default: 0, null: false
t.integer "y", default: 0, null: false
t.integer "x", default: 0, null: false
t.integer "y", default: 0, null: false
t.integer "my_module_group_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "archived", default: false, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "archived", default: false, null: false
t.datetime "archived_on"
t.integer "created_by_id"
t.integer "last_modified_by_id"
t.integer "archived_by_id"
t.integer "restored_by_id"
t.datetime "restored_on"
t.integer "nr_of_assigned_samples", default: 0
t.integer "workflow_order", default: -1, null: false
t.integer "experiment_id", default: 0, null: false
t.integer "nr_of_assigned_samples", default: 0
t.integer "workflow_order", default: -1, null: false
t.integer "experiment_id", default: 0, null: false
t.integer "state", limit: 2, default: 0
t.datetime "completed_on"
end
add_index "my_modules", ["archived_by_id"], name: "index_my_modules_on_archived_by_id", using: :btree
@ -238,13 +239,6 @@ ActiveRecord::Schema.define(version: 20170124135736) do
add_index "notifications", ["created_at"], name: "index_notifications_on_created_at", using: :btree
create_table "project_comments", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "comment_id", null: false
end
add_index "project_comments", ["project_id", "comment_id"], name: "index_project_comments_on_project_id_and_comment_id", using: :btree
create_table "projects", force: :cascade do |t|
t.string "name", null: false
t.integer "visibility", default: 0, null: false
@ -367,13 +361,6 @@ ActiveRecord::Schema.define(version: 20170124135736) do
add_index "result_assets", ["result_id", "asset_id"], name: "index_result_assets_on_result_id_and_asset_id", using: :btree
create_table "result_comments", force: :cascade do |t|
t.integer "result_id", null: false
t.integer "comment_id", null: false
end
add_index "result_comments", ["result_id", "comment_id"], name: "index_result_comments_on_result_id_and_comment_id", using: :btree
create_table "result_tables", force: :cascade do |t|
t.integer "result_id", null: false
t.integer "table_id", null: false
@ -410,13 +397,6 @@ ActiveRecord::Schema.define(version: 20170124135736) do
add_index "results", ["restored_by_id"], name: "index_results_on_restored_by_id", using: :btree
add_index "results", ["user_id"], name: "index_results_on_user_id", using: :btree
create_table "sample_comments", force: :cascade do |t|
t.integer "sample_id", null: false
t.integer "comment_id", null: false
end
add_index "sample_comments", ["sample_id", "comment_id"], name: "index_sample_comments_on_sample_id_and_comment_id", using: :btree
create_table "sample_custom_fields", force: :cascade do |t|
t.string "value", null: false
t.integer "custom_field_id", null: false
@ -495,6 +475,13 @@ ActiveRecord::Schema.define(version: 20170124135736) do
add_index "samples_tables", ["team_id"], name: "index_samples_tables_on_team_id", using: :btree
add_index "samples_tables", ["user_id"], name: "index_samples_tables_on_user_id", using: :btree
create_table "settings", force: :cascade do |t|
t.text "type", null: false
t.jsonb "values", default: {}, null: false
end
add_index "settings", ["type"], name: "index_settings_on_type", unique: true, using: :btree
create_table "step_assets", force: :cascade do |t|
t.integer "step_id", null: false
t.integer "asset_id", null: false
@ -502,13 +489,6 @@ ActiveRecord::Schema.define(version: 20170124135736) do
add_index "step_assets", ["step_id", "asset_id"], name: "index_step_assets_on_step_id_and_asset_id", using: :btree
create_table "step_comments", force: :cascade do |t|
t.integer "step_id", null: false
t.integer "comment_id", null: false
end
add_index "step_comments", ["step_id", "comment_id"], name: "index_step_comments_on_step_id_and_comment_id", using: :btree
create_table "step_tables", force: :cascade do |t|
t.integer "step_id", null: false
t.integer "table_id", null: false
@ -590,6 +570,12 @@ ActiveRecord::Schema.define(version: 20170124135736) do
t.datetime "file_updated_at"
end
create_table "tokens", force: :cascade do |t|
t.string "token", null: false
t.integer "ttl", null: false
t.integer "user_id", null: false
end
create_table "user_my_modules", force: :cascade do |t|
t.integer "user_id", null: false
t.integer "my_module_id", null: false
@ -691,6 +677,29 @@ ActiveRecord::Schema.define(version: 20170124135736) 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"
@ -715,8 +724,6 @@ ActiveRecord::Schema.define(version: 20170124135736) do
add_foreign_key "experiments", "users", column: "last_modified_by_id"
add_foreign_key "experiments", "users", column: "restored_by_id"
add_foreign_key "logs", "teams"
add_foreign_key "my_module_comments", "comments"
add_foreign_key "my_module_comments", "my_modules"
add_foreign_key "my_module_groups", "experiments"
add_foreign_key "my_module_groups", "users", column: "created_by_id"
add_foreign_key "my_module_tags", "users", column: "created_by_id"
@ -727,8 +734,6 @@ ActiveRecord::Schema.define(version: 20170124135736) do
add_foreign_key "my_modules", "users", column: "last_modified_by_id"
add_foreign_key "my_modules", "users", column: "restored_by_id"
add_foreign_key "notifications", "users", column: "generator_user_id"
add_foreign_key "project_comments", "comments"
add_foreign_key "project_comments", "projects"
add_foreign_key "projects", "teams"
add_foreign_key "projects", "users", column: "archived_by_id"
add_foreign_key "projects", "users", column: "created_by_id"
@ -757,8 +762,6 @@ ActiveRecord::Schema.define(version: 20170124135736) do
add_foreign_key "reports", "users", column: "last_modified_by_id"
add_foreign_key "result_assets", "assets"
add_foreign_key "result_assets", "results"
add_foreign_key "result_comments", "comments"
add_foreign_key "result_comments", "results"
add_foreign_key "result_tables", "results"
add_foreign_key "result_tables", "tables"
add_foreign_key "result_texts", "results"
@ -767,8 +770,6 @@ ActiveRecord::Schema.define(version: 20170124135736) do
add_foreign_key "results", "users", column: "archived_by_id"
add_foreign_key "results", "users", column: "last_modified_by_id"
add_foreign_key "results", "users", column: "restored_by_id"
add_foreign_key "sample_comments", "comments"
add_foreign_key "sample_comments", "samples"
add_foreign_key "sample_custom_fields", "custom_fields"
add_foreign_key "sample_custom_fields", "samples"
add_foreign_key "sample_groups", "teams"
@ -787,8 +788,6 @@ ActiveRecord::Schema.define(version: 20170124135736) do
add_foreign_key "samples", "users", column: "last_modified_by_id"
add_foreign_key "step_assets", "assets"
add_foreign_key "step_assets", "steps"
add_foreign_key "step_comments", "comments"
add_foreign_key "step_comments", "steps"
add_foreign_key "step_tables", "steps"
add_foreign_key "step_tables", "tables"
add_foreign_key "steps", "protocols"
@ -801,6 +800,7 @@ ActiveRecord::Schema.define(version: 20170124135736) do
add_foreign_key "tags", "users", column: "last_modified_by_id"
add_foreign_key "teams", "users", column: "created_by_id"
add_foreign_key "teams", "users", column: "last_modified_by_id"
add_foreign_key "tokens", "users"
add_foreign_key "user_my_modules", "my_modules"
add_foreign_key "user_my_modules", "users"
add_foreign_key "user_my_modules", "users", column: "assigned_by_id"
@ -813,4 +813,6 @@ ActiveRecord::Schema.define(version: 20170124135736) do
add_foreign_key "user_teams", "users"
add_foreign_key "user_teams", "users", column: "assigned_by_id"
add_foreign_key "users", "teams", column: "current_team_id"
add_foreign_key "wopi_actions", "wopi_apps"
add_foreign_key "wopi_apps", "wopi_discoveries"
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

6
test/fixtures/wopi_discoveries.yml vendored Normal file
View file

@ -0,0 +1,6 @@
first:
expires: <%= Time.now + 1.days %>
proof_key_mod: '0HOWUPFFgmSYHbLZZzdWO/HUOr8YNfx5NAl7GUytooHZ7B9QxQKTJpj0NIJ4XEskQW8e4dLzRrPbNOOJ+KpWHttXz8HoQXkkZV/gYNxaNHJ8/pRXGMZzfVM5vchhx/2C7ULPTrpBsSpmfWQ6ShaVoQzfThFUd0MsBvIN7HVtqzPx9jbSV04wAqyNjcro7F3iu9w7AEsMejHbFlWoN+J05dP5ixryF7+2U5RVmjMt7/dYUdCoiXvCMt2CaVr0XEG6udHU4iDKVKZjmUBc7cTWRzhqEL7lZ1yQfylp38Nd2xxVJ0sSU7OkC1bBDlePcYGaF3JjJgsmp/H5BNnlW9gSxQ=='
proof_key_exp: 'AQAB'
proof_key_old_mod: 'u/ppb/da4jeKQ+XzKr69VJTqR7wgQp2jzDIaEPQVzfwod+pc1zvO7cwjNgfzF/KQGkltoOi9KdtMzR0qmX8C5wZI6wGpS8S4pTFAZPhXg5w4EpyR8fAagrnlOgaVLs0oX5UuBqKndCQyM7Vj5nFd+r53giS0ch7zDW0uB1G+ZWqTZ1TwbtV6dmlpVuJYeIPonOJgo2iuh455KuS2gvxZKOKR27Uq7W949oM8sqRjvfaVf4xDmyor++98XX0zadnf4pMWfPr3XE+bCXtB9jIPAxxMrALf5ncNRhnx0Wyf8zfM7Rfq+omp/HxCgusF5MC2/Ffnn7me/628zzioAMy5pQ=='
proof_key_old_exp: 'AQAB'

View file

@ -0,0 +1,218 @@
class WopiDiscoveryTest < ActiveSupport::TestCase
# These tests are taken from WOPI official documentation
# https://github.com/Microsoft/Office-Online-Test-Tools-and-Documentation/
# blob/master/samples/python/proof_keys/tests.py
def setup
@discovery = wopi_discoveries(:first)
end
test 'Verify X-WOPI-Proof with current discovery proof key1.' do
assert @discovery.verify_proof(
'yZhdN1qgywcOQWhyEMVpB6NE3pvBksvcLXsrFKXNtBeDTPW%2fu62g2t%' \
'2fOCWSlb3jUGaz1zc%2fzOzbNgAredLdhQI1Q7sPPqUv2owO78olmN74D' \
'V%2fv52OZIkBG%2b8jqjwmUobcjXVIC1BG9g%2fynMN0itZklL2x27Z2i' \
'mCF6xELcQUuGdkoXBj%2bI%2bTlKM',
635655897610773532,
'IflL8OWCOCmws5qnDD5kYMraMGI3o+T+hojoDREbjZSkxbbx7XIS1Av85' \
'lohPKjyksocpeVwqEYm9nVWfnq05uhDNGp2MsNyhPO9unZ6w25Rjs1hDF' \
'M0dmvYx8wlQBNZ/CFPaz3inCMaaP4PtU85YepaDccAjNc1gikdy3kSMeG' \
'1XZuaDixHvMKzF/60DMfLMBIu5xP4Nt8i8Gi2oZs4REuxi6yxOv2vQJQ5' \
'+8Wu2Olm8qZvT4FEIQT9oZAXebn/CxyvyQv+RVpoU2gb4BreXAdfKthWF' \
'67GpJyhr+ibEVDoIIolUvviycyEtjsaEBpOf6Ne/OLRNu98un7WNDzMTQ==',
'lWBTpWW8q80WC1eJEH5HMnGka4/LUF7zjUPqBwRMO0JzVcnjICvMP2TZP' \
'B2lJfy/4ctIstCN6P1t38NCTTbLWlXuE',
'https://contoso.com/wopi/files/vHxYyRGM8VfmSGwGYDBMIQPzuE' \
'+sSC6kw+zWZw2Nyg?access_token=yZhdN1qgywcOQWhyEMVpB6NE3pv' \
'BksvcLXsrFKXNtBeDTPW%2fu62g2t%2fOCWSlb3jUGaz1zc%2fzOzbNgA' \
'redLdhQI1Q7sPPqUv2owO78olmN74DV%2fv52OZIkBG%2b8jqjwmUobcj' \
'XVIC1BG9g%2fynMN0itZklL2x27Z2imCF6xELcQUuGdkoXBj%2bI%2bTlKM'
)
end
test 'Verify X-WOPI-Proof with current discovery proof key2.' do
assert @discovery.verify_proof(
'RLoY%2f3D73%2fjwt6IQqR1wHqCEKDxRf2v0GPDa0ZHTlA6ik1%2fNBHDD' \
'6bHCI0BQrvacjNBL8ok%2fZsVPI%2beAIA5mHSOUbhW9ohowwD6Ljlwro2' \
'n5PkTBh6GEYi2afuCIQ8mjXAUdvEDg3um2GjJKtA%3d%3d',
635655897361394523,
'x0IeSOjUQNH2pvjMPkP4Jotzs5Weeqms4AlPxMQ5CipssUJbyKFjLWnwPg' \
'1Ac0XtSTiPD177BmQ1+KtmYvDTWQ1FmBuvpvKZDSKzXoT6Qj4LCYYQ0Txn' \
'N/OT231+qd50sOD8zAxhfXP56qND9tj5xqoHMa+lbuvNCqiOBTZw5f/dkl' \
'SK7Wgdx7ST3Dq6S9xxDUfsLC4Tjq+EsvcdSNIWL/W6NRZdyWqlgRgE6X8t' \
'/2iyyMypURdOW2Rztc6w/iYhbuh22Ul6Jfu14KaDo6YkvBr8iHlK4CcQST' \
'9i0u044y1Jnh34UK4EPdVRZrvTmeJ/5DFLWOqEwvBlW2bpoYF+9A==',
'etYRI9UT6q8jA6PHMMmuGa8NbyIlbTHMHmJZqaCOh2GCpv7um2q7+7oaDF' \
'qAV/UP+2N85yZXvZgt9kTOUCwIdggUQVeJluNCwf1B5NsN/7n2aQF9LnWy' \
'YK8kK/3xvQKQrj4n24jk2MnHcX1tk8H/qLxq2DbPzi6ROoSgs2ZK8nmzhS' \
'PF74jnLCrwiwGgnVZV6gIhlAKCcUGtdrT60sgD/wpJGQQ0M59VDQhf1aDj' \
'5bUotf8RXovY8Gl0lpguvN4+EsEjpbVjdV9hxWs7c03JDdoz7mzFUWErSl' \
'y9IzYXNfuFZMnSXpF3lGiprVJvW34Bu2gTo/cLq4LQoABkNCmd4g==',
'https://contoso.com/wopi/files/JIB9h+LJpZWBDwvoIiQ5p3115zJ' \
'WDecpGF9aCm1vOa5UMllgC7w?access_token=RLoY%2f3D73%2fjwt6IQ' \
'qR1wHqCEKDxRf2v0GPDa0ZHTlA6ik1%2fNBHDD6bHCI0BQrvacjNBL8ok%' \
'2fZsVPI%2beAIA5mHSOUbhW9ohowwD6Ljlwro2n5PkTBh6GEYi2afuCIQ8' \
'mjXAUdvEDg3um2GjJKtA%3d%3d'
)
end
test 'Verify X-WOPI-ProofOld with current discovery proof key1.' do
assert @discovery.verify_proof(
'zo7pjAbo%2fyof%2bvtUJn5gXpYcSl7TSSx0TbQGJbWJSll9PTjRRsAbG%' \
'2fSNL7FM2n5Ei3jQ8UJsW4RidT5R4tl1lrCi1%2bhGjxfWAC9pRRl6J3M1' \
'wZk9uFkWEeGzbtGByTkaGJkBqgKV%2ffxg%2bvATAhVr6E3LHCBAN91Wi8UG',
635655898374047766,
'qQhMjQf9Zohj+S/wvhe+RD6W5TIEqJwDWO3zX9DB85yRe3Ide7EPQDCY9dA' \
'ZtJpWkIDDzU+8FEwnexF0EhPimfCkmAyoPpkl2YYvQvvwUK2gdlk3WboWOV' \
'szm17p4dSDA0TDMPYsjaAGHKM/nPnTyIMzRyArEzoy2vNkLEP6qdBIuMP2a' \
'CtGsciwMjYifHYRIRenB7H7I+FkwH0UaoTUCoo2PJkyZjy1nK6OwGVWaWG0' \
'G8g7Zy+K3bRYV+7cNaM5SB720ezhmYYJJvsIdRvO7pLsjAuTo4KJhvmVFCi' \
'pwyCdllVHY83GjuGOsAbHIIohl0Ttq59o0jp4w2wUs8U+mQ==',
'PjKR1BTNNnfOrUzfo27cLIhlrbSiOVZaANadDyHxKij/77ZYId+liyXoawv' \
'vQQPgnBH1dW6jqpr6fh5ZxZ9IOtaV+cTSUGnGdRSn7FyKs1ClpApKsZBO/i' \
'RBLXw3HDWOLc0jnA2bnxY8yqbEPmH5IBC9taYzxnf7aGjc6AWFHfs6AEQ8l' \
'Mio6UoASNzjy3VVNzUX+CK+e5Z45coT0X60mjaJmidGfPdWIfyUw8sSuUwx' \
'Qa1uNXAd8IceRUL7j5s9/kk7EwsihCw1Y3L+XJGG5zMsGhM9bTK5mvxj30U' \
'dmZORouNHdywOfdHaB1iOeKOk+yvWFMW3JsYShWbUhZUOEQ==',
'https://contoso.com/wopi/files/RVQ29k8tf3h8cJ/Endy+aAMPy0iG' \
'hLatGNrhvKofPY9p2w?access_token=zo7pjAbo%2fyof%2bvtUJn5gXpY' \
'cSl7TSSx0TbQGJbWJSll9PTjRRsAbG%2fSNL7FM2n5Ei3jQ8UJsW4RidT5R' \
'4tl1lrCi1%2bhGjxfWAC9pRRl6J3M1wZk9uFkWEeGzbtGByTkaGJkBqgKV%' \
'2ffxg%2bvATAhVr6E3LHCBAN91Wi8UG'
)
end
test 'Verify X-WOPI-ProofOld with current discovery proof key2.' do
assert @discovery.verify_proof(
'2rZvm60097vFfXK01cylEhaYU26CbpsoEdVdk%2fDK2JzBjg185nE69CCkl' \
'Y36u2Thrx9DyLdNZGsbRorX12TK%2b6XW8ka%2fKSukXl9N3dkOLlZxe%2b' \
'TQhhDlcymhYZ%2fnYx282ZrnI8gdIob0nmlYAIaSDRg0ZVx%2f%2bnSV8X8' \
'cHsiMvftBkQQ%2bJgqOU1le3OimjHOiDg%3d%3d',
635655899172093398,
'nLwR4nbFq8F/RLuAQ0Yby/avSJcpktZh+JlWonkXuVunTw+BZ+F84c3i1+0' \
'o7QoPQRkYmy/HEYhFoYVghkhQJzFXf98sYxb52SvDz/pl3G/gTgCcdcqkfR' \
'SMtaslkRX1VKC3YX42ksIL0LzM4wiCOJfT70i+2Yr5EQRWi6b2JELrAqPuN' \
'kxpuUUZ3DtEXsO9r6a4WkEwJesmqcqaQpMUvQ6cAkShgY7p0gAQeL//GG6w' \
'SpDaaA3QqKBcmBgzAatCaqg9NXiPjviZdoHgnbIRjBEhtH/WG00py/tsGMo' \
'Ih6R9VfnC4jEWWymB5mIiXxW1gBuL63QvQqiL1S8FQ2Xo+g==',
'M7R/iGyWInpWKl1+aAPuB2yVAwWaHTi504c87V0p35PzDyTh+K6fn20ygKm' \
'xf9suN9pq+/rMtUXhvM/zCyam0dZFuWpFOI22U0AjQxIJ4ZBH+zT+e/QeNm' \
'S7w4ctxUnDJ0zGsJ/DxaC0/DAkiIMOfXgb8FMPA/Z/HY+e1C6rRsoMXy0XG' \
'oTwzIDiI6wzPVr6FJpkktwO+eCNMyxnAODhlK+nubIhvmVEzWtenMXX8a2e' \
'J3mFquNC7Le6shiUxZA03MgqknwjasNaGPrdpJYjxCwzH3LnHPOHxVY2Hcj' \
'jTQTNHO8HEPCoSw9bi5a7XFS2loQM/tm0WknBe9+q4fpMMw==',
'https://contoso.com/wopi/files/s/3BPN8mUjJJAQS+aOFtFm6CU+YN' \
'pRICIR2M7mQLeQ90BA?access_token=2rZvm60097vFfXK01cylEhaYU26' \
'CbpsoEdVdk%2fDK2JzBjg185nE69CCklY36u2Thrx9DyLdNZGsbRorX12TK' \
'%2b6XW8ka%2fKSukXl9N3dkOLlZxe%2bTQhhDlcymhYZ%2fnYx282ZrnI8g' \
'dIob0nmlYAIaSDRg0ZVx%2f%2bnSV8X8cHsiMvftBkQQ%2bJgqOU1le3Oim' \
'jHOiDg%3d%3d'
)
end
test 'Verify X-WOPI-Proof with old discovery proof key1.' do
assert @discovery.verify_proof(
'pbocsujrb9BafFujWh%2fuh7Y6S5nBnonddEzDzV0zEFrBwhiu5lzjXRezX' \
'DC9N4acvJeGVB5CWAcxPz6cJ6FzJmwA4ZgGP6FaV%2b6CDkJYID3FJhHFrb' \
'w8f2kRfaceRjV1PzXEvFXulnz2K%2fwwv0rF2B4A1wGQrnmwxGIv9cL5PBC4',
635655898062751632,
'qF15pAAnOATqpUTLHIS/Z5K7OYFVjWcgKGbHPa0eHRayXsb6JKTelGQhvs7' \
'4gEFgg1mIgcCORwAtMzLmEFmOHgrdvkGvRzT3jtVVtwkxEhQt8aQL20N0Nw' \
'n4wNah0HeBHskdvmA1G/qcaFp8uTgHpRYFoBaSHEP3AZVNFg5y2jyYR34nN' \
'j359gktc2ZyLel3J3j7XtyjpRPHvvYVQfh7RsArLQ0VGp8sL4/BDHdSsUyJ' \
'8FXe67TSrz6TMZPwhEUR8dYHYek9qbQjC+wxPpo3G/yusucm1gHo0BjW/l3' \
'6cI8FRmNs1Fqaeppxqu31FhR8dEl7w5dwefa9wOUKcChF6A==',
'KmYzHJ9tu4SXfoiWzOkUIc0Bh8H3eJrA3OnDSbu2hT68EuLTp2vmvvFcHyH' \
'IiO8DuKj7u13MxkpuUER6VSIJp3nYfm91uEE/3g61V3SzaeRXdnkcKUa5x+' \
'ulKViECL2n4mpHzNnymxojFW5Y4lKUU4qEGzjE71K1DSFTU/CBkdqycsuy/' \
'Oct8G4GhA3O4MynlCf64B9LIhlWe4G+hxZgxIO0pq7w/1SH27nvScWiljVq' \
'gOAKr0Oidk/7sEfyBcOlerLgS/A00nJYYJk23DjrKGTKz1YY0CMEsROJCMi' \
'W11caxr0aKseOYlfmb6K1RXxtmiDpJ2T4y8jintjEdzEWDA==',
'https://contoso.com/wopi/files/DJNj59eQlM6BvwzAHkykiB1vNOWR' \
'uxT487+guv3v7HexfA?access_token=pbocsujrb9BafFujWh%2fuh7Y6S' \
'5nBnonddEzDzV0zEFrBwhiu5lzjXRezXDC9N4acvJeGVB5CWAcxPz6cJ6Fz' \
'JmwA4ZgGP6FaV%2b6CDkJYID3FJhHFrbw8f2kRfaceRjV1PzXEvFXulnz2K' \
'%2fwwv0rF2B4A1wGQrnmwxGIv9cL5PBC4',
)
end
test 'Verify X-WOPI-Proof with old discovery proof key2.' do
assert @discovery.verify_proof(
'%2foBUJRjT2EVSVaB0Bjl2BCC7bXkN674bwppkzj2H9Elj2G%2bVGl06EzV' \
'gv62BqIW17gZZUweK%2fBPCOWN%2bbwoHy2BWES7J4OrDgFpErCsRmRkUr9' \
'82LaFC2Nxb0%2bH6u6MrWfRMK5dX6w0cltOtRU%2fnsJ9JasAq0J2%2fEzh' \
'RbQwIyMQ%3d',
635655898147049273,
'egEcu5FlU9q+u+d5WQ/Z1nYbttR+0iWsLh2BgiyzAq93sQFyvQ5XSCW5i/V' \
'7rt9HDewnmvIXsak5ayeoX2aDNgsVM8gSI533tO9dv/33vBM6MAzNLXP3v5' \
'rNI/TaTXcfm4Q1bIoE5RFVZUSPH73B3p1Oon0Hhhd1CysFGv6ue00CT1Dgp' \
'u/w5cVElQxZmWmguD5hEFgmg9IGKuSJ1CdVeJLcTbdKWhJnTl/G4+SpXfWB' \
'nvTBXSzLK+PdtfVCxh/bPO/FJNRWNAg+UgdVAXnLN79faiioG27lXGVrodp' \
'VgW8qdANvDkjQbn7z7jXmQmv8ksVQgHfZel963NXRty5pDg==',
'AQUuboqy0JFnMgWfaz8o6V3YDCd6fwQlmkM5cNxCK1ikLK03W4R3xjazBev' \
'Nja70i3y0JYZ3GvTlJy4ZsEfYf+DRhfzgSuxExpTvNB5RMUvi/kNRTlrMNm' \
'6JQrLr2rTY5A+FvQ8YErnKSl5gqh9vb4b47sRdQNvYL5hiVI+Qd4AGAwN9d' \
'Sq/IJo3jHtivPSGMEfuqozoX3/GQEYfRf5dv58Rjeo5XRiDvuSDhPOacdoL' \
'/DFc/ksC0u9vN4qmoTSvt313p/FhFZ9T3NZYi/dv63XP8DJLWs3HMauzGiK' \
'FbvKaxDYb2K2llM1CRM2PbKproARMUR6oYYzwNKtg/Q7qIw==',
'https://contoso.com/wopi/files/UzfEboK8w9Eal86Vk4xzIkGwUeMA' \
'iFaNF14FfQETzQ72LPw?access_token=%2foBUJRjT2EVSVaB0Bjl2BCC7' \
'bXkN674bwppkzj2H9Elj2G%2bVGl06EzVgv62BqIW17gZZUweK%2fBPCOWN' \
'%2bbwoHy2BWES7J4OrDgFpErCsRmRkUr982LaFC2Nxb0%2bH6u6MrWfRMK5' \
'dX6w0cltOtRU%2fnsJ9JasAq0J2%2fEzhRbQwIyMQ%3d'
)
end
test 'Invalid proof key1.' do
assert_not @discovery.verify_proof(
'7DHGrfxYtgXxfaXF%2benHF3PTGMrgNlYktkbQB8q%2fn2aQwzYQ6qTmqNJR' \
'nJm5QIMXS7WbIxMy0LXaova2h687Md4%2bNTazty3P7HD3j5q9anbCuLsUJH' \
'tSXfKANUetLyAjFWq6egtMZJSHzDajO0EaHTeA9M7zJg1j69dEMoLmIbxP03' \
'kwAvBrdVQmjFdFryKw',
635655899260461032,
'Y/wVmPuvWJ5Q/Gl/a5mCkTrCKbfHWYiMG6cxmJgD+M/yYFzTsfcgbK2IRAlC' \
'R2eqGx3a5wh5bQzlC6Y/sKo2IE9Irz/NHFpV55zYfdwDi5ccwXSd34jWVUgk' \
'M3uL1r6KVmHcQH/ew10p54FVatXatuGp2Y+cq9BScYV1a45U8fs9zYoZcTAY' \
'vdWeYXmJbRGMOLLxab3fOiulPmbw+gOqpYPbuInc0yut6eGxAUmY1ENxpN7n' \
'bUqI3LbJvmE3PuX8Ifgg3RCsaFJEqC6JR36MjG+VqoKaFI6hh/ZWLR4y7FBa' \
'rSmQBr2VlAHrcWCJIMXoOOKaHdsDfNCb3A24LFvdAQ==',
'Pdbk1FoAB4zhxaDptfVOwTCmrNgWO6zdokoI3VYO8eshE9nJR1Rzr9K2666z' \
'a29IfT050jJX0EBanIXAawL4rFA6swPHYQAzf3pWJqwvqIbaLYvi4104IBWh' \
'm9XdZ7C1jDUmG8DgwbKrXZfg7xxZ/hzPlwEp5Y9ZijD/ReAgRs0Va8/ytWc3' \
'AJ+121Q1Ss9U8vD08K5+wg1PVYyNa2YGBpVJbt2ZSt8dvuciWZujFDTzgLvR' \
'r6w17kg6+jkiwJyz2ZIL6ytyiUE1oJzsbslIZN3yGHEcmXZZ8Xz5q8fzrLUV' \
'mRx1kX6FE2QzRe4+6Q+qNeI8Ct7dj7JBBdbK2Jq+6A==',
'https://contoso.com/wopi/files/Dy07US/uXVeOMwgyEqGqeVNnyOoaR' \
'xR+es2atR08tZPMatf0gf0?access_token=7DHGrfxYtgXxfaXF%2benHF3' \
'PTGMrgNlYktkbQB8q%2fn2aQwzYQ6qTmqNJRnJm5QIMXS7WbIxMy0LXaova2' \
'h687Md4%2bNTazty3P7HD3j5q9anbCuLsUJHtSXfKANUetLyAjFWq6egtMZJ' \
'SHzDajO0EaHTeA9M7zJg1j69dEMoLmIbxP03kwAvBrdVQmjFdFryKw'
)
end
test 'Invalid proof key2.' do
assert_not @discovery.verify_proof(
'itKqYThsIRPrrctbu%2bh7%2fMcYvNZiVKc5Un50ZqjKtvhVwz0XcObcEVm5' \
'rghR4PmrUG0P28QnQTYrcQ65%2bLtrIfbTBferY4zhPKt9qKaf6HKEyRU7ut' \
'szLoIG7P7XIhmdTo0RftsCAO3gLp4ZqZiQi4icNS6mMGYboNZ1pHaNCRmR31' \
'oZjmqWgcknWow8',
635655897692506269,
'Bu+myEy45g0hyKJycD7BYoDH+s10yvuH79Iq3OLz4w/o7gT8orKRrZ4KggJU' \
'WMzRPPSgg+CqEf6zdNm6kFrxf/zXCXTqa/bEzmJPNBby+r0jlyz2KFsOkxJ/' \
'mzVMKJltG0le56NJim21ZU4AwN0KiB6TZ56ruu/ma014proZWJt/H0qye/pN' \
'z5ZyYdyc1khgzKf/8o6sCgh6+VEZx93BjAUyMJr6OS9nYq1B8XGei/sE4xdg' \
'brIGW0Jwjl2hxqBQTn1VL0jyljbjuG+vy83QaeBSB4TGLljHZFgYp6ARCX7v' \
'3NHO0MicJtTpLdpsB9dlLkbms/BcJd1gK7m5dDJK1A==',
'CDU9l+cyr82vrUnyhdTh1P4jDwL82c//XrTWU/rPeTyB9Ko6z2aNCs893GEB' \
'3jICY7mDZ4t0sA5z/lpN83UyfH1uVqnEB92zCQsJTkXMdNG9dsK64G6RxIVb' \
'mhJkV5xiJvERCRnJJTcQg4ymeXQ3cuXAcN9XhStK8FCxBhxGMJsmlY2tahxd' \
'TEIHXHVagvdyB1UY0V1rv+OAOnCGaTs0yyBA0Mo5nmTSNF8VnYmqsmbXyyDP' \
'JJTsqallyJPxvdrLPeiBq1JHMr3b8dgel1jfweDR4mD9pHn3O9eaHN8b6xlL' \
'c+dIpwvcflzyhhEL5JKEUtO6EKbfakZTUeS86BuBZA==',
'https://contoso.com/wopi/files/6XZhdjvt6/cdJvXdBI1bdlIkhIjJP' \
'nb9HgWzZX7V2d8?access_token=itKqYThsIRPrrctbu%2bh7%2fMcYvNZi' \
'VKc5Un50ZqjKtvhVwz0XcObcEVm5rghR4PmrUG0P28QnQTYrcQ65%2bLtrIf' \
'bTBferY4zhPKt9qKaf6HKEyRU7utszLoIG7P7XIhmdTo0RftsCAO3gLp4ZqZ' \
'iQi4icNS6mMGYboNZ1pHaNCRmR31oZjmqWgcknWow8'
)
end
end