Merge pull request #595 from carlolars/OfficeSupport

Office support
This commit is contained in:
Karel Picman 2016-11-09 08:47:28 +01:00 committed by GitHub
commit 44bf173845
8 changed files with 134 additions and 97 deletions

View File

@ -61,7 +61,7 @@ module RedmineDmsf
end
# Generate HTML for Get requests, or Head requests if no_body is true
def html_display(no_body = false)
def html_display
@response.body = ''
Confict unless collection?
entities = children.map{|child|
@ -80,7 +80,7 @@ module RedmineDmsf
'',
'',
] + entities if parent
@response.body << index_page % [ path.empty? ? '/' : path, path.empty? ? '/' : path, entities ] unless no_body
@response.body << index_page % [ path.empty? ? '/' : path, path.empty? ? '/' : path, entities ]
end
# Run method through proxy class - ensuring always compatible child is generated

View File

@ -30,31 +30,43 @@ module RedmineDmsf
# Return response to OPTIONS
def options
# exist? returns false if user is anonymous for ProjectResource and DmsfResource, but not for IndexResource.
unless(resource.exist? || (User.current && User.current.anonymous?))
# Return NotFound if resource does not exist and the request is not anonymous.
NotFound
if resource.exist?
# resource exists and user is not anonymous.
super
elsif resource.really_exist? &&
!request.user_agent.nil? && request.user_agent.downcase.include?('microsoft office') &&
User.current && User.current.anonymous?
# resource actually exist, but this was an anonymous request from MsOffice so respond with 405,
# hopefully the resource did actually exist but failed because of anon.
# If responding with 401 then MsOffice will fail.
# If responding with 200 then MsOffice will think that anonymous access is ok for everything.
# Responding with 405 is a workaround found in https://support.microsoft.com/en-us/kb/2019105
MethodNotAllowed
else
if request.env.has_key?('HTTP_X_OFFICE_MAJOR_VERSION') && User.current && User.current.anonymous?
# Anonymous request from MsOffice, respond 405.
# If responding with 401 then MsOffice will fail.
# If responding with 200 then MsOffice will think that anonymous access is ok for everything.
# Responding with 405 is a workaround found in https://support.microsoft.com/en-us/kb/2019105
MethodNotAllowed
else
resource.options
end
# Return NotFound if resource does not exist and the request is not anonymous from MsOffice
NotFound
end
end
# Return response to HEAD
def head
# exist? returns false if user is anonymous for ProjectResource and DmsfResource, but not for IndexResource.
unless(resource.exist? || (request.env.has_key?('HTTP_X_OFFICE_MAJOR_VERSION') && User.current && User.current.anonymous?))
# Return NotFound if resource does not exist and the request is not from an anonymous MsOffice product.
NotFound
if resource.exist?
# resource exists and user is not anonymous.
super
elsif resource.really_exist? &&
!request.user_agent.nil? && request.user_agent.downcase.include?('microsoft office') &&
User.current && User.current.anonymous?
# resource said it don't exist, but this was an anonymous request from MsOffice so respond anyway
# Can not call super here since it calls resource.exist? which will fail
response['Etag'] = resource.etag
response['Content-Type'] = resource.content_type
response['Last-Modified'] = resource.last_modified.httpdate
OK
else
resource.head(request, response)
end
# Return NotFound if resource does not exist and the request is not anonymous from MsOffice
NotFound
end
end
# Return response to PROPFIND

View File

@ -76,6 +76,10 @@ module RedmineDmsf
(User.current.admin? || User.current.allowed_to?(:view_dmsf_folders, project))
end
def really_exist?
return project && project.module_enabled?('dmsf') && (folder || file)
end
# Is this entity a folder?
def collection?
folder.present? # No need to check if entity exists, as false is returned if entity does not exist anyways
@ -195,23 +199,6 @@ module RedmineDmsf
OK
end
# Process incoming HEAD request
#
# MsOFfice uses anonymous HEAD requests, so always return a response.
# See https://support.microsoft.com/en-us/kb/2019105
##
def head(request, response)
raise NotFound unless project && project.module_enabled?('dmsf') && (folder || file)
if collection?
html_display(true)
response['Content-Length'] = response.body.bytesize.to_s
else
response.body = ''
end
OK
end
# Process incoming MKCOL request
#
# Create a DmsfFolder at location requested, only if parent is a folder (or root)
@ -641,13 +628,6 @@ module RedmineDmsf
end
end
def options_req
response["Allow"] = 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK'
response["Dav"] = '1, 2'
response["Ms-Author-Via"] = "DAV"
OK
end
private
# Prepare file for download using Rack functionality:
# Download (see RedmineDmsf::Webdav::Download) extends Rack::File to allow single-file

