From f9a1620657325465ce6d5fac0c0d0613d1a1e34d Mon Sep 17 00:00:00 2001 From: "COLA@Redminetest" Date: Sun, 6 Nov 2016 14:39:39 +0100 Subject: [PATCH 1/6] fixed typo --- lib/redmine_dmsf/webdav/dmsf_resource.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/redmine_dmsf/webdav/dmsf_resource.rb b/lib/redmine_dmsf/webdav/dmsf_resource.rb index eb7f6a9d..0d40f5ca 100644 --- a/lib/redmine_dmsf/webdav/dmsf_resource.rb +++ b/lib/redmine_dmsf/webdav/dmsf_resource.rb @@ -52,7 +52,7 @@ module RedmineDmsf # Our already quite heavy usage of DB would just get silly every time we called # this method. def children - unless @childern + unless @children @children = [] if collection? folder.dmsf_folders.select(:title).visible.map do |p| From 988799d13b00bd3dc03216d87bedd4239e8e1229 Mon Sep 17 00:00:00 2001 From: "COLA@Redminetest" Date: Mon, 7 Nov 2016 00:06:46 +0100 Subject: [PATCH 2/6] MOVEing a file from .tmp also destroys the .tmp file. (MsOffice rename) --- lib/redmine_dmsf/webdav/dmsf_resource.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/redmine_dmsf/webdav/dmsf_resource.rb b/lib/redmine_dmsf/webdav/dmsf_resource.rb index 0d40f5ca..daceee2b 100644 --- a/lib/redmine_dmsf/webdav/dmsf_resource.rb +++ b/lib/redmine_dmsf/webdav/dmsf_resource.rb @@ -297,8 +297,8 @@ module RedmineDmsf # Save new_revision.save && resource.file.save - # Delete the file that should have been renamed. - file.delete(false) ? NoContent : Conflict + # Delete (and destroy) the file that should have been renamed and return what should have been returned in case of a copy + file.delete(true) ? Created : PreconditionFailed else # Files cannot be merged at this point, until a decision is made on how to merge them # ideally, we would merge revision history for both, ensuring the origin file wins with latest revision. From 86fdb76c1e058c4874eed65348a57cc68e1089ff Mon Sep 17 00:00:00 2001 From: "COLA@Redminetest" Date: Mon, 7 Nov 2016 00:14:44 +0100 Subject: [PATCH 3/6] If reusing a revision because the last had zero size then don't generate a new filename. --- lib/redmine_dmsf/webdav/dmsf_resource.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/redmine_dmsf/webdav/dmsf_resource.rb b/lib/redmine_dmsf/webdav/dmsf_resource.rb index daceee2b..fbe02371 100644 --- a/lib/redmine_dmsf/webdav/dmsf_resource.rb +++ b/lib/redmine_dmsf/webdav/dmsf_resource.rb @@ -513,6 +513,7 @@ module RedmineDmsf end new_revision = DmsfFileRevision.new + reuse_revision = false if exist? # We're over-writing something, so ultimately a new revision f = file @@ -520,6 +521,7 @@ module RedmineDmsf if last_revision.size == 0 new_revision = last_revision new_revision.minor_version -= 1 + reuse_revision = true else new_revision.source_revision = last_revision if last_revision @@ -561,7 +563,7 @@ module RedmineDmsf raise InternalServerError unless new_revision.valid? && f.save - new_revision.disk_filename = new_revision.new_storage_filename + new_revision.disk_filename = new_revision.new_storage_filename unless reuse_revision if new_revision.save f.reload From 4ae5c379664c1ed9bc1d7225487c15731c8ea185 Mon Sep 17 00:00:00 2001 From: "COLA@Redminetest" Date: Mon, 7 Nov 2016 00:16:03 +0100 Subject: [PATCH 4/6] When deleting temporary MsOffice files then also destroy the file. --- lib/redmine_dmsf/webdav/dmsf_resource.rb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/redmine_dmsf/webdav/dmsf_resource.rb b/lib/redmine_dmsf/webdav/dmsf_resource.rb index fbe02371..cf3c5e37 100644 --- a/lib/redmine_dmsf/webdav/dmsf_resource.rb +++ b/lib/redmine_dmsf/webdav/dmsf_resource.rb @@ -226,7 +226,19 @@ module RedmineDmsf def delete if file raise Forbidden unless User.current.admin? || User.current.allowed_to?(:file_delete, project) - file.delete(false) ? NoContent : Conflict + if file.name.match(/.\.tmp$/i) + # .tmp files should be destroyed (MsOffice file) + destroy = true + elsif file.name.match(/^\~\$/i) + # Files starting with ~$ should be destroyed (MsOffice file) + destroy = true + elsif file.last_revision.size == 0 + # Zero-sized files should be destroyed + destroy = true + else + destroy = false + end + file.delete(destroy) ? NoContent : Conflict elsif folder raise Forbidden unless User.current.admin? || User.current.allowed_to?(:folder_manipulation, project) folder.delete(false) ? NoContent : Conflict From 3812be1cea9d38510bd7a34029e4a8242854e2ba Mon Sep 17 00:00:00 2001 From: "COLA@Redminetest" Date: Sun, 6 Nov 2016 22:58:56 +0100 Subject: [PATCH 5/6] Implemented OPTIONS and HEAD requests, used by MsOffice. MsOffice use anonymous OPTIONS and HEAD requests. OPTIONS always returns MethodNotAllowed(405). HEAD (hopefully) returns a valid response. See https://support.microsoft.com/en-us/kb/2019105 for details. --- lib/redmine_dmsf/webdav/base_resource.rb | 8 ++++---- lib/redmine_dmsf/webdav/controller.rb | 14 ++++++++++++++ lib/redmine_dmsf/webdav/dmsf_resource.rb | 16 ++++++++++++++++ lib/redmine_dmsf/webdav/index_resource.rb | 6 ++++++ lib/redmine_dmsf/webdav/project_resource.rb | 8 +++++++- lib/redmine_dmsf/webdav/resource_proxy.rb | 9 ++++++++- 6 files changed, 55 insertions(+), 6 deletions(-) diff --git a/lib/redmine_dmsf/webdav/base_resource.rb b/lib/redmine_dmsf/webdav/base_resource.rb index 3f1ff69b..517ad763 100644 --- a/lib/redmine_dmsf/webdav/base_resource.rb +++ b/lib/redmine_dmsf/webdav/base_resource.rb @@ -60,8 +60,8 @@ module RedmineDmsf @public_path.force_encoding('utf-8') end - # Generate HTML for Get requests - def html_display + # Generate HTML for Get requests, or Head requests if no_body is true + def html_display(no_body = false) @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 ] + @response.body << index_page % [ path.empty? ? '/' : path, path.empty? ? '/' : path, entities ] unless no_body end # Run method through proxy class - ensuring always compatible child is generated @@ -170,4 +170,4 @@ module RedmineDmsf end end end -end \ No newline at end of file +end diff --git a/lib/redmine_dmsf/webdav/controller.rb b/lib/redmine_dmsf/webdav/controller.rb index 26a07b13..9d52032c 100644 --- a/lib/redmine_dmsf/webdav/controller.rb +++ b/lib/redmine_dmsf/webdav/controller.rb @@ -26,6 +26,20 @@ module RedmineDmsf class Controller < DAV4Rack::Controller include DAV4Rack::Utils + # Return response to OPTIONS + def options + # Always return MethodNotAllowed(405) because how MsOffice uses anonymous OPTIONS request. + # See https://support.microsoft.com/en-us/kb/2019105 + MethodNotAllowed + end + + # Return response to HEAD + def head + # Don't check resource.exist? because it returns false for anonymous requests and MsOffice's uses anonymous HEAD requests. + # See https://support.microsoft.com/en-us/kb/2019105 + resource.head(request, response) + end + # Return response to PROPFIND def propfind unless(resource.exist?) diff --git a/lib/redmine_dmsf/webdav/dmsf_resource.rb b/lib/redmine_dmsf/webdav/dmsf_resource.rb index cf3c5e37..f12dde95 100644 --- a/lib/redmine_dmsf/webdav/dmsf_resource.rb +++ b/lib/redmine_dmsf/webdav/dmsf_resource.rb @@ -194,6 +194,22 @@ 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) diff --git a/lib/redmine_dmsf/webdav/index_resource.rb b/lib/redmine_dmsf/webdav/index_resource.rb index bc39e8b4..0acf48e0 100644 --- a/lib/redmine_dmsf/webdav/index_resource.rb +++ b/lib/redmine_dmsf/webdav/index_resource.rb @@ -68,6 +68,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 diff --git a/lib/redmine_dmsf/webdav/project_resource.rb b/lib/redmine_dmsf/webdav/project_resource.rb index 459f0d96..36a9588b 100644 --- a/lib/redmine_dmsf/webdav/project_resource.rb +++ b/lib/redmine_dmsf/webdav/project_resource.rb @@ -80,6 +80,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 @@ -95,4 +101,4 @@ module RedmineDmsf end end -end \ No newline at end of file +end diff --git a/lib/redmine_dmsf/webdav/resource_proxy.rb b/lib/redmine_dmsf/webdav/resource_proxy.rb index 15b933d4..782ff67d 100644 --- a/lib/redmine_dmsf/webdav/resource_proxy.rb +++ b/lib/redmine_dmsf/webdav/resource_proxy.rb @@ -51,7 +51,10 @@ module RedmineDmsf # 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?) + #return true if @request.request_method.downcase == 'options' && (path == '/' || path.empty?) + # MsOffice does anonymous OPTIONS and HEAD requests. + return true if @request.request_method.downcase == 'options' + return true if @request.request_method.downcase == 'head' return false unless username && password User.current = User.try_to_login(username, password) return User.current && !User.current.anonymous? @@ -97,6 +100,10 @@ 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 From e160df298c1c57f881f031d57e00b9d58ca2316d Mon Sep 17 00:00:00 2001 From: "COLA@Redminetest" Date: Mon, 7 Nov 2016 18:28:56 +0100 Subject: [PATCH 6/6] MsOffice wants response 405 on anonymous OPTIONS requests. --- lib/redmine_dmsf/webdav/controller.rb | 28 ++++++++++++++++----- lib/redmine_dmsf/webdav/dmsf_resource.rb | 8 ++++++ lib/redmine_dmsf/webdav/index_resource.rb | 8 ++++++ lib/redmine_dmsf/webdav/project_resource.rb | 18 ++++++++++--- lib/redmine_dmsf/webdav/resource_proxy.rb | 8 +++++- 5 files changed, 60 insertions(+), 10 deletions(-) diff --git a/lib/redmine_dmsf/webdav/controller.rb b/lib/redmine_dmsf/webdav/controller.rb index 9d52032c..19a33570 100644 --- a/lib/redmine_dmsf/webdav/controller.rb +++ b/lib/redmine_dmsf/webdav/controller.rb @@ -28,16 +28,32 @@ module RedmineDmsf # Return response to OPTIONS def options - # Always return MethodNotAllowed(405) because how MsOffice uses anonymous OPTIONS request. - # See https://support.microsoft.com/en-us/kb/2019105 - MethodNotAllowed + # 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 + 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 + end end # Return response to HEAD def head - # Don't check resource.exist? because it returns false for anonymous requests and MsOffice's uses anonymous HEAD requests. - # See https://support.microsoft.com/en-us/kb/2019105 - resource.head(request, response) + # 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 + else + resource.head(request, response) + end end # Return response to PROPFIND diff --git a/lib/redmine_dmsf/webdav/dmsf_resource.rb b/lib/redmine_dmsf/webdav/dmsf_resource.rb index f12dde95..befbdbc0 100644 --- a/lib/redmine_dmsf/webdav/dmsf_resource.rb +++ b/lib/redmine_dmsf/webdav/dmsf_resource.rb @@ -201,6 +201,7 @@ module RedmineDmsf ## 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 @@ -628,6 +629,13 @@ 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 diff --git a/lib/redmine_dmsf/webdav/index_resource.rb b/lib/redmine_dmsf/webdav/index_resource.rb index 0acf48e0..7ad71a92 100644 --- a/lib/redmine_dmsf/webdav/index_resource.rb +++ b/lib/redmine_dmsf/webdav/index_resource.rb @@ -80,6 +80,14 @@ module RedmineDmsf 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 diff --git a/lib/redmine_dmsf/webdav/project_resource.rb b/lib/redmine_dmsf/webdav/project_resource.rb index 36a9588b..47a79df0 100644 --- a/lib/redmine_dmsf/webdav/project_resource.rb +++ b/lib/redmine_dmsf/webdav/project_resource.rb @@ -81,9 +81,14 @@ module RedmineDmsf end def head(request, response) - html_display(true) - response['Content-Length'] = response.body.bytesize.to_s - OK + # 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) @@ -92,6 +97,13 @@ module RedmineDmsf 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 diff --git a/lib/redmine_dmsf/webdav/resource_proxy.rb b/lib/redmine_dmsf/webdav/resource_proxy.rb index 782ff67d..06999d0d 100644 --- a/lib/redmine_dmsf/webdav/resource_proxy.rb +++ b/lib/redmine_dmsf/webdav/resource_proxy.rb @@ -52,9 +52,12 @@ module RedmineDmsf # 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?) - # MsOffice does anonymous OPTIONS and HEAD requests. + + # Allow anonymous OPTIONS requests. return true if @request.request_method.downcase == 'options' + # Allow anonymous HEAD requests. return true if @request.request_method.downcase == 'head' + return false unless username && password User.current = User.try_to_login(username, password) return User.current && !User.current.anonymous? @@ -164,6 +167,9 @@ module RedmineDmsf @resource_c.properties end + def options + @resource_c.options_req + end end end end