redmine_dmsf/lib/dav4rack/controller.rb
2021-02-11 14:34:53 +01:00

430 lines
12 KiB
Ruby

# frozen_string_literal: true
require 'uri'
require 'dav4rack/destination_header'
require 'dav4rack/request'
require 'dav4rack/xml_elements'
require 'dav4rack/xml_response'
module DAV4Rack
class Controller
include DAV4Rack::HTTPStatus
include DAV4Rack::Utils
attr_reader :request, :response, :resource
# request:: DAV4Rack::Request
# response:: Rack::Response
# options:: Options hash
# Create a new Controller.
#
# options will be passed on to the resource
def initialize(request, response, options={})
@options = options
@request = request
@response = response
@dav_extensions = options[:dav_extensions]
@always_include_dav_header = !!options[:always_include_dav_header]
@allow_unauthenticated_options_on_root = !!options[:allow_unauthenticated_options_on_root]
setup_resource
if @always_include_dav_header
add_dav_header
end
end
# main entry point, called by the Handler
def process
if skip_authorization? || authenticate
status = process_action || OK
else
status = HTTPStatus::Unauthorized
end
rescue HTTPStatus::Status => e
status = e
ensure
if status
response.status = status.code
if status.code == 401
response.body = authentication_error_message
response['WWW-Authenticate'] = "Basic realm=\"#{authentication_realm}\""
end
end
end
private
# delegates to the handler method matching this requests http method.
# must return an HTTPStatus. If nil / false, the resulting status will be
# 200/OK
def process_action
send request.request_method.downcase
end
# if true, resource#authenticate will never be called
def skip_authorization?
# Microsoft Web Client workaround (from RedmineDmsf plugin):
#
# MS web client will not attempt any authentication (it'd seem)
# until it's acknowledged a completed OPTIONS request.
#
# If the request method is OPTIONS return true, controller will simply
# call the options method within, which accesses nothing, just returns
# headers about the dav env.
return @allow_unauthenticated_options_on_root &&
request.request_method.downcase == 'options' &&
(request.path_info == '/' || request.path_info.empty?)
end
# Return response to OPTIONS
def options
status = resource.options request, response
if(status == OK)
add_dav_header
response['Allow'] ||= 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK'
response['Ms-Author-Via'] ||= 'DAV'
end
status
end
# Return response to HEAD
def head
if(resource.exist?)
res = resource.head(request, response)
if(res == OK)
response['Etag'] ||= resource.etag
response['Content-Type'] ||= resource.content_type
response['Content-Length'] ||= resource.content_length.to_s
response['Last-Modified'] ||= resource.last_modified.httpdate
end
res
else
NotFound
end
end
# Return response to GET
def get
if(resource.exist?)
res = resource.get(request, response)
if(res == OK && !resource.collection?)
response['Etag'] ||= resource.etag
response['Content-Type'] ||= resource.content_type
response['Content-Length'] ||= resource.content_length.to_s
response['Last-Modified'] ||= resource.last_modified.httpdate
end
res
else
NotFound
end
end
# Return response to PUT
def put
if request.get_header('HTTP_CONTENT_RANGE')
# An origin server that allows PUT on a given target resource MUST send
# a 400 (Bad Request) response to a PUT request that contains a
# Content-Range header field.
# Reference: http://tools.ietf.org/html/rfc7231#section-4.3.4
Logger.error 'Content-Range on PUT requests is forbidden.'
BadRequest
elsif(resource.collection?)
Forbidden
elsif(!resource.parent_exists? || !resource.parent_collection?)
Conflict
else
resource.lock_check if resource.supports_locking?
status = resource.put(request, response)
if status == Created
response['Location'] ||= request.url_for resource.path
end
response.body = response['Location'] || ''
status
end
end
# Return response to POST
def post
resource.post(request, response)
end
# Return response to DELETE
def delete
if(resource.exist?)
resource.lock_check if resource.supports_locking?
resource.delete
else
NotFound
end
end
# Return response to MKCOL
def mkcol
if request.content_length.to_i > 0
return UnsupportedMediaType
end
return MethodNotAllowed if resource.exist?
resource.lock_check if resource.supports_locking?
status = resource.make_collection
if(resource.use_compat_mkcol_response?)
url = request.url_for resource.path, collection: true
r = XmlResponse.new(response, resource.namespaces)
r.multistatus do |xml|
xml << r.response(url, status)
end
MultiStatus
else
status
end
end
# Move Resource to new location.
def move
return NotFound unless(resource.exist?)
return BadRequest unless request.depth == :infinity
return BadRequest unless dest = request.destination
if status = dest.validate(host: request.host,
resource_path: resource.path)
return status
end
resource.lock_check if resource.supports_locking?
return resource.move dest.path_info, request.overwrite?
end
# Return response to COPY
def copy
return NotFound unless(resource.exist?)
return BadRequest unless request.depth == :infinity or request.depth == 0
return BadRequest unless dest = request.destination
if status = dest.validate(host: request.host,
resource_path: resource.path)
return status
end
resource.copy dest.path_info, request.overwrite?, request.depth
end
# Return response to PROPFIND
def propfind
return NotFound unless resource.exist?
ns = request.ns
document = request.document if request.content_length.to_i > 0
propfind = document.xpath("//#{ns}propfind") if document
# propname request
if propfind and propfind.xpath("//#{ns}propname").first
r = XmlResponse.new(response, resource.namespaces)
r.multistatus do |xml|
xml << Ox::Raw.new(resource.propnames_xml_with_depth)
end
return MultiStatus
end
properties = if propfind.nil? or
propfind.empty? or
propfind.xpath("//#{ns}allprop").first
resource.properties
elsif prop = propfind.xpath("//#{ns}prop").first
prop.children
.find_all{ |item| item.element? }
.map{ |item|
# We should do this, but Nokogiri transforms prefix w/ null href
# into something valid. Oops.
# TODO: Hacky grep fix that's horrible
hsh = to_element_hash(item)
if(hsh.namespace.nil? && !ns.empty?)
return BadRequest if prop.to_s.scan(%r{<#{item.name}[^>]+xmlns=""}).empty?
end
hsh
}.compact
else
Logger.error "no properties found"
raise BadRequest
end
properties = properties.map{|property| {element: property}}
properties = resource.propfind_add_additional_properties(properties)
prop_xml = resource.properties_xml_with_depth(get: properties)
r = XmlResponse.new(response, resource.namespaces)
r.multistatus do |xml|
xml << r.raw(prop_xml)
end
MultiStatus
end
# Return response to PROPPATCH
def proppatch
return NotFound unless resource.exist?
return BadRequest unless doc = request.document
resource.lock_check if resource.supports_locking?
properties = {}
doc.xpath("/#{request.ns}propertyupdate").children.each do |element|
action = element.name
if action == 'set' || action == 'remove'
properties[action] ||= []
if prp = element.children.detect{|e|e.name == 'prop'}
prp.children.each do |elm|
next if elm.name == 'text'
properties[action] << { element: to_element_hash(elm), value: elm.text }
end
end
end
end
r = XmlResponse.new(response, resource.namespaces)
r.multistatus do |xml|
xml << Ox::Raw.new(resource.properties_xml_with_depth(properties))
end
MultiStatus
end
# Lock current resource
# NOTE: This will pass an argument hash to Resource#lock and
# wait for a success/failure response.
def lock
depth = request.depth
return BadRequest unless depth == 0 || depth == :infinity
asked = { depth: depth }
if timeout = request.env['Timeout']
asked[:timeout] = timeout.split(',').map{|x|x.strip}
end
ns = request.ns
if doc = request.document and lockinfo = doc.xpath("//#{ns}lockinfo")
asked[:scope] = lockinfo.xpath("//#{ns}lockscope").children.find_all{|n|n.element?}.map{|n|n.name}.first
asked[:type] = lockinfo.xpath("#{ns}locktype").children.find_all{|n|n.element?}.map{|n|n.name}.first
asked[:owner] = lockinfo.xpath("//#{ns}owner").children.map{|n|n.text}.first
end
r = XmlResponse.new(response, resource.namespaces)
begin
lock_time, locktoken = resource.lock(asked)
r.render_lockdiscovery(
time: lock_time,
token: locktoken,
depth: asked[:depth].to_s,
scope: asked[:scope],
type: asked[:type],
owner: asked[:owner]
)
response.headers['Lock-Token'] = "<#{locktoken}>"
return resource.exist? ? OK : Created
rescue LockFailure => e
r.multistatus do |xml|
e.path_status.each_pair do |path, status|
xml << r.response(path, status)
end
end
return MultiStatus
end
end
# Unlock current resource
def unlock
resource.unlock(request.lock_token)
end
# Perform authentication
#
# implement your authentication by overriding Resource#authenticate
def authenticate
uname = nil
password = nil
if request.authorization?
auth = Rack::Auth::Basic::Request.new(request.env)
if(auth.basic? && auth.credentials)
uname = auth.credentials[0]
password = auth.credentials[1]
end
end
resource.authenticate uname, password
end
def authentication_error_message
resource.authentication_error_message
end
def authentication_realm
resource.authentication_realm
end
# override this for custom resource initialization
def setup_resource
@resource = resource_class.new(
request.unescaped_path_info, request, response, @options
)
end
# Class of the resource in use
def resource_class
unless @options[:resource_class]
require 'dav4rack/resources/file_resource'
@options[:resource_class] = FileResource
@options[:root] ||= Dir.pwd
end
@options[:resource_class]
end
def add_dav_header
unless response['Dav']
dav_support = ['1']
if resource.supports_locking?
# compliance is resource specific, only advertise 2 (locking) if
# supported on the resource.
dav_support << '2'
end
dav_support += @dav_extensions if @dav_extensions
response['Dav'] = dav_support * ', '
end
end
end
end