#1133 Litmus test suite

This commit is contained in:
karel.picman@lbcfree.net 2021-01-29 08:54:20 +01:00
parent 3f22ce6f05
commit 6faed31397
32 changed files with 384 additions and 207 deletions

View File

@ -273,6 +273,15 @@ class DmsfFile < ActiveRecord::Base
file = DmsfFile.new file = DmsfFile.new
file.dmsf_folder_id = folder.id if folder file.dmsf_folder_id = folder.id if folder
file.project_id = project.id 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.name = filename
file.notification = Setting.plugin_redmine_dmsf['dmsf_default_notifications'].present? file.notification = Setting.plugin_redmine_dmsf['dmsf_default_notifications'].present?
if file.save && last_revision if file.save && last_revision

View File

@ -416,6 +416,8 @@ cs:
label_scroll_down: Posunout se dolů label_scroll_down: Posunout se dolů
note_webdav_disabled: WebDAV je zablokovaný. Kontaktujte administrátora. note_webdav_disabled: WebDAV je zablokovaný. Kontaktujte administrátora.
dmsf_copy: "Kopie (%{n})"
easy_pages: easy_pages:
modules: modules:
dmsf_locked_documents: My locked documents dmsf_locked_documents: My locked documents

View File

@ -415,6 +415,8 @@ de:
label_scroll_down: Runterscrollen label_scroll_down: Runterscrollen
note_webdav_disabled: WebDAV is disabled. Contact the administrator. note_webdav_disabled: WebDAV is disabled. Contact the administrator.
dmsf_copy: "Kopie (%{n})"
easy_pages: easy_pages:
modules: modules:
dmsf_locked_documents: Von mir gesperrte Dokumente dmsf_locked_documents: Von mir gesperrte Dokumente

View File

@ -416,6 +416,8 @@ en:
label_scroll_down: Scroll down label_scroll_down: Scroll down
note_webdav_disabled: WebDAV is disabled. Contact the administrator. note_webdav_disabled: WebDAV is disabled. Contact the administrator.
dmsf_copy: "Copy (%{n})"
easy_pages: easy_pages:
modules: modules:
dmsf_locked_documents: My locked documents dmsf_locked_documents: My locked documents

View File

@ -416,6 +416,8 @@ es:
label_scroll_down: Scroll down label_scroll_down: Scroll down
note_webdav_disabled: WebDAV is disabled. Contact the administrator. note_webdav_disabled: WebDAV is disabled. Contact the administrator.
dmsf_copy: "Copy (%{n})"
easy_pages: easy_pages:
modules: modules:
dmsf_locked_documents: My locked documents dmsf_locked_documents: My locked documents

View File

@ -416,6 +416,8 @@ fr:
label_scroll_down: Scroll down label_scroll_down: Scroll down
note_webdav_disabled: WebDAV is disabled. Contact the administrator. note_webdav_disabled: WebDAV is disabled. Contact the administrator.
dmsf_copy: "Copy (%{n})"
easy_pages: easy_pages:
modules: modules:
dmsf_locked_documents: My locked documents dmsf_locked_documents: My locked documents

View File

@ -415,6 +415,8 @@ hu:
label_scroll_down: Scroll down label_scroll_down: Scroll down
note_webdav_disabled: WebDAV is disabled. Contact the administrator. note_webdav_disabled: WebDAV is disabled. Contact the administrator.
dmsf_copy: "Copy (%{n})"
easy_pages: easy_pages:
modules: modules:
dmsf_locked_documents: My locked documents dmsf_locked_documents: My locked documents

View File

@ -416,6 +416,8 @@ it: # Italian strings thx 2 Matteo Arceci!
label_scroll_down: Scroll down label_scroll_down: Scroll down
note_webdav_disabled: WebDAV is disabled. Contact the administrator. note_webdav_disabled: WebDAV is disabled. Contact the administrator.
dmsf_copy: "Copy (%{n})"
easy_pages: easy_pages:
modules: modules:
dmsf_locked_documents: My locked documents dmsf_locked_documents: My locked documents

View File

@ -416,6 +416,8 @@ ja:
label_scroll_down: Scroll down label_scroll_down: Scroll down
note_webdav_disabled: WebDAV is disabled. Contact the administrator. note_webdav_disabled: WebDAV is disabled. Contact the administrator.
dmsf_copy: "Copy (%{n})"
easy_pages: easy_pages:
modules: modules:
dmsf_locked_documents: 自分がロック中の文書 dmsf_locked_documents: 自分がロック中の文書

View File

@ -415,6 +415,8 @@ ko:
label_scroll_down: Scroll down label_scroll_down: Scroll down
note_webdav_disabled: WebDAV is disabled. Contact the administrator. note_webdav_disabled: WebDAV is disabled. Contact the administrator.
dmsf_copy: "Copy (%{n})"
easy_pages: easy_pages:
modules: modules:
dmsf_locked_documents: 내 잠긴 파일 dmsf_locked_documents: 내 잠긴 파일

View File

@ -416,6 +416,8 @@ nl:
label_scroll_down: Scroll down label_scroll_down: Scroll down
note_webdav_disabled: WebDAV is disabled. Contact the administrator. note_webdav_disabled: WebDAV is disabled. Contact the administrator.
dmsf_copy: "Copy (%{n})"
easy_pages: easy_pages:
modules: modules:
dmsf_locked_documents: My locked documents dmsf_locked_documents: My locked documents

