First working version of office integration

Only works on scinote-preview
This commit is contained in:
Nejc Bernot 2016-08-10 17:49:25 +02:00 committed by Jure Grabnar
parent 9a8d49d513
commit 1b260785ec
14 changed files with 417 additions and 28 deletions

View file

@ -65,6 +65,18 @@ class AssetsController < ApplicationController
end
end
def edit
@action_url = @asset.get_action_url(current_user,"edit",false)
@token = current_user.get_wopi_token
@ttl = (current_user.wopi_token_ttl*1000).to_s
end
def view
@action_url = @asset.get_action_url(current_user,"view",false)
@token = current_user.get_wopi_token
@ttl = (current_user.wopi_token_ttl*1000).to_s
end
private
def load_vars

View file

@ -1,28 +1,35 @@
class WopiController < ActionController::Base
before_action :authenticate_user_from_token!, :load_vars
before_action :load_vars,:authenticate_user_from_token!
#before_action :verify_proof!
def get_file
def get_file_endpoint
Rails.logger.warn "get_file called"
#Only used for checkfileinfo
check_file_info
end
def get_file_contents
def get_file_contents_endpoint
Rails.logger.warn "get_file_contents called"
#Only used for getfile
get_file
end
def post_file
def post_file_endpoint
Rails.logger.warn "post_file called"
override = request.headers["X-WOPI-Override"]
case override
when "GET_LOCK"
get_lock
when "PUT_RELATIVE"
put_relative
when "LOCK"
old_lock = request.headers["X-WOPI-OldLock"]
if old_lock.nil?
lock
else
unlock_and_relock
end
when "UNLOCK"
unlock
when "REFRESH_LOCK"
@ -32,13 +39,14 @@ class WopiController < ActionController::Base
end
end
def post_file_contents
def post_file_contents_endpoint
Rails.logger.warn "post_file_contents called"
#Only used for putfile
put_file
end
def check_file_info
Rails.logger.warn "Check file info started"
msg = { :BaseFileName => @asset.file_file_name,
:OwnerId => @asset.created_by_id.to_s,
:Size => @asset.file_file_size,
@ -53,18 +61,175 @@ class WopiController < ActionController::Base
:UserFriendlyName => @user.name,
#TODO Check user permisisons
:ReadOnly => false,
:UserCanNotWriteRelative => true,
:UserCanWrite => true,
#TODO decide what to put here
:CloseUrl => "",
:CloseUrl => "https://scinote-preview.herokuapp.com",
:DownloadUrl => url_for(controller: 'assets',action: 'download',id: @asset.id),
:HostEditUrl => url_for(controller: 'assets',action: 'edit',id: @asset.id),
:HostViewUrl => url_for(controller: 'assets',action: 'view',id: @asset.id)
#TODO breadcrumbs?
#:FileExtension
}
render json:msg
response.headers['X-WOPI-HostEndpoint'] = ENV["WOPI_ENDPOINT_URL"]
response.headers['X-WOPI-MachineName'] = ENV["WOPI_ENDPOINT_URL"]
response.headers['X-WOPI-ServerVersion'] = APP_VERSION
render json:msg and return
end
def get_file
Rails.logger.warn "getting file"
response.headers["X-WOPI-ItemVersion"] = @asset.version
response.body = Paperclip.io_adapters.for(@asset.file).read
send_data response.body, disposition: "inline", :content_type => 'text/plain'
end
def put_relative
Rails.logger.warn "put relative"
render :nothing => true, :status => 501 and return
end
def lock
Rails.logger.warn "lock"
lock = request.headers["X-WOPI-Lock"]
if lock.nil? || lock.blank?
render :nothing => true, :status => 400 and return
end
@asset.with_lock do
if @asset.is_locked
if @asset.lock == lock
@asset.refresh_lock
response.headers["X-WOPI-ItemVersion"] = @asset.version
render :nothing => true, :status => 200 and return
else
response.headers["X-WOPI-Lock"] = @asset.lock
render :nothing => true, :status => 409 and return
end
else
@asset.lock_asset(lock)
response.headers["X-WOPI-ItemVersion"] = @asset.version
render :nothing => true, :status => 200 and return
end
end
end
def unlock_and_relock
Rails.logger.warn "lock and relock"
lock = request.headers["X-WOPI-Lock"]
old_lock = request.headers["X-WOPI-OldLock"]
if lock.nil? || lock.blank? || old_lock.blank?
render :nothing => true, :status => 400 and return
end
@asset.with_lock do
if @asset.is_locked
if @asset.lock == old_lock
@asset.unlock
@asset.lock_asset(lock)
response.headers["X-WOPI-ItemVersion"] = @asset.version
render :nothing => true, :status => 200 and return
else
response.headers["X-WOPI-Lock"] = @asset.lock
render :nothing => true, :status => 409 and return
end
else
response.headers["X-WOPI-Lock"] = ""
render :nothing => true, :status => 409 and return
end
end
end
def unlock
Rails.logger.warn "unlock"
lock = request.headers["X-WOPI-Lock"]
if lock.nil? || lock.blank?
render :nothing => true, :status => 400 and return
end
@asset.with_lock do
if @asset.is_locked
Rails.logger.warn "Current asset lock: #{@asset.lock}, unlocking lock #{lock}"
if @asset.lock == lock
@asset.unlock
response.headers["X-WOPI-ItemVersion"] = @asset.version
render :nothing => true, :status => 200 and return
else
response.headers["X-WOPI-Lock"] = @asset.lock
render :nothing => true, :status => 409 and return
end
else
Rails.logger.warn "Tried to unlock non-locked file"
response.headers["X-WOPI-Lock"] = ""
render :nothing => true, :status => 409 and return
end
end
end
def refresh_lock
Rails.logger.warn "refresh lock"
lock = request.headers["X-WOPI-Lock"]
if lock.nil? || lock.blank?
render :nothing => true, :status => 400 and return
end
@asset.with_lock do
if @asset.is_locked
if @asset.lock == lock
@asset.refresh_lock
response.headers["X-WOPI-ItemVersion"] = @asset.version
response.headers["X-WOPI-ItemVersion"] = @asset.version
render :nothing => true, :status => 200 and return
else
response.headers["X-WOPI-Lock"] = @asset.lock
render :nothing => true, :status => 409 and return
end
else
response.headers["X-WOPI-Lock"] = ""
render :nothing => true, :status => 409 and return
end
end
end
def get_lock
Rails.logger.warn "get lock"
@asset.with_lock do
if @asset.is_locked
response.headers["X-WOPI-Lock"] = @asset.lock
render :nothing => true, :status => 200 and return
else
response.headers["X-WOPI-Lock"] = ""
render :nothing => true, :status => 200 and return
end
end
end
# TODO When should we extract file text?
def put_file
Rails.logger.warn "put file"
@asset.with_lock do
lock = request.headers["X-WOPI-Lock"]
if @asset.is_locked
if @asset.lock == lock
Rails.logger.warn "replacing file"
@asset.update_contents(request.body)
response.headers["X-WOPI-ItemVersion"] = @asset.version
render :nothing => true, :status => 200 and return
else
Rails.logger.warn "wrong lock used to try and modify file"
response.headers["X-WOPI-Lock"] = @asset.lock
render :nothing => true, :status => 409 and return
end
else
if !@asset.file_file_size.nil? and @asset.file_file_size==0
Rails.logger.warn "initializing empty file"
@asset.update_contents(request.body)
response.headers["X-WOPI-ItemVersion"] = @asset.version
render :nothing => true, :status => 200 and return
else
Rails.logger.warn "trying to modify unlocked file"
response.headers["X-WOPI-Lock"] = ""
render :nothing => true, :status => 409 and return
end
end
end
end
def load_vars
@ -72,7 +237,7 @@ class WopiController < ActionController::Base
if @asset.nil?
render :nothing => true, :status => 404 and return
else
Rails.logger.warn "Found asset with id: #{params[:id]}, (id: #{@asset.id})"
Rails.logger.warn "Found asset"
step_assoc = @asset.step
result_assoc = @asset.result
@assoc = step_assoc if not step_assoc.nil?
@ -99,6 +264,7 @@ class WopiController < ActionController::Base
Rails.logger.warn "no user with this token found"
render :nothing => true, :status => 401 and return
end
Rails.logger.warn "user found by token"
#TODO check if the user can do anything with the file
end
@ -118,7 +284,6 @@ class WopiController < ActionController::Base
Rails.logger.warn "PROOF: #{proof}"
xml_doc = Nokogiri::XML("<root><aliens><alien><name>Alf</name></alien></aliens></root>")
rescue
Rails.logger.warn "proof verification failed"
render :nothing => true, :status => 401 and return

