3 The MiniDav class is a complete rewrite of PEAR::WebDAV_Server and
4 strives to be easier to use. There are still enough similarities,
5 but this version removes many features and comes without PHP license
6 preamble (this is Public Domain).
7 + adds Content-Encoding support for XML command in/out and GET,PUT
8 + more senseful xml namespace handling (->xmlns[] keeps a prefix list)
9 + PROPFIND uses simple property lists, with path and status inside
10 + LOCK, PROPATCH, MOVE, COPY, DELETE are unimplemented as of yet
11 + all M$-DAV compatibility workarounds have been disabled
12 + the GET() interface will NOT work with file handles and big files
14 This class automatically adds and removes the base URL from incoming
15 and sent pathnames. It also translates XML namespace qualifiers, if
16 they get registered correctly beforehand. Most request parameters are
17 available as object properties AND as method parameters.
19 File lists for PROPFIND and PROPPATCH simply contain a "path" (again
20 without the base URL) and multiple DAV: properties (without any xmlns
21 monikers). "status" is of course optional, and custom (from other XML
22 namespaces) entries may be there. While it is possible to treat some
23 of them specially, most should be simple name-value pairs.
25 Simply derive a class from "MiniDav" and implement at least GET and
26 PROPFIND as described by the out-commented examples herein. Then make
27 an object instance "$dav = new YourDavClass();" and call the main
28 "$dav->ServerRequest();" method. Place an exit; after that into your
31 This class does not handle authentication (one of the many parts of
32 the WebDAV spec everybody ignores). Implement an ->auth() method,
33 which is already called for all RW-methods (PUT, DELELTE, MKCOL).
36 define("MINIDAV_IGNORE_MIME", 0); // there are a few bogus clients
46 var $version = NULL; // no SVN proto support
49 #-- both must be lowercase here
50 var $int_charset = "iso-8859-1";
51 var $ext_charset = "iso-8859-1";
53 #-- namespace mapping (we don't want to deal with ugly URIs throughout the code)
54 var $xmlns = array(); // gets fed directly into xml parsers
56 #-- pre-defined values (constants)
57 var $base_url = ""; // will get prefixed to PROPFIND response URLs
58 // and stripped from incoming path names
61 var $prop_as_tag = array(
64 var $prop_as_date = array(
65 "creationdate" => "%G-%m-%dT%TZ",
66 "getlastmodified" => "%a, %d %b %G %T %Z",
73 #-- you have to set all desired namespace prefixes beforehand
74 //@FIX: list not yet feeded into XML parser
75 $this->xmlns[""] = "DAV:"; // <default xmlns=...>
76 $this->xmlns["wiki1"] = "http://purl.org/rss/1.0/modules/wiki/";
77 #<eee># $this->xmlns["ms-time-compat"] = "urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/";
82 function ServeRequest($path="") {
86 $path = $_SERVER["PATH_INFO"];
91 if (!$this->base_url) {
92 $port = $_SERVER["SERVER_PORT"];
93 $s = ($_SERVER["HTTPS"] && $_SERVER["SSL_PROTOCOL"] || ($port == 443) ? "s" : "");
94 $port = ($port != ($s?443:80) ? ":$port" : "");
95 $this->base_url = "http" . $s . "://" . $_SERVER["SERVER_NAME"] . $port . $_SERVER["SCRIPT_NAME"];
99 if (isset($_SERVER["HTTP_DEPTH"])) {
100 $d = trim(strtolower($_SERVER["HTTP_DEPTH"]));
101 if ($d == "infinity") {
104 $this->depth = (int)$d;
108 if ($this->dest = trim($_SERVER["HTTP_DESTINATION"])) {
109 if (!strncmp($this->dest, $this->base_url, strlen($this->base_url))) {
110 $this->dest = substr($this->dest, strlen($this->base_url));
118 $this->overwrite = (($_SERVER["HTTP_OVERWRITE"] != "F") ? 1 : 0);
124 # $this->request_debug();
126 $method = strtoupper($_SERVER["REQUEST_METHOD"]);
127 $handler = $method."_request";
128 if (method_exists($this, $handler) and method_exists($this, $method)) {
129 call_user_method("$handler", $this);
132 $this->http_error("405 Unimplemented Request Method"); // or 501 ?
133 $this->OPTIONS_request();
138 # (this is handled by the caller typically)
143 #------------------------------------------------------- request methods ---
149 #-- retrieve a single document
150 function GET_request() {
151 $ok = $this->GET( $this->path );
152 $this->http_error($ok);
156 function GET($path) {
157 - just call the ->GET_response() handler with some file $data
162 function GET_response($data, $mtime=0, $ctype="") {
169 $ctype = "application/octet-stream";
171 if (is_resource($data)) {
172 $data = fread($data, 1<<22);
176 header("Content-Type: $ctype");
177 header("Last-Modified: ".gmstrftime($this->prop_as_date["getlastmodified"], $mtime));
178 $this->cut_content($data);
179 $this->content_encode($data);
190 #-- we handle this by calling ->GET()
191 function HEAD_request() {
192 $this->HEAD( $this->path );
195 #-- HEAD() gets automatically emulated
196 function HEAD($path) {
206 #-- store resources under given filename
207 function PUT_request() {
211 list($content_type, $charset) = $this->get_ctype();
212 if (!$content_type) {
213 $this->http_error("415 Use a MIME compliant client!");
216 $ok = $this->PUT( $this->path, $content_type, $charset );
217 $this->http_error($ok);
222 function PUT($path, $ctype, $charset) {
223 - simply calls $this->get_body() to get the decompressed
225 - checks $overwrite and $ctype, $charset
234 #-- sort of directory listing
235 function PROPFIND_request() {
238 if (($ctype = $this->get_ctype()) and ($this->is_xml($ctype[0]))
239 and ($body = $this->get_body()))
241 $p = & new MiniDav_PropFind($body, $ctype[1], $this->int_charset, $this->xmlns);
244 if (isset($p->prop)) {
245 $this->props = array_keys($p->prop);
247 elseif (isset($p->allprop)) {
250 elseif (isset($p->propname)) {
254 $this->http_error("400 Couldn't determine <propfind> request type");
259 $this->props = 1; // ALL
261 #echo "NON($ctype[0],$body)";
266 $files = $this->PROPFIND($this->path, $this->props);
268 #-- send transformed list
269 $this->PROPFIND_response($files);
273 function PROPFIND($path, $wanted_props) {
274 - searches resources for given $path up to ->depth subdirs
275 - generates a result list in a $files[] array with $wanted_props:
277 "path"=>"/fn/...", // ALWAYS!, without URL prefix
278 "status"=>"200 OK", // optional
279 "getcontenttype"=>"...", // in DAV: default namespace
280 "myXmlNs:p1"=>"...", // other XML namespace
282 - $wanted_props is either 1 or 0 or an array (1 = ALL fields, 0 = only NAMES
283 of fields, or ARRAY = the respective list of requested property field names)
284 - send the collected file info list to ->PROPFIND_response() finally
289 function PROPFIND_response($files) {
292 header("Content-Type: application/xml");
293 echo "<?xml version=\"1.0\" encoding=\"{$this->int_charset}\" ?>\n";
294 echo "<multistatus" . $this->xmlns_out() . ">\n";
298 if (is_array($this->props)) {
299 foreach ($this->props as $id) {
300 $add_req[$id] = false; // become <empty/> nodes
307 foreach ($files as $row) {
309 #-- transform/add fields
310 $href = $this->xmlentities($this->base_url . $row["path"]);
311 if (!$href) { continue; }
313 $status = $row["status"] ? $row["status"] : "HTTP/1.0 200 OK";
314 unset($row["status"]);
316 #-- add _required_ fields (but empty then)
318 $row = $row + $add_req;
321 #-- throw entry + properties
322 echo " <response>\n";
323 echo " <href>$href</href>\n";
324 echo " <propstat>\n";
328 foreach ($row as $id=>$value) {
331 if (!$this->props || ($value===false)) {
336 if (is_array($this->props) && !in_array($id, $this->props)) {
340 if ($this->prop_as_tag[$id]) {
341 if (!strlen($value)) {
342 $value = "<!--empty-->";
345 $value = "<$value/>";
348 elseif ($sft = $this->prop_as_date[$id]) {
349 $value = gmstrftime($sft, $value);
352 $value = $this->xmlentities($value);
354 echo "\t<$id>$value</$id>\n";
358 echo " <status>$status</status>\n";
359 echo " </propstat>\n";
360 echo " </response>\n";
364 echo "</multistatus>\n";
371 function PROPPATCH_request() {
375 function PROPPATCH($path, $set, $remove) {
376 - $set and $remove are associate arrays, with property names
377 and values ($set only) inside
378 - $remove only contains false as values, and tells to unset
379 the according property from all matched files
380 - $path is either a file or a directory again (should honor
382 - returns a $files[] list, similar to the PROPFIND() method,
383 which contains property names associated to status values:
384 array( "getcontenttype" => "500 bad request", )
385 (1 is allowed as status value)
393 #-- create a collection (sub directory)
394 function MKCOL_request() {
396 $r = $this->MKCOL($this->path);
397 $this->http_error($r);
401 function MKCOL($dir) {
410 function COPY_request() {
412 $this->check_src_dest();
413 $r = $this->COPY($this->path, $this->dest, $this->overwrite, $this->depth);
414 $this->http_error($r);
418 function COPY($src, $dest, $overwrite, $depth) {
419 - duplicates a $src file or a directory tree ($depth) to the
421 - if $overwrite is 1, then existing files should be ->DELETEd
422 automatically (simply overwrite)
427 #-- simple pre-conditions for COPY and MOVE
428 function check_src_dest() {
430 $this->http_error("400 No valid Destination: given");
433 elseif (trim($this->dest, "/") == trim($this->path, "/")) {
434 $this->http_error("409 Source and Destination are the same");
444 function MOVE_request() {
446 $this->check_src_dest();
447 $r = $this->MOVE($this->path, $this->dest, $this->overwrite, $this->depth);
448 $this->http_error($r);
452 function MOVE($src, $dest, $overwrite, $depth) {
453 - works like COPY, but that all source files must be deleted
454 after a successful move
463 function DELETE_request() {
465 $this->DELETE($this->path);
469 function DELETE($path) {
470 - purge the given file if it wants to
471 - watch out for directories and $this->depth
479 #-- yields some informational headers
480 function OPTIONS_request() {
481 header("Allow: " . implode(", ", $this->get_options()));
482 header("DAV: 1"); // version 1 means without locking support
489 #-- list of defined request method handler pairs
490 function get_options() {
491 $class = get_class($this);
493 foreach (get_class_methods($class) as $fn) {
494 #-- for every "METHOD()", there must be a "METHOD_request()"
495 if (!strpos($fn, "_") && method_exists($this, "{$fn}_request")) {
496 $r[] = strtoupper($fn);
500 $r[] = "TRACE"; // Apache must handle this
521 #------------------------------------------------------ utility code ---
524 #-- is called for all writing methods
526 // Makes it easy to bring up authentication.
527 // If you want to "protect" the read-only WebDAV method as
528 // well, then just wrap this into your interface script.
530 # include("ewiki/plugins/../fragments/funcs/auth.php");
534 #-- encode string values
535 function xmlentities($s) {
536 return xmlentities($s);
540 #-- serialize all valid $this->xmlns[]
541 function xmlns_out() {
543 foreach ($this->xmlns as $prefix=>$uri) {
544 if (strpos($uri, ":")) {
545 $s .= " xmlns" . ($prefix ? ":$prefix" : "") ."=\"" . $uri . "\"";
554 #------------------------------------------------------- http in/out ---
557 #-- compress page content (prior sending)
558 function content_encode(&$body) {
559 $ae = strtolower($_SERVER["ACCEPT_ENCODING"]);
560 $alg = array("gzip"=>"gzencode", "deflate"=>"gzdeflate", "compress"=>"gzcompress", "x-bzip2"=>"bzcompress");
562 foreach (explode(",", $ae) as $ae) {
563 $ae = trim(strtok($ae, ";"));
564 if ($pf = $alg[$ae]) {
566 header("Content-Encoding: $ae");
571 // unset($_SERVER["ACCEPT_ENCODING"]); //@HACK: prevent accidential double encoding - ewiki plugins could do this automatically
575 #-- or decompress received body
576 function content_decode(&$body) {
577 $de = strtolower(trim($_SERVER["HTTP_CONTENT_ENCODING"]));
578 if (!strlen($de)) { /* nop */ }
579 elseif ($de == "gzip") { $body = function_exists("gzdecode") ? gzdecode($body) : gzinflate(substr($body, 10, strlen($body) - 18)); }
580 elseif ($de == "deflate") { $body = gzinflate($body); }
581 elseif ($de == "compress") { $body = gzuncompress($body); }
582 elseif ($de == "x-bzip2") { $body = bzuncompress($body); }
583 // unset($_SERVER["HTTP_CONTENT_ENCODING"]); //@HACK: prevent accidential double decoding, wrong here
587 #-- convinience function for sending HTTP status responses
588 function http_error($w=false, $def_success="200 OkiDoki") {
590 $w = "500 Internal Error";
592 elseif (($w === true) || ($w === 1)) {
595 if (!headers_sent() && !isset($this->no_status)) {
596 if (ini_get("cgi.rfc2616_headers")) {
597 header("HTTP/1.1 $w");
599 header("Status: $w"); // always ok
604 #-- check content type and charset
605 function get_ctype() {
606 $ct = trim(strtolower(strtok($_SERVER["CONTENT_TYPE"], ";,(")));
607 $rest = strtok(",(");
608 if (preg_match("#charset=[\"\']?([-\d\w]+)#", $rest, $uu)) {
611 elseif ($ct == "text/xml") {
612 $charset = "iso-8859-1";
614 return(array($ct, $charset));
618 #-- check content-type for being */*xml*
619 function is_xml($ct) {
620 if (preg_match("#(^x\.?ml/...|...[/+]xml$)#", $ct) || MINIDAV_IGNORE_MIME) {
626 #-- get request body, decoded
627 function get_body() {
630 $f = fopen("php://input", "rb");
631 $body = fread($f, 1<<22);
635 $this->content_decode($body);
641 #-- partial responses (only contiguous ranges)
642 function cut_content(&$data) {
644 if (($h = $_SERVER["HTTP_RANGE"])
645 and preg_match("/^bytes=(\d*)-(\d+)$/", trim($h), $uu))
647 list($uu, $start, $end) = $uu;
649 #-- correct positions
650 $len = strlen($data);
651 if (!strlen($start)) {
656 $start = $len - $end;
661 $this->http_error("416 Unsatisfiable Range:");
666 $data = substr($data, $start, $end - $start + 1);
669 header("Content-Range: bytes $start-$end/$len");
670 $this->http_error("206 Partial Content");
671 $this->no_status = 1;
676 #---------------------------------------------------------- old code ---
679 #-- output raw page data
680 function out_content(&$data) {
681 $c = & $data["content"];
682 $this->content_encode($c);
683 header("Content-Length: ".strlen($c));
690 function request_debug() {
693 echo $this->get_body();
695 $d = ob_get_contents();
697 file_put_contents("/tmp/minidav.".time().".".rand(0, 99), $d);
707 #------------------------------------------------------------------ xml ---
710 #-- <propfind> request bodies
711 class MiniDav_PropFind extends easy_xml {
713 function MiniDav_PropFind($xml, $ctype="", $uu=NULL, $addns=array()) {
714 parent::easy_xml($xml, $ctype);
715 $this->xmlns2["dav"] = "";
716 $this->xmlns += array_flip($addns);
719 function start($xp, $tag, $attr) {
720 parent::start($xp, $tag, $attr);
722 $this->{$this->parent}[$tag] = true;