View File

@ -416,6 +416,8 @@ pl:
label_scroll_down: Scroll down label_scroll_down: Scroll down
note_webdav_disabled: WebDAV is disabled. Contact the administrator. note_webdav_disabled: WebDAV is disabled. Contact the administrator.
dmsf_copy: "Copy (%{n})"
easy_pages: easy_pages:
modules: modules:
dmsf_locked_documents: My locked documents dmsf_locked_documents: My locked documents

View File

@ -416,6 +416,8 @@ pt-BR:
label_scroll_down: Scroll down label_scroll_down: Scroll down
note_webdav_disabled: WebDAV is disabled. Contact the administrator. note_webdav_disabled: WebDAV is disabled. Contact the administrator.
dmsf_copy: "Copy (%{n})"
easy_pages: easy_pages:
modules: modules:
dmsf_locked_documents: My locked documents dmsf_locked_documents: My locked documents

View File

@ -416,6 +416,8 @@ ru:
label_scroll_down: Scroll down label_scroll_down: Scroll down
note_webdav_disabled: WebDAV is disabled. Contact the administrator. note_webdav_disabled: WebDAV is disabled. Contact the administrator.
dmsf_copy: "Copy (%{n})"
easy_pages: easy_pages:
modules: modules:
dmsf_locked_documents: Мои заблокированные документы dmsf_locked_documents: Мои заблокированные документы

View File

@ -416,6 +416,8 @@ sl:
label_scroll_down: Scroll down label_scroll_down: Scroll down
note_webdav_disabled: WebDAV is disabled. Contact the administrator. note_webdav_disabled: WebDAV is disabled. Contact the administrator.
dmsf_copy: "Copy (%{n})"
easy_pages: easy_pages:
modules: modules:
dmsf_locked_documents: My locked documents dmsf_locked_documents: My locked documents

View File

@ -415,6 +415,8 @@ zh-TW:
label_scroll_down: Scroll down label_scroll_down: Scroll down
note_webdav_disabled: WebDAV is disabled. Contact the administrator. note_webdav_disabled: WebDAV is disabled. Contact the administrator.
dmsf_copy: "Copy (%{n})"
easy_pages: easy_pages:
modules: modules:
dmsf_locked_documents: My locked documents dmsf_locked_documents: My locked documents

View File

@ -416,6 +416,8 @@ zh:
label_scroll_down: Scroll down label_scroll_down: Scroll down
note_webdav_disabled: WebDAV is disabled. Contact the administrator. note_webdav_disabled: WebDAV is disabled. Contact the administrator.
dmsf_copy: "Copy (%{n})"
easy_pages: easy_pages:
modules: modules:
dmsf_locked_documents: My locked documents dmsf_locked_documents: My locked documents

View File

@ -0,0 +1,28 @@
# encoding: utf-8
#
# Redmine plugin for Document Management System "Features"
#
# Copyright © 2011-20 Karel Pičman <karel.picman@kontron.com>
# 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

View File