View file

@ -5,6 +5,8 @@ class Asset < ActiveRecord::Base
include WopiUtil
require 'tempfile'
# Lock duration set to 30 minutes
LOCK_DURATION = 60*30
# Paperclip validation
has_attached_file :file, {
@ -296,23 +298,88 @@ class Asset < ActiveRecord::Base
cache
end
def can_perform_action(action)
file_ext = file_file_name.split(".").last
action = get_action(file_ext,action)
if action.nil?
return false
end
true
end
def get_action_path(user,action)
def get_action_url(user,action,with_tokens = true)
file_ext = file_file_name.split(".").last
action = get_action(file_ext,action)
if !action.nil?
edit_url = action.urlsrc
edit_url = edit_url.gsub(/<IsLicensedUser=BUSINESS_USER&>/, "IsLicensedUser=1&")
edit_url = edit_url.gsub(/<IsLicensedUser=BUSINESS_USER>/, "IsLicensedUser=1")
edit_url = edit_url.gsub(/<.*?=.*?>/, "")
#This does not work yet - provides path instead of absolute url
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)
edit_url = edit_url + "WOPISrc=#{rest_url}&access_token=#{user.get_wopi_token}&access_token_ttl=#{user.wopi_token_ttl.to_s}"
action_url = action_url + "WOPISrc=#{rest_url}"
if with_tokens
action_url = action_url + "&access_token=#{user.get_wopi_token}&access_token_ttl=#{(user.wopi_token_ttl*1000).to_s}"
else
action_url
end
else
return nil
end
end
#is_locked, lock_asset and refresh_lock rely on the asset being locked in the database to prevent race conditions
def is_locked
if lock.nil?
return false
else
return true
end
end
def lock_asset(lock_string)
self.lock = lock_string
self.lock_ttl = Time.now.to_i + LOCK_DURATION
delay(queue: :assets, run_at: LOCK_DURATION.seconds.from_now).unlock_expired
self.save!
end
def refresh_lock
self.lock_ttl = Time.now.to_i + LOCK_DURATION
delay(queue: :assets, run_at: LOCK_DURATION.seconds.from_now).unlock_expired
self.save!
end
def unlock
self.lock = nil
self.lock_ttl = nil
self.save!
end
def unlock_expired
self.with_lock do
if !self.lock_ttl.nil? and self.lock_ttl>= Time.now.to_i
self.lock = nil
self.lock_ttl = nil
self.save!
end
end
end
def update_contents(new_file)
new_file.class.class_eval { attr_accessor :original_filename }
new_file.original_filename = self.file_file_name
self.file = new_file
if self.version.nil?
self.version = 1
else
self.version = self.version + 1
end
self.save
end
protected
# Checks if attachments is an image (in post processing imagemagick will

