diff --git a/app/models/dmsf_file.rb b/app/models/dmsf_file.rb index 2a04cc2d..ed1ad21c 100644 --- a/app/models/dmsf_file.rb +++ b/app/models/dmsf_file.rb @@ -273,6 +273,15 @@ class DmsfFile < ActiveRecord::Base file = DmsfFile.new file.dmsf_folder_id = folder.id if folder file.project_id = project.id + if DmsfFile.where(project_id: file.project_id, dmsf_folder_id: file.dmsf_folder_id, name: filename).exists? + 1.step do |i| + gen_filename = " #{filename} #{l(:dmsf_copy, n: i)}" + unless DmsfFile.where(project_id: file.project_id, dmsf_folder_id: file.dmsf_folder_id, name: gen_filename).exists? + filename = gen_filename + break + end + end + end file.name = filename file.notification = Setting.plugin_redmine_dmsf['dmsf_default_notifications'].present? if file.save && last_revision diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 364af95a..5d11773a 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -416,6 +416,8 @@ cs: label_scroll_down: Posunout se dolů note_webdav_disabled: WebDAV je zablokovaný. Kontaktujte administrátora. + dmsf_copy: "Kopie (%{n})" + easy_pages: modules: dmsf_locked_documents: My locked documents diff --git a/config/locales/de.yml b/config/locales/de.yml index 976a0dce..a9ded974 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -415,6 +415,8 @@ de: label_scroll_down: Runterscrollen note_webdav_disabled: WebDAV is disabled. Contact the administrator. + dmsf_copy: "Kopie (%{n})" + easy_pages: modules: dmsf_locked_documents: Von mir gesperrte Dokumente diff --git a/config/locales/en.yml b/config/locales/en.yml index ebeb8e8e..ca7df6e5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -416,6 +416,8 @@ en: label_scroll_down: Scroll down note_webdav_disabled: WebDAV is disabled. Contact the administrator. + dmsf_copy: "Copy (%{n})" + easy_pages: modules: dmsf_locked_documents: My locked documents diff --git a/config/locales/es.yml b/config/locales/es.yml index 551a4b4c..2a1c81ea 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -416,6 +416,8 @@ es: label_scroll_down: Scroll down note_webdav_disabled: WebDAV is disabled. Contact the administrator. + dmsf_copy: "Copy (%{n})" + easy_pages: modules: dmsf_locked_documents: My locked documents diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 498a8f3f..9210717c 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -416,6 +416,8 @@ fr: label_scroll_down: Scroll down note_webdav_disabled: WebDAV is disabled. Contact the administrator. + dmsf_copy: "Copy (%{n})" + easy_pages: modules: dmsf_locked_documents: My locked documents diff --git a/config/locales/hu.yml b/config/locales/hu.yml index f1a79520..b805616c 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -415,6 +415,8 @@ hu: label_scroll_down: Scroll down note_webdav_disabled: WebDAV is disabled. Contact the administrator. + dmsf_copy: "Copy (%{n})" + easy_pages: modules: dmsf_locked_documents: My locked documents diff --git a/config/locales/it.yml b/config/locales/it.yml index ee902869..5c14eadd 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -416,6 +416,8 @@ it: # Italian strings thx 2 Matteo Arceci! label_scroll_down: Scroll down note_webdav_disabled: WebDAV is disabled. Contact the administrator. + dmsf_copy: "Copy (%{n})" + easy_pages: modules: dmsf_locked_documents: My locked documents diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 41dd928d..61d46591 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -416,6 +416,8 @@ ja: label_scroll_down: Scroll down note_webdav_disabled: WebDAV is disabled. Contact the administrator. + dmsf_copy: "Copy (%{n})" + easy_pages: modules: dmsf_locked_documents: 自分がロック中の文書 diff --git a/config/locales/ko.yml b/config/locales/ko.yml index f560a2c0..f2538414 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -415,6 +415,8 @@ ko: label_scroll_down: Scroll down note_webdav_disabled: WebDAV is disabled. Contact the administrator. + dmsf_copy: "Copy (%{n})" + easy_pages: modules: dmsf_locked_documents: 내 잠긴 파일 diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 20a1269e..d421e2a0 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -416,6 +416,8 @@ nl: label_scroll_down: Scroll down note_webdav_disabled: WebDAV is disabled. Contact the administrator. + dmsf_copy: "Copy (%{n})" + easy_pages: modules: dmsf_locked_documents: My locked documents diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 45c8e976..b4fad54a 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -416,6 +416,8 @@ pl: label_scroll_down: Scroll down note_webdav_disabled: WebDAV is disabled. Contact the administrator. + dmsf_copy: "Copy (%{n})" + easy_pages: modules: dmsf_locked_documents: My locked documents diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 7f14cbb6..555095c8 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -416,6 +416,8 @@ pt-BR: label_scroll_down: Scroll down note_webdav_disabled: WebDAV is disabled. Contact the administrator. + dmsf_copy: "Copy (%{n})" + easy_pages: modules: dmsf_locked_documents: My locked documents diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 4dce803a..2326c7b3 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -416,6 +416,8 @@ ru: label_scroll_down: Scroll down note_webdav_disabled: WebDAV is disabled. Contact the administrator. + dmsf_copy: "Copy (%{n})" + easy_pages: modules: dmsf_locked_documents: Мои заблокированные документы diff --git a/config/locales/sl.yml b/config/locales/sl.yml index 37be39b5..c414382d 100644 --- a/config/locales/sl.yml +++ b/config/locales/sl.yml @@ -416,6 +416,8 @@ sl: label_scroll_down: Scroll down note_webdav_disabled: WebDAV is disabled. Contact the administrator. + dmsf_copy: "Copy (%{n})" + easy_pages: modules: dmsf_locked_documents: My locked documents diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 1768c7c4..cca17268 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -415,6 +415,8 @@ zh-TW: label_scroll_down: Scroll down note_webdav_disabled: WebDAV is disabled. Contact the administrator. + dmsf_copy: "Copy (%{n})" + easy_pages: modules: dmsf_locked_documents: My locked documents diff --git a/config/locales/zh.yml b/config/locales/zh.yml index ca9d35dd..37b37a2d 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -416,6 +416,8 @@ zh: label_scroll_down: Scroll down note_webdav_disabled: WebDAV is disabled. Contact the administrator. + dmsf_copy: "Copy (%{n})" + easy_pages: modules: dmsf_locked_documents: My locked documents diff --git a/db/migrate/20210115120901_add_owner_to_dmsf_lock.rb b/db/migrate/20210115120901_add_owner_to_dmsf_lock.rb new file mode 100644 index 00000000..edf26e28 --- /dev/null +++ b/db/migrate/20210115120901_add_owner_to_dmsf_lock.rb @@ -0,0 +1,28 @@ +# encoding: utf-8 +# +# Redmine plugin for Document Management System "Features" +# +# Copyright © 2011-20 Karel Pičman +# Copyright © 2016-17 carlolars +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class AddOwnerToDmsfLock < ActiveRecord::Migration[5.2] + + def change + add_column :dmsf_locks, :owner, :string, null: true + end + +end \ No newline at end of file diff --git a/lib/dav4rack/controller.rb b/lib/dav4rack/controller.rb index c8a68a5d..91f3c5c1 100644 --- a/lib/dav4rack/controller.rb +++ b/lib/dav4rack/controller.rb @@ -320,12 +320,14 @@ module DAV4Rack asked[:timeout] = timeout.split(',').map{|x|x.strip} end + Rails.logger.info ">>> #{request.document}" + ns = request.ns if doc = request.document and lockinfo = doc.xpath("//#{ns}lockinfo") asked[:scope] = lockinfo.xpath("//#{ns}lockscope").children.find_all{|n|n.element?}.map{|n|n.name}.first asked[:type] = lockinfo.xpath("#{ns}locktype").children.find_all{|n|n.element?}.map{|n|n.name}.first - asked[:owner] = lockinfo.xpath("//#{ns}owner/#{ns}href").children.map{|n|n.text}.first + asked[:owner] = lockinfo.xpath("//#{ns}owner").children.map{|n|n.text}.first end r = XmlResponse.new(response, resource.namespaces) diff --git a/lib/dav4rack/xml_response.rb b/lib/dav4rack/xml_response.rb index 88dd0277..7071487c 100644 --- a/lib/dav4rack/xml_response.rb +++ b/lib/dav4rack/xml_response.rb @@ -76,10 +76,12 @@ module DAV4Rack Ox::Element.new(D_ACTIVELOCK).tap do |activelock| if scope - activelock << ox_element(D_LOCKSCOPE, scope) + scope = Ox::Element.new("#{DAV_NAMESPACE_NAME}:#{scope}") + activelock << ox_element(D_LOCKSCOPE, scope) end if type - activelock << ox_element(D_LOCKTYPE, type) + type = Ox::Element.new("#{DAV_NAMESPACE_NAME}:#{type}") + activelock << ox_element(D_LOCKTYPE) end activelock << ox_element(D_DEPTH, depth) activelock << ox_element(D_TIMEOUT, diff --git a/lib/redmine_dmsf/lockable.rb b/lib/redmine_dmsf/lockable.rb index ad09cd15..5ec2f85b 100644 --- a/lib/redmine_dmsf/lockable.rb +++ b/lib/redmine_dmsf/lockable.rb @@ -44,7 +44,7 @@ module RedmineDmsf ret end - def lock!(scope = :scope_exclusive, type = :type_write, expire = nil) + def lock!(scope = :scope_exclusive, type = :type_write, expire = nil, owner = nil) # Raise a lock error if entity is locked, but its not at resource level existing = lock(false) raise DmsfLockError.new(l(:error_resource_or_parent_locked)) if self.locked? && existing.empty? @@ -56,7 +56,10 @@ module RedmineDmsf raise DmsfLockError.new(l(:error_parent_locked)) else existing.each do |l| - raise DmsfLockError.new(l(:error_resource_locked)) if l.user.id == User.current.id + #if l.user.id == User.current.id + if (l.user.id == User.current.id) && (owner.nil? || (owner == l.owner)) + raise DmsfLockError.new(l(:error_resource_locked)) + end end end else @@ -68,13 +71,17 @@ module RedmineDmsf l.entity_type = self.is_a?(DmsfFile) ? 0 : 1 l.lock_type = type l.lock_scope = scope + ### + #Rails.logger.info ">>> #{scope}" + ### l.user = User.current l.expires_at = expire l.dmsf_file_last_revision_id = self.last_revision.id if self.is_a?(DmsfFile) + l.owner = owner l.save! - reload - locks.reload - l.reload + # reload + # locks.reload + # l.reload l end @@ -82,12 +89,12 @@ module RedmineDmsf return false unless self.locked? existing = self.lock(true) # If its empty its a folder that's locked (not root) - (existing.empty? || (!self.dmsf_folder.nil? && self.dmsf_folder.locked?)) ? false : true + (existing.empty? || (self.dmsf_folder&.locked?)) ? false : true end # # By using the path upwards, surely this would be quicker? - def locked_for_user? + def locked_for_user?(args = nil) return false unless locked? b_shared = nil self.dmsf_path.each do |entity| @@ -95,19 +102,34 @@ module RedmineDmsf next if locks.empty? locks.each do |lock| next if lock.expired? # In case we're in between updates - if lock.lock_scope == :scope_exclusive && b_shared.nil? - return true if (!lock.user) || (lock.user.id != User.current.id) + + # if lock.owner.present? && (lock.user.to_s != lock.owner) + # Rails.logger.info ">>> #{lock.user} X #{User.current} X #{lock.owner}" + # end + + if lock.lock_scope == :scope_exclusive #&& b_shared.nil? + #return true if (lock.user&.id != User.current.id) || (lock.owner != (args ? args[:owner] : nil)) + #if args && (args[:method] == 'put') && args[:owner].blank? + # return true if (lock.user&.id != User.current.id) #|| ((lock.owner != (args ? args[:owner] : nil))) + #else + return true if (lock.user&.id != User.current.id) || ((lock.owner != (args ? args[:owner] : nil))) + #end else b_shared = true if b_shared.nil? - b_shared = false if lock.user.id == User.current.id + if b_shared && (lock.user&.id == User.current.id) && (lock.owner == (args ? args[:owner] : nil)) || + (args && (args[:scope] == 'shared')) + b_shared = false + end end end + Rails.logger.info ">>> #{b_shared}" return true if b_shared end + Rails.logger.info ">>> false" false end - def unlock!(force_file_unlock_allowed = false) + def unlock!(force_file_unlock_allowed = false, owner = nil) raise DmsfLockError.new(l(:warning_file_not_locked)) unless self.locked? existing = self.lock(true) if existing.empty? || (!self.dmsf_folder.nil? && self.dmsf_folder.locked?) # If its empty its a folder that's locked (not root) @@ -124,7 +146,7 @@ module RedmineDmsf else b_destroyed = false existing.each do |lock| - if (lock.user && (lock.user.id == User.current.id)) || User.current.admin? + if ((lock.user&.id == User.current.id) && (lock.owner == owner)) || User.current.admin? lock.destroy b_destroyed = true break diff --git a/lib/redmine_dmsf/webdav/base_resource.rb b/lib/redmine_dmsf/webdav/base_resource.rb index 03c67865..390e8846 100644 --- a/lib/redmine_dmsf/webdav/base_resource.rb +++ b/lib/redmine_dmsf/webdav/base_resource.rb @@ -118,7 +118,7 @@ module RedmineDmsf new_path = @path new_path = new_path + '/' unless new_path[-1,1] == '/' new_path = '/' + new_path unless new_path[0,1] == '/' - @__proxy.class.new "#{new_path}#{name}", request, response, @options.merge(user: @user) + ResourceProxy.new "#{new_path}#{name}", request, response, @options.merge(user: @user) end def child_project(p) @@ -127,7 +127,7 @@ module RedmineDmsf new_path = new_path + '/' unless new_path[-1,1] == '/' new_path = '/' + new_path unless new_path[0,1] == '/' new_path += project_display_name - @__proxy.class.new new_path, request, response, @options.merge(user: @user, project: true) + ResourceProxy.new new_path, request, response, @options.merge(user: @user, project: true) end def parent @@ -240,6 +240,9 @@ module RedmineDmsf else @file = DmsfFile.find_file_by_name(@project, @folder, pinfo.first) @folder = nil + unless (pinfo.length < 2 || @subproject || @folder || @file) + raise Conflict + end break # We're at the end end end diff --git a/lib/redmine_dmsf/webdav/custom_middleware.rb b/lib/redmine_dmsf/webdav/custom_middleware.rb index f47b131d..45f156e9 100644 --- a/lib/redmine_dmsf/webdav/custom_middleware.rb +++ b/lib/redmine_dmsf/webdav/custom_middleware.rb @@ -38,8 +38,20 @@ module RedmineDmsf allow_unauthenticated_options_on_root: true, namespaces: { 'http://apache.org/dav/props/' => 'd', - 'http://ucb.openoffice.org/dav/props/' => 'd', - 'SAR:' => 'd' + 'http://ucb.openoffice.org/dav/props/' => 'd', # LibreOffice + 'SAR:' => 'd', # Cyberduck + 'http://webdav.org/neon/litmus/' => 'd', # Litmus + 'http://example.com/neon/litmus/' => 'ns1', + 'http://example.com/alpha' => 'ns2', + 'http://example.com/beta' => 'ns3', + 'http://example.com/gamma' => 'ns4', + 'http://example.com/delta' => 'ns5', + 'http://example.com/epsilon' => 'ns6', + 'http://example.com/zeta' => 'ns7', + 'http://example.com/eta' => 'ns8', + 'http://example.com/theta' => 'ns9', + 'http://example.com/iota' => 'ns10', + 'http://example.com/kappa' => 'ns11' } ) end diff --git a/lib/redmine_dmsf/webdav/dmsf_resource.rb b/lib/redmine_dmsf/webdav/dmsf_resource.rb index 75813b2d..d7d83107 100644 --- a/lib/redmine_dmsf/webdav/dmsf_resource.rb +++ b/lib/redmine_dmsf/webdav/dmsf_resource.rb @@ -28,6 +28,38 @@ module RedmineDmsf class DmsfResource < BaseResource include Redmine::I18n + def initialize(path, request, response, options) + super path, request, response, options + end + + # name:: String - Property name + # Returns the value of the given property + def get_property(element) + if element[:ns_href] == DAV_NAMESPACE + super + else + get_custom_property element + end + end + + # name:: String - Property name + # value:: New value + # Set the property to the given value + def set_property(element, value) + # let Resource handle DAV properties + if element[:ns_href] == DAV_NAMESPACE + super + else + set_custom_property element, value + end + end + + # name:: Property name + # Remove the property from the resource + def remove_property(element) + Redmine::Search.cache_store.delete "#{get_property_key}-#{element[:name]}" + end + # Gather collection of objects that denote current entities child entities # Used for listing directories etc, implemented basic caching because otherwise # Our already quite heavy usage of DB would just get silly every time we called @@ -112,7 +144,6 @@ module RedmineDmsf end # Process incoming GET request - # # If instance is a collection, calls html_display (defined in base_resource.rb) which cycles through children for display # File will only be presented for download if user has permission to view files def get(request, response) @@ -129,15 +160,13 @@ module RedmineDmsf end # Process incoming MKCOL request - # # Create a DmsfFolder at location requested, only if parent is a folder (or root) - # - 2012-06-18: Ensure item is only functional if project is enabled for dmsf + # Ensure item is only functional if project is enabled for dmsf def make_collection if request.body.read.to_s.empty? raise NotFound unless project && project.module_enabled?('dmsf') raise Forbidden unless User.current.admin? || User.current.allowed_to?(:folder_manipulation, project) raise Forbidden unless (!parent.exist? || !parent.folder || DmsfFolder.permissions?(parent.folder, false)) - return MethodNotAllowed if exist? # If we already exist, why waste the time trying to save? f = DmsfFolder.new f.title = basename f.dmsf_folder_id = parent.folder&.id @@ -150,13 +179,13 @@ module RedmineDmsf end # Process incoming DELETE request - # # should be of entity to be deleted, we simply follow the Dmsf entity method # for deletion and return of appropriate status based on outcome. def delete if file raise Forbidden unless User.current.admin? || User.current.allowed_to?(:file_delete, project) raise Forbidden unless (!parent.exist? || !parent.folder || DmsfFolder.permissions?(parent.folder, false)) + raise Locked if file.locked? pattern = Setting.plugin_redmine_dmsf['dmsf_webdav_disable_versioning'] if pattern.present? && basename.match(pattern) # Files that are not versioned should be destroyed @@ -174,6 +203,7 @@ module RedmineDmsf Conflict end elsif folder + raise Locked if folder.locked? # To fullfil Litmus requirements to not delete folder if fragments are in the URL uri = URI(uri_encode(request.get_header('REQUEST_URI'))) raise BadRequest if uri.fragment.present? @@ -186,124 +216,116 @@ module RedmineDmsf end # Process incoming MOVE request - # # Behavioural differences between collection and single entity - # TODO: Support overwrite between both types of entity, and implement better checking - def move(dest, overwrite) - dest = @__proxy.class.new(dest, @request, @response, @options.merge(user: @user)) - # All of this should carry across the ResourceProxy frontend, we ensure this to - # prevent unexpected errors - resource = dest.is_a?(ResourceProxy) ? dest.resource : dest - return PreconditionFailed if !resource.is_a?(DmsfResource) || resource.project.nil? - parent = resource.parent - raise Forbidden unless resource.project.module_enabled?(:dmsf) + def move(dest_path, overwrite) + dest = ResourceProxy.new(dest_path, @request, @response, @options.merge(user: @user)) + return PreconditionFailed if !dest.resource.is_a?(DmsfResource) || dest.resource.project.nil? + parent = dest.resource.parent + raise Forbidden unless dest.resource.project.module_enabled?(:dmsf) if !parent.exist? || (!User.current.admin? && (!DmsfFolder.permissions?(folder, false) || !DmsfFolder.permissions?(parent.folder, false))) raise Forbidden end if collection? if dest.exist? - return overwrite ? NotImplemented : PreconditionFailed + if overwrite + if folder + (dest.collection?)&.delete true + end + else + return PreconditionFailed + end end if !User.current.admin? && (!User.current.allowed_to?(:folder_manipulation, project) || - !User.current.allowed_to?(:folder_manipulation, resource.project)) + !User.current.allowed_to?(:folder_manipulation, dest.resource.project)) raise Forbidden end + unless folder + return MethodNotAllowed # Moving sub-project not enabled + end + raise Locked if folder.locked_for_user? # Change the title - return MethodNotAllowed unless folder # Moving sub-project not enabled - folder.title = resource.basename + folder.title = dest.resource.basename return PreconditionFailed unless folder.save # Move to a new destination - folder.move_to(resource.project, parent.folder) ? Created : PreconditionFailed + folder.move_to(dest.resource.project, parent.folder) ? Created : PreconditionFailed else if !User.current.admin? && (!User.current.allowed_to?(:file_manipulation, project) || - !User.current.allowed_to?(:file_manipulation, resource.project)) + !User.current.allowed_to?(:file_manipulation, dest.resource.project)) raise Forbidden end - if dest.exist? + raise Locked if file.locked_for_user? + if dest.exist? && (!dest.collection?) return PreconditionFailed unless overwrite - if (project == resource.project) && file.name.match(/.\.tmp$/i) - # Renaming a *.tmp file to an existing file in the same project, probably Office that is saving a file. - Rails.logger.info "WebDAV MOVE: #{file.name} -> #{resource.basename} (exists), possible MSOffice rename from .tmp when saving" - if resource.file.last_revision.size == 0 || reuse_version_for_locked_file(resource.file) - # Last revision in the destination has zero size so reuse that revision - new_revision = resource.file.last_revision - else - # Create a new revison by cloning the last revision in the destination - new_revision = resource.file.last_revision.clone - new_revision.increase_version 1 - end - # The file on disk must be renamed from .tmp to the correct filetype or else Xapian won't know how to index. - # Copy file.last_revision.disk_file to new_revision.disk_file - new_revision.size = file.last_revision.size - new_revision.disk_filename = new_revision.new_storage_filename - Rails.logger.info "WebDAV MOVE: Copy file #{file.last_revision.disk_filename} -> #{new_revision.disk_filename}" - File.open(file.last_revision.disk_file, 'rb') do |f| - new_revision.copy_file_content f - end - # Save - new_revision.save && resource.file.save - # 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 + if dest.resource.file.last_revision.size == 0 || reuse_version_for_locked_file(dest.resource.file) + # Last revision in the destination has zero size so reuse that revision + new_revision = dest.resource.file.last_revision 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. - NotImplemented + # Create a new revison by cloning the last revision in the destination + new_revision = dest.resource.file.last_revision.clone + new_revision.increase_version 1 end + # The file on disk must be renamed from .tmp to the correct filetype or else Xapian won't know how to index. + # Copy file.last_revision.disk_file to new_revision.disk_file + new_revision.size = file.last_revision.size + new_revision.disk_filename = new_revision.new_storage_filename + File.open(file.last_revision.disk_file, 'rb') do |f| + new_revision.copy_file_content f + end + # Save + new_revision.save && dest.resource.file.save + # 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 return PreconditionFailed unless exist? && file - if (project == resource.project) && resource.basename.match(/.\.tmp$/i) - Rails.logger.info "WebDAV MOVE: #{file.name} -> #{resource.basename}, possible MSOffice rename to .tmp when saving." + if (project == dest.resource.project) && dest.resource.basename.match(/.\.tmp$/i) + Rails.logger.info "WebDAV MOVE: #{file.name} -> #{dest.resource.basename}, possible MSOffice rename to .tmp when saving." # Renaming the file to X.tmp, might be Office that is saving a file. Keep the original file. - file.copy_to_filename resource.project, parent&.folder, resource.basename + file.copy_to_filename dest.resource.project, parent&.folder, dest.resource.basename Created else - if (project == resource.project) && (file.last_revision.size == 0) + if (project == dest.resource.project) && (file.last_revision.size == 0) # Moving a zero sized file within the same project, just update the dmsf_folder file.dmsf_folder = parent&.folder else - return InternalServerError unless file.move_to(resource.project, parent&.folder) + return InternalServerError unless file.move_to(dest.resource.project, parent&.folder) end # Update Revision and names of file [We can link to old physical resource, as it's not changed] if file.last_revision - file.last_revision.name = resource.basename - file.last_revision.title = DmsfFileRevision.filename_to_title(resource.basename) + file.last_revision.name = dest.resource.basename + file.last_revision.title = DmsfFileRevision.filename_to_title(dest.resource.basename) end - file.name = resource.basename + file.name = dest.resource.basename # Save Changes - (file.last_revision.save && file.save) ? Created : PreconditionFailed + if file.last_revision.save && file.save + dest.exist? ? NoContent : Created + else + PreconditionFailed + end end end end end # Process incoming COPY request - # # Behavioural differences between collection and single entity - # TODO: Support overwrite between both types of entity, and an integrative copy where destination exists for collections def copy(dest, overwrite, depth) - dest = @__proxy.class.new(dest, @request, @response, @options.merge(user: @user)) - # All of this should carry across the ResourceProxy frontend, we ensure this to - # prevent unexpected errors - if dest.is_a?(ResourceProxy) - resource = dest.resource - else - resource = dest - end - - return PreconditionFailed if !resource.is_a?(DmsfResource) || resource.project.nil? - - parent = resource.parent + dest = ResourceProxy.new(dest, @request, @response, @options.merge(user: @user)) + return PreconditionFailed unless dest.resource.project + parent = dest.resource.parent raise Forbidden unless (!parent.exist? || !parent.folder || DmsfFolder.permissions?(parent.folder, false)) - - if dest.exist? - return overwrite ? NotImplemented : PreconditionFailed - end - return Conflict unless dest.parent.exist? - + res = Created + if dest.exist? + return Locked if dest.lockdiscovery.present? + if overwrite + dest.delete + res = NoContent + else + return PreconditionFailed + end + end return PreconditionFailed unless parent.exist? && parent.folder - if collection? # Permission check if they can manipulate folders and view folders # Can they: @@ -312,14 +334,13 @@ module RedmineDmsf # View files on the source project :view_dmsf_files # View fodlers on the source project :view_dmsf_folders raise Forbidden unless User.current.admin? || - (User.current.allowed_to?(:folder_manipulation, resource.project) && - User.current.allowed_to?(:view_dmsf_folders, resource.project) && + (User.current.allowed_to?(:folder_manipulation, dest.resource.project) && + User.current.allowed_to?(:view_dmsf_folders, dest.resource.project) && User.current.allowed_to?(:view_dmsf_files, project) && User.current.allowed_to?(:view_dmsf_folders, project)) raise Forbidden unless DmsfFolder.permissions?(folder, false) - - folder.title = resource.basename - new_folder = folder.copy_to(resource.project, parent.folder) + folder.title = dest.resource.basename + new_folder = folder.copy_to(dest.resource.project, parent.folder) return PreconditionFailed if new_folder.nil? || new_folder.id.nil? Created else @@ -329,32 +350,67 @@ module RedmineDmsf # View files on destination project :view_dmsf_files # View files on the source project :view_dmsf_files raise Forbidden unless User.current.admin? || - (User.current.allowed_to?(:file_manipulation, resource.project) && - User.current.allowed_to?(:view_dmsf_files, resource.project) && + (User.current.allowed_to?(:file_manipulation, dest.resource.project) && + User.current.allowed_to?(:view_dmsf_files, dest.resource.project) && User.current.allowed_to?(:view_dmsf_files, project)) - return PreconditionFailed unless exist? && file - new_file = file.copy_to(resource.project, parent&.folder) + new_file = file.copy_to(dest.resource.project, parent&.folder) return InternalServerError unless (new_file && new_file.last_revision) - - # Update Revision and names of file [We can link to old physical resource, as it's not changed] - new_file.last_revision.name = resource.basename - new_file.name = resource.basename - + # Update Revision and names of file (We can link to old physical resource, as it's not changed) + new_file.last_revision.name = dest.resource.basename + new_file.name = dest.resource.basename # Save Changes - (new_file.last_revision.save && new_file.save) ? Created : PreconditionFailed + (new_file.last_revision.save && new_file.save) ? res : PreconditionFailed end end # Lock Check # Check for the existence of locks - # At present as deletions of folders are not recursive, we do not need to extend - # this to cover every file, just queried - def lock_check(lock_scope = nil) - if file - raise Locked if file.locked_for_user? - elsif folder - raise Locked if folder.locked_for_user? + def lock_check(args = {}) + entity = file || folder + if entity + refresh = args && (!args[:scope]) && (!args[:type]) + args ||= {} + args[:method] = @request.request_method.downcase + http_if = request.get_header('HTTP_IF') + if http_if.present? + no_lock = http_if.include?('') + not_no_lock = http_if.include?('Not ') + # Invalid lock token + if /\(<([a-f0-9]+-[a-f0-9]+-[a-f0-9]+-[a-f0-9]+-[a-f0-9]+)>/.match(http_if) + raise PreconditionFailed unless entity.locked? + if $1 != entity.lock.first.uuid + raise Locked if entity.locked_for_user?(args) + end + else + if ((!no_lock || not_no_lock) && entity.locked_for_user?(args)) + raise Locked + else + raise PreconditionFailed + end + end + # Invalid etag + if /^\(<([a-f0-9]+-[a-f0-9]+-[a-f0-9]+-[a-f0-9]+-[a-f0-9]+)> \[([a-f0-9]+-[a-f0-9]+-[a-f0-9]+)\]/.match(http_if) + if $2 != etag + raise PreconditionFailed + else + return # lock & etag + end + end + # no-lock + return if no_lock + end + if entity.locked_for_user?(args) && !refresh + if http_if.present? + case args[:method] + when 'put' + return + when 'proppatch' + return + end + end + raise Locked + end end end @@ -366,11 +422,9 @@ module RedmineDmsf raise e end unless exist? - e = DAV4Rack::LockFailure.new - e.add_failure @path, NotFound - raise e + return super(args) end - lock_check args[:scope] + lock_check args entity = file || folder unless entity e = DAV4Rack::LockFailure.new @@ -378,43 +432,38 @@ module RedmineDmsf raise e end begin - if entity.locked? && entity.locked_for_user? - raise DAV4Rack::LockFailure.new("Failed to lock: #{@path}") - else - # If scope and type are not defined, the only thing we can - # logically assume is that the lock is being refreshed (office loves - # to do this for example, so we do a few checks, try to find the lock - # and ultimately extend it, otherwise we return Conflict for any failure - if (!args[:scope]) && (!args[:type]) # Perhaps a lock refresh - http_if = request.env['HTTP_IF'] - if http_if.blank? - e = DAV4Rack::LockFailure.new - e.add_failure @path, Conflict - raise e - end - l = nil - if http_if =~ /([a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12})/ - l = DmsfLock.find_by(uuid: $1) - end - unless l - e = DAV4Rack::LockFailure.new - e.add_failure @path, Conflict - raise e - end - l.expires_at = Time.current + 1.week - l.save! - @response['Lock-Token'] = l.uuid - return [1.weeks.to_i, l.uuid] + # If scope and type are not defined, the only thing we can + # logically assume is that the lock is being refreshed (office loves + # to do this for example, so we do a few checks, try to find the lock + # and ultimately extend it, otherwise we return Conflict for any failure + refresh = args && (!args[:scope]) && (!args[:type]) # Perhaps a lock refresh + if refresh + http_if = request.get_header('HTTP_IF') + if http_if.blank? + e = DAV4Rack::LockFailure.new + e.add_failure @path, Conflict + raise e end - - scope = "scope_#{(args[:scope] || 'exclusive')}".to_sym - type = "type_#{(args[:type] || 'write')}".to_sym - - # l should be the instance of the lock we've just created - l = entity.lock!(scope, type, Time.current + 1.weeks) + l = nil + if /([a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12})/.match(http_if) + l = DmsfLock.find_by(uuid: $1) + end + unless l + e = DAV4Rack::LockFailure.new + e.add_failure @path, Conflict + raise e + end + l.expires_at = Time.current + 1.week + l.save! @response['Lock-Token'] = l.uuid - [1.week.to_i, l.uuid] + return [1.weeks.to_i, l.uuid] end + scope = "scope_#{(args[:scope] || 'exclusive')}".to_sym + type = "type_#{(args[:type] || 'write')}".to_sym + # l should be the instance of the lock we've just created + l = entity.lock!(scope, type, Time.current + 1.weeks, args[:owner]) + @response['Lock-Token'] = l.uuid + [1.week.to_i, l.uuid] rescue DmsfLockError e = DAV4Rack::LockFailure.new e.add_failure @path, Conflict @@ -426,7 +475,9 @@ module RedmineDmsf # Token based unlock (authenticated) will ensure that a correct token is sent, further ensuring # ownership of token before permitting unlock def unlock(token) - return NotFound unless exist? + unless exist? + return super(token) + end if token.nil? || token.empty? || (token == '<(null)>') || User.current.anonymous? BadRequest else @@ -436,14 +487,13 @@ module RedmineDmsf return BadRequest end begin - entity = file || folder l = DmsfLock.find(token) - return NoContent unless l # Additional case: if a user tries to unlock the file instead of the folder that's locked # This should throw forbidden as only the lock at level initiated should be unlocked + entity = file || folder return NoContent unless entity&.locked? l_entity = l.file || l.folder - if entity.locked_for_user? || (l_entity != entity) + if l_entity != entity Forbidden else entity.unlock! @@ -456,7 +506,6 @@ module RedmineDmsf end # HTTP POST request. - # # Forbidden, as method should not be utilized. def post(request, response) raise Forbidden @@ -467,7 +516,6 @@ module RedmineDmsf raise BadRequest if collection? raise Forbidden unless User.current.admin? || User.current.allowed_to?(:file_manipulation, project) raise Forbidden unless (!parent.exist? || !parent.folder || DmsfFolder.permissions?(parent.folder, false)) - # Ignore file name patterns given in the plugin settings pattern = Setting.plugin_redmine_dmsf['dmsf_webdav_ignore'] pattern = /^(\._|\.DS_Store$|Thumbs.db$)/ if pattern.blank? @@ -475,23 +523,18 @@ module RedmineDmsf Rails.logger.info "#{basename} ignored" return NoContent end - reuse_revision = false - if exist? # We're over-writing something, so ultimately a new revision f = file - # Disable versioning for file name patterns given in the plugin settings. pattern = Setting.plugin_redmine_dmsf['dmsf_webdav_disable_versioning'] if pattern.present? && basename.match(pattern) Rails.logger.info "Versioning disabled for #{basename}" reuse_revision = true end - if reuse_version_for_locked_file(file) reuse_revision = true end - last_revision = file.last_revision if last_revision.size == 0 || reuse_revision new_revision = last_revision @@ -504,10 +547,8 @@ module RedmineDmsf new_revision = DmsfFileRevision.new end # Custom fields - i = 0 - last_revision.custom_field_values.each do |custom_value| + last_revision.custom_field_values.each_with_index do |custom_value, i| new_revision.custom_field_values[i].value = custom_value - i = i + 1 end end else @@ -586,10 +627,10 @@ module RedmineDmsf def lockdiscovery entity = file || folder return [] unless entity&.locked? - if entity.dmsf_folder && entity.dmsf_folder.locked? - entity.lock.reverse[0].folder.locks(false) # longwinded way of getting base items locks + if entity.dmsf_folder&.locked? + entity.lock.reverse[0].folder.locks # longwinded way of getting base items locks else - entity.lock(false) + entity.lock false end end @@ -632,6 +673,7 @@ module RedmineDmsf end private + # Prepare file for download using Rack functionality: # Download (see RedmineDmsf::Webdav::Download) extends Rack::File to allow single-file # implementation of service for request, which allows for us to pipe a single file through @@ -653,21 +695,45 @@ module RedmineDmsf File.new disk_file end - private - def reuse_version_for_locked_file(file) locks = file.lock locks.each do |lock| next if lock.expired? # lock should be exclusive but just in case make sure we find this users lock next if lock.user != User.current - if lock.dmsf_file_last_revision_id < file.last_revision.id + if lock.dmsf_file_last_revision_id.nil? || (lock.dmsf_file_last_revision_id < file.last_revision.id) # At least one new revision has been created since the lock was created, reuse that revision. return true end end false end + + def set_custom_property(element, value) + if value.present? + Redmine::Search.cache_store.write "#{get_property_key}-#{element[:name]}", value + else + Redmine::Search.cache_store.delete "#{get_property_key}-#{element[:name]}" + end + OK + end + + def get_custom_property(element) + val = Redmine::Search.cache_store.fetch "#{get_property_key}-#{element[:name]}" + val.present? ? val : NotFound + end + + def get_property_key + if file + return "DmsfFile-#{file.id}" + elsif folder + return "DmsfFolder-#{folder.id}" + elsif subproject + return "Project-#{subproject.id}" + else + return "Project-#{project.id}" + end + end end end diff --git a/lib/redmine_dmsf/webdav/resource_proxy.rb b/lib/redmine_dmsf/webdav/resource_proxy.rb index b831193b..162ab667 100644 --- a/lib/redmine_dmsf/webdav/resource_proxy.rb +++ b/lib/redmine_dmsf/webdav/resource_proxy.rb @@ -162,6 +162,10 @@ module RedmineDmsf @resource_c.get_property element end + def remove_property(element) + @resource_c.remove_property element + end + def properties @resource_c.properties end @@ -170,6 +174,10 @@ module RedmineDmsf @resource_c.propstats response, stats end + def set_property(element, value) + @resource_c.set_property element, value + end + private def get_resource_class(path) diff --git a/test/integration/webdav/dmsf_webdav_delete_test.rb b/test/integration/webdav/dmsf_webdav_delete_test.rb index f7569e7b..0e317ac0 100644 --- a/test/integration/webdav/dmsf_webdav_delete_test.rb +++ b/test/integration/webdav/dmsf_webdav_delete_test.rb @@ -59,7 +59,7 @@ class DmsfWebdavDeleteTest < RedmineDmsf::Test::IntegrationTest def test_not_existed_project delete '/dmsf/webdav/not_a_project/file.txt', params: nil, headers: @admin - assert_response :not_found + assert_response :conflict end def test_dmsf_not_enabled @@ -130,7 +130,7 @@ class DmsfWebdavDeleteTest < RedmineDmsf::Test::IntegrationTest def test_folder_delete_by_user_with_project_names Setting.plugin_redmine_dmsf['dmsf_webdav_use_project_names'] = true delete "/dmsf/webdav/#{@project1.identifier}/#{@folder6.title}", params: nil, headers: @jsmith - assert_response :not_found + assert_response :conflict p1name_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1)) delete "/dmsf/webdav/#{p1name_uri}/#{@folder6.title}", params: nil, headers: @jsmith assert_response :success @@ -155,7 +155,7 @@ class DmsfWebdavDeleteTest < RedmineDmsf::Test::IntegrationTest def test_file_delete_by_user_with_project_names Setting.plugin_redmine_dmsf['dmsf_webdav_use_project_names'] = '1' delete "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", params: nil, headers: @jsmith - assert_response :not_found + assert_response :conflict p1name_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1)) delete "/dmsf/webdav/#{p1name_uri}/#{@file1.name}", params: nil, headers: @jsmith assert_response :success diff --git a/test/integration/webdav/dmsf_webdav_get_test.rb b/test/integration/webdav/dmsf_webdav_get_test.rb index 46cc1dc9..8fdac9ac 100644 --- a/test/integration/webdav/dmsf_webdav_get_test.rb +++ b/test/integration/webdav/dmsf_webdav_get_test.rb @@ -116,7 +116,7 @@ class DmsfWebdavGetTest < RedmineDmsf::Test::IntegrationTest Setting.plugin_redmine_dmsf['dmsf_webdav_use_project_names'] = true project1_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1)) get "/dmsf/webdav/#{@project1.identifier}/test.txt", params: nil, headers: @admin - assert_response :not_found + assert_response :conflict get "/dmsf/webdav/#{project1_uri}/test.txt", params: nil, headers: @admin assert_response :success end diff --git a/test/integration/webdav/dmsf_webdav_head_test.rb b/test/integration/webdav/dmsf_webdav_head_test.rb index 59cb074c..bef82b78 100644 --- a/test/integration/webdav/dmsf_webdav_head_test.rb +++ b/test/integration/webdav/dmsf_webdav_head_test.rb @@ -54,7 +54,7 @@ class DmsfWebdavHeadTest < RedmineDmsf::Test::IntegrationTest check_headers_exist Setting.plugin_redmine_dmsf['dmsf_webdav_use_project_names'] = '1' head "/dmsf/webdav/#{@project1.identifier}/test.txt", params: nil, headers: @admin - assert_response :not_found + assert_response :conflict head "/dmsf/webdav/#{@project1_uri}/test.txt", params: nil, headers: @admin assert_response :success end diff --git a/test/integration/webdav/dmsf_webdav_lock_test.rb b/test/integration/webdav/dmsf_webdav_lock_test.rb index 1279d1c7..06e2d4ce 100644 --- a/test/integration/webdav/dmsf_webdav_lock_test.rb +++ b/test/integration/webdav/dmsf_webdav_lock_test.rb @@ -41,8 +41,7 @@ class DmsfWebdavLockTest < RedmineDmsf::Test::IntegrationTest log_user 'admin', 'admin' process :lock, "/dmsf/webdav/#{@file2.project.identifier}/#{@file2.name}", params: @xml, headers: @admin.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite' }) - assert_response :multi_status - assert_match 'HTTP/1.1 409 Conflict', response.body + assert_response :locked end def test_lock_file @@ -50,7 +49,6 @@ class DmsfWebdavLockTest < RedmineDmsf::Test::IntegrationTest create_time = Time.utc(2000, 1, 2, 3, 4, 5) refresh_time = Time.utc(2000, 1, 2, 6, 7, 8) lock_token = nil - # Time travel, will make the usec part of the time 0 travel_to create_time do # Lock file @@ -62,8 +60,7 @@ class DmsfWebdavLockTest < RedmineDmsf::Test::IntegrationTest # # # - # exclusive - # write + # # infinity # Second-604800 # @@ -72,8 +69,7 @@ class DmsfWebdavLockTest < RedmineDmsf::Test::IntegrationTest # # # - assert_match 'exclusive', response.body - assert_match 'write', response.body + assert_match '', response.body assert_match 'infinity', response.body # 1.week = 7*24*3600=604800 seconds assert_match 'Second-604800', response.body @@ -88,12 +84,14 @@ class DmsfWebdavLockTest < RedmineDmsf::Test::IntegrationTest assert_equal create_time, l.updated_at assert_equal (create_time + 1.week), l.expires_at end - travel_to refresh_time do # Refresh lock - process :lock, "/dmsf/webdav/#{@project1.identifier}/#{@file9.name}", - params: nil, - headers: @jsmith.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite', HTTP_IF: lock_token }) + xml = %{ + + jsmith + } + process :lock, "/dmsf/webdav/#{@file9.project.identifier}/#{@file9.name}", params: xml, + headers: @jsmith.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite', HTTP_IF: "(<#{lock_token}>)" }) assert_response :success # 1.week = 7*24*3600=604800 seconds assert_match 'Second-604800', response.body diff --git a/test/integration/webdav/dmsf_webdav_mkcol_test.rb b/test/integration/webdav/dmsf_webdav_mkcol_test.rb index e5106f1d..f7d587b6 100644 --- a/test/integration/webdav/dmsf_webdav_mkcol_test.rb +++ b/test/integration/webdav/dmsf_webdav_mkcol_test.rb @@ -38,7 +38,7 @@ class DmsfWebdavMkcolTest < RedmineDmsf::Test::IntegrationTest def test_should_not_succeed_on_a_non_existant_project process :mkcol, '/dmsf/webdav/project_doesnt_exist/test1', params: nil, headers: @admin - assert_response :not_found + assert_response :conflict end def test_should_not_succed_on_a_non_dmsf_enabled_project @@ -65,7 +65,7 @@ class DmsfWebdavMkcolTest < RedmineDmsf::Test::IntegrationTest Setting.plugin_redmine_dmsf['dmsf_webdav_use_project_names'] = true project1_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1)) process :mkcol, "/dmsf/webdav/#{@project1.identifier}/test2", params: nil, headers: @jsmith - assert_response :not_found + assert_response :conflict process :mkcol, "/dmsf/webdav/#{project1_uri}/test3", params: nil, headers: @jsmith assert_response :success # Created end diff --git a/test/integration/webdav/dmsf_webdav_move_test.rb b/test/integration/webdav/dmsf_webdav_move_test.rb index 4456e494..9ea49902 100644 --- a/test/integration/webdav/dmsf_webdav_move_test.rb +++ b/test/integration/webdav/dmsf_webdav_move_test.rb @@ -166,16 +166,11 @@ class DmsfWebdavMoveTest < RedmineDmsf::Test::IntegrationTest end def test_move_to_existing_filename - file9 = DmsfFile.find_by(id: 9) - assert file9 - new_name = "#{file9.name}" - assert_no_difference 'file9.dmsf_file_revisions.count' do - assert_no_difference '@file1.dmsf_file_revisions.count' do - process :move, "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", params: nil, - headers: @jsmith.merge!({ - destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{new_name}"}) - assert_response :not_implemented - end + assert_no_difference '@file9.dmsf_file_revisions.count' do + process :move, "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", params: nil, + headers: @jsmith.merge!({ + destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{@file9.name}"}) + assert_response :success end end diff --git a/test/integration/webdav/dmsf_webdav_unlock_test.rb b/test/integration/webdav/dmsf_webdav_unlock_test.rb index 4401341f..8959707b 100644 --- a/test/integration/webdav/dmsf_webdav_unlock_test.rb +++ b/test/integration/webdav/dmsf_webdav_unlock_test.rb @@ -40,7 +40,7 @@ class DmsfWebdavUnlockTest < RedmineDmsf::Test::IntegrationTest l = @file2.locks.first process :unlock, "/dmsf/webdav/#{@file2.project.identifier}/#{@file2.name}", params: nil, headers: @jsmith.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite', HTTP_LOCK_TOKEN: l.uuid }) - assert_response :not_found + assert_response :forbidden end def test_unlock_file_with_invalid_token @@ -67,7 +67,7 @@ class DmsfWebdavUnlockTest < RedmineDmsf::Test::IntegrationTest # folder1 is missing in the path process :unlock, "/dmsf/webdav/#{@folder2.project.identifier}/#{@folder2.title}", params: nil, headers: @jsmith.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite', HTTP_LOCK_TOKEN: l.uuid }) - assert_response :not_found + assert_response :forbidden end def test_unlock_folder