@ -320,12 +320,14 @@ module DAV4Rack
asked[:timeout] = timeout.split(',').map{|x|x.strip} asked[:timeout] = timeout.split(',').map{|x|x.strip}
end end
Rails.logger.info ">>> #{request.document}"
ns = request.ns ns = request.ns
if doc = request.document and lockinfo = doc.xpath("//#{ns}lockinfo") 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[: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[: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 end
r = XmlResponse.new(response, resource.namespaces) r = XmlResponse.new(response, resource.namespaces)

View File

@ -76,10 +76,12 @@ module DAV4Rack
Ox::Element.new(D_ACTIVELOCK).tap do |activelock| Ox::Element.new(D_ACTIVELOCK).tap do |activelock|
if scope if scope
scope = Ox::Element.new("#{DAV_NAMESPACE_NAME}:#{scope}")
activelock << ox_element(D_LOCKSCOPE, scope) activelock << ox_element(D_LOCKSCOPE, scope)
end end
if type if type
activelock << ox_element(D_LOCKTYPE, type) type = Ox::Element.new("#{DAV_NAMESPACE_NAME}:#{type}")
activelock << ox_element(D_LOCKTYPE)
end end
activelock << ox_element(D_DEPTH, depth) activelock << ox_element(D_DEPTH, depth)
activelock << ox_element(D_TIMEOUT, activelock << ox_element(D_TIMEOUT,

View File

@ -44,7 +44,7 @@ module RedmineDmsf
ret ret
end 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 # Raise a lock error if entity is locked, but its not at resource level
existing = lock(false) existing = lock(false)
raise DmsfLockError.new(l(:error_resource_or_parent_locked)) if self.locked? && existing.empty? 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)) raise DmsfLockError.new(l(:error_parent_locked))
else else
existing.each do |l| 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
end end
else else
@ -68,13 +71,17 @@ module RedmineDmsf
l.entity_type = self.is_a?(DmsfFile) ? 0 : 1 l.entity_type = self.is_a?(DmsfFile) ? 0 : 1
l.lock_type = type l.lock_type = type
l.lock_scope = scope l.lock_scope = scope
###
#Rails.logger.info ">>> #{scope}"
###
l.user = User.current l.user = User.current
l.expires_at = expire l.expires_at = expire
l.dmsf_file_last_revision_id = self.last_revision.id if self.is_a?(DmsfFile) l.dmsf_file_last_revision_id = self.last_revision.id if self.is_a?(DmsfFile)
l.owner = owner
l.save! l.save!
reload # reload
locks.reload # locks.reload
l.reload # l.reload
l l
end end
@ -82,12 +89,12 @@ module RedmineDmsf
return false unless self.locked? return false unless self.locked?
existing = self.lock(true) existing = self.lock(true)
# If its empty its a folder that's locked (not root) # 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 end
# #
# By using the path upwards, surely this would be quicker? # By using the path upwards, surely this would be quicker?
def locked_for_user? def locked_for_user?(args = nil)
return false unless locked? return false unless locked?
b_shared = nil b_shared = nil
self.dmsf_path.each do |entity| self.dmsf_path.each do |entity|
@ -95,19 +102,34 @@ module RedmineDmsf
next if locks.empty? next if locks.empty?
locks.each do |lock| locks.each do |lock|
next if lock.expired? # In case we're in between updates 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 else
b_shared = true if b_shared.nil? 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 end
end
Rails.logger.info ">>> #{b_shared}"
return true if b_shared return true if b_shared
end end
Rails.logger.info ">>> false"
false false
end 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? raise DmsfLockError.new(l(:warning_file_not_locked)) unless self.locked?
existing = self.lock(true) 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) 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 else
b_destroyed = false b_destroyed = false
existing.each do |lock| 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 lock.destroy
b_destroyed = true b_destroyed = true
break break

View File

@ -118,7 +118,7 @@ module RedmineDmsf
new_path = @path new_path = @path
new_path = new_path + '/' unless new_path[-1,1] == '/' new_path = new_path + '/' unless new_path[-1,1] == '/'
new_path = '/' + new_path unless new_path[0,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 end
def child_project(p) 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[-1,1] == '/'
new_path = '/' + new_path unless new_path[0,1] == '/' new_path = '/' + new_path unless new_path[0,1] == '/'
new_path += project_display_name 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 end
def parent def parent
@ -240,6 +240,9 @@ module RedmineDmsf
else else
@file = DmsfFile.find_file_by_name(@project, @folder, pinfo.first) @file = DmsfFile.find_file_by_name(@project, @folder, pinfo.first)
@folder = nil @folder = nil
unless (pinfo.length < 2 || @subproject || @folder || @file)
raise Conflict
end
break # We're at the end break # We're at the end
end end
end end

View File

@ -38,8 +38,20 @@ module RedmineDmsf
allow_unauthenticated_options_on_root: true, allow_unauthenticated_options_on_root: true,
namespaces: { namespaces: {
'http://apache.org/dav/props/' => 'd', 'http://apache.org/dav/props/' => 'd',
'http://ucb.openoffice.org/dav/props/' => 'd', 'http://ucb.openoffice.org/dav/props/' => 'd', # LibreOffice
'SAR:' => 'd' '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 end

View File

@ -28,6 +28,38 @@ module RedmineDmsf
class DmsfResource < BaseResource class DmsfResource < BaseResource
include Redmine::I18n 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 # Gather collection of objects that denote current entities child entities
# Used for listing directories etc, implemented basic caching because otherwise # 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 # Our already quite heavy usage of DB would just get silly every time we called
@ -112,7 +144,6 @@ module RedmineDmsf
end end
# Process incoming GET request # Process incoming GET request
#
# If instance is a collection, calls html_display (defined in base_resource.rb) which cycles through children for display # 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 # File will only be presented for download if user has permission to view files
def get(request, response) def get(request, response)
@ -129,15 +160,13 @@ module RedmineDmsf
end end
# Process incoming MKCOL request # Process incoming MKCOL request
#
# Create a DmsfFolder at location requested, only if parent is a folder (or root) # 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 def make_collection
if request.body.read.to_s.empty? if request.body.read.to_s.empty?
raise NotFound unless project && project.module_enabled?('dmsf') raise NotFound unless project && project.module_enabled?('dmsf')
raise Forbidden unless User.current.admin? || User.current.allowed_to?(:folder_manipulation, project) 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)) 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 = DmsfFolder.new
f.title = basename f.title = basename
f.dmsf_folder_id = parent.folder&.id f.dmsf_folder_id = parent.folder&.id
@ -150,13 +179,13 @@ module RedmineDmsf
end end
# Process incoming DELETE request # Process incoming DELETE request
#
# <instance> should be of entity to be deleted, we simply follow the Dmsf entity method # <instance> should be of entity to be deleted, we simply follow the Dmsf entity method
# for deletion and return of appropriate status based on outcome. # for deletion and return of appropriate status based on outcome.
def delete def delete
if file if file
raise Forbidden unless User.current.admin? || User.current.allowed_to?(:file_delete, project) 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 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'] pattern = Setting.plugin_redmine_dmsf['dmsf_webdav_disable_versioning']
if pattern.present? && basename.match(pattern) if pattern.present? && basename.match(pattern)
# Files that are not versioned should be destroyed # Files that are not versioned should be destroyed
@ -174,6 +203,7 @@ module RedmineDmsf
Conflict Conflict
end end
elsif folder elsif folder
raise Locked if folder.locked?
# To fullfil Litmus requirements to not delete folder if fragments are in the URL # To fullfil Litmus requirements to not delete folder if fragments are in the URL
uri = URI(uri_encode(request.get_header('REQUEST_URI'))) uri = URI(uri_encode(request.get_header('REQUEST_URI')))
raise BadRequest if uri.fragment.present? raise BadRequest if uri.fragment.present?
@ -186,124 +216,116 @@ module RedmineDmsf
end end
# Process incoming MOVE request # Process incoming MOVE request
#
# Behavioural differences between collection and single entity # Behavioural differences between collection and single entity
# TODO: Support overwrite between both types of entity, and implement better checking def move(dest_path, overwrite)
def move(dest, overwrite) dest = ResourceProxy.new(dest_path, @request, @response, @options.merge(user: @user))
dest = @__proxy.class.new(dest, @request, @response, @options.merge(user: @user)) return PreconditionFailed if !dest.resource.is_a?(DmsfResource) || dest.resource.project.nil?
# All of this should carry across the ResourceProxy frontend, we ensure this to parent = dest.resource.parent
# prevent unexpected errors raise Forbidden unless dest.resource.project.module_enabled?(:dmsf)
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)
if !parent.exist? || (!User.current.admin? && (!DmsfFolder.permissions?(folder, false) || if !parent.exist? || (!User.current.admin? && (!DmsfFolder.permissions?(folder, false) ||
!DmsfFolder.permissions?(parent.folder, false))) !DmsfFolder.permissions?(parent.folder, false)))
raise Forbidden raise Forbidden
end end
if collection? if collection?
if dest.exist? if dest.exist?
return overwrite ? NotImplemented : PreconditionFailed if overwrite
if folder
(dest.collection?)&.delete true
end
else
return PreconditionFailed
end
end end
if !User.current.admin? && (!User.current.allowed_to?(:folder_manipulation, project) || 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 raise Forbidden
end end
unless folder
return MethodNotAllowed # Moving sub-project not enabled
end
raise Locked if folder.locked_for_user?
# Change the title # Change the title
return MethodNotAllowed unless folder # Moving sub-project not enabled folder.title = dest.resource.basename
folder.title = resource.basename
return PreconditionFailed unless folder.save return PreconditionFailed unless folder.save
# Move to a new destination # 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 else
if !User.current.admin? && (!User.current.allowed_to?(:file_manipulation, project) || 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 raise Forbidden
end end
if dest.exist? raise Locked if file.locked_for_user?
if dest.exist? && (!dest.collection?)
return PreconditionFailed unless overwrite return PreconditionFailed unless overwrite
if (project == resource.project) && file.name.match(/.\.tmp$/i) if dest.resource.file.last_revision.size == 0 || reuse_version_for_locked_file(dest.resource.file)
# 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 # Last revision in the destination has zero size so reuse that revision
new_revision = resource.file.last_revision new_revision = dest.resource.file.last_revision
else else
# Create a new revison by cloning the last revision in the destination # Create a new revison by cloning the last revision in the destination
new_revision = resource.file.last_revision.clone new_revision = dest.resource.file.last_revision.clone
new_revision.increase_version 1 new_revision.increase_version 1
end end
# The file on disk must be renamed from .tmp to the correct filetype or else Xapian won't know how to index. # 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 # Copy file.last_revision.disk_file to new_revision.disk_file
new_revision.size = file.last_revision.size new_revision.size = file.last_revision.size
new_revision.disk_filename = new_revision.new_storage_filename 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| File.open(file.last_revision.disk_file, 'rb') do |f|
new_revision.copy_file_content f new_revision.copy_file_content f
end end
# Save # Save
new_revision.save && resource.file.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 # 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 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.
NotImplemented
end
else else
return PreconditionFailed unless exist? && file return PreconditionFailed unless exist? && file
if (project == resource.project) && resource.basename.match(/.\.tmp$/i) if (project == dest.resource.project) && dest.resource.basename.match(/.\.tmp$/i)
Rails.logger.info "WebDAV MOVE: #{file.name} -> #{resource.basename}, possible MSOffice rename to .tmp when saving." 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. # 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 Created
else 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 # Moving a zero sized file within the same project, just update the dmsf_folder
file.dmsf_folder = parent&.folder file.dmsf_folder = parent&.folder
else else
return InternalServerError unless file.move_to(resource.project, parent&.folder) return InternalServerError unless file.move_to(dest.resource.project, parent&.folder)
end end
# Update Revision and names of file [We can link to old physical resource, as it's not changed] # Update Revision and names of file [We can link to old physical resource, as it's not changed]
if file.last_revision if file.last_revision
file.last_revision.name = resource.basename file.last_revision.name = dest.resource.basename
file.last_revision.title = DmsfFileRevision.filename_to_title(resource.basename) file.last_revision.title = DmsfFileRevision.filename_to_title(dest.resource.basename)
end end
file.name = resource.basename file.name = dest.resource.basename
# Save Changes # 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
end end
end end
# Process incoming COPY request # Process incoming COPY request
#
# Behavioural differences between collection and single entity # 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) def copy(dest, overwrite, depth)
dest = @__proxy.class.new(dest, @request, @response, @options.merge(user: @user)) dest = ResourceProxy.new(dest, @request, @response, @options.merge(user: @user))
# All of this should carry across the ResourceProxy frontend, we ensure this to return PreconditionFailed unless dest.resource.project
# prevent unexpected errors parent = dest.resource.parent
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
raise Forbidden unless (!parent.exist? || !parent.folder || DmsfFolder.permissions?(parent.folder, false)) 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? 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 return PreconditionFailed unless parent.exist? && parent.folder
if collection? if collection?
# Permission check if they can manipulate folders and view folders # Permission check if they can manipulate folders and view folders
# Can they: # Can they:
@ -312,14 +334,13 @@ module RedmineDmsf
# View files on the source project :view_dmsf_files # View files on the source project :view_dmsf_files
# View fodlers on the source project :view_dmsf_folders # View fodlers on the source project :view_dmsf_folders
raise Forbidden unless User.current.admin? || raise Forbidden unless User.current.admin? ||
(User.current.allowed_to?(:folder_manipulation, resource.project) && (User.current.allowed_to?(:folder_manipulation, dest.resource.project) &&
User.current.allowed_to?(:view_dmsf_folders, 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_files, project) &&
User.current.allowed_to?(:view_dmsf_folders, project)) User.current.allowed_to?(:view_dmsf_folders, project))
raise Forbidden unless DmsfFolder.permissions?(folder, false) raise Forbidden unless DmsfFolder.permissions?(folder, false)
folder.title = dest.resource.basename
folder.title = resource.basename new_folder = folder.copy_to(dest.resource.project, parent.folder)
new_folder = folder.copy_to(resource.project, parent.folder)
return PreconditionFailed if new_folder.nil? || new_folder.id.nil? return PreconditionFailed if new_folder.nil? || new_folder.id.nil?
Created Created
else else
@ -329,32 +350,67 @@ module RedmineDmsf
# View files on destination project :view_dmsf_files # View files on destination project :view_dmsf_files
# View files on the source project :view_dmsf_files # View files on the source project :view_dmsf_files
raise Forbidden unless User.current.admin? || raise Forbidden unless User.current.admin? ||
(User.current.allowed_to?(:file_manipulation, resource.project) && (User.current.allowed_to?(:file_manipulation, dest.resource.project) &&
User.current.allowed_to?(:view_dmsf_files, resource.project) && User.current.allowed_to?(:view_dmsf_files, dest.resource.project) &&
User.current.allowed_to?(:view_dmsf_files, project)) User.current.allowed_to?(:view_dmsf_files, project))
return PreconditionFailed unless exist? && file 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) 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)
# 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.last_revision.name = resource.basename new_file.name = dest.resource.basename
new_file.name = resource.basename
# Save Changes # Save Changes
(new_file.last_revision.save && new_file.save) ? Created : PreconditionFailed (new_file.last_revision.save && new_file.save) ? res : PreconditionFailed
end end
end end
# Lock Check # Lock Check
# Check for the existence of locks # Check for the existence of locks
# At present as deletions of folders are not recursive, we do not need to extend def lock_check(args = {})
# this to cover every file, just queried entity = file || folder
def lock_check(lock_scope = nil) if entity
if file refresh = args && (!args[:scope]) && (!args[:type])
raise Locked if file.locked_for_user? args ||= {}
elsif folder args[:method] = @request.request_method.downcase
raise Locked if folder.locked_for_user? http_if = request.get_header('HTTP_IF')
if http_if.present?
no_lock = http_if.include?('<DAV:no-lock>')
not_no_lock = http_if.include?('Not <DAV:no-lock>')
# 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
end end
@ -366,11 +422,9 @@ module RedmineDmsf
raise e raise e
end end
unless exist? unless exist?
e = DAV4Rack::LockFailure.new return super(args)
e.add_failure @path, NotFound
raise e
end end
lock_check args[:scope] lock_check args
entity = file || folder entity = file || folder
unless entity unless entity
e = DAV4Rack::LockFailure.new e = DAV4Rack::LockFailure.new
@ -378,22 +432,20 @@ module RedmineDmsf
raise e raise e
end end
begin 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 # If scope and type are not defined, the only thing we can
# logically assume is that the lock is being refreshed (office loves # 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 # 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 # and ultimately extend it, otherwise we return Conflict for any failure
if (!args[:scope]) && (!args[:type]) # Perhaps a lock refresh refresh = args && (!args[:scope]) && (!args[:type]) # Perhaps a lock refresh
http_if = request.env['HTTP_IF'] if refresh
http_if = request.get_header('HTTP_IF')
if http_if.blank? if http_if.blank?
e = DAV4Rack::LockFailure.new e = DAV4Rack::LockFailure.new
e.add_failure @path, Conflict e.add_failure @path, Conflict
raise e raise e
end end
l = nil 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})/ 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) l = DmsfLock.find_by(uuid: $1)
end end
unless l unless l
@ -406,15 +458,12 @@ module RedmineDmsf
@response['Lock-Token'] = l.uuid @response['Lock-Token'] = l.uuid
return [1.weeks.to_i, l.uuid] return [1.weeks.to_i, l.uuid]
end end
scope = "scope_#{(args[:scope] || 'exclusive')}".to_sym scope = "scope_#{(args[:scope] || 'exclusive')}".to_sym
type = "type_#{(args[:type] || 'write')}".to_sym type = "type_#{(args[:type] || 'write')}".to_sym
# l should be the instance of the lock we've just created # l should be the instance of the lock we've just created
l = entity.lock!(scope, type, Time.current + 1.weeks) l = entity.lock!(scope, type, Time.current + 1.weeks, args[:owner])
@response['Lock-Token'] = l.uuid @response['Lock-Token'] = l.uuid
[1.week.to_i, l.uuid] [1.week.to_i, l.uuid]
end
rescue DmsfLockError rescue DmsfLockError
e = DAV4Rack::LockFailure.new e = DAV4Rack::LockFailure.new
e.add_failure @path, Conflict 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 # Token based unlock (authenticated) will ensure that a correct token is sent, further ensuring
# ownership of token before permitting unlock # ownership of token before permitting unlock
def unlock(token) def unlock(token)
return NotFound unless exist? unless exist?
return super(token)
end
if token.nil? || token.empty? || (token == '<(null)>') || User.current.anonymous? if token.nil? || token.empty? || (token == '<(null)>') || User.current.anonymous?
BadRequest BadRequest
else else
@ -436,14 +487,13 @@ module RedmineDmsf
return BadRequest return BadRequest
end end
begin begin
entity = file || folder
l = DmsfLock.find(token) 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 # 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 # This should throw forbidden as only the lock at level initiated should be unlocked
entity = file || folder
return NoContent unless entity&.locked? return NoContent unless entity&.locked?
l_entity = l.file || l.folder l_entity = l.file || l.folder
if entity.locked_for_user? || (l_entity != entity) if l_entity != entity
Forbidden Forbidden
else else
entity.unlock! entity.unlock!
@ -456,7 +506,6 @@ module RedmineDmsf
end end
# HTTP POST request. # HTTP POST request.
#
# Forbidden, as method should not be utilized. # Forbidden, as method should not be utilized.
def post(request, response) def post(request, response)
raise Forbidden raise Forbidden
@ -467,7 +516,6 @@ module RedmineDmsf
raise BadRequest if collection? raise BadRequest if collection?
raise Forbidden unless User.current.admin? || User.current.allowed_to?(:file_manipulation, project) 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)) raise Forbidden unless (!parent.exist? || !parent.folder || DmsfFolder.permissions?(parent.folder, false))
# Ignore file name patterns given in the plugin settings # Ignore file name patterns given in the plugin settings
pattern = Setting.plugin_redmine_dmsf['dmsf_webdav_ignore'] pattern = Setting.plugin_redmine_dmsf['dmsf_webdav_ignore']
pattern = /^(\._|\.DS_Store$|Thumbs.db$)/ if pattern.blank? pattern = /^(\._|\.DS_Store$|Thumbs.db$)/ if pattern.blank?
@ -475,23 +523,18 @@ module RedmineDmsf
Rails.logger.info "#{basename} ignored" Rails.logger.info "#{basename} ignored"
return NoContent return NoContent
end end
reuse_revision = false reuse_revision = false
if exist? # We're over-writing something, so ultimately a new revision if exist? # We're over-writing something, so ultimately a new revision
f = file f = file
# Disable versioning for file name patterns given in the plugin settings. # Disable versioning for file name patterns given in the plugin settings.
pattern = Setting.plugin_redmine_dmsf['dmsf_webdav_disable_versioning'] pattern = Setting.plugin_redmine_dmsf['dmsf_webdav_disable_versioning']
if pattern.present? && basename.match(pattern) if pattern.present? && basename.match(pattern)
Rails.logger.info "Versioning disabled for #{basename}" Rails.logger.info "Versioning disabled for #{basename}"
reuse_revision = true reuse_revision = true
end end
if reuse_version_for_locked_file(file) if reuse_version_for_locked_file(file)
reuse_revision = true reuse_revision = true
end end
last_revision = file.last_revision last_revision = file.last_revision
if last_revision.size == 0 || reuse_revision if last_revision.size == 0 || reuse_revision
new_revision = last_revision new_revision = last_revision
@ -504,10 +547,8 @@ module RedmineDmsf
new_revision = DmsfFileRevision.new new_revision = DmsfFileRevision.new
end end
# Custom fields # Custom fields
i = 0 last_revision.custom_field_values.each_with_index do |custom_value, i|
last_revision.custom_field_values.each do |custom_value|
new_revision.custom_field_values[i].value = custom_value new_revision.custom_field_values[i].value = custom_value
i = i + 1
end end
end end
else else
@ -586,10 +627,10 @@ module RedmineDmsf
def lockdiscovery def lockdiscovery
entity = file || folder entity = file || folder
return [] unless entity&.locked? return [] unless entity&.locked?
if entity.dmsf_folder && entity.dmsf_folder.locked? if entity.dmsf_folder&.locked?
entity.lock.reverse[0].folder.locks(false) # longwinded way of getting base items locks entity.lock.reverse[0].folder.locks # longwinded way of getting base items locks
else else
entity.lock(false) entity.lock false
end end
end end
@ -632,6 +673,7 @@ module RedmineDmsf
end end
private private
# Prepare file for download using Rack functionality: # Prepare file for download using Rack functionality:
# Download (see RedmineDmsf::Webdav::Download) extends Rack::File to allow single-file # 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 # implementation of service for request, which allows for us to pipe a single file through
@ -653,15 +695,13 @@ module RedmineDmsf
File.new disk_file File.new disk_file
end end
private
def reuse_version_for_locked_file(file) def reuse_version_for_locked_file(file)
locks = file.lock locks = file.lock
locks.each do |lock| locks.each do |lock|
next if lock.expired? next if lock.expired?
# lock should be exclusive but just in case make sure we find this users lock # lock should be exclusive but just in case make sure we find this users lock
next if lock.user != User.current 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. # At least one new revision has been created since the lock was created, reuse that revision.
return true return true
end end
@ -669,6 +709,32 @@ module RedmineDmsf
false false
end 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
end end
end end