View File

@ -56,6 +56,11 @@ module RedmineDmsf
def exist?
true
end
# Index resource ALWAYS really exists
def really_exist?
true
end
def etag
sprintf('%x-%x-%x', children.count, 4096, Time.now.to_i)
@ -69,26 +74,12 @@ module RedmineDmsf
4096
end
def head(request, response)
html_display(true)
response['Content-Length'] = response.body.bytesize.to_s
OK
end
def get(request, response)
html_display
response['Content-Length'] = response.body.bytesize.to_s
OK
end
def options_req
response["Allow"] = 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK'
#response["Allow"] = 'OPTIONS,PROPFIND'
response["Dav"] = '1, 2'
response["Ms-Author-Via"] = "DAV"
OK
end
# Bugfix: Ensure that this level never indicates a parent
def parent
nil

View File

@ -49,6 +49,12 @@ module RedmineDmsf
User.current.admin? || User.current.allowed_to?(:view_dmsf_folders, project)
end
def really_exist?
return false if project.nil?
return false unless project.module_enabled?('dmsf')
true
end
def collection?
exist?
end
@ -85,30 +91,12 @@ module RedmineDmsf
4096
end
def head(request, response)
# HEAD must be allowed even for anonymous users, so just verify that the project exists and that the dmsf module is enabled.
if project.nil? || !project.module_enabled?('dmsf')
NotFound
else
html_display(true)
response['Content-Length'] = response.body.bytesize.to_s
OK
end
end
def get(request, response)
html_display
response['Content-Length'] = response.body.bytesize.to_s
OK
end
def options_req
response["Allow"] = 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK'
response["Dav"] = '1, 2'
response["Ms-Author-Via"] = "DAV"
OK
end
def folder
nil
end

View File

@ -45,19 +45,21 @@ module RedmineDmsf
end
def authenticate(username, password)
# Bugfix: Current DAV4Rack (including production) authenticate against ALL requests
# Microsoft Web Client will not attempt any authentication (it'd seem) until it's acknowledged
# a completed OPTIONS request. Ideally this is a flaw with the controller, however as I'm not
# going to fork it to ensure compliance, checking the request method in the authentication
# seems the next best step, if the request method is OPTIONS return true, controller will simply
# call the options method within, which accesses nothing, just returns headers about dav env.
#return true if @request.request_method.downcase == 'options' && (path == '/' || path.empty?)
# Allow anonymous OPTIONS requests.
return true if @request.request_method.downcase == 'options'
# Allow anonymous HEAD requests.
return true if @request.request_method.downcase == 'head'
unless username && password
# Bugfix: Current DAV4Rack (including production) authenticate against ALL requests
# Microsoft Web Client will not attempt any authentication (it'd seem) until it's acknowledged
# a completed OPTIONS request. Ideally this is a flaw with the controller, however as I'm not
# going to fork it to ensure compliance, checking the request method in the authentication
# seems the next best step, if the request method is OPTIONS return true, controller will simply
# call the options method within, which accesses nothing, just returns headers about dav env.
return true if @request.request_method.downcase == 'options' && (path == '/' || path.empty?)
# Allow anonymous OPTIONS requests from MsOffice
return true if @request.request_method.downcase == 'options' && !@request.user_agent.nil? && @request.user_agent.downcase.include?('microsoft office')
# Allow anonymous HEAD requests from MsOffice
return true if @request.request_method.downcase == 'head' && !@request.user_agent.nil? && request.user_agent.downcase.include?('microsoft office')
end
return false unless username && password
User.current = User.try_to_login(username, password)
return User.current && !User.current.anonymous?
@ -79,6 +81,10 @@ module RedmineDmsf
@resource_c.exist?
end
def really_exist?
@resource_c.really_exist?
end
def creation_date
@resource_c.creation_date
end
@ -99,10 +105,6 @@ module RedmineDmsf
@resource_c.content_length
end
def head(request,response)
@resource_c.head(request, response)
end
def get(request, response)
@resource_c.get(request, response)
end
@ -162,10 +164,6 @@ module RedmineDmsf
def properties
@resource_c.properties
end
def options
@resource_c.options_req
end
end
end
end

View File

