2 // +----------------------------------------------------------------------+
4 // +----------------------------------------------------------------------+
5 // | Copyright (c) 1997-2003 The PHP Group |
6 // +----------------------------------------------------------------------+
7 // | This source file is subject to version 3.0 of the PHP license, |
8 // | that is bundled with this package in the file LICENSE, and is |
9 // | available at through the world-wide-web at |
10 // | http://www.php.net/license/3_0.txt. |
11 // | If you did not receive a copy of the PHP license and are unable to |
12 // | obtain it through the world-wide-web, please send a note to |
13 // | license@php.net so we can mail you a copy immediately. |
14 // +----------------------------------------------------------------------+
15 // | Authors: Hartmut Holzgraefe <hholzgra@php.net> |
16 // | Christian Stocker <chregu@bitflux.ch> |
17 // +----------------------------------------------------------------------+
21 require_once "include/HTTP_WebDAV_Server/Tools/_parse_propfind.php";
22 require_once "include/HTTP_WebDAV_Server/Tools/_parse_proppatch.php";
23 require_once "include/HTTP_WebDAV_Server/Tools/_parse_lockinfo.php";
28 * Virtual base class for implementing WebDAV servers
30 * WebDAV server base class, needs to be extended to do useful work
32 * @package HTTP_WebDAV_Server
33 * @author Hartmut Holzgraefe <hholzgra@php.net>
36 class HTTP_WebDAV_Server
38 // {{{ Member Variables
41 * URI path for this request
48 * Realm string to be used in authentification popups
52 var $http_auth_realm = "PHP WebDAV";
55 * String to be used in "X-Dav-Powered-By" header
59 var $dav_powered_by = "";
62 * Remember parsed If: (RFC2518/9.4) header conditions
66 var $_if_header_uris = array();
69 * HTTP response status/message
73 var $_http_status = "200 OK";
76 * encoding of property values passed in
80 var $_prop_encoding = "utf-8";
91 function HTTP_WebDAV_Server()
93 // PHP messages destroy XML output -> switch them off
94 ini_set("display_errors", 0);
101 * Serve WebDAV HTTP request
103 * dispatch WebDAV HTTP request to the apropriate method handler
108 function ServeRequest()
110 // identify ourselves
111 if (empty($this->dav_powered_by)) {
112 header("X-Dav-Powered-By: PHP class: ".get_class($this));
114 header("X-Dav-Powered-By: ".$this->dav_powered_by );
117 // check authentication
118 if (!$this->_check_auth()) {
119 $this->http_status('401 Unauthorized');
121 // RFC2518 says we must use Digest instead of Basic
122 // but Microsoft Clients do not support Digest
123 // and we don't support NTLM and Kerberos
124 // so we are stuck with Basic here
125 header('WWW-Authenticate: Basic realm="'.($this->http_auth_realm).'"');
131 if(! $this->_check_if_header_conditions()) {
132 $this->http_status("412 Precondition failed");
137 $this->path = $this->_urldecode(!empty($_SERVER["PATH_INFO"]) ? $_SERVER["PATH_INFO"] : "/");
138 if(ini_get("magic_quotes_gpc")) {
139 $this->path = stripslashes($this->path);
143 // detect requested method names
144 $method = strtolower($_SERVER["REQUEST_METHOD"]);
145 $wrapper = "http_".$method;
147 // activate HEAD emulation by GET if no HEAD method found
148 if ($method == "head" && !method_exists($this, "head")) {
152 if (method_exists($this, $wrapper) && ($method == "options" || method_exists($this, $method))) {
153 $this->$wrapper(); // call method by name
154 } else { // method not found/implemented
155 if ($_SERVER["REQUEST_METHOD"] == "LOCK") {
156 $this->http_status("412 Precondition failed");
158 $this->http_status("405 Method not allowed");
159 header("Allow: ".join(", ", $this->_allow())); // tell client what's allowed
166 // {{{ abstract WebDAV methods
172 * overload this method to retrieve resources from your server
177 * @param array &$params Array of input and output parameters
178 * <br><b>input</b><ul>
181 * <br><b>output</b><ul>
184 * @returns int HTTP-Statuscode
188 function GET(&$params)
190 // dummy entry for PHPDoc
203 * @param array &$params
204 * @returns int HTTP-Statuscode
210 // dummy entry for PHPDoc
219 * COPY implementation
221 * COPY implementation
224 * @param array &$params
225 * @returns int HTTP-Statuscode
231 // dummy entry for PHPDoc
240 * MOVE implementation
242 * MOVE implementation
245 * @param array &$params
246 * @returns int HTTP-Statuscode
252 // dummy entry for PHPDoc
261 * DELETE implementation
263 * DELETE implementation
266 * @param array &$params
267 * @returns int HTTP-Statuscode
273 // dummy entry for PHPDoc
281 * PROPFIND implementation
283 * PROPFIND implementation
286 * @param array &$params
287 * @returns int HTTP-Statuscode
293 // dummy entry for PHPDoc
302 * PROPPATCH implementation
304 * PROPPATCH implementation
307 * @param array &$params
308 * @returns int HTTP-Statuscode
314 // dummy entry for PHPDoc
322 * LOCK implementation
324 * LOCK implementation
327 * @param array &$params
328 * @returns int HTTP-Statuscode
334 // dummy entry for PHPDoc
342 * UNLOCK implementation
344 * UNLOCK implementation
347 * @param array &$params
348 * @returns int HTTP-Statuscode
354 // dummy entry for PHPDoc
361 // {{{ other abstract methods
366 * check authentication
368 * overload this method to retrieve and confirm authentication information
371 * @param string type Authentication type, e.g. "basic" or "digest"
372 * @param string username Transmitted username
373 * @param string passwort Transmitted password
374 * @returns bool Authentication status
378 function checkAuth($type, $username, $password)
380 // dummy entry for PHPDoc
389 * check lock status for a resource
391 * overload this method to return shared and exclusive locks
392 * active for this resource
395 * @param string resource Resource path to check
396 * @returns array An array of lock entries each consisting
397 * of 'type' ('shared'/'exclusive'), 'token' and 'timeout'
401 function checklock($resource)
403 // dummy entry for PHPDoc
411 // {{{ WebDAV HTTP method wrappers
413 // {{{ http_OPTIONS()
416 * OPTIONS method handler
418 * The OPTIONS method handler creates a valid OPTIONS reply
419 * including Dav: and Allowed: heaers
420 * based on the implemented methods found in the actual instance
425 function http_OPTIONS()
427 // Microsoft clients default to the Frontpage protocol
428 // unless we tell them to use WebDAV
429 header("MS-Author-Via: DAV");
431 // get allowed methods
432 $allow = $this->_allow();
435 $dav = array(1); // assume we are always dav class 1 compliant
436 if (isset($allow['LOCK'])) {
437 $dav[] = 2; // dav class 2 requires that locking is supported
440 // tell clients what we found
441 $this->http_status("200 OK");
442 header("DAV: " .join("," , $dav));
443 header("Allow: ".join(", ", $allow));
449 // {{{ http_PROPFIND()
452 * PROPFIND method handler
457 function http_PROPFIND()
460 $options["path"] = $this->path;
462 // search depth from header (default is "infinity)
463 if (isset($_SERVER['HTTP_DEPTH'])) {
464 $options["depth"] = $_SERVER["HTTP_DEPTH"];
466 $options["depth"] = "infinity";
469 // analyze request payload
470 $propinfo = new _parse_propfind("php://input");
471 if (!$propinfo->success) {
472 $this->http_status("400 Error");
475 $options['props'] = $propinfo->props;
478 if (!$this->propfind($options, $files)) {
479 $this->http_status("404 Not Found");
483 // collect namespaces here
486 // Microsoft Clients need this special namespace for date and time values
487 $ns_defs = "xmlns:ns0=\"urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/\"";
489 // now we loop over all returned file entries
490 foreach($files["files"] as $filekey => $file) {
492 // nothing to do if no properties were returend for a file
493 if (!isset($file["props"]) || !is_array($file["props"])) {
497 // now loop over all returned properties
498 foreach($file["props"] as $key => $prop) {
499 // as a convenience feature we do not require that user handlers
500 // restrict returned properties to the requested ones
501 // here we strip all unrequested entries out of the response
503 switch($options['props']) {
509 // only the names of all existing properties were requested
510 // so we remove all values
511 unset($files["files"][$filekey]["props"][$key]["val"]);
517 // search property name in requested properties
518 foreach((array)$options["props"] as $reqprop) {
519 if ( $reqprop["name"] == $prop["name"]
520 && $reqprop["xmlns"] == $prop["ns"]) {
526 // unset property and continue with next one if not found/requested
528 $files["files"][$filekey]["props"][$key]="";
534 // namespace handling
535 if (empty($prop["ns"])) continue; // no namespace
537 if ($ns == "DAV:") continue; // default namespace
538 if (isset($ns_hash[$ns])) continue; // already known
540 // register namespace
541 $ns_name = "ns".(count($ns_hash) + 1);
542 $ns_hash[$ns] = $ns_name;
543 $ns_defs .= " xmlns:$ns_name=\"$ns\"";
546 // we also need to add empty entries for properties that were requested
547 // but for which no values where returned by the user handler
548 if (is_array($options['props'])) {
549 foreach($options["props"] as $reqprop) {
550 if($reqprop['name']=="") continue; // skip empty entries
554 // check if property exists in result
555 foreach($file["props"] as $prop) {
556 if ( $reqprop["name"] == $prop["name"]
557 && $reqprop["xmlns"] == $prop["ns"]) {
564 if($reqprop["xmlns"]==="DAV:" && $reqprop["name"]==="lockdiscovery") {
565 // lockdiscovery is handled by the base class
566 $files["files"][$filekey]["props"][]
567 = $this->mkprop("DAV:",
569 $this->lockdiscovery($files["files"][$filekey]['path']));
571 // add empty value for this property
572 $files["files"][$filekey]["noprops"][] =
573 $this->mkprop($reqprop["xmlns"], $reqprop["name"], "");
575 // register property namespace if not known yet
576 if ($reqprop["xmlns"] != "DAV:" && !isset($ns_hash[$reqprop["xmlns"]])) {
577 $ns_name = "ns".(count($ns_hash) + 1);
578 $ns_hash[$reqprop["xmlns"]] = $ns_name;
579 $ns_defs .= " xmlns:$ns_name=\"$reqprop[xmlns]\"";
587 // now we generate the reply header ...
588 $this->http_status("207 Multi-Status");
589 header('Content-Type: text/xml; charset="utf-8"');
592 echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
593 echo "<D:multistatus xmlns:D=\"DAV:\">\n";
595 foreach($files["files"] as $file) {
596 // ignore empty or incomplete entries
597 if(!is_array($file) || empty($file) || !isset($file["path"])) continue;
598 $path = $file['path'];
599 if(!is_string($path) || $path==="") continue;
601 echo " <D:response $ns_defs>\n";
603 $href = (@$_SERVER["HTTPS"] === "on" ? "https:" : "http:");
604 $href.= "//".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'];
606 //TODO make sure collection resource pathes end in a trailing slash
608 echo " <D:href>$href</D:href>\n";
610 // report all found properties and their values (if any)
611 if (isset($file["props"]) && is_array($file["props"])) {
612 echo " <D:propstat>\n";
615 foreach($file["props"] as $key => $prop) {
617 if (!is_array($prop)) continue;
618 if (!isset($prop["name"])) continue;
620 if (!isset($prop["val"]) || $prop["val"] === "" || $prop["val"] === false) {
621 // empty properties (cannot use empty() for check as "0" is a legal value here)
622 if($prop["ns"]=="DAV:") {
623 echo " <D:$prop[name]/>\n";
624 } else if(!empty($prop["ns"])) {
625 echo " <".$ns_hash[$prop["ns"]].":$prop[name]/>\n";
627 echo " <$prop[name] xmlns=\"\"/>";
629 } else if ($prop["ns"] == "DAV:") {
630 // some WebDAV properties need special treatment
631 switch ($prop["name"]) {
633 echo " <D:creationdate ns0:dt=\"dateTime.tz\">"
634 . gmdate("Y-m-d\\TH:i:s\\Z",$prop['val'])
635 . "</D:creationdate>\n";
637 case "getlastmodified":
638 echo " <D:getlastmodified ns0:dt=\"dateTime.rfc1123\">"
639 . TimeDate::httptime($prop['val'])
640 . "</D:getlastmodified>\n";
643 echo " <D:resourcetype><D:$prop[val]/></D:resourcetype>\n";
645 case "supportedlock":
646 echo " <D:supportedlock>$prop[val]</D:supportedlock>\n";
648 case "lockdiscovery":
649 echo " <D:lockdiscovery>\n";
651 echo " </D:lockdiscovery>\n";
654 echo " <D:$prop[name]>"
655 . $this->_prop_encode(htmlspecialchars($prop['val']))
656 . "</D:$prop[name]>\n";
660 // properties from namespaces != "DAV:" or without any namespace
662 echo " <" . $ns_hash[$prop["ns"]] . ":$prop[name]>"
663 . $this->_prop_encode(htmlspecialchars($prop['val']))
664 . "</" . $ns_hash[$prop["ns"]] . ":$prop[name]>\n";
666 echo " <$prop[name] xmlns=\"\">"
667 . $this->_prop_encode(htmlspecialchars($prop['val']))
668 . "</$prop[name]>\n";
674 echo " <D:status>HTTP/1.1 200 OK</D:status>\n";
675 echo " </D:propstat>\n";
678 // now report all properties requested bot not found
679 if (isset($file["noprops"])) {
680 echo " <D:propstat>\n";
683 foreach($file["noprops"] as $key => $prop) {
684 if ($prop["ns"] == "DAV:") {
685 echo " <D:$prop[name]/>\n";
686 } else if ($prop["ns"] == "") {
687 echo " <$prop[name] xmlns=\"\"/>\n";
689 echo " <" . $ns_hash[$prop["ns"]] . ":$prop[name]/>\n";
694 echo " <D:status>HTTP/1.1 404 Not Found</D:status>\n";
695 echo " </D:propstat>\n";
698 echo " </D:response>\n";
701 echo "</D:multistatus>\n";
707 // {{{ http_PROPPATCH()
710 * PROPPATCH method handler
715 function http_PROPPATCH()
717 if($this->_check_lock_status($this->path)) {
719 $options["path"] = $this->path;
721 $propinfo = new _parse_proppatch("php://input");
723 if (!$propinfo->success) {
724 $this->http_status("400 Error");
728 $options['props'] = $propinfo->props;
730 $responsedescr = $this->proppatch($options);
732 $this->http_status("207 Multi-Status");
733 header('Content-Type: text/xml; charset="utf-8"');
735 echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
737 echo "<D:multistatus xmlns:D=\"DAV:\">\n";
738 echo " <D:response>\n";
739 echo " <D:href>".$this->_urlencode($_SERVER["SCRIPT_NAME"].$this->path)."</D:href>\n";
741 foreach($options["props"] as $prop) {
742 echo " <D:propstat>\n";
743 echo " <D:prop><$prop[name] xmlns=\"$prop[ns]\"/></D:prop>\n";
744 echo " <D:status>HTTP/1.1 $prop[status]</D:status>\n";
745 echo " </D:propstat>\n";
748 if ($responsedescr) {
749 echo " <D:responsedescription>".
750 $this->_prop_encode(htmlspecialchars($responsedescr)).
751 "</D:responsedescription>\n";
754 echo " </D:response>\n";
755 echo "</D:multistatus>\n";
757 $this->http_status("423 Locked");
767 * MKCOL method handler
772 function http_MKCOL()
775 $options["path"] = $this->path;
777 $stat = $this->mkcol($options);
779 $this->http_status($stat);
795 // TODO check for invalid stream
797 $options["path"] = $this->path;
799 $this->_get_ranges($options);
801 if (true === ($status = $this->get($options))) {
802 if (!headers_sent()) {
805 if (!isset($options['mimetype'])) {
806 $options['mimetype'] = "application/octet-stream";
808 header("Content-type: $options[mimetype]");
810 if (isset($options['mtime'])) {
811 header("Last-modified: ".TimeDate::httpTime());
814 if (isset($options['stream'])) {
815 // GET handler returned a stream
816 if (!empty($options['ranges']) && (0===fseek($options['stream'], 0, SEEK_SET))) {
817 // partial request and stream is seekable
819 if (count($options['ranges']) === 1) {
820 $range = $options['ranges'][0];
822 if (isset($range['start'])) {
823 fseek($options['stream'], $range['start'], SEEK_SET);
824 if (feof($options['stream'])) {
825 http_status("416 Requested range not satisfiable");
829 if (isset($range['end'])) {
830 $size = $range['end']-$range['start']+1;
831 http_status("206 partial");
832 header("Content-length: $size");
833 header("Content-range: $range[start]-$range[end]/"
834 . (isset($options['size']) ? $options['size'] : "*"));
835 while ($size && !feof($options['stream'])) {
836 $buffer = fread($options['stream'], 4096);
837 $size -= strlen($buffer);
841 http_status("206 partial");
842 if (isset($options['size'])) {
843 header("Content-length: ".($options['size'] - $range['start']));
844 header("Content-range: $start-$end/"
845 . (isset($options['size']) ? $options['size'] : "*"));
847 fpassthru($options['stream']);
850 header("Content-length: ".$range['last']);
851 fseek($options['stream'], -$range['last'], SEEK_END);
852 fpassthru($options['stream']);
855 $this->_multipart_byterange_header(); // init multipart
856 foreach ($options['ranges'] as $range) {
857 // TODO what if size unknown? 500?
858 if (isset($range['start'])) {
859 $from = $range['start'];
860 $to = !empty($range['end']) ? $range['end'] : $options['size']-1;
862 $from = $options['size'] - $range['last']-1;
863 $to = $options['size'] -1;
865 $total = isset($options['size']) ? $options['size'] : "*";
866 $size = $to - $from + 1;
867 $this->_multipart_byterange_header($options['mimetype'], $from, $to, $total);
870 fseek($options['stream'], $start, SEEK_SET);
871 while ($size && !feof($options['stream'])) {
872 $buffer = fread($options['stream'], 4096);
873 $size -= strlen($buffer);
877 $this->_multipart_byterange_header(); // end multipart
880 // normal request or stream isn't seekable, return full content
881 if (isset($options['size'])) {
882 header("Content-length: ".$options['size']);
884 fpassthru($options['stream']);
885 return; // no more headers
887 } elseif (isset($options['data'])) {
888 if (is_array($options['data'])) {
889 // reply to partial request
891 header("Content-length: ".strlen($options['data']));
892 echo $options['data'];
898 if (false === $status) {
899 $this->http_status("404 not found");
902 $this->http_status("$status");
907 * parse HTTP Range: header
909 * @param array options array to store result in
912 function _get_ranges(&$options)
914 // process Range: header if present
915 if (isset($_SERVER['HTTP_RANGE'])) {
917 // we only support standard "bytes" range specifications for now
918 if (preg_match("/bytes[[:space:]]*=[[:space:]]*(.+)/", $_SERVER['HTTP_RANGE'], $matches)) {
919 $options["ranges"] = array();
921 // ranges are comma separated
922 foreach (explode(",", $matches[1]) as $range) {
923 // ranges are either from-to pairs or just end positions
924 list($start, $end) = explode("-", $range);
925 $options["ranges"][] = ($start==="")
926 ? array("last"=>$end)
927 : array("start"=>$start, "end"=>$end);
934 * generate separator headers for multipart response
936 * first and last call happen without parameters to generate
937 * the initial header and closing sequence, all calls inbetween
938 * require content mimetype, start and end byte position and
939 * optionaly the total byte length of the requested resource
941 * @param string mimetype
942 * @param int start byte position
943 * @param int end byte position
944 * @param int total resource byte size
946 function _multipart_byterange_header($mimetype = false, $from = false, $to=false, $total=false)
948 if ($mimetype === false) {
949 if (!isset($this->multipart_separator)) {
952 // a little naive, this sequence *might* be part of the content
953 // but it's really not likely and rather expensive to check
954 $this->multipart_separator = "SEPARATOR_".md5(microtime());
956 // generate HTTP header
957 header("Content-type: multipart/byteranges; boundary=".$this->multipart_separator);
961 // generate closing multipart sequence
962 echo "\n--{$this->multipart_separator}--";
965 // generate separator and header for next part
966 echo "\n--{$this->multipart_separator}\n";
967 echo "Content-type: $mimetype\n";
968 echo "Content-range: $from-$to/". ($total === false ? "*" : $total);
980 * HEAD method handler
989 $options["path"] = $this->path;
991 if (method_exists($this, "HEAD")) {
992 $status = $this->head($options);
993 } else if (method_exists($this, "GET")) {
995 $status = $this->GET($options);
999 if($status===true) $status = "200 OK";
1000 if($status===false) $status = "404 Not found";
1002 $this->http_status($status);
1010 * PUT method handler
1017 if ($this->_check_lock_status($this->path)) {
1019 $options["path"] = $this->path;
1020 $options["content_length"] = $_SERVER["CONTENT_LENGTH"];
1022 // get the Content-type
1023 if (isset($_SERVER["CONTENT_TYPE"])) {
1024 // for now we do not support any sort of multipart requests
1025 if (!strncmp($_SERVER["CONTENT_TYPE"], "multipart/", 10)) {
1026 $this->http_status("501 not implemented");
1027 echo "The service does not support mulipart PUT requests";
1030 $options["content_type"] = $_SERVER["CONTENT_TYPE"];
1032 // default content type if none given
1033 $options["content_type"] = "application/octet-stream";
1036 /* RFC 2616 2.6 says: "The recipient of the entity MUST NOT
1037 ignore any Content-* (e.g. Content-Range) headers that it
1038 does not understand or implement and MUST return a 501
1039 (Not Implemented) response in such cases."
1041 foreach ($_SERVER as $key => $val) {
1042 if (strncmp($key, "HTTP_CONTENT", 11)) continue;
1044 case 'HTTP_CONTENT_ENCODING': // RFC 2616 14.11
1045 // TODO support this if ext/zlib filters are available
1046 $this->http_status("501 not implemented");
1047 echo "The service does not support '$val' content encoding";
1050 case 'HTTP_CONTENT_LANGUAGE': // RFC 2616 14.12
1051 // we assume it is not critical if this one is ignored
1052 // in the actual PUT implementation ...
1053 $options["content_language"] = $value;
1056 case 'HTTP_CONTENT_LOCATION': // RFC 2616 14.14
1057 /* The meaning of the Content-Location header in PUT
1058 or POST requests is undefined; servers are free
1059 to ignore it in those cases. */
1062 case 'HTTP_CONTENT_RANGE': // RFC 2616 14.16
1063 // single byte range requests are supported
1064 // the header format is also specified in RFC 2616 14.16
1065 // TODO we have to ensure that implementations support this or send 501 instead
1066 if (!preg_match('@bytes\s+(\d+)-(\d+)/((\d+)|\*)@', $value, $matches)) {
1067 $this->http_status("400 bad request");
1068 echo "The service does only support single byte ranges";
1072 $range = array("start"=>$matches[1], "end"=>$matches[2]);
1073 if (is_numeric($matches[3])) {
1074 $range["total_length"] = $matches[3];
1076 $option["ranges"][] = $range;
1078 // TODO make sure the implementation supports partial PUT
1079 // this has to be done in advance to avoid data being overwritten
1080 // on implementations that do not support this ...
1083 case 'HTTP_CONTENT_MD5': // RFC 2616 14.15
1084 // TODO: maybe we can just pretend here?
1085 $this->http_status("501 not implemented");
1086 echo "The service does not support content MD5 checksum verification";
1090 // any other unknown Content-* headers
1091 $this->http_status("501 not implemented");
1092 echo "The service does not support '$key'";
1097 $options["stream"] = fopen("php://input", "r");
1099 $stat = $this->PUT($options);
1101 if (is_resource($stat) && get_resource_type($stat) == "stream") {
1103 if (!empty($options["ranges"])) {
1104 // TODO multipart support is missing (see also above)
1105 // TODO error checking
1106 $stat = fseek($stream, $range[0]["start"], SEEK_SET);
1107 fwrite($stream, fread($options["stream"], $range[0]["end"]-$range[0]["start"]+1));
1109 while (!feof($options["stream"])) {
1110 fwrite($stream, fread($options["stream"], 4096));
1115 $stat = $options["new"] ? "201 Created" : "204 No Content";
1118 $this->http_status($stat);
1120 $this->http_status("423 Locked");
1127 // {{{ http_DELETE()
1130 * DELETE method handler
1135 function http_DELETE()
1137 // check RFC 2518 Section 9.2, last paragraph
1138 if (isset($_SERVER["HTTP_DEPTH"])) {
1139 if ($_SERVER["HTTP_DEPTH"] != "infinity") {
1140 $this->http_status("400 Bad Request");
1145 // check lock status
1146 if ($this->_check_lock_status($this->path)) {
1149 $options["path"] = $this->path;
1151 $stat = $this->delete($options);
1153 $this->http_status($stat);
1155 // sorry, its locked
1156 $this->http_status("423 Locked");
1165 * COPY method handler
1170 function http_COPY()
1172 // no need to check source lock status here
1173 // destination lock status is always checked by the helper method
1174 $this->_copymove("copy");
1182 * MOVE method handler
1187 function http_MOVE()
1189 if ($this->_check_lock_status($this->path)) {
1190 // destination lock status is always checked by the helper method
1191 $this->_copymove("move");
1193 $this->http_status("423 Locked");
1203 * LOCK method handler
1208 function http_LOCK()
1211 $options["path"] = $this->path;
1213 if (isset($_SERVER['HTTP_DEPTH'])) {
1214 $options["depth"] = $_SERVER["HTTP_DEPTH"];
1216 $options["depth"] = "infinity";
1219 if (isset($_SERVER["HTTP_TIMEOUT"])) {
1220 $options["timeout"] = explode(",", $_SERVER["HTTP_TIMEOUT"]);
1223 if(empty($_SERVER['CONTENT_LENGTH']) && !empty($_SERVER['HTTP_IF'])) {
1224 // check if locking is possible
1225 if(!$this->_check_lock_status($this->path)) {
1226 $this->http_status("423 Locked");
1231 $options["update"] = substr($_SERVER['HTTP_IF'], 2, -2);
1232 $stat = $this->lock($options);
1234 // extract lock request information from request XML payload
1235 $lockinfo = new _parse_lockinfo("php://input");
1236 if (!$lockinfo->success) {
1237 $this->http_status("400 bad request");
1240 // check if locking is possible
1241 if(!$this->_check_lock_status($this->path, $lockinfo->lockscope === "shared")) {
1242 $this->http_status("423 Locked");
1247 $options["scope"] = $lockinfo->lockscope;
1248 $options["type"] = $lockinfo->locktype;
1249 $options["owner"] = $lockinfo->owner;
1251 $options["locktoken"] = $this->_new_locktoken();
1253 $stat = $this->lock($options);
1256 if(is_bool($stat)) {
1257 $http_stat = $stat ? "200 OK" : "423 Locked";
1262 $this->http_status($http_stat);
1264 if ($http_stat{0} == 2) { // 2xx states are ok
1265 if($options["timeout"]) {
1266 // more than a million is considered an absolute timestamp
1267 // less is more likely a relative value
1268 if($options["timeout"]>1000000) {
1269 $timeout = "Second-".($options['timeout']-time());
1271 $timeout = "Second-$options[timeout]";
1274 $timeout = "Infinite";
1277 header('Content-Type: text/xml; charset="utf-8"');
1278 header("Lock-Token: <$options[locktoken]>");
1279 echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
1280 echo "<D:prop xmlns:D=\"DAV:\">\n";
1281 echo " <D:lockdiscovery>\n";
1282 echo " <D:activelock>\n";
1283 echo " <D:lockscope><D:$options[scope]/></D:lockscope>\n";
1284 echo " <D:locktype><D:$options[type]/></D:locktype>\n";
1285 echo " <D:depth>$options[depth]</D:depth>\n";
1286 echo " <D:owner>$options[owner]</D:owner>\n";
1287 echo " <D:timeout>$timeout</D:timeout>\n";
1288 echo " <D:locktoken><D:href>$options[locktoken]</D:href></D:locktoken>\n";
1289 echo " </D:activelock>\n";
1290 echo " </D:lockdiscovery>\n";
1291 echo "</D:prop>\n\n";
1298 // {{{ http_UNLOCK()
1301 * UNLOCK method handler
1306 function http_UNLOCK()
1309 $options["path"] = $this->path;
1311 if (isset($_SERVER['HTTP_DEPTH'])) {
1312 $options["depth"] = $_SERVER["HTTP_DEPTH"];
1314 $options["depth"] = "infinity";
1317 // strip surrounding <>
1318 $options["token"] = substr(trim($_SERVER["HTTP_LOCK_TOKEN"]), 1, -1);
1321 $stat = $this->unlock($options);
1323 $this->http_status($stat);
1332 function _copymove($what)
1335 $options["path"] = $this->path;
1337 if (isset($_SERVER["HTTP_DEPTH"])) {
1338 $options["depth"] = $_SERVER["HTTP_DEPTH"];
1340 $options["depth"] = "infinity";
1343 extract(parse_url($_SERVER["HTTP_DESTINATION"]));
1345 if (isset($port) && $port != 80)
1346 $http_host.= ":$port";
1348 list($http_header_host,$http_header_port) = explode(":",$_SERVER["HTTP_HOST"]);
1349 if (isset($http_header_port) && $http_header_port != 80) {
1350 $http_header_host .= ":".$http_header_port;
1353 if ($http_host == $http_header_host &&
1354 !strncmp($_SERVER["SCRIPT_NAME"], $path,
1355 strlen($_SERVER["SCRIPT_NAME"]))) {
1356 $options["dest"] = substr($path, strlen($_SERVER["SCRIPT_NAME"]));
1357 if (!$this->_check_lock_status($options["dest"])) {
1358 $this->http_status("423 Locked");
1363 $options["dest_url"] = $_SERVER["HTTP_DESTINATION"];
1366 // see RFC 2518 Sections 9.6, 8.8.4 and 8.9.3
1367 if (isset($_SERVER["HTTP_OVERWRITE"])) {
1368 $options["overwrite"] = $_SERVER["HTTP_OVERWRITE"] == "T";
1370 $options["overwrite"] = true;
1373 $stat = $this->$what($options);
1374 $this->http_status($stat);
1382 * check for implemented HTTP methods
1385 * @return array something
1389 // OPTIONS is always there
1390 $allow = array("OPTIONS" =>"OPTIONS");
1392 // all other METHODS need both a http_method() wrapper
1393 // and a method() implementation
1394 // the base class supplies wrappers only
1395 foreach(get_class_methods($this) as $method) {
1396 if (!strncmp("http_", $method, 5)) {
1397 $method = strtoupper(substr($method, 5));
1398 if (method_exists($this, $method)) {
1399 $allow[$method] = $method;
1404 // we can emulate a missing HEAD implemetation using GET
1405 if (isset($allow["GET"]))
1406 $allow["HEAD"] = "HEAD";
1408 // no LOCK without checklok()
1409 if (!method_exists($this, "checklock")) {
1410 unset($allow["LOCK"]);
1411 unset($allow["UNLOCK"]);
1420 * helper for property element creation
1422 * @param string XML namespace (optional)
1423 * @param string property name
1424 * @param string property value
1425 * @return array property array
1429 $args = func_get_args();
1430 if (count($args) == 3) {
1431 return array("ns" => $args[0],
1435 return array("ns" => "DAV:",
1444 * check authentication if check is implemented
1447 * @return bool true if authentication succeded or not necessary
1449 function _check_auth()
1451 if (method_exists($this, "checkAuth")) {
1452 // PEAR style method name
1453 return $this->checkAuth(@$_SERVER["AUTH_TYPE"],
1454 @$_SERVER["PHP_AUTH_USER"],
1455 @$_SERVER["PHP_AUTH_PW"]);
1456 } else if (method_exists($this, "check_auth")) {
1457 // old (pre 1.0) method name
1458 return $this->check_auth(@$_SERVER["AUTH_TYPE"],
1459 @$_SERVER["PHP_AUTH_USER"],
1460 @$_SERVER["PHP_AUTH_PW"]);
1462 // no method found -> no authentication required
1472 * generate Unique Universal IDentifier for lock token
1475 * @return string a new UUID
1477 function _new_uuid()
1479 // use uuid extension from PECL if available
1480 if (function_exists("uuid_create")) {
1481 return uuid_create();
1485 $uuid = md5(microtime().getmypid()); // this should be random enough for now
1487 // set variant and version fields for 'true' random uuid
1489 $n = 8 + (ord($uuid{16}) & 3);
1490 $hex = "0123456789abcdef";
1491 $uuid{16} = $hex{$n};
1493 // return formated uuid
1494 return substr($uuid, 0, 8)."-"
1495 . substr($uuid, 8, 4)."-"
1496 . substr($uuid, 12, 4)."-"
1497 . substr($uuid, 16, 4)."-"
1498 . substr($uuid, 20);
1502 * create a new opaque lock token as defined in RFC2518
1505 * @return string new RFC2518 opaque lock token
1507 function _new_locktoken()
1509 return "opaquelocktoken:".$this->_new_uuid();
1514 // {{{ WebDAV If: header parsing
1519 * @param string header string to parse
1520 * @param int current parsing position
1521 * @return array next token (type and value)
1523 function _if_header_lexer($string, &$pos)
1526 while (ctype_space($string{$pos})) {
1530 // already at end of string?
1531 if (strlen($string) <= $pos) {
1535 // get next character
1536 $c = $string{$pos++};
1538 // now it depends on what we found
1541 // URIs are enclosed in <...>
1542 $pos2 = strpos($string, ">", $pos);
1543 $uri = substr($string, $pos, $pos2 - $pos);
1545 return array("URI", $uri);
1548 //Etags are enclosed in [...]
1549 if ($string{$pos} == "W") {
1550 $type = "ETAG_WEAK";
1553 $type = "ETAG_STRONG";
1555 $pos2 = strpos($string, "]", $pos);
1556 $etag = substr($string, $pos + 1, $pos2 - $pos - 2);
1558 return array($type, $etag);
1561 // "N" indicates negation
1563 return array("NOT", "Not");
1566 // anything else is passed verbatim char by char
1567 return array("CHAR", $c);
1574 * @param string header string
1575 * @return array URIs and their conditions
1577 function _if_header_parser($str)
1580 $len = strlen($str);
1585 while ($pos < $len) {
1587 $token = $this->_if_header_lexer($str, $pos);
1590 if ($token[0] == "URI") {
1591 $uri = $token[1]; // remember URI
1592 $token = $this->_if_header_lexer($str, $pos); // get next token
1598 if ($token[0] != "CHAR" || $token[1] != "(") {
1606 $token = $this->_if_header_lexer($str, $pos);
1607 if ($token[0] == "NOT") {
1611 switch ($token[0]) {
1613 switch ($token[1]) {
1626 $list[] = $not."<$token[1]>";
1630 $list[] = $not."[W/'$token[1]']>";
1634 $list[] = $not."['$token[1]']>";
1643 if (@is_array($uris[$uri])) {
1644 $uris[$uri] = array_merge($uris[$uri],$list);
1646 $uris[$uri] = $list;
1654 * check if conditions from "If:" headers are meat
1656 * the "If:" header is an extension to HTTP/1.1
1657 * defined in RFC 2518 section 9.4
1662 function _check_if_header_conditions()
1664 if (isset($_SERVER["HTTP_IF"])) {
1665 $this->_if_header_uris =
1666 $this->_if_header_parser($_SERVER["HTTP_IF"]);
1668 foreach($this->_if_header_uris as $uri => $conditions) {
1670 // default uri is the complete request uri
1671 $uri = (@$_SERVER["HTTPS"] === "on" ? "https:" : "http:");
1672 $uri.= "//$_SERVER[HTTP_HOST]$_SERVER[SCRIPT_NAME]$_SERVER[PATH_INFO]";
1676 foreach($conditions as $condition) {
1677 // lock tokens may be free form (RFC2518 6.3)
1678 // but if opaquelocktokens are used (RFC2518 6.4)
1679 // we have to check the format (litmus tests this)
1680 if (!strncmp($condition, "<opaquelocktoken:", strlen("<opaquelocktoken"))) {
1681 if (!preg_match("/^<opaquelocktoken:[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}>$/", $condition)) {
1685 if (!$this->_check_uri_condition($uri, $condition)) {
1692 if ($state == true) {
1702 * Check a single URI condition parsed from an if-header
1704 * Check a single URI condition parsed from an if-header
1707 * @param string $uri URI to check
1708 * @param string $condition Condition to check for this URI
1709 * @returns bool Condition check result
1711 function _check_uri_condition($uri, $condition)
1713 // not really implemented here,
1714 // implementations must override
1722 * @param string path of resource to check
1723 * @param bool exclusive lock?
1725 function _check_lock_status($path, $exclusive_only = false)
1727 // FIXME depth -> ignored for now
1728 if (method_exists($this, "checkLock")) {
1730 $lock = $this->checkLock($path);
1732 // ... and lock is not owned?
1733 if (is_array($lock) && count($lock)) {
1734 // FIXME doesn't check uri restrictions yet
1735 if (!strstr($_SERVER["HTTP_IF"], $lock["token"])) {
1736 if (!$exclusive_only || ($lock["scope"] !== "shared"))
1749 * Generate lockdiscovery reply from checklock() result
1751 * @param string resource path to check
1752 * @return string lockdiscovery response
1754 function lockdiscovery($path)
1756 // no lock support without checklock() method
1757 if (!method_exists($this, "checklock")) {
1761 // collect response here
1764 // get checklock() reply
1765 $lock = $this->checklock($path);
1767 // generate <activelock> block for returned data
1768 if (is_array($lock) && count($lock)) {
1769 // check for 'timeout' or 'expires'
1770 if (!empty($lock["expires"])) {
1771 $timeout = "Second-".($lock["expires"] - time());
1772 } else if (!empty($lock["timeout"])) {
1773 $timeout = "Second-$lock[timeout]";
1775 $timeout = "Infinite";
1778 // genreate response block
1781 <D:lockscope><D:$lock[scope]/></D:lockscope>
1782 <D:locktype><D:$lock[type]/></D:locktype>
1783 <D:depth>$lock[depth]</D:depth>
1784 <D:owner>$lock[owner]</D:owner>
1785 <D:timeout>$timeout</D:timeout>
1786 <D:locktoken><D:href>$lock[token]</D:href></D:locktoken>
1791 // return generated response
1792 return $activelocks;
1796 * set HTTP return status and mirror it in a private header
1798 * @param string status code and message
1801 function http_status($status)
1803 // simplified success case
1804 if($status === true) {
1809 $this->_http_status = $status;
1811 // generate HTTP status response
1812 header("HTTP/1.1 $status");
1813 header("X-WebDAV-Status: $status", true);
1817 * private minimalistic version of PHP urlencode()
1819 * only blanks and XML special chars must be encoded here
1820 * full urlencode() encoding confuses some clients ...
1822 * @param string URL to encode
1823 * @return string encoded URL
1825 function _urlencode($url)
1827 return strtr($url, array(" "=>"%20",
1835 * private version of PHP urldecode
1837 * not really needed but added for completenes
1839 * @param string URL to decode
1840 * @return string decoded URL
1842 function _urldecode($path)
1844 return urldecode($path);
1848 * UTF-8 encode property values if not already done so
1850 * @param string text to encode
1851 * @return string utf-8 encoded text
1853 function _prop_encode($text)
1855 switch (strtolower($this->_prop_encoding)) {
1862 return utf8_encode($text);