Add support for Extended MKCOL (RFC5689)
This commit is contained in:
committed by
Brandon Robins
parent
46ff7a934f
commit
3b65768e40
@@ -1,9 +1,10 @@
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.3
|
||||
Exclude:
|
||||
- 'spec/**/*'
|
||||
- 'spec/dummy/**/*'
|
||||
Metrics/BlockLength:
|
||||
Exclude:
|
||||
- 'spec/**/*'
|
||||
- 'Rakefile'
|
||||
Metrics/ClassLength:
|
||||
Exclude:
|
||||
@@ -15,6 +16,8 @@ Metrics/AbcSize:
|
||||
Metrics/LineLength:
|
||||
Exclude:
|
||||
- 'lib/calligraphy/rails/mapper.rb'
|
||||
- 'spec/spec_helper.rb'
|
||||
- 'spec/rails_helper.rb'
|
||||
Metrics/MethodLength:
|
||||
Exclude:
|
||||
- 'lib/calligraphy/rails/mapper.rb'
|
||||
|
||||
@@ -44,7 +44,12 @@ module Calligraphy
|
||||
end
|
||||
|
||||
def mkcol
|
||||
Calligraphy::Mkcol.new(web_dav_request).execute
|
||||
mkcol_request = Calligraphy::Mkcol.new(web_dav_request)
|
||||
|
||||
precondition_response = mkcol_request.preconditions
|
||||
return precondition_response unless precondition_response.nil?
|
||||
|
||||
mkcol_request.execute
|
||||
end
|
||||
|
||||
def propfind
|
||||
|
||||
@@ -44,6 +44,23 @@ module Calligraphy
|
||||
File.directory? @src_path
|
||||
end
|
||||
|
||||
# Responsible for creating a duplicate of the resource in
|
||||
# `options[:destination]` (see section 9.8 of RFC4918).
|
||||
#
|
||||
# Used in COPY and MOVE (which inherits from COPY) requests.
|
||||
def copy(options)
|
||||
destination = copy_destination options
|
||||
to_path = join_paths @root_dir, destination
|
||||
to_path_exists = File.exist? to_path
|
||||
|
||||
preserve_existing = false? options[:overwrite]
|
||||
|
||||
copy_resource_to_path to_path, preserve_existing
|
||||
copy_pstore_to_path to_path, preserve_existing
|
||||
|
||||
to_path_exists
|
||||
end
|
||||
|
||||
# Responsible for returning a hash with keys indicating if the resource
|
||||
# can be copied, if an ancestor exists, or if the copy destinatin is
|
||||
# locked.
|
||||
@@ -65,23 +82,6 @@ module Calligraphy
|
||||
copy_options
|
||||
end
|
||||
|
||||
# Responsible for creating a duplicate of the resource in
|
||||
# `options[:destination]` (see section 9.8 of RFC4918).
|
||||
#
|
||||
# Used in COPY and MOVE (which inherits from COPY) requests.
|
||||
def copy(options)
|
||||
destination = copy_destination options
|
||||
to_path = join_paths @root_dir, destination
|
||||
to_path_exists = File.exist? to_path
|
||||
|
||||
preserve_existing = false? options[:overwrite]
|
||||
|
||||
copy_resource_to_path to_path, preserve_existing
|
||||
copy_pstore_to_path to_path, preserve_existing
|
||||
|
||||
to_path_exists
|
||||
end
|
||||
|
||||
# Responsible for creating a new collection based on the resource (see
|
||||
# section 9.3 of RFC4918).
|
||||
#
|
||||
@@ -99,6 +99,12 @@ module Calligraphy
|
||||
FileUtils.rm_r @store_path if store_exist?
|
||||
end
|
||||
|
||||
# Responsible for returning a boolean indicating whether the resource
|
||||
# supports Extended MKCOL (see RFC5689).
|
||||
def enable_extended_mkcol?
|
||||
true
|
||||
end
|
||||
|
||||
# Responsible for returning unique identifier used to create an etag.
|
||||
#
|
||||
# Used in precondition validation, as well as GET, HEAD, and PROPFIND
|
||||
@@ -185,6 +191,8 @@ module Calligraphy
|
||||
#
|
||||
# Used in PROPPATCH requests.
|
||||
def proppatch(nodes)
|
||||
init_pstore unless exists?
|
||||
|
||||
actions = { set: [], remove: [] }
|
||||
|
||||
@store.transaction do
|
||||
@@ -584,6 +592,8 @@ module Calligraphy
|
||||
def add_properties(node, actions)
|
||||
node.children.each do |prop|
|
||||
prop.children.each do |property|
|
||||
next unless node.is_a? Nokogiri::XML::Element
|
||||
|
||||
node = Calligraphy::XML::Node.new property
|
||||
prop_sym = property.name.to_sym
|
||||
|
||||
|
||||
@@ -36,6 +36,14 @@ module Calligraphy
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Responsible for creating a duplicate of the resource in
|
||||
# `options[:destination]` (see section 9.8 of RFC4918).
|
||||
#
|
||||
# Used in COPY and MOVE (which inherits from COPY) requests.
|
||||
def copy(_options)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Responsible for returning a hash with keys indicating if the resource
|
||||
# can be copied, if an ancestor exists, or if the copy destinatin is
|
||||
# locked.
|
||||
@@ -48,14 +56,6 @@ module Calligraphy
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Responsible for creating a duplicate of the resource in
|
||||
# `options[:destination]` (see section 9.8 of RFC4918).
|
||||
#
|
||||
# Used in COPY and MOVE (which inherits from COPY) requests.
|
||||
def copy(_options)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Responsible for creating a new collection based on the resource (see
|
||||
# section 9.3 of RFC4918).
|
||||
#
|
||||
@@ -70,7 +70,10 @@ module Calligraphy
|
||||
#
|
||||
# Used in OPTIONS requests.
|
||||
def dav_compliance
|
||||
'1, 2, 3'
|
||||
compliance_classes = %w[1 2 3]
|
||||
compliance_classes.push 'extended-mkcol' if enable_extended_mkcol?
|
||||
|
||||
compliance_classes.join ', '
|
||||
end
|
||||
|
||||
# Responsible for deleting a resource collection (see section 9.6 of
|
||||
@@ -81,6 +84,12 @@ module Calligraphy
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Responsible for returning a boolean indicating whether the resource
|
||||
# supports Extended MKCOL (see RFC5689).
|
||||
def enable_extended_mkcol?
|
||||
false
|
||||
end
|
||||
|
||||
# Responsible for returning unique identifier used to create an etag.
|
||||
#
|
||||
# Used in precondition validation, as well as GET, HEAD, and PROPFIND
|
||||
@@ -179,6 +188,16 @@ module Calligraphy
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Responsible for declaring the valid `resourcetypes` for a resource. If
|
||||
# an extended MKCOL request is made using an invalid `resourcetype` the
|
||||
# request will fail with a 403 (Forbidden) and will return an XML response
|
||||
# with the `mkcol-response` element (see section 3.3 and 3.5 of RFC5689).
|
||||
#
|
||||
# Used in Extended MKCOL requests.
|
||||
def valid_resourcetypes
|
||||
%w[collection]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# DAV property which can be retrieved by a PROPFIND request. `creationdate`
|
||||
|
||||
@@ -4,20 +4,90 @@ module Calligraphy
|
||||
# Responsible for creating a new collection resource at the location
|
||||
# specified by the request.
|
||||
class Mkcol < WebDavRequest
|
||||
include Calligraphy::XML::Utils
|
||||
|
||||
# Responsible for evaluating preconditions for the WebDAV request.
|
||||
def preconditions
|
||||
return :unsupported_media_type unless validate_request_body
|
||||
return [:forbidden, mkcol_response] unless validate_resourcetypes
|
||||
end
|
||||
|
||||
# Executes the WebDAV request for a particular resource.
|
||||
def execute
|
||||
return :method_not_allowed if @resource.exists?
|
||||
return :conflict unless @resource.ancestor_exist?
|
||||
return :unsupported_media_type unless @resource.request_body.blank?
|
||||
|
||||
xml = @resource.enable_extended_mkcol? ? extended_mkcol_xml : nil
|
||||
|
||||
@resource.create_collection
|
||||
set_content_location_header
|
||||
|
||||
post_mkcol_actions xml
|
||||
|
||||
:created
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_request_body
|
||||
xml = @resource.enable_extended_mkcol? ? extended_mkcol_xml : nil
|
||||
|
||||
if xml == :bad_request
|
||||
false
|
||||
elsif @resource.enable_extended_mkcol?
|
||||
true
|
||||
else
|
||||
@resource.request_body.blank? ? false : true
|
||||
end
|
||||
end
|
||||
|
||||
def validate_resourcetypes
|
||||
return true if body.blank?
|
||||
|
||||
xml = search_xml_for(body: body, search: 'resourcetype').first
|
||||
resourcetypes = xml.children.map do |node|
|
||||
next unless node.is_a? Nokogiri::XML::Element
|
||||
|
||||
node.name
|
||||
end.compact
|
||||
|
||||
resourcetypes.each do |rt|
|
||||
return false unless @resource.valid_resourcetypes.include? rt
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def mkcol_response
|
||||
xml_builder.mkcol_response prepare_mkcol_response_xml
|
||||
end
|
||||
|
||||
def prepare_mkcol_response_xml
|
||||
nodes = search_xml_for(body: body, search: 'prop').first.children
|
||||
|
||||
separate_nodes_by_name nodes, 'resourcetype'
|
||||
end
|
||||
|
||||
def extended_mkcol_xml
|
||||
return nil if body.blank?
|
||||
|
||||
# The `mkcol` tag specifies properties to be set in an extended MKCOL
|
||||
# request, as well as any additional information needed when creating
|
||||
# the resource.
|
||||
xml_for body: body, node: 'mkcol'
|
||||
end
|
||||
|
||||
def post_mkcol_actions(xml)
|
||||
apply_extended_mkcol_properties xml
|
||||
|
||||
set_content_location_header
|
||||
end
|
||||
|
||||
def apply_extended_mkcol_properties(xml)
|
||||
return nil if xml.nil?
|
||||
|
||||
@resource.proppatch xml
|
||||
end
|
||||
|
||||
def set_content_location_header
|
||||
@response.headers['Content-Location'] = @resource.full_request_path
|
||||
end
|
||||
|
||||
@@ -19,6 +19,11 @@ module Calligraphy
|
||||
@resource = resource
|
||||
end
|
||||
|
||||
# Responsible for evaluating preconditions for the WebDAV request.
|
||||
def preconditions
|
||||
raise NotImplemented
|
||||
end
|
||||
|
||||
# Executes the WebDAV request for a particular resource.
|
||||
def execute
|
||||
raise NotImplemented
|
||||
@@ -35,7 +40,7 @@ module Calligraphy
|
||||
end
|
||||
|
||||
def xml_builder
|
||||
protocol = @request.env['SERVER_PROTOCOL']
|
||||
protocol = @request.env['SERVER_PROTOCOL'] || 'HTTP/1.1'
|
||||
|
||||
Calligraphy::XML::Builder.new server_protocol: protocol
|
||||
end
|
||||
|
||||
@@ -86,17 +86,17 @@ module Calligraphy
|
||||
end
|
||||
|
||||
def prop(xml, property_set)
|
||||
xml[@dav_ns].prop do
|
||||
iterate_and_drilldown xml, property_set
|
||||
end
|
||||
xml[@dav_ns].prop { iterate_and_drilldown xml, property_set }
|
||||
end
|
||||
|
||||
def propstat(xml, property_set, status = :ok)
|
||||
def propstat(xml, property_set, status, error_tag: nil, description: nil)
|
||||
return if property_set.empty?
|
||||
|
||||
xml[@dav_ns].propstat do
|
||||
prop xml, property_set
|
||||
status xml, status
|
||||
error xml, error_tag unless error_tag.nil?
|
||||
responsedescription xml, description unless description.nil?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -113,6 +113,16 @@ module Calligraphy
|
||||
Rack::Utils::HTTP_STATUS_CODES[status_code]
|
||||
].join ' '
|
||||
end
|
||||
|
||||
def error(xml, error)
|
||||
xml.error { self_closing_tag xml, error }
|
||||
end
|
||||
|
||||
def responsedescription(xml, description)
|
||||
xml.responsedescription do
|
||||
xml.text description
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,25 +4,50 @@ module Calligraphy
|
||||
module XML
|
||||
# Miscellaneous XML convenience methods.
|
||||
module Utils
|
||||
# Returns the XML for a given XML body and node/CSS selector.
|
||||
# Returns the inner XML for a given XML body and node/CSS selector.
|
||||
def xml_for(body:, node:)
|
||||
xml = Nokogiri::XML body
|
||||
return :bad_request unless xml.errors.empty?
|
||||
|
||||
namespace = nil
|
||||
xml.root.namespace_definitions.each do |n|
|
||||
namespace = "#{n.prefix}|" if dav_namespace n
|
||||
namespace = dav_namespace xml
|
||||
|
||||
xml.css("dav|#{node}", 'dav': namespace).children
|
||||
end
|
||||
|
||||
# Searches XML body for a given node/CSS selector and returns that
|
||||
# node/CSS selector.
|
||||
def search_xml_for(body:, search:)
|
||||
xml = Nokogiri::XML body
|
||||
|
||||
[].tap do |results|
|
||||
xml.namespaces.each_value do |v|
|
||||
results << xml.css("cs|#{search}", 'cs': v)
|
||||
end
|
||||
end.flatten
|
||||
end
|
||||
|
||||
# Iterates through top level nodes, finds node names that match and
|
||||
# separates matching nodes from non-matching nodes.
|
||||
def separate_nodes_by_name(nodes, match_name)
|
||||
{ found: [], not_found: [] }.tap do |property|
|
||||
nodes.each do |node|
|
||||
next unless node.is_a? Nokogiri::XML::Element
|
||||
|
||||
if node.name == match_name
|
||||
property[:found].push Calligraphy::XML::Node.new node
|
||||
else
|
||||
property[:not_found].push Calligraphy::XML::Node.new node
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
node = node.split(' ').map! { |n| namespace + n }.join(' ') if namespace
|
||||
|
||||
xml.css(node).children
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def dav_namespace(namespace)
|
||||
namespace&.href == Calligraphy::DAV_NS && !namespace.prefix.nil?
|
||||
def dav_namespace(xml)
|
||||
xml.namespaces.each_value do |v|
|
||||
return v if v == Calligraphy::DAV_NS
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,10 +8,10 @@ module Calligraphy
|
||||
activelock allprop collection creationdate depth displayname error
|
||||
exclusive getcontentlanguage getcontentlength getcontenttype getetag
|
||||
getlastmodified href include location lockdiscovery lockentry lockinfo
|
||||
lockroot lockscope locktoken locktype multistatus owner prop
|
||||
propertyupdate propfind propname propstat remove response
|
||||
lockroot lockscope locktoken locktype mkcol-response multistatus owner
|
||||
prop propertyupdate propfind propname propstat remove response
|
||||
responsedescription resourcetype set shared status supportedlock
|
||||
timeout write
|
||||
timeout valid-resourcetype write
|
||||
].freeze
|
||||
|
||||
DAV_NS_METHODS = %w[resourcetype supportedlock timeout].freeze
|
||||
@@ -29,6 +29,21 @@ module Calligraphy
|
||||
end
|
||||
end
|
||||
|
||||
# Build an XML response for a failed MKCOL request.
|
||||
def mkcol_response(properties)
|
||||
description = 'Resource type is not supported by this server'
|
||||
error = 'valid-resourcetype'
|
||||
|
||||
build 'mkcol-response' do |xml|
|
||||
propstat(xml, properties[:found],
|
||||
:forbidden, error_tag: error, description: description)
|
||||
|
||||
if properties[:not_found].length.positive?
|
||||
propstat xml, properties[:not_found], :failed_dependency
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Build an XML response for a PROPFIND request.
|
||||
def propfind_response(path, properties)
|
||||
multistatus do |xml|
|
||||
@@ -42,8 +57,8 @@ module Calligraphy
|
||||
def proppatch_response(path, actions)
|
||||
multistatus do |xml|
|
||||
href xml, path
|
||||
propstat xml, actions[:set]
|
||||
propstat xml, actions[:remove]
|
||||
propstat xml, actions[:set], :ok
|
||||
propstat xml, actions[:remove], :ok
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
51
spec/requests/mkcol_spec.rb
Normal file
51
spec/requests/mkcol_spec.rb
Normal file
@@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
require 'support/request_helpers'
|
||||
require 'support/examples/ext_mkcol'
|
||||
|
||||
RSpec.describe 'mkcol', type: :request do
|
||||
before(:all) do
|
||||
tmp_dir = Rails.root.join('../../tmp').to_path
|
||||
Dir.mkdir tmp_dir unless File.exists? tmp_dir
|
||||
|
||||
webdav_dir = Rails.root.join('../../tmp/webdav').to_path
|
||||
FileUtils.rm_r webdav_dir if File.exists? webdav_dir
|
||||
Dir.mkdir webdav_dir
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
allow(Calligraphy).to receive(:enable_digest_authentication)
|
||||
.and_return(false)
|
||||
end
|
||||
|
||||
it 'creates a collection with additional properties' do
|
||||
allow_any_instance_of(Calligraphy::FileResource).to receive(
|
||||
:valid_resourcetypes
|
||||
).and_return(%w[collection special-resource])
|
||||
|
||||
expect(Dir).to receive(:mkdir).and_call_original
|
||||
expect_any_instance_of(Calligraphy::FileResource).to receive(
|
||||
:proppatch
|
||||
)
|
||||
|
||||
mkcol '/webdav/special', headers: {
|
||||
RAW_POST_DATA: Support::Examples::ExtMkcol.rfc5689_3_4
|
||||
}
|
||||
|
||||
expect(response.body.empty?).to eq(true)
|
||||
expect(response.status).to eq(201)
|
||||
end
|
||||
|
||||
context 'with an invalid resource type' do
|
||||
it 'returns an error response' do
|
||||
mkcol '/webdav/special', headers: {
|
||||
RAW_POST_DATA: Support::Examples::ExtMkcol.rfc5689_3_4
|
||||
}
|
||||
|
||||
expect(response.status).to eq(403)
|
||||
expect(response.body).to include('mkcol-response')
|
||||
expect(response.body).to include('valid-resourcetype')
|
||||
end
|
||||
end
|
||||
end
|
||||
51
spec/requests/options_spec.rb
Normal file
51
spec/requests/options_spec.rb
Normal file
@@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
require 'support/request_helpers'
|
||||
|
||||
RSpec.describe 'OPTIONS', type: :request do
|
||||
before(:each) do
|
||||
allow(Calligraphy).to receive(:enable_digest_authentication)
|
||||
.and_return(false)
|
||||
end
|
||||
|
||||
context 'when not using extended MKCOL support' do
|
||||
before(:each) do
|
||||
allow_any_instance_of(Calligraphy::FileResource).to receive(
|
||||
:enable_extended_mkcol?
|
||||
).and_return(false)
|
||||
end
|
||||
|
||||
it 'advertises support for all 3 WebDAV classes' do
|
||||
options '/webdav/special'
|
||||
|
||||
%w[1 2 3].each { |c| expect(response.headers['DAV']).to include(c) }
|
||||
end
|
||||
|
||||
it 'does not advertise support for extended-mkcol' do
|
||||
options '/webdav/special'
|
||||
|
||||
expect(response.headers['DAV']).to_not include('extended-mkcol')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using extended MKCOL support' do
|
||||
before(:each) do
|
||||
allow_any_instance_of(Calligraphy::FileResource).to receive(
|
||||
:enable_extended_mkcol?
|
||||
).and_return(true)
|
||||
end
|
||||
|
||||
it 'advertises support for all 3 WebDAV classes' do
|
||||
options '/webdav/special'
|
||||
|
||||
%w[1 2 3].each { |c| expect(response.headers['DAV']).to include(c) }
|
||||
end
|
||||
|
||||
it 'advertises support for extended-mkcol' do
|
||||
options '/webdav/special'
|
||||
|
||||
expect(response.headers['DAV']).to include('extended-mkcol')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,22 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
require 'support/request_helpers'
|
||||
|
||||
RSpec.describe 'Resource' do
|
||||
context 'base method' do
|
||||
resource_methods_without_inputs = %w(
|
||||
resource_methods_without_inputs = %w[
|
||||
ancestor_exist? collection? create_collection delete_collection etag
|
||||
exists? lock_is_exclusive? locked? read readable? refresh_lock
|
||||
creationdate displayname getcontentlanguage getcontentlength getcontenttype
|
||||
getetag getlastmodified lockdiscovery resourcetype supportedlock
|
||||
)
|
||||
resource_methods_with_inputs = %w(
|
||||
creationdate displayname getcontentlanguage getcontentlength
|
||||
getcontenttype getetag getlastmodified lockdiscovery resourcetype
|
||||
supportedlock
|
||||
]
|
||||
resource_methods_with_inputs = %w[
|
||||
copy copy_options lock locked_to_user? propfind proppatch unlock write
|
||||
)
|
||||
]
|
||||
|
||||
resource_methods_without_inputs.each do |method|
|
||||
describe "##{method}" do
|
||||
it 'raises NotImplementedError' do
|
||||
resource = Calligraphy::Resource.new
|
||||
expect{resource.send(method)}.to raise_exception(NotImplementedError)
|
||||
|
||||
expect { resource.send(method) }.to raise_exception(
|
||||
NotImplementedError
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -25,7 +32,10 @@ RSpec.describe 'Resource' do
|
||||
describe "##{method}" do
|
||||
it 'raises NotImplementedError' do
|
||||
resource = Calligraphy::Resource.new
|
||||
expect{resource.send(method, nil)}.to raise_exception(NotImplementedError)
|
||||
|
||||
expect { resource.send(method, nil) }.to raise_exception(
|
||||
NotImplementedError
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
26
spec/support/examples/ext_mkcol.rb
Normal file
26
spec/support/examples/ext_mkcol.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: false
|
||||
|
||||
module Support
|
||||
module Examples
|
||||
module ExtMkcol
|
||||
# RFC5689: 3.4. Successful Extended MKCOL Request
|
||||
def self.rfc5689_3_4
|
||||
<<~XML
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:mkcol xmlns:D="DAV:"
|
||||
xmlns:E="http://example.com/ns/">
|
||||
<D:set>
|
||||
<D:prop>
|
||||
<D:resourcetype>
|
||||
<D:collection/>
|
||||
<E:special-resource/>
|
||||
</D:resourcetype>
|
||||
<D:displayname>Special Resource</D:displayname>
|
||||
</D:prop>
|
||||
</D:set>
|
||||
</D:mkcol>
|
||||
XML
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
module ActionDispatch
|
||||
module Integration
|
||||
module RequestHelpers
|
||||
%w[copy move mkcol propfind proppatch lock unlock].each do |method|
|
||||
%w[copy move mkcol options propfind proppatch lock unlock].each do |method|
|
||||
define_method method do |path, **args|
|
||||
process method.to_sym, path, **args
|
||||
end
|
||||
Reference in New Issue
Block a user