]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - include/HTTP_WebDAV_Server/Server.php
Release 6.5.0
[Github/sugarcrm.git] / include / HTTP_WebDAV_Server / Server.php
1 <?php //
2 // +----------------------------------------------------------------------+
3 // | PHP Version 4                                                        |
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 // +----------------------------------------------------------------------+
18 //
19
20 //
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";
24
25
26
27 /**
28  * Virtual base class for implementing WebDAV servers
29  *
30  * WebDAV server base class, needs to be extended to do useful work
31  *
32  * @package HTTP_WebDAV_Server
33  * @author Hartmut Holzgraefe <hholzgra@php.net>
34  * @version 0.99.1dev
35  */
36 class HTTP_WebDAV_Server
37 {
38     // {{{ Member Variables
39
40     /**
41      * URI path for this request
42      *
43      * @var string
44      */
45     var $path;
46
47     /**
48      * Realm string to be used in authentification popups
49      *
50      * @var string
51      */
52     var $http_auth_realm = "PHP WebDAV";
53
54     /**
55      * String to be used in "X-Dav-Powered-By" header
56      *
57      * @var string
58      */
59     var $dav_powered_by = "";
60
61     /**
62      * Remember parsed If: (RFC2518/9.4) header conditions
63      *
64      * @var array
65      */
66     var $_if_header_uris = array();
67
68     /**
69      * HTTP response status/message
70      *
71      * @var string
72      */
73     var $_http_status = "200 OK";
74
75     /**
76      * encoding of property values passed in
77      *
78      * @var string
79      */
80     var $_prop_encoding = "utf-8";
81
82     // }}}
83
84     // {{{ Constructor
85
86     /**
87      * Constructor
88      *
89      * @param void
90      */
91     function HTTP_WebDAV_Server()
92     {
93         // PHP messages destroy XML output -> switch them off
94         ini_set("display_errors", 0);
95     }
96
97     // }}}
98
99     // {{{ ServeRequest()
100     /**
101      * Serve WebDAV HTTP request
102      *
103      * dispatch WebDAV HTTP request to the apropriate method handler
104      *
105      * @param  void
106      * @return void
107      */
108     function ServeRequest()
109     {
110         // identify ourselves
111         if (empty($this->dav_powered_by)) {
112             header("X-Dav-Powered-By: PHP class: ".get_class($this));
113         } else {
114             header("X-Dav-Powered-By: ".$this->dav_powered_by );
115         }
116
117         // check authentication
118         if (!$this->_check_auth()) {
119             $this->http_status('401 Unauthorized');
120
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).'"');
126
127             return;
128         }
129
130         // check
131         if(! $this->_check_if_header_conditions()) {
132             $this->http_status("412 Precondition failed");
133             return;
134         }
135
136         // set path
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);
140         }
141
142
143         // detect requested method names
144         $method = strtolower($_SERVER["REQUEST_METHOD"]);
145         $wrapper = "http_".$method;
146
147         // activate HEAD emulation by GET if no HEAD method found
148         if ($method == "head" && !method_exists($this, "head")) {
149             $method = "get";
150         }
151
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");
157             } else {
158                 $this->http_status("405 Method not allowed");
159                 header("Allow: ".join(", ", $this->_allow()));  // tell client what's allowed
160             }
161         }
162     }
163
164     // }}}
165
166     // {{{ abstract WebDAV methods
167
168     // {{{ GET()
169     /**
170      * GET implementation
171      *
172      * overload this method to retrieve resources from your server
173      * <br>
174      *
175      *
176      * @abstract
177      * @param array &$params Array of input and output parameters
178      * <br><b>input</b><ul>
179      * <li> path -
180      * </ul>
181      * <br><b>output</b><ul>
182      * <li> size -
183      * </ul>
184      * @returns int HTTP-Statuscode
185      */
186
187     /* abstract
188        function GET(&$params)
189        {
190            // dummy entry for PHPDoc
191        }
192      */
193
194     // }}}
195
196     // {{{ PUT()
197     /**
198      * PUT implementation
199      *
200      * PUT implementation
201      *
202      * @abstract
203      * @param array &$params
204      * @returns int HTTP-Statuscode
205      */
206
207     /* abstract
208        function PUT()
209        {
210            // dummy entry for PHPDoc
211        }
212     */
213
214     // }}}
215
216     // {{{ COPY()
217
218     /**
219      * COPY implementation
220      *
221      * COPY implementation
222      *
223      * @abstract
224      * @param array &$params
225      * @returns int HTTP-Statuscode
226      */
227
228     /* abstract
229        function COPY()
230        {
231            // dummy entry for PHPDoc
232        }
233      */
234
235     // }}}
236
237     // {{{ MOVE()
238
239     /**
240      * MOVE implementation
241      *
242      * MOVE implementation
243      *
244      * @abstract
245      * @param array &$params
246      * @returns int HTTP-Statuscode
247      */
248
249     /* abstract
250        function MOVE()
251        {
252            // dummy entry for PHPDoc
253        }
254      */
255
256     // }}}
257
258     // {{{ DELETE()
259
260     /**
261      * DELETE implementation
262      *
263      * DELETE implementation
264      *
265      * @abstract
266      * @param array &$params
267      * @returns int HTTP-Statuscode
268      */
269
270     /* abstract
271        function DELETE()
272        {
273            // dummy entry for PHPDoc
274        }
275      */
276     // }}}
277
278     // {{{ PROPFIND()
279
280     /**
281      * PROPFIND implementation
282      *
283      * PROPFIND implementation
284      *
285      * @abstract
286      * @param array &$params
287      * @returns int HTTP-Statuscode
288      */
289
290     /* abstract
291        function PROPFIND()
292        {
293            // dummy entry for PHPDoc
294        }
295      */
296
297     // }}}
298
299     // {{{ PROPPATCH()
300
301     /**
302      * PROPPATCH implementation
303      *
304      * PROPPATCH implementation
305      *
306      * @abstract
307      * @param array &$params
308      * @returns int HTTP-Statuscode
309      */
310
311     /* abstract
312        function PROPPATCH()
313        {
314            // dummy entry for PHPDoc
315        }
316      */
317     // }}}
318
319     // {{{ LOCK()
320
321     /**
322      * LOCK implementation
323      *
324      * LOCK implementation
325      *
326      * @abstract
327      * @param array &$params
328      * @returns int HTTP-Statuscode
329      */
330
331     /* abstract
332        function LOCK()
333        {
334            // dummy entry for PHPDoc
335        }
336      */
337     // }}}
338
339     // {{{ UNLOCK()
340
341     /**
342      * UNLOCK implementation
343      *
344      * UNLOCK implementation
345      *
346      * @abstract
347      * @param array &$params
348      * @returns int HTTP-Statuscode
349      */
350
351     /* abstract
352        function UNLOCK()
353        {
354            // dummy entry for PHPDoc
355        }
356      */
357     // }}}
358
359     // }}}
360
361     // {{{ other abstract methods
362
363     // {{{ check_auth()
364
365     /**
366      * check authentication
367      *
368      * overload this method to retrieve and confirm authentication information
369      *
370      * @abstract
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
375      */
376
377     /* abstract
378        function checkAuth($type, $username, $password)
379        {
380            // dummy entry for PHPDoc
381        }
382     */
383
384     // }}}
385
386     // {{{ checklock()
387
388     /**
389      * check lock status for a resource
390      *
391      * overload this method to return shared and exclusive locks
392      * active for this resource
393      *
394      * @abstract
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'
398      */
399
400     /* abstract
401        function checklock($resource)
402        {
403            // dummy entry for PHPDoc
404        }
405      */
406
407     // }}}
408
409     // }}}
410
411     // {{{ WebDAV HTTP method wrappers
412
413     // {{{ http_OPTIONS()
414
415     /**
416      * OPTIONS method handler
417      *
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
421      *
422      * @param  void
423      * @return void
424      */
425     function http_OPTIONS()
426     {
427         // Microsoft clients default to the Frontpage protocol
428         // unless we tell them to use WebDAV
429         header("MS-Author-Via: DAV");
430
431         // get allowed methods
432         $allow = $this->_allow();
433
434         // dav header
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
438         }
439
440         // tell clients what we found
441         $this->http_status("200 OK");
442         header("DAV: "  .join("," , $dav));
443         header("Allow: ".join(", ", $allow));
444     }
445
446     // }}}
447
448
449     // {{{ http_PROPFIND()
450
451     /**
452      * PROPFIND method handler
453      *
454      * @param  void
455      * @return void
456      */
457     function http_PROPFIND()
458     {
459         $options = Array();
460         $options["path"] = $this->path;
461
462         // search depth from header (default is "infinity)
463         if (isset($_SERVER['HTTP_DEPTH'])) {
464             $options["depth"] = $_SERVER["HTTP_DEPTH"];
465         } else {
466             $options["depth"] = "infinity";
467         }
468
469         // analyze request payload
470         $propinfo = new _parse_propfind("php://input");
471         if (!$propinfo->success) {
472             $this->http_status("400 Error");
473             return;
474         }
475         $options['props'] = $propinfo->props;
476
477         // call user handler
478         if (!$this->propfind($options, $files)) {
479             $this->http_status("404 Not Found");
480             return;
481         }
482
483         // collect namespaces here
484         $ns_hash = array();
485
486         // Microsoft Clients need this special namespace for date and time values
487         $ns_defs = "xmlns:ns0=\"urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/\"";
488
489         // now we loop over all returned file entries
490         foreach($files["files"] as $filekey => $file) {
491
492             // nothing to do if no properties were returend for a file
493             if (!isset($file["props"]) || !is_array($file["props"])) {
494                 continue;
495             }
496
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
502
503                 switch($options['props']) {
504                 case "all":
505                     // nothing to remove
506                     break;
507
508                 case "names":
509                     // only the names of all existing properties were requested
510                     // so we remove all values
511                     unset($files["files"][$filekey]["props"][$key]["val"]);
512                     break;
513
514                 default:
515                     $found = false;
516
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"]) {
521                             $found = true;
522                             break;
523                         }
524                     }
525
526                     // unset property and continue with next one if not found/requested
527                     if (!$found) {
528                         $files["files"][$filekey]["props"][$key]="";
529                         continue(2);
530                     }
531                     break;
532                 }
533
534                 // namespace handling
535                 if (empty($prop["ns"])) continue; // no namespace
536                 $ns = $prop["ns"];
537                 if ($ns == "DAV:") continue; // default namespace
538                 if (isset($ns_hash[$ns])) continue; // already known
539
540                 // register namespace
541                 $ns_name = "ns".(count($ns_hash) + 1);
542                 $ns_hash[$ns] = $ns_name;
543                 $ns_defs .= " xmlns:$ns_name=\"$ns\"";
544             }
545
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
551
552                     $found = false;
553
554                     // check if property exists in result
555                     foreach($file["props"] as $prop) {
556                         if (   $reqprop["name"]  == $prop["name"]
557                             && $reqprop["xmlns"] == $prop["ns"]) {
558                             $found = true;
559                             break;
560                         }
561                     }
562
563                     if (!$found) {
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:",
568                                                 "lockdiscovery" ,
569                                                 $this->lockdiscovery($files["files"][$filekey]['path']));
570                         } else {
571                             // add empty value for this property
572                             $files["files"][$filekey]["noprops"][] =
573                                 $this->mkprop($reqprop["xmlns"], $reqprop["name"], "");
574
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]\"";
580                             }
581                         }
582                     }
583                 }
584             }
585         }
586
587         // now we generate the reply header ...
588         $this->http_status("207 Multi-Status");
589         header('Content-Type: text/xml; charset="utf-8"');
590
591         // ... and payload
592         echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
593         echo "<D:multistatus xmlns:D=\"DAV:\">\n";
594
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;
600
601             echo " <D:response $ns_defs>\n";
602
603             $href = (@$_SERVER["HTTPS"] === "on" ? "https:" : "http:");
604             $href.= "//".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'];
605             $href.= $path;
606             //TODO make sure collection resource pathes end in a trailing slash
607
608             echo "  <D:href>$href</D:href>\n";
609
610             // report all found properties and their values (if any)
611             if (isset($file["props"]) && is_array($file["props"])) {
612                 echo "   <D:propstat>\n";
613                 echo "    <D:prop>\n";
614
615                 foreach($file["props"] as $key => $prop) {
616
617                     if (!is_array($prop)) continue;
618                     if (!isset($prop["name"])) continue;
619
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";
626                         } else {
627                             echo "     <$prop[name] xmlns=\"\"/>";
628                         }
629                     } else if ($prop["ns"] == "DAV:") {
630                         // some WebDAV properties need special treatment
631                         switch ($prop["name"]) {
632                         case "creationdate":
633                             echo "     <D:creationdate ns0:dt=\"dateTime.tz\">"
634                                 . gmdate("Y-m-d\\TH:i:s\\Z",$prop['val'])
635                                 . "</D:creationdate>\n";
636                             break;
637                         case "getlastmodified":
638                             echo "     <D:getlastmodified ns0:dt=\"dateTime.rfc1123\">"
639                                 . TimeDate::httptime($prop['val'])
640                                 . "</D:getlastmodified>\n";
641                             break;
642                         case "resourcetype":
643                             echo "     <D:resourcetype><D:$prop[val]/></D:resourcetype>\n";
644                             break;
645                         case "supportedlock":
646                             echo "     <D:supportedlock>$prop[val]</D:supportedlock>\n";
647                             break;
648                         case "lockdiscovery":
649                             echo "     <D:lockdiscovery>\n";
650                             echo $prop["val"];
651                             echo "     </D:lockdiscovery>\n";
652                             break;
653                         default:
654                             echo "     <D:$prop[name]>"
655                                 . $this->_prop_encode(htmlspecialchars($prop['val']))
656                                 .     "</D:$prop[name]>\n";
657                             break;
658                         }
659                     } else {
660                         // properties from namespaces != "DAV:" or without any namespace
661                         if ($prop["ns"]) {
662                             echo "     <" . $ns_hash[$prop["ns"]] . ":$prop[name]>"
663                                 . $this->_prop_encode(htmlspecialchars($prop['val']))
664                                 . "</" . $ns_hash[$prop["ns"]] . ":$prop[name]>\n";
665                         } else {
666                             echo "     <$prop[name] xmlns=\"\">"
667                                 . $this->_prop_encode(htmlspecialchars($prop['val']))
668                                 . "</$prop[name]>\n";
669                         }
670                     }
671                 }
672
673                 echo "   </D:prop>\n";
674                 echo "   <D:status>HTTP/1.1 200 OK</D:status>\n";
675                 echo "  </D:propstat>\n";
676             }
677
678             // now report all properties requested bot not found
679             if (isset($file["noprops"])) {
680                 echo "   <D:propstat>\n";
681                 echo "    <D:prop>\n";
682
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";
688                     } else {
689                         echo "     <" . $ns_hash[$prop["ns"]] . ":$prop[name]/>\n";
690                     }
691                 }
692
693                 echo "   </D:prop>\n";
694                 echo "   <D:status>HTTP/1.1 404 Not Found</D:status>\n";
695                 echo "  </D:propstat>\n";
696             }
697
698             echo " </D:response>\n";
699         }
700
701         echo "</D:multistatus>\n";
702     }
703
704
705     // }}}
706
707     // {{{ http_PROPPATCH()
708
709     /**
710      * PROPPATCH method handler
711      *
712      * @param  void
713      * @return void
714      */
715     function http_PROPPATCH()
716     {
717         if($this->_check_lock_status($this->path)) {
718             $options = Array();
719             $options["path"] = $this->path;
720
721             $propinfo = new _parse_proppatch("php://input");
722
723             if (!$propinfo->success) {
724                 $this->http_status("400 Error");
725                 return;
726             }
727
728             $options['props'] = $propinfo->props;
729
730             $responsedescr = $this->proppatch($options);
731
732             $this->http_status("207 Multi-Status");
733             header('Content-Type: text/xml; charset="utf-8"');
734
735             echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
736
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";
740
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";
746             }
747
748             if ($responsedescr) {
749                 echo "  <D:responsedescription>".
750                     $this->_prop_encode(htmlspecialchars($responsedescr)).
751                     "</D:responsedescription>\n";
752             }
753
754             echo " </D:response>\n";
755             echo "</D:multistatus>\n";
756         } else {
757             $this->http_status("423 Locked");
758         }
759     }
760
761     // }}}
762
763
764     // {{{ http_MKCOL()
765
766     /**
767      * MKCOL method handler
768      *
769      * @param  void
770      * @return void
771      */
772     function http_MKCOL()
773     {
774         $options = Array();
775         $options["path"] = $this->path;
776
777         $stat = $this->mkcol($options);
778
779         $this->http_status($stat);
780     }
781
782     // }}}
783
784
785     // {{{ http_GET()
786
787     /**
788      * GET method handler
789      *
790      * @param void
791      * @returns void
792      */
793     function http_GET()
794     {
795         // TODO check for invalid stream
796         $options = Array();
797         $options["path"] = $this->path;
798
799         $this->_get_ranges($options);
800
801         if (true === ($status = $this->get($options))) {
802             if (!headers_sent()) {
803                 $status = "200 OK";
804
805                 if (!isset($options['mimetype'])) {
806                     $options['mimetype'] = "application/octet-stream";
807                 }
808                 header("Content-type: $options[mimetype]");
809
810                 if (isset($options['mtime'])) {
811                     header("Last-modified: ".TimeDate::httpTime());
812                 }
813
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
818
819                         if (count($options['ranges']) === 1) {
820                             $range = $options['ranges'][0];
821
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");
826                                     exit;
827                                 }
828
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);
838                                         echo $buffer;
839                                     }
840                                 } else {
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'] : "*"));
846                                     }
847                                     fpassthru($options['stream']);
848                                 }
849                             } else {
850                                 header("Content-length: ".$range['last']);
851                                 fseek($options['stream'], -$range['last'], SEEK_END);
852                                 fpassthru($options['stream']);
853                             }
854                         } else {
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;
861                                 } else {
862                                     $from = $options['size'] - $range['last']-1;
863                                     $to = $options['size'] -1;
864                                 }
865                                 $total = isset($options['size']) ? $options['size'] : "*";
866                                 $size = $to - $from + 1;
867                                 $this->_multipart_byterange_header($options['mimetype'], $from, $to, $total);
868
869
870                                 fseek($options['stream'], $start, SEEK_SET);
871                                 while ($size && !feof($options['stream'])) {
872                                     $buffer = fread($options['stream'], 4096);
873                                     $size -= strlen($buffer);
874                                     echo $buffer;
875                                 }
876                             }
877                             $this->_multipart_byterange_header(); // end multipart
878                         }
879                     } else {
880                         // normal request or stream isn't seekable, return full content
881                         if (isset($options['size'])) {
882                             header("Content-length: ".$options['size']);
883                         }
884                         fpassthru($options['stream']);
885                         return; // no more headers
886                     }
887                 } elseif (isset($options['data']))  {
888                     if (is_array($options['data'])) {
889                         // reply to partial request
890                     } else {
891                         header("Content-length: ".strlen($options['data']));
892                         echo $options['data'];
893                     }
894                 }
895             }
896         }
897
898         if (false === $status) {
899             $this->http_status("404 not found");
900         }
901
902         $this->http_status("$status");
903     }
904
905
906     /**
907      * parse HTTP Range: header
908      *
909      * @param  array options array to store result in
910      * @return void
911      */
912     function _get_ranges(&$options)
913     {
914         // process Range: header if present
915         if (isset($_SERVER['HTTP_RANGE'])) {
916
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();
920
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);
928                 }
929             }
930         }
931     }
932
933     /**
934      * generate separator headers for multipart response
935      *
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
940      *
941      * @param  string  mimetype
942      * @param  int     start byte position
943      * @param  int     end   byte position
944      * @param  int     total resource byte size
945      */
946     function _multipart_byterange_header($mimetype = false, $from = false, $to=false, $total=false)
947     {
948         if ($mimetype === false) {
949             if (!isset($this->multipart_separator)) {
950                 // initial
951
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());
955
956                 // generate HTTP header
957                 header("Content-type: multipart/byteranges; boundary=".$this->multipart_separator);
958             } else {
959                 // final
960
961                 // generate closing multipart sequence
962                 echo "\n--{$this->multipart_separator}--";
963             }
964         } else {
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);
969             echo "\n\n";
970         }
971     }
972
973
974
975     // }}}
976
977     // {{{ http_HEAD()
978
979     /**
980      * HEAD method handler
981      *
982      * @param  void
983      * @return void
984      */
985     function http_HEAD()
986     {
987         $status = false;
988         $options = Array();
989         $options["path"] = $this->path;
990
991         if (method_exists($this, "HEAD")) {
992             $status = $this->head($options);
993         } else if (method_exists($this, "GET")) {
994             ob_start();
995             $status = $this->GET($options);
996             ob_end_clean();
997         }
998
999         if($status===true)  $status = "200 OK";
1000         if($status===false) $status = "404 Not found";
1001
1002         $this->http_status($status);
1003     }
1004
1005     // }}}
1006
1007     // {{{ http_PUT()
1008
1009     /**
1010      * PUT method handler
1011      *
1012      * @param  void
1013      * @return void
1014      */
1015     function http_PUT()
1016     {
1017         if ($this->_check_lock_status($this->path)) {
1018             $options = Array();
1019             $options["path"] = $this->path;
1020             $options["content_length"] = $_SERVER["CONTENT_LENGTH"];
1021
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";
1028                     return;
1029                 }
1030                 $options["content_type"] = $_SERVER["CONTENT_TYPE"];
1031             } else {
1032                 // default content type if none given
1033                 $options["content_type"] = "application/octet-stream";
1034             }
1035
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."
1040             */
1041             foreach ($_SERVER as $key => $val) {
1042                 if (strncmp($key, "HTTP_CONTENT", 11)) continue;
1043                 switch ($key) {
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";
1048                     return;
1049
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;
1054                     break;
1055
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. */
1060                     break;
1061
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";
1069                         return;
1070                     }
1071
1072                     $range = array("start"=>$matches[1], "end"=>$matches[2]);
1073                     if (is_numeric($matches[3])) {
1074                         $range["total_length"] = $matches[3];
1075                     }
1076                     $option["ranges"][] = $range;
1077
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 ...
1081                     break;
1082
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";
1087                     return;
1088
1089                 default:
1090                     // any other unknown Content-* headers
1091                     $this->http_status("501 not implemented");
1092                     echo "The service does not support '$key'";
1093                     return;
1094                 }
1095             }
1096
1097             $options["stream"] = fopen("php://input", "r");
1098
1099             $stat = $this->PUT($options);
1100
1101             if (is_resource($stat) && get_resource_type($stat) == "stream") {
1102                 $stream = $stat;
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));
1108                 } else {
1109                     while (!feof($options["stream"])) {
1110                         fwrite($stream, fread($options["stream"], 4096));
1111                     }
1112                 }
1113                 fclose($stream);
1114
1115                 $stat = $options["new"] ? "201 Created" : "204 No Content";
1116             }
1117
1118             $this->http_status($stat);
1119         } else {
1120             $this->http_status("423 Locked");
1121         }
1122     }
1123
1124     // }}}
1125
1126
1127     // {{{ http_DELETE()
1128
1129     /**
1130      * DELETE method handler
1131      *
1132      * @param  void
1133      * @return void
1134      */
1135     function http_DELETE()
1136     {
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");
1141                 return;
1142             }
1143         }
1144
1145         // check lock status
1146         if ($this->_check_lock_status($this->path)) {
1147             // ok, proceed
1148             $options = Array();
1149             $options["path"] = $this->path;
1150
1151             $stat = $this->delete($options);
1152
1153             $this->http_status($stat);
1154         } else {
1155             // sorry, its locked
1156             $this->http_status("423 Locked");
1157         }
1158     }
1159
1160     // }}}
1161
1162     // {{{ http_COPY()
1163
1164     /**
1165      * COPY method handler
1166      *
1167      * @param  void
1168      * @return void
1169      */
1170     function http_COPY()
1171     {
1172         // no need to check source lock status here
1173         // destination lock status is always checked by the helper method
1174         $this->_copymove("copy");
1175     }
1176
1177     // }}}
1178
1179     // {{{ http_MOVE()
1180
1181     /**
1182      * MOVE method handler
1183      *
1184      * @param  void
1185      * @return void
1186      */
1187     function http_MOVE()
1188     {
1189         if ($this->_check_lock_status($this->path)) {
1190             // destination lock status is always checked by the helper method
1191             $this->_copymove("move");
1192         } else {
1193             $this->http_status("423 Locked");
1194         }
1195     }
1196
1197     // }}}
1198
1199
1200     // {{{ http_LOCK()
1201
1202     /**
1203      * LOCK method handler
1204      *
1205      * @param  void
1206      * @return void
1207      */
1208     function http_LOCK()
1209     {
1210         $options = Array();
1211         $options["path"] = $this->path;
1212
1213         if (isset($_SERVER['HTTP_DEPTH'])) {
1214             $options["depth"] = $_SERVER["HTTP_DEPTH"];
1215         } else {
1216             $options["depth"] = "infinity";
1217         }
1218
1219         if (isset($_SERVER["HTTP_TIMEOUT"])) {
1220             $options["timeout"] = explode(",", $_SERVER["HTTP_TIMEOUT"]);
1221         }
1222
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");
1227                 return;
1228             }
1229
1230             // refresh lock
1231             $options["update"] = substr($_SERVER['HTTP_IF'], 2, -2);
1232             $stat = $this->lock($options);
1233         } else {
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");
1238             }
1239
1240             // check if locking is possible
1241             if(!$this->_check_lock_status($this->path, $lockinfo->lockscope === "shared")) {
1242                 $this->http_status("423 Locked");
1243                 return;
1244             }
1245
1246             // new lock
1247             $options["scope"] = $lockinfo->lockscope;
1248             $options["type"]  = $lockinfo->locktype;
1249             $options["owner"] = $lockinfo->owner;
1250
1251             $options["locktoken"] = $this->_new_locktoken();
1252
1253             $stat = $this->lock($options);
1254         }
1255
1256         if(is_bool($stat)) {
1257             $http_stat = $stat ? "200 OK" : "423 Locked";
1258         } else {
1259             $http_stat = $stat;
1260         }
1261
1262         $this->http_status($http_stat);
1263
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());
1270                 } else {
1271                     $timeout = "Second-$options[timeout]";
1272                 }
1273             } else {
1274                 $timeout = "Infinite";
1275             }
1276
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";
1292         }
1293     }
1294
1295
1296     // }}}
1297
1298     // {{{ http_UNLOCK()
1299
1300     /**
1301      * UNLOCK method handler
1302      *
1303      * @param  void
1304      * @return void
1305      */
1306     function http_UNLOCK()
1307     {
1308         $options = Array();
1309         $options["path"] = $this->path;
1310
1311         if (isset($_SERVER['HTTP_DEPTH'])) {
1312             $options["depth"] = $_SERVER["HTTP_DEPTH"];
1313         } else {
1314             $options["depth"] = "infinity";
1315         }
1316
1317         // strip surrounding <>
1318         $options["token"] = substr(trim($_SERVER["HTTP_LOCK_TOKEN"]), 1, -1);
1319
1320         // call user method
1321         $stat = $this->unlock($options);
1322
1323         $this->http_status($stat);
1324     }
1325
1326     // }}}
1327
1328     // }}}
1329
1330     // {{{ _copymove()
1331
1332     function _copymove($what)
1333     {
1334         $options = Array();
1335         $options["path"] = $this->path;
1336
1337         if (isset($_SERVER["HTTP_DEPTH"])) {
1338             $options["depth"] = $_SERVER["HTTP_DEPTH"];
1339         } else {
1340             $options["depth"] = "infinity";
1341         }
1342
1343         extract(parse_url($_SERVER["HTTP_DESTINATION"]));
1344         $http_host = $host;
1345         if (isset($port) && $port != 80)
1346             $http_host.= ":$port";
1347
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;
1351         }
1352
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");
1359                 return;
1360             }
1361
1362         } else {
1363             $options["dest_url"] = $_SERVER["HTTP_DESTINATION"];
1364         }
1365
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";
1369         } else {
1370             $options["overwrite"] = true;
1371         }
1372
1373         $stat = $this->$what($options);
1374         $this->http_status($stat);
1375     }
1376
1377     // }}}
1378
1379     // {{{ _allow()
1380
1381     /**
1382      * check for implemented HTTP methods
1383      *
1384      * @param  void
1385      * @return array something
1386      */
1387     function _allow()
1388     {
1389         // OPTIONS is always there
1390         $allow = array("OPTIONS" =>"OPTIONS");
1391
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;
1400                 }
1401             }
1402         }
1403
1404         // we can emulate a missing HEAD implemetation using GET
1405         if (isset($allow["GET"]))
1406             $allow["HEAD"] = "HEAD";
1407
1408         // no LOCK without checklok()
1409         if (!method_exists($this, "checklock")) {
1410             unset($allow["LOCK"]);
1411             unset($allow["UNLOCK"]);
1412         }
1413
1414         return $allow;
1415     }
1416
1417     // }}}
1418
1419     /**
1420      * helper for property element creation
1421      *
1422      * @param  string  XML namespace (optional)
1423      * @param  string  property name
1424      * @param  string  property value
1425      * @return array   property array
1426      */
1427     function mkprop()
1428     {
1429         $args = func_get_args();
1430         if (count($args) == 3) {
1431             return array("ns"   => $args[0],
1432                          "name" => $args[1],
1433                          "val"  => $args[2]);
1434         } else {
1435             return array("ns"   => "DAV:",
1436                          "name" => $args[0],
1437                          "val"  => $args[1]);
1438         }
1439     }
1440
1441     // {{{ _check_auth
1442
1443     /**
1444      * check authentication if check is implemented
1445      *
1446      * @param  void
1447      * @return bool  true if authentication succeded or not necessary
1448      */
1449     function _check_auth()
1450     {
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"]);
1461         } else {
1462             // no method found -> no authentication required
1463             return true;
1464         }
1465     }
1466
1467     // }}}
1468
1469     // {{{ UUID stuff
1470
1471     /**
1472      * generate Unique Universal IDentifier for lock token
1473      *
1474      * @param  void
1475      * @return string  a new UUID
1476      */
1477     function _new_uuid()
1478     {
1479         // use uuid extension from PECL if available
1480         if (function_exists("uuid_create")) {
1481             return uuid_create();
1482         }
1483
1484         // fallback
1485         $uuid = md5(microtime().getmypid());    // this should be random enough for now
1486
1487         // set variant and version fields for 'true' random uuid
1488         $uuid{12} = "4";
1489         $n = 8 + (ord($uuid{16}) & 3);
1490         $hex = "0123456789abcdef";
1491         $uuid{16} = $hex{$n};
1492
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);
1499     }
1500
1501     /**
1502      * create a new opaque lock token as defined in RFC2518
1503      *
1504      * @param  void
1505      * @return string  new RFC2518 opaque lock token
1506      */
1507     function _new_locktoken()
1508     {
1509         return "opaquelocktoken:".$this->_new_uuid();
1510     }
1511
1512     // }}}
1513
1514     // {{{ WebDAV If: header parsing
1515
1516     /**
1517      *
1518      *
1519      * @param  string  header string to parse
1520      * @param  int     current parsing position
1521      * @return array   next token (type and value)
1522      */
1523     function _if_header_lexer($string, &$pos)
1524     {
1525         // skip whitespace
1526         while (ctype_space($string{$pos})) {
1527             ++$pos;
1528         }
1529
1530         // already at end of string?
1531         if (strlen($string) <= $pos) {
1532             return false;
1533         }
1534
1535         // get next character
1536         $c = $string{$pos++};
1537
1538         // now it depends on what we found
1539         switch ($c) {
1540             case "<":
1541                 // URIs are enclosed in <...>
1542                 $pos2 = strpos($string, ">", $pos);
1543                 $uri = substr($string, $pos, $pos2 - $pos);
1544                 $pos = $pos2 + 1;
1545                 return array("URI", $uri);
1546
1547             case "[":
1548                 //Etags are enclosed in [...]
1549                 if ($string{$pos} == "W") {
1550                     $type = "ETAG_WEAK";
1551                     $pos += 2;
1552                 } else {
1553                     $type = "ETAG_STRONG";
1554                 }
1555                 $pos2 = strpos($string, "]", $pos);
1556                 $etag = substr($string, $pos + 1, $pos2 - $pos - 2);
1557                 $pos = $pos2 + 1;
1558                 return array($type, $etag);
1559
1560             case "N":
1561                 // "N" indicates negation
1562                 $pos += 2;
1563                 return array("NOT", "Not");
1564
1565             default:
1566                 // anything else is passed verbatim char by char
1567                 return array("CHAR", $c);
1568         }
1569     }
1570
1571     /**
1572      * parse If: header
1573      *
1574      * @param  string  header string
1575      * @return array   URIs and their conditions
1576      */
1577     function _if_header_parser($str)
1578     {
1579         $pos = 0;
1580         $len = strlen($str);
1581
1582         $uris = array();
1583
1584         // parser loop
1585         while ($pos < $len) {
1586             // get next token
1587             $token = $this->_if_header_lexer($str, $pos);
1588
1589             // check for URI
1590             if ($token[0] == "URI") {
1591                 $uri = $token[1]; // remember URI
1592                 $token = $this->_if_header_lexer($str, $pos); // get next token
1593             } else {
1594                 $uri = "";
1595             }
1596
1597             // sanity check
1598             if ($token[0] != "CHAR" || $token[1] != "(") {
1599                 return false;
1600             }
1601
1602             $list = array();
1603             $level = 1;
1604             $not = "";
1605             while ($level) {
1606                 $token = $this->_if_header_lexer($str, $pos);
1607                 if ($token[0] == "NOT") {
1608                     $not = "!";
1609                     continue;
1610                 }
1611                 switch ($token[0]) {
1612                     case "CHAR":
1613                         switch ($token[1]) {
1614                             case "(":
1615                                 $level++;
1616                                 break;
1617                             case ")":
1618                                 $level--;
1619                                 break;
1620                             default:
1621                                 return false;
1622                         }
1623                         break;
1624
1625                     case "URI":
1626                         $list[] = $not."<$token[1]>";
1627                         break;
1628
1629                     case "ETAG_WEAK":
1630                         $list[] = $not."[W/'$token[1]']>";
1631                         break;
1632
1633                     case "ETAG_STRONG":
1634                         $list[] = $not."['$token[1]']>";
1635                         break;
1636
1637                     default:
1638                         return false;
1639                 }
1640                 $not = "";
1641             }
1642
1643             if (@is_array($uris[$uri])) {
1644                 $uris[$uri] = array_merge($uris[$uri],$list);
1645             } else {
1646                 $uris[$uri] = $list;
1647             }
1648         }
1649
1650         return $uris;
1651     }
1652
1653     /**
1654      * check if conditions from "If:" headers are meat
1655      *
1656      * the "If:" header is an extension to HTTP/1.1
1657      * defined in RFC 2518 section 9.4
1658      *
1659      * @param  void
1660      * @return void
1661      */
1662     function _check_if_header_conditions()
1663     {
1664         if (isset($_SERVER["HTTP_IF"])) {
1665             $this->_if_header_uris =
1666                 $this->_if_header_parser($_SERVER["HTTP_IF"]);
1667
1668             foreach($this->_if_header_uris as $uri => $conditions) {
1669                 if ($uri == "") {
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]";
1673                 }
1674                 // all must match
1675                 $state = true;
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)) {
1682                             return false;
1683                         }
1684                     }
1685                     if (!$this->_check_uri_condition($uri, $condition)) {
1686                         $state = false;
1687                         break;
1688                     }
1689                 }
1690
1691                 // any match is ok
1692                 if ($state == true) {
1693                     return true;
1694                 }
1695             }
1696             return false;
1697         }
1698         return true;
1699     }
1700
1701     /**
1702      * Check a single URI condition parsed from an if-header
1703      *
1704      * Check a single URI condition parsed from an if-header
1705      *
1706      * @abstract
1707      * @param string $uri URI to check
1708      * @param string $condition Condition to check for this URI
1709      * @returns bool Condition check result
1710      */
1711     function _check_uri_condition($uri, $condition)
1712     {
1713         // not really implemented here,
1714         // implementations must override
1715         return true;
1716     }
1717
1718
1719     /**
1720      *
1721      *
1722      * @param  string  path of resource to check
1723      * @param  bool    exclusive lock?
1724      */
1725     function _check_lock_status($path, $exclusive_only = false)
1726     {
1727         // FIXME depth -> ignored for now
1728         if (method_exists($this, "checkLock")) {
1729             // is locked?
1730             $lock = $this->checkLock($path);
1731
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"))
1737                         return false;
1738                 }
1739             }
1740         }
1741         return true;
1742     }
1743
1744
1745     // }}}
1746
1747
1748     /**
1749      * Generate lockdiscovery reply from checklock() result
1750      *
1751      * @param   string  resource path to check
1752      * @return  string  lockdiscovery response
1753      */
1754     function lockdiscovery($path)
1755     {
1756         // no lock support without checklock() method
1757         if (!method_exists($this, "checklock")) {
1758             return "";
1759         }
1760
1761         // collect response here
1762         $activelocks = "";
1763
1764         // get checklock() reply
1765         $lock = $this->checklock($path);
1766
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]";
1774             } else {
1775                 $timeout = "Infinite";
1776             }
1777
1778             // genreate response block
1779             $activelocks.= "
1780               <D:activelock>
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>
1787               </D:activelock>
1788              ";
1789         }
1790
1791         // return generated response
1792         return $activelocks;
1793     }
1794
1795     /**
1796      * set HTTP return status and mirror it in a private header
1797      *
1798      * @param  string  status code and message
1799      * @return void
1800      */
1801     function http_status($status)
1802     {
1803         // simplified success case
1804         if($status === true) {
1805             $status = "200 OK";
1806         }
1807
1808         // remember status
1809         $this->_http_status = $status;
1810
1811         // generate HTTP status response
1812         header("HTTP/1.1 $status");
1813         header("X-WebDAV-Status: $status", true);
1814     }
1815
1816     /**
1817      * private minimalistic version of PHP urlencode()
1818      *
1819      * only blanks and XML special chars must be encoded here
1820      * full urlencode() encoding confuses some clients ...
1821      *
1822      * @param  string  URL to encode
1823      * @return string  encoded URL
1824      */
1825     function _urlencode($url)
1826     {
1827         return strtr($url, array(" "=>"%20",
1828                                  "&"=>"%26",
1829                                  "<"=>"%3C",
1830                                  ">"=>"%3E",
1831                                  ));
1832     }
1833
1834     /**
1835      * private version of PHP urldecode
1836      *
1837      * not really needed but added for completenes
1838      *
1839      * @param  string  URL to decode
1840      * @return string  decoded URL
1841      */
1842     function _urldecode($path)
1843     {
1844         return urldecode($path);
1845     }
1846
1847     /**
1848      * UTF-8 encode property values if not already done so
1849      *
1850      * @param  string  text to encode
1851      * @return string  utf-8 encoded text
1852      */
1853     function _prop_encode($text)
1854     {
1855         switch (strtolower($this->_prop_encoding)) {
1856         case "utf-8":
1857             return $text;
1858         case "iso-8859-1":
1859         case "iso-8859-15":
1860         case "latin-1":
1861         default:
1862             return utf8_encode($text);
1863         }
1864     }
1865 }
1866
1867   /*
1868    * Local variables:
1869    * tab-width: 4
1870    * c-basic-offset: 4
1871    * End:
1872    */
1873 ?>