Plasma GitLab Archive
Projects Blog Knowledge

(* $Id: webdav_client_methods.ml 9 2015-01-12 20:00:52Z gerd $ *)

open Webdav_xml
open Webdav_http
open Webdav_compat
open Printf

type call_status =
    [ Http_client.status
    | `Multi_status
    ]

type property = Webdav_xml.property
type prepost_code = Webdav_xml.prepost_code
class type propstat_t = Webdav_xml.propstat_t
class type response_t = Webdav_xml.response_t

class type webdav_call_t =
object
  inherit Http_client.http_call
  method response_webdav_status : webdav_status
  method call_status : call_status
  method multistatus : response_t list
  method query_path : string
  method effective_query_path : string
  method paths : string list
  method good_paths : string list
  method bad_paths : string list
  method fully_successful : bool
  method response_of_path : string -> response_t 
  method responsedescription : string
  method status_report : string
end

class type propfind_call_t = webdav_call_t
class type list_t = webdav_call_t
class type filelist_t = webdav_call_t
class type mkcol_call_t = webdav_call_t
class type get_call_t = webdav_call_t
class type delete_call_t = webdav_call_t
class type put_call_t = webdav_call_t
class type copy_call_t = webdav_call_t
class type move_call_t = webdav_call_t


type propfind_request =
  [ `Prop of property list
  | `Propname
  | `Allprop of property list
  ]


type list_request =
    [ `Existence | `Standard ]

type proppatch_instruction = Webdav_xml.proppatch_instruction
type proppatch_request = Webdav_xml.proppatch_request


let status_of_code code =
  if code >= 200 && code <= 299 then
    `Successful
  else if code >= 300 && code <= 399 then
    `Redirection
  else if code >= 400 && code <= 499 then
    `Client_error
  else
    `Server_error
      