View File

@ -162,6 +162,10 @@ module RedmineDmsf
@resource_c.get_property element @resource_c.get_property element
end end
def remove_property(element)
@resource_c.remove_property element
end
def properties def properties
@resource_c.properties @resource_c.properties
end end
@ -170,6 +174,10 @@ module RedmineDmsf
@resource_c.propstats response, stats @resource_c.propstats response, stats
end end
def set_property(element, value)
@resource_c.set_property element, value
end
private private
def get_resource_class(path) def get_resource_class(path)

View File

@ -59,7 +59,7 @@ class DmsfWebdavDeleteTest < RedmineDmsf::Test::IntegrationTest
def test_not_existed_project def test_not_existed_project
delete '/dmsf/webdav/not_a_project/file.txt', params: nil, headers: @admin delete '/dmsf/webdav/not_a_project/file.txt', params: nil, headers: @admin
assert_response :not_found assert_response :conflict
end end
def test_dmsf_not_enabled def test_dmsf_not_enabled
@ -130,7 +130,7 @@ class DmsfWebdavDeleteTest < RedmineDmsf::Test::IntegrationTest
def test_folder_delete_by_user_with_project_names def test_folder_delete_by_user_with_project_names
Setting.plugin_redmine_dmsf['dmsf_webdav_use_project_names'] = true Setting.plugin_redmine_dmsf['dmsf_webdav_use_project_names'] = true
delete "/dmsf/webdav/#{@project1.identifier}/#{@folder6.title}", params: nil, headers: @jsmith 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)) p1name_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1))
delete "/dmsf/webdav/#{p1name_uri}/#{@folder6.title}", params: nil, headers: @jsmith delete "/dmsf/webdav/#{p1name_uri}/#{@folder6.title}", params: nil, headers: @jsmith
assert_response :success assert_response :success
@ -155,7 +155,7 @@ class DmsfWebdavDeleteTest < RedmineDmsf::Test::IntegrationTest
def test_file_delete_by_user_with_project_names def test_file_delete_by_user_with_project_names
Setting.plugin_redmine_dmsf['dmsf_webdav_use_project_names'] = '1' Setting.plugin_redmine_dmsf['dmsf_webdav_use_project_names'] = '1'
delete "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", params: nil, headers: @jsmith 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)) p1name_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1))
delete "/dmsf/webdav/#{p1name_uri}/#{@file1.name}", params: nil, headers: @jsmith delete "/dmsf/webdav/#{p1name_uri}/#{@file1.name}", params: nil, headers: @jsmith
assert_response :success assert_response :success

