# frozen_string_literal: true

require "#{File.dirname(__FILE__)}/uri"
require 'addressable/uri'

module Dav4rack
  # Request
  class Request < Rack::Request
    # Root URI path for the resource
    attr_reader :root_uri_path

    # options:
    #
    # recursive_propfind_allowed (true) : set to false to disable
    # potentially expensive recursive propfinds
    #
    def initialize(env, options = {})
      super(env)
      @options = { recursive_propfind_allowed: true }.merge options
      self.path_info = expand_path path_info
    end

    def authorization?
      !!authorization
    end

    def authorization
      get_header('HTTP_AUTHORIZATION') ||
        get_header('X-HTTP_AUTHORIZATION') ||
        get_header('X_HTTP_AUTHORIZATION') ||
        get_header('REDIRECT_X_HTTP_AUTHORIZATION')
    end

    # path relative to root uri
    def unescaped_path_info
      @unescaped_path_info ||= self.class.unescape_path path_info
    end

    # the full path (script_name aka rack mount point + path_info)
    def unescaped_path
      @unescaped_path ||= self.class.unescape_path path
    end

    def self.unescape_path(path)
      p = path.dup
      p.force_encoding 'UTF-8'
      Addressable::URI.unencode p
    end

    # Namespace being used within XML document
    def ns(wanted_uri = XmlElements::DAV_NAMESPACE)
      if document && (root = document.root) && (ns_defs = root.namespace_definitions) && !ns_defs.empty?
        result = ns_defs.detect { |nd| nd.href == wanted_uri } || ns_defs.first
        result = result.prefix.nil? ? 'xmlns' : result.prefix.to_s
        result += ':' unless result.empty?
        result
      else
        ''
      end
    end

    # Lock token if provided by client
    def lock_token
      get_header 'HTTP_LOCK_TOKEN'
    end

    # Requested depth
    def depth
      @depth ||= if (d = get_header('HTTP_DEPTH')) && %w[0 1].include?(d)
                   d.to_i
                 elsif infinity_depth_allowed?
                   'infinity'
                 else
                   1
                 end
    end

    # Destination header
    def destination
      unless @destination
        h = get_header('HTTP_DESTINATION')
        @destination = DestinationHeader.new Dav4rack::Uri.new(h, script_name: script_name) if h
      end
      @destination
    end

    # Overwrite is allowed
    def overwrite?
      get_header('HTTP_OVERWRITE').to_s.casecmp('F').zero?
    end

    # content_length as a Fixnum (nil if the header is unset / empty)
    def content_length
      length = super || get_header('HTTP_X_EXPECTED_ENTITY_LENGTH')
      length&.to_i
    end

    # parsed XML request body if any (Nokogiri XML doc)
    def document
      @request_document ||= parse_request_body if content_length&.positive?
    end

    # builds a URL for path using this requests scheme, host, port and
    # script_name
    # path must be valid UTF-8 and will be url encoded by this method
    def url_for(rel_path, collection: false)
      path = path_for rel_path, collection: collection
      "#{scheme}://#{host}:#{port}#{path}"
    end

    # returns an url encoded, absolute path for the given relative path
    def path_for(rel_path, collection: false)
      path = Addressable::URI.encode_component rel_path, Addressable::URI::CharacterClasses::PATH
      path << '/' if collection && path[-1] != '/'
      "#{script_name}#{expand_path path}"
    end

    # returns the given path, but with the leading script_name removed. Will
    # return nil if the path does not begin with the script_name
    def path_info_for(full_path, script_name: self.script_name)
      uri = Dav4rack::Uri.new full_path, script_name: script_name
      uri.path_info
    end

    # expands '/foo/../bar' to '/bar', peserving trailing slash and normalizing
    # consecutive slashes. adds a leading slash if missing
    def expand_path(path)
      path = path.squeeze '/'
      path.prepend '/' unless path[0] == '/'
      collection = path.end_with?('/')
      path = ::File.expand_path path
      path << '/' if collection && !path.end_with?('/')
      # remove a drive letter in Windows
      path.gsub(%r{^([^/]*)/}, '/')
    end

    REDIRECTABLE_CLIENTS = [
      /cyberduck/i,
      /konqueror/i
    ].freeze

    # Does client allow GET redirection
    # TODO: Get a comprehensive list in here.
    # TODO: Allow this to be dynamic so users can add regexes to match if they know of a client
    # that can be supported that is not listed.
    def client_allows_redirect?
      ua = user_agent
      REDIRECTABLE_CLIENTS.any? { |re| ua =~ re }
    end

    def get_header(name)
      @env[name]
    end

    private

    # true if Depth: Infinity is allowed for this request.
    #
    # http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
    # Servers ... should support "infinity" requests. In practice,
    # support for infinite-depth requests may be disabled, due to the
    # performance and security concerns associated with this behavior
    def infinity_depth_allowed?
      request_method != 'PROPFIND' or @options[:recursive_propfind_allowed]
    end

    def parse_request_body
      Nokogiri.XML(body.read, &:strict) if body
    rescue StandardError => e
      Rails.logger.error e.message
      raise Dav4rack::HttpStatus::BadRequest
    end
  end
end