View file

@ -270,6 +270,7 @@ class User < ActiveRecord::Base
unless token_valid
# if current token is not valid generate a new one with a one day TTL
self.wopi_token = Devise.friendly_token(20)
# WOPI uses millisecond TTLs
self.wopi_token_ttl = Time.now.to_i + 60*60*24
self.save
Rails.logger.warn("Generating new token #{self.wopi_token}")

View file

@ -1,7 +1,7 @@
class WopiApp < ActiveRecord::Base
belongs_to :wopi_discovery, :foreign_key => 'wopi_discovery_id', :class_name => 'WopiDiscovery'
has_many :wopi_actions, class_name: 'WopiAction', foreign_key: 'wopi_app_id', :dependent => :delete_all
has_many :wopi_actions, class_name: 'WopiAction', foreign_key: 'wopi_app_id', :dependent => :destroy
validates :name, :icon, :wopi_discovery, presence: true

View file

@ -1,6 +1,6 @@
class WopiDiscovery < ActiveRecord::Base
has_many :wopi_apps, class_name: 'WopiApp', foreign_key: 'wopi_discovery_id', :dependent => :delete_all
has_many :wopi_apps, class_name: 'WopiApp', foreign_key: 'wopi_discovery_id', :dependent => :destroy
validates :expires, :proof_key_mod, :proof_key_exp, :proof_key_old_mod, :proof_key_old_exp, presence: true
end