View File

@ -116,7 +116,7 @@ class DmsfWebdavGetTest < RedmineDmsf::Test::IntegrationTest
Setting.plugin_redmine_dmsf['dmsf_webdav_use_project_names'] = true Setting.plugin_redmine_dmsf['dmsf_webdav_use_project_names'] = true
project1_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1)) project1_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1))
get "/dmsf/webdav/#{@project1.identifier}/test.txt", params: nil, headers: @admin 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 get "/dmsf/webdav/#{project1_uri}/test.txt", params: nil, headers: @admin
assert_response :success assert_response :success
end end

View File

@ -54,7 +54,7 @@ class DmsfWebdavHeadTest < RedmineDmsf::Test::IntegrationTest
check_headers_exist check_headers_exist
Setting.plugin_redmine_dmsf['dmsf_webdav_use_project_names'] = '1' Setting.plugin_redmine_dmsf['dmsf_webdav_use_project_names'] = '1'
head "/dmsf/webdav/#{@project1.identifier}/test.txt", params: nil, headers: @admin 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 head "/dmsf/webdav/#{@project1_uri}/test.txt", params: nil, headers: @admin
assert_response :success assert_response :success
end end

View File

@ -41,8 +41,7 @@ class DmsfWebdavLockTest < RedmineDmsf::Test::IntegrationTest
log_user 'admin', 'admin' log_user 'admin', 'admin'
process :lock, "/dmsf/webdav/#{@file2.project.identifier}/#{@file2.name}", params: @xml, process :lock, "/dmsf/webdav/#{@file2.project.identifier}/#{@file2.name}", params: @xml,
headers: @admin.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite' }) headers: @admin.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite' })
assert_response :multi_status assert_response :locked
assert_match '<d:status>HTTP/1.1 409 Conflict</d:status>', response.body
end end
def test_lock_file def test_lock_file
@ -50,7 +49,6 @@ class DmsfWebdavLockTest < RedmineDmsf::Test::IntegrationTest
create_time = Time.utc(2000, 1, 2, 3, 4, 5) create_time = Time.utc(2000, 1, 2, 3, 4, 5)
refresh_time = Time.utc(2000, 1, 2, 6, 7, 8) refresh_time = Time.utc(2000, 1, 2, 6, 7, 8)
lock_token = nil lock_token = nil
# Time travel, will make the usec part of the time 0 # Time travel, will make the usec part of the time 0
travel_to create_time do travel_to create_time do
# Lock file # Lock file
@ -62,8 +60,7 @@ class DmsfWebdavLockTest < RedmineDmsf::Test::IntegrationTest
# <d:prop xmlns:d=\"DAV:\"> # <d:prop xmlns:d=\"DAV:\">
# <d:lockdiscovery> # <d:lockdiscovery>
# <d:activelock> # <d:activelock>
# <d:lockscope>exclusive</d:lockscope> # <d:lockscope><d:exclusive/></d:lockscope>
# <d:locktype>write</d:locktype>
# <d:depth>infinity</d:depth> # <d:depth>infinity</d:depth>
# <d:timeout>Second-604800</d:timeout> # <d:timeout>Second-604800</d:timeout>
# <d:locktoken> # <d:locktoken>
@ -72,8 +69,7 @@ class DmsfWebdavLockTest < RedmineDmsf::Test::IntegrationTest
# </d:activelock> # </d:activelock>
# </d:lockdiscovery> # </d:lockdiscovery>
# </d:prop> # </d:prop>
assert_match '<d:lockscope>exclusive</d:lockscope>', response.body assert_match '<d:lockscope><d:exclusive/></d:lockscope>', response.body
assert_match '<d:locktype>write</d:locktype>', response.body
assert_match '<d:depth>infinity</d:depth>', response.body assert_match '<d:depth>infinity</d:depth>', response.body
# 1.week = 7*24*3600=604800 seconds # 1.week = 7*24*3600=604800 seconds
assert_match '<d:timeout>Second-604800</d:timeout>', response.body assert_match '<d:timeout>Second-604800</d:timeout>', response.body
@ -88,12 +84,14 @@ class DmsfWebdavLockTest < RedmineDmsf::Test::IntegrationTest
assert_equal create_time, l.updated_at assert_equal create_time, l.updated_at
assert_equal (create_time + 1.week), l.expires_at assert_equal (create_time + 1.week), l.expires_at
end end
travel_to refresh_time do travel_to refresh_time do
# Refresh lock # Refresh lock
process :lock, "/dmsf/webdav/#{@project1.identifier}/#{@file9.name}", xml = %{<?xml version="1.0" encoding="utf-8" ?>
params: nil, <d:lockinfo xmlns:d="DAV:">
headers: @jsmith.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite', HTTP_IF: lock_token }) <d:owner>jsmith</d:owner>
</d:lockinfo>}
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 assert_response :success
# 1.week = 7*24*3600=604800 seconds # 1.week = 7*24*3600=604800 seconds
assert_match '<d:timeout>Second-604800</d:timeout>', response.body assert_match '<d:timeout>Second-604800</d:timeout>', response.body

