Add support for Extended MKCOL (RFC5689)
This commit is contained in:
committed by
Brandon Robins
parent
46ff7a934f
commit
3b65768e40
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user