View file

@ -58,7 +58,10 @@ module WopiUtil
end
rescue
Rails.logger.warn "Initialization failed"
WopiDiscovery.first.destroy
discovery = WopiDiscovery.first
unless discovery.nil?
discovery.destroy
end
end
end

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

@ -0,0 +1,64 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<!-- Enable IE Standards mode -->
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title></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="" />
<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>

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

@ -0,0 +1,64 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<!-- Enable IE Standards mode -->
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title></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="" />
<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

@ -72,6 +72,14 @@
<%= link_to download_asset_path(asset), data: {no_turbolink: true, id: true, status: "asset-present"} do %>
<%= image_tag preview_asset_path(asset) if asset.is_image? %>
<p><%= truncate(asset.file_file_name, length: 50) %></p>
<% if asset.can_perform_action("view") %>
<%= link_to "View", view_asset_url(id: asset) %>
<% end %>
<% if asset.can_perform_action("edit") %>
<%= link_to "Edit", edit_asset_url(id: asset) %>
<% end %>
<% else %>
<%= asset_loading_span(asset) %>
<% end %>
<% else %>
<%= asset_loading_span(asset) %>

View file

@ -30,7 +30,13 @@ module Scinote
"[#{datetime}] #{severity}: #{msg}\n"
end
config.action_dispatch.default_headers = {
'X-WOPI-Lock' => "",
'Random-header' => "with value",
'Random-non-special-header' => "a"
}
# Paperclip spoof checking
Paperclip.options[:content_type_mappings] = {:csv => "text/plain"}
Paperclip.options[:content_type_mappings] = {:csv => "text/plain", wopitest: ['text/plain', 'inode/x-empty'] }
end
end

View file

@ -267,11 +267,10 @@ Rails.application.routes.draw do
end
# Office integration
get "wopi/files/:id/contents", to: "wopi#get_file_contents"
post "wopi/files/:id/contents", to: "wopi#post_file_contents"
get "wopi/files/:id", to: "wopi#get_file", as: 'wopi_rest_endpoint'
post "wopi/files/:id", to: "wopi#post_file"
get "wopi/files/:id/contents", to: "wopi#get_file_contents_endpoint"
post "wopi/files/:id/contents", to: "wopi#post_file_contents_endpoint"
get "wopi/files/:id", to: "wopi#get_file_endpoint", as: 'wopi_rest_endpoint'
post "wopi/files/:id", to: "wopi#post_file_endpoint"
end

View file

@ -6,7 +6,7 @@ class AddWopi< ActiveRecord::Migration
add_column :assets, :lock, :string, :limit => 1024
add_column :assets, :lock_ttl, :integer
add_column :assets, :version, :integer
add_column :assets, :version, :integer, default: 1
create_table :wopi_discoveries do |t|
t.integer :expires, null: false

View file

@ -58,7 +58,7 @@ ActiveRecord::Schema.define(version: 20160809074757) do
t.boolean "file_present", default: false, null: false
t.string "lock", limit: 1024
t.integer "lock_ttl"
t.integer "version"
t.integer "version", default: 1
end
add_index "assets", ["created_at"], name: "index_assets_on_created_at", using: :btree