View File

@ -38,7 +38,7 @@ class DmsfWebdavMkcolTest < RedmineDmsf::Test::IntegrationTest
def test_should_not_succeed_on_a_non_existant_project def test_should_not_succeed_on_a_non_existant_project
process :mkcol, '/dmsf/webdav/project_doesnt_exist/test1', params: nil, headers: @admin process :mkcol, '/dmsf/webdav/project_doesnt_exist/test1', params: nil, headers: @admin
assert_response :not_found assert_response :conflict
end end
def test_should_not_succed_on_a_non_dmsf_enabled_project 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 Setting.plugin_redmine_dmsf['dmsf_webdav_use_project_names'] = true
project1_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1)) project1_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1))
process :mkcol, "/dmsf/webdav/#{@project1.identifier}/test2", params: nil, headers: @jsmith 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 process :mkcol, "/dmsf/webdav/#{project1_uri}/test3", params: nil, headers: @jsmith
assert_response :success # Created assert_response :success # Created
end end

View File

@ -166,16 +166,11 @@ class DmsfWebdavMoveTest < RedmineDmsf::Test::IntegrationTest
end end
def test_move_to_existing_filename def test_move_to_existing_filename
file9 = DmsfFile.find_by(id: 9) assert_no_difference '@file9.dmsf_file_revisions.count' do
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, process :move, "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", params: nil,
headers: @jsmith.merge!({ headers: @jsmith.merge!({
destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{new_name}"}) destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{@file9.name}"})
assert_response :not_implemented assert_response :success
end
end end
end end

View File

@ -40,7 +40,7 @@ class DmsfWebdavUnlockTest < RedmineDmsf::Test::IntegrationTest
l = @file2.locks.first l = @file2.locks.first
process :unlock, "/dmsf/webdav/#{@file2.project.identifier}/#{@file2.name}", params: nil, 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 }) headers: @jsmith.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite', HTTP_LOCK_TOKEN: l.uuid })
assert_response :not_found assert_response :forbidden
end end
def test_unlock_file_with_invalid_token def test_unlock_file_with_invalid_token
@ -67,7 +67,7 @@ class DmsfWebdavUnlockTest < RedmineDmsf::Test::IntegrationTest
# folder1 is missing in the path # folder1 is missing in the path
process :unlock, "/dmsf/webdav/#{@folder2.project.identifier}/#{@folder2.title}", params: nil, 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 }) headers: @jsmith.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite', HTTP_LOCK_TOKEN: l.uuid })
assert_response :not_found assert_response :forbidden
end end
def test_unlock_folder def test_unlock_folder