let string_of_call_status (st:call_status) =
  match st with
    | `Unserved ->
	"Unserved"
    | `Http_protocol_error e -> 
	"Http_protocol_error(" ^ Printexc.to_string e ^ ")"
    | `Successful ->
	"Successful"
    | `Redirection ->
	"Redirection"
    | `Client_error ->
	"Client_error"
    | `Server_error ->
	"Server_error"
    | `Multi_status ->
	"Multi_status"

let name_of_prop p =
  p#localname  (* CHECK, maybe we want to know more *)


let allowed_xml_types =
  [ "application/xml"; "text/xml" ]


let rm_trailing_slash p =
  let q = List.rev p in
  match q with
    | [] -> [""]
    | [""] -> [""]
    | "" :: q' -> List.rev q'
    | _ -> p


let url_append base_url path =
  let b = Neturl.parse_url base_url in
  let p1 = Neturl.split_path path in
  if p1=[] || List.hd p1 <> "" then 
    failwith "Webdav_client_method.url_append: path is not absolute";
  let p2 = Neturl.norm_path p1 in
  if List.mem "." p2 || List.mem ".." p2 then
    failwith "Webdav_client_method.url_append: bad use of . or .. in path";
  Neturl.string_of_url
    (Neturl.modify_url
       ~path:(rm_trailing_slash(Neturl.url_path b) @ List.tl p2)
       b)

class virtual webdav_call_mixin =
object(self)
  (* from Http_client.http_call: *)
  method virtual response_status_code : int
  method virtual response_status_text : string
  method virtual response_header : Netmime.mime_header
  method virtual response_body : Netmime.mime_body
  method virtual request_uri : string
  method virtual effective_request_uri : string
  method virtual status : Http_client.status

  val mutable multistatus_lz = lazy (assert false)
  val mutable paths = []
  val mutable good_paths = []
  val mutable bad_paths = []
  val mutable response_tbl = Hashtbl.create 1
  val mutable strip_prefix_p = None

  initializer (
    multistatus_lz <- lazy (self # real_multistatus)
  )

  method response_webdav_status =
    let code = self#response_status_code in
    webdav_status_of_int code

  method call_status =
    match self#status with
      | `Successful ->
	  let code = self#response_status_code in
	  if code = 207 then 
	    `Multi_status
	  else
	    `Successful
      | st ->
	  (st :> call_status)

  method query_path =
    url_path ?strip_prefix:strip_prefix_p self#request_uri

  method effective_query_path =
    url_path ?strip_prefix:strip_prefix_p self#effective_request_uri

  method private response_is_good (r : response_t) =
    (* may be overridden in subclass *)
    let rcode = r#status_code in
    rcode >= 200 && rcode <= 299

  method multistatus =
    match self#response_webdav_status with
      | `Multi_status ->
	  (Lazy.force multistatus_lz) # responses
      | _ -> 
	  []

  method responsedescription =
    match self#response_webdav_status with
      | `Multi_status ->
	  (Lazy.force multistatus_lz) # responsedescription
      | _ -> 
	  ""

  method private real_multistatus =
    let hd = self # response_header in
    let content_type = try hd # field "Content-type" with Not_found -> "" in
    let content_type_dec,_ = get_content_type hd in
    if not (List.mem content_type_dec allowed_xml_types) then
      failwith "WebDAV client error: multistatus response is not XML";
    let ms = 
      Webdav_xml.parse_multistatus_body 
	(* ~namespace_manager:XXX *)
        ?strip_prefix:strip_prefix_p
	~content_type
	(self # response_body # open_value_rd()) in
    let tbl = Hashtbl.create 42 in
    List.iter
      (fun r ->
	 List.iter
	   (fun p -> Hashtbl.replace tbl p r)
	   r#paths
      )
      ms#responses;
    let plist =
      List.sort
	String.compare
	(Hashtbl.fold (fun p _ acc -> p::acc) tbl []) in
    paths <- plist;
    response_tbl <- tbl;
    let good, bad =
      List.partition
	(fun p ->
	   let r = Hashtbl.find tbl p  in
	   self # response_is_good r
	)
	plist in
    good_paths <- good;
    bad_paths <- bad;
    ms

  method paths =
    ignore(self#multistatus);
    paths
    
  method good_paths =
    ignore(self#multistatus);
    good_paths
    
  method bad_paths =
    ignore(self#multistatus);
    bad_paths
    
  method response_of_path p =
    ignore(self#multistatus);
    Hashtbl.find response_tbl p

  method fully_successful =
    match self#call_status with
      | `Successful -> true
      | `Multi_status -> self#bad_paths = []
      | _ -> false

  method status_report =
    let b = Buffer.create 1001 in

    let indent = "    " in

    let add_line url msg =
      bprintf b "%s:\n%s%s\n" url indent msg in

    let base_url = self # effective_request_uri in

    let status_class code =
      if code >= 200 && code <= 299 then
	"Success"
      else if code >= 300 && code <= 399 then
	"Redirection"
      else if code >= 400 && code <= 499 then
	"Client-side error"
      else if code >= 500 && code <= 599 then
	"Server-side error"
      else
	"Unknown type" in

    ( match self#status with
	| `Unserved ->
	    add_line base_url "Unserved"
	| `Http_protocol_error e ->
	    add_line base_url ("Exception " ^ Printexc.to_string e)
	| `Redirection | `Client_error | `Server_error ->
	    add_line base_url 
	      (sprintf "%s (%d %s)" 
		 (status_class self#response_status_code)
		 self#response_status_code self#response_status_text)
	| `Successful ->
	    if self#response_status_code = 207 then (
	      let rlist = self # multistatus in
	      List.iter
		(fun r ->
		   List.iter
		     (fun u -> bprintf b "%s:\n" u)
		     r#href;
		   let plist = r # propstat in
		   if plist = [] then
		     bprintf b "%s%s (%d %s)\n"
		       indent (status_class r#status_code)
		       r#status_code r#status_text
		   else (
		     (* Sort props by status_code *)
		     let props_of_code = Hashtbl.create 42 in
		     let text_of_code = Hashtbl.create 42 in
		     List.iter
		       (fun p ->
			  let code = p#status_code in
			  let text = p#status_text in
			  let old_props = 
			    try Hashtbl.find props_of_code code
			    with Not_found -> [] in
			  let new_props =
			    List.map name_of_prop p#properties in
			  Hashtbl.replace 
			    props_of_code code (new_props @ old_props);
			  Hashtbl.replace 
			    text_of_code code text
		       )
		       plist;
		     let codes =
		       Hashtbl.fold (fun k v acc -> k :: acc) props_of_code [] in
		     let codes' =
		       List.sort Pervasives.compare codes in
		     List.iter
		       (fun code ->
			  let props = Hashtbl.find props_of_code code in
			  bprintf b "%s%s properties (%d %s)\n"
			    indent (status_class code) code
			    (Hashtbl.find text_of_code code);
			  List.iter
			    (fun prop ->
			       bprintf b "%s%s%s\n" indent indent prop
			    )
			    props
		       )
		       codes'
		   )
		)
		rlist
	    )
	    else
	      add_line base_url 
		(sprintf "Success (%d %s)" 
		   self#response_status_code self#response_status_text)
    );
    Buffer.contents b
end



let propname_creationdate = 
  Webdav_xml.propname_creationdate
let propname_displayname = 
  Webdav_xml.propname_displayname
let propname_getcontentlanguage =
  Webdav_xml.propname_getcontentlanguage
let propname_getcontentlength = 
  Webdav_xml.propname_getcontentlength
let propname_getcontenttype = 
  Webdav_xml.propname_getcontenttype
let propname_getetag = 
  Webdav_xml.propname_getetag
let propname_getlastmodified = 
  Webdav_xml.propname_getlastmodified
let propname_resourcetype = 
  Webdav_xml.propname_resourcetype


class propfind_call =
object(self)
  inherit Http_client.generic_call
  inherit webdav_call_mixin
  method private fixup_request() = ()
  method private def_request_method = "PROPFIND"
  method private def_is_idempotent = true
  method private def_has_req_body = true
  method private def_has_resp_body = true
  method private def_empty_path_replacement = "/"

end


class propfind ?depth ?propfind_request ?strip_prefix url =
object(self)
  inherit propfind_call

  val mutable ref_body = (new Netmime.memory_mime_body "", 0)

  initializer (
    self # set_request_uri url;
    ( match depth with
	| None -> ()
	| Some d ->
	    Header.set_depth
	      (self # request_header `Base)
	      d
    );
    let b = Buffer.create 507 in
    ( match propfind_request with
	| None -> ()
	| Some req ->
	    let ch = new Netchannels.output_buffer b in
	    Webdav_xml.write_propfind_request ch req;
	    ch # close_out();
    );
    let s = Buffer.contents b in
    let body = ro_mime_body s in
    self # set_request_body body;
    ref_body <- (body, String.length s);
    strip_prefix_p <- strip_prefix;
  )

  method private fixup_request() =
    let (p_body, p_len) = ref_body in
    let body = self # request_body in
    if body == p_body then (
      (* Set the length only when we are sure the user did not mess with
         the body
       *)
      Nethttp.Header.set_content_length
	(self # request_header `Effective)
	(Int64.of_int p_len)
    )

  method private response_is_good (r : response_t) =
    let plist = r#propstat in
    List.for_all
      (fun p ->
	 let pcode = p # status_code in
	 pcode >= 200 && pcode <= 299
      )
      plist

end


class filelist ?depth ?strip_prefix (lreq : list_request) url =
  let propfind_request =
    match lreq with
      | `Existence ->
	  `Prop [ propname_resourcetype ]
      | `Standard ->
	  `Allprop [] in
object(self)
  inherit propfind ?depth ~propfind_request ?strip_prefix url

  method private response_is_good (r : response_t) =
    r # prop_resourcetype_is_collection <> None
end


class type proppatch_call_t = webdav_call_t

class proppatch_call =
object(self)
  inherit Http_client.generic_call
  inherit webdav_call_mixin
  method private fixup_request() = ()
  method private def_request_method = "PROPPATCH"
  method private def_is_idempotent = true
  method private def_has_req_body = true
  method private def_has_resp_body = true
  method private def_empty_path_replacement = "/"
end


class proppatch ?strip_prefix ~proppatch_request url =
object(self)
  inherit proppatch_call

  val mutable ref_body = (new Netmime.memory_mime_body "", 0)

  initializer (
    self # set_request_uri url;
    let b = Buffer.create 507 in
    let ch = new Netchannels.output_buffer b in
    Webdav_xml.write_proppatch_request ch proppatch_request;
    ch # close_out();
    let s = Buffer.contents b in
    let body = ro_mime_body s in
    self # set_request_body body;
    ref_body <- (body, String.length s);
    strip_prefix_p <- strip_prefix;
  )

  method private fixup_request() =
    let (p_body, p_len) = ref_body in
    let body = self # request_body in
    if body == p_body then (
      (* Set the length only when we are sure the user did not mess with
         the body
       *)
      Nethttp.Header.set_content_length
	(self # request_header `Effective)
	(Int64.of_int p_len)
    )

  method private response_is_good (r : response_t) =
    let plist = r#propstat in
    List.for_all
      (fun p ->
	 let pcode = p # status_code in
	 pcode >= 200 && pcode <= 299
      )
      plist
end


class mkcol_call =
object(self)
  inherit Http_client.generic_call
  inherit webdav_call_mixin
  method private fixup_request() = ()
  method private def_request_method = "MKCOL"
  method private def_is_idempotent = true
  method private def_has_req_body = true   (* it may have *)
  method private def_has_resp_body = true
  method private def_empty_path_replacement = "/"
end


class mkcol ?strip_prefix url =
object(self)
  inherit mkcol_call

  initializer
    self # set_request_uri url;
    strip_prefix_p <- strip_prefix

end

class get_call =
object(self)
  inherit Http_client.get_call
  inherit webdav_call_mixin
end


class get ?strip_prefix url =
object(self)
  inherit get_call
  initializer
    self # set_request_uri url;
    strip_prefix_p <- strip_prefix;
end


class delete_call =
object(self)
  inherit Http_client.generic_call
  inherit webdav_call_mixin
  method private fixup_request() = ()
  method private def_request_method = "DELETE"
  method private def_is_idempotent = true
  method private def_has_req_body = true   (* it may have in WebDAV ctx *)
  method private def_has_resp_body = true
  method private def_empty_path_replacement = "/"
end


class delete ?strip_prefix url =
object(self)
  inherit delete_call
  initializer
    self # set_request_uri url;
    strip_prefix_p <- strip_prefix;
end


class put_call =
object(self)
  inherit Http_client.generic_call
  inherit webdav_call_mixin
  method private fixup_request() = ()
  method private def_request_method = "PUT"
  method private def_is_idempotent = true
  method private def_has_req_body = true
  method private def_has_resp_body = true
  method private def_empty_path_replacement = "/"
end


class put ?content_type ?content_length ?(expect_handshake=false) ?strip_prefix
          url body =
object(self)
  inherit put_call
  initializer
    self # set_request_uri url;
    self # set_request_body body;
    ( match content_type with
	| None -> ()
	| Some ct ->
	    (self # request_header `Base) # update_field "Content-type" ct
    );
    ( match content_length with
	| None -> ()
	| Some n ->
	    (self # request_header `Base) # update_field "Content-length"
	      (Int64.to_string n)
    );
    if expect_handshake then
      (self # request_header `Base) # update_field "Expect" "100-continue";
    strip_prefix_p <- strip_prefix;

end


class copy_call =
object(self)
  inherit Http_client.generic_call
  inherit webdav_call_mixin
  method private fixup_request() = ()
  method private def_request_method = "COPY"
  method private def_is_idempotent = true
  method private def_has_req_body = true
  method private def_has_resp_body = true
  method private def_empty_path_replacement = "/"
end


class copy ?depth ?overwrite ?strip_prefix src_url dest_url =
object(self)
  inherit copy_call

  initializer
    self # set_request_uri src_url;
    let hdr = self # request_header `Base in
    Header.set_destination hdr dest_url;
    ( match depth with
	| None -> ()
	| Some d -> Header.set_depth hdr d
    );
    ( match overwrite with
	| None -> ()
	| Some b -> Header.set_overwrite hdr b
    );
    strip_prefix_p <- strip_prefix;
end


class move_call =
object(self)
  inherit Http_client.generic_call
  inherit webdav_call_mixin
  method private fixup_request() = ()
  method private def_request_method = "MOVE"
  method private def_is_idempotent = true
  method private def_has_req_body = true
  method private def_has_resp_body = true
  method private def_empty_path_replacement = "/"
end


class move ?overwrite ?strip_prefix src_url dest_url =
object(self)
  inherit move_call

  initializer
    self # set_request_uri src_url;
    let hdr = self # request_header `Base in
    Header.set_destination hdr dest_url;
    ( match overwrite with
	| None -> ()
	| Some b -> Header.set_overwrite hdr b
    );
    strip_prefix_p <- strip_prefix;
end


class list = filelist

This web site is published by Informatikbüro Gerd Stolpmann
Powered by Caml