@ -54,6 +54,18 @@ class DmsfWebdavHeadTest < RedmineDmsf::Test::IntegrationTest
check_headers_exist
end
def test_head_responds_anonymous_msoffice_user_agent
head "/dmsf/webdav/#{@project1.identifier}", nil, {:HTTP_USER_AGENT => "Microsoft Office Word 2014"}
assert_response :success
check_headers_exist
end
def test_head_responds_anonymous_other_user_agent
head "/dmsf/webdav/#{@project1.identifier}", nil, {:HTTP_USER_AGENT => "Other"}
assert_response 401
check_headers_dont_exist
end
# Note:
# At present we use Rack to serve the file, this makes life easy however it removes the Etag
# header and invalidates the test - where as a folder listing will always not include a last-modified
@ -65,18 +77,54 @@ class DmsfWebdavHeadTest < RedmineDmsf::Test::IntegrationTest
check_headers_exist # Note it'll allow 1 out of the 3 expected to fail
end
def test_head_responds_to_file_anonymous_msoffice_user_agent
head "/dmsf/webdav/#{@project1.identifier}/test.txt", nil, {:HTTP_USER_AGENT => "Microsoft Office Word 2014"}
assert_response :success
check_headers_exist # Note it'll allow 1 out of the 3 expected to fail
end
def test_head_responds_to_file_anonymous_other_user_agent
head "/dmsf/webdav/#{@project1.identifier}/test.txt", nil, {:HTTP_USER_AGENT => "Other"}
assert_response 401
check_headers_dont_exist
end
def test_head_fails_when_file_not_found
head "/dmsf/webdav/#{@project1.identifier}/not_here.txt", nil, @admin
assert_response :missing
check_headers_dont_exist
end
def test_head_fails_when_file_not_found_anonymous_msoffice_user_agent
head "/dmsf/webdav/#{@project1.identifier}/not_here.txt", nil, {:HTTP_USER_AGENT => "Microsoft Office Word 2014"}
assert_response :missing
check_headers_dont_exist
end
def test_head_fails_when_file_not_found_anonymous_other_user_agent
head "/dmsf/webdav/#{@project1.identifier}/not_here.txt", nil, {:HTTP_USER_AGENT => "Other"}
assert_response 401
check_headers_dont_exist
end
def test_head_fails_when_folder_not_found
head '/dmsf/webdav/folder_not_here', nil, @admin
assert_response :missing
check_headers_dont_exist
end
def test_head_fails_when_folder_not_found_anonymous_msoffice_user_agent
head '/dmsf/webdav/folder_not_here', nil, {:HTTP_USER_AGENT => "Microsoft Office Word 2014"}
assert_response :missing
check_headers_dont_exist
end
def test_head_fails_when_folder_not_found_anonymous_other_user_agent
head '/dmsf/webdav/folder_not_here', nil, {:HTTP_USER_AGENT => "Other"}
assert_response 401
check_headers_dont_exist
end
def test_head_fails_when_project_is_not_enabled_for_dmsf
head "/dmsf/webdav/#{@project2.identifier}/test.txt", nil, @jsmith
assert_response :missing
@ -116,4 +164,4 @@ class DmsfWebdavHeadTest < RedmineDmsf::Test::IntegrationTest
end
end
end
end

View File

@ -117,6 +117,26 @@ class DmsfWebdavOptionsTest < RedmineDmsf::Test::IntegrationTest
assert response.headers['Ms-Author-Via'] == 'DAV', 'Ms-Author-Via header - expected: DAV'
end
def test_un_authenticated_options_for_msoffice_user_agent
xml_http_request :options, "/dmsf/webdav/#{@project1.identifier}", nil, {:HTTP_USER_AGENT => "Microsoft Office Word 2014"}
assert_response 405
end
def test_authenticated_options_for_msoffice_user_agent
xml_http_request :options, "/dmsf/webdav/#{@project1.identifier}", nil, @admin.merge!({:HTTP_USER_AGENT => "Microsoft Office Word 2014"})
assert_response :success
end
def test_un_authenticated_options_for_other_user_agent
xml_http_request :options, "/dmsf/webdav/#{@project1.identifier}", nil, {:HTTP_USER_AGENT => "Other"}
assert_response 401
end
def test_authenticated_options_for_other_user_agent
xml_http_request :options, "/dmsf/webdav/#{@project1.identifier}", nil, @admin.merge!({:HTTP_USER_AGENT => "Other"})
assert_response :success
end
# TODO: It doesn't work
# def test_authenticated_options_returns_401_for_non_dmsf_enabled_items
# @project2.disable_module! :dmsf
@ -129,4 +149,4 @@ class DmsfWebdavOptionsTest < RedmineDmsf::Test::IntegrationTest
# assert_response 401 # refused
# end
end
end