Platform  3.1
PHP API documentation
 All Data Structures Namespaces Files Functions Variables Pages
Class.ServerDav.php
Go to the documentation of this file.
1 <?php
2 /*
3  * @author Anakeen
4  * @license http://creativecommons.org/licenses/by-nc-sa/2.0/fr/ Anakeen - licence CC
5  * @package FDL
6 */
7 /**
8  * Virtual base class for implementing WebDAV servers
9  *
10  * WebDAV server base class, needs to be extended to do useful work
11  *
12  * @package FDL
13  * @author Hartmut Holzgraefe <hholzgra@php.net>
14  * @version @package_version@
15  */
16 /**
17  */
18 //
19 // +----------------------------------------------------------------------+
20 // | PHP Version 4 |
21 // +----------------------------------------------------------------------+
22 // | Copyright (c) 1997-2003 The PHP Group |
23 // +----------------------------------------------------------------------+
24 // | This source file is subject to version 2.02 of the PHP license, |
25 // | that is bundled with this package in the file LICENSE, and is |
26 // | available at through the world-wide-web at |
27 // | http://www.php.net/license/2_02.txt. |
28 // | If you did not receive a copy of the PHP license and are unable to |
29 // | obtain it through the world-wide-web, please send a note to |
30 // | license@php.net so we can mail you a copy immediately. |
31 // +----------------------------------------------------------------------+
32 // | Authors: Hartmut Holzgraefe <hholzgra@php.net> |
33 // | Christian Stocker <chregu@bitflux.ch> |
34 // +----------------------------------------------------------------------+
35 //
36 // $Id: Class.ServerDav.php,v 1.8 2006/11/22 10:33:59 eric Exp $
37 //
38 require_once "DAV/_parse_propfind.php";
39 require_once "DAV/_parse_proppatch.php";
40 require_once "DAV/_parse_lockinfo.php";
41 
43 {
44  /**
45  * @var bool set to true to see xml response in error_log
46  */
47  public $debug = false;
48  // {{{ Member Variables
49 
50  /**
51  * complete URI for this request
52  *
53  * @var string
54  */
55  var $uri;
56  /**
57  * base URI for this request
58  *
59  * @var string
60  */
61  var $base_uri;
62  /**
63  * URI path for this request
64  *
65  * @var string
66  */
67  var $path;
68  /**
69  * Realm string to be used in authentification popups
70  *
71  * @var string
72  */
73  var $http_auth_realm = "PHP WebDAV";
74  /**
75  * String to be used in "X-Dav-Powered-By" header
76  *
77  * @var string
78  */
79  var $dav_powered_by = "";
80  /**
81  * Remember parsed If: (RFC2518/9.4) header conditions
82  *
83  * @var array
84  */
85  var $_if_header_uris = array();
86  /**
87  * HTTP response status/message
88  *
89  * @var string
90  */
91  var $_http_status = "200 OK";
92  /**
93  * encoding of property values passed in
94  *
95  * @var string
96  */
97  var $_prop_encoding = "utf-8";
98  /**
99  * Copy of $_SERVER superglobal array
100  *
101  * Derived classes may extend the constructor to
102  * modify its contents
103  *
104  * @var array
105  */
106  var $_SERVER;
107  // }}}
108  // {{{ Constructor
109 
110  /**
111  * Constructor
112  *
113  * @param void
114  */
116  {
117  // PHP messages destroy XML output -> switch them off
118  ini_set("display_errors", 0);
119  // copy $_SERVER variables to local _SERVER array
120  // so that derived classes can simply modify these
121  $this->_SERVER = $_SERVER;
122  }
123  // }}}
124  // {{{ ServeRequest()
125 
126  /**
127  * Serve WebDAV HTTP request
128  *
129  * dispatch WebDAV HTTP request to the apropriate method handler
130  *
131  * @param void
132  * @return void
133  */
134  function ServeRequest()
135  {
136  // prevent warning in litmus check 'delete_fragment'
137  if (strstr($this->_SERVER["REQUEST_URI"], '#')) {
138  $this->http_status("400 Bad Request");
139  return;
140  }
141  // default uri is the complete request uri
142  $uri = (@$this->_SERVER["HTTPS"] === "on" ? "https:" : "http:");
143  $uri.= "//$this->_SERVER[HTTP_HOST]$this->_SERVER[SCRIPT_NAME]";
144 
145  $path_info = empty($this->_SERVER["PATH_INFO"]) ? "/" : $this->_SERVER["PATH_INFO"];
146 
147  $this->base_uri = $uri;
148  $this->uri = $uri . $path_info;
149  // set path
150  $this->path = $this->_urldecode($path_info);
151  if (!strlen($this->path)) {
152  if ($this->_SERVER["REQUEST_METHOD"] == "GET") {
153  // redirect clients that try to GET a collection
154  // WebDAV clients should never try this while
155  // regular HTTP clients might ...
156  header("Location: " . $this->base_uri . "/");
157  return;
158  } else {
159  // if a WebDAV client didn't give a path we just assume '/'
160  $this->path = "/";
161  }
162  }
163 
164  if (ini_get("magic_quotes_gpc")) {
165  $this->path = stripslashes($this->path);
166  }
167  // identify ourselves
168  if (empty($this->dav_powered_by)) {
169  header("X-Dav-Powered-By: PHP class: " . get_class($this));
170  } else {
171  header("X-Dav-Powered-By: " . $this->dav_powered_by);
172  }
173  // check authentication
174  // for the motivation for not checking OPTIONS requests on / see
175  // http://pear.php.net/bugs/bug.php?id=5363
176  if ((!(($this->_SERVER['REQUEST_METHOD'] == 'OPTIONS') && ($this->path == "/"))) && (!$this->_check_auth())) {
177  // RFC2518 says we must use Digest instead of Basic
178  // but Microsoft Clients do not support Digest
179  // and we don't support NTLM and Kerberos
180  // so we are stuck with Basic here
181  header('WWW-Authenticate: Basic realm="' . ($this->http_auth_realm) . '"');
182  // Windows seems to require this being the last header sent
183  // (changed according to PECL bug #3138)
184  $this->http_status('401 Unauthorized');
185 
186  return;
187  }
188  // check
189  if (!$this->_check_if_header_conditions()) {
190  return;
191  }
192  // detect requested method names
193  $method = strtolower($this->_SERVER["REQUEST_METHOD"]);
194  $wrapper = "http_" . $method;
195  // activate HEAD emulation by GET if no HEAD method found
196  if ($method == "head" && !method_exists($this, "head")) {
197  $method = "get";
198  }
199 
200  if (method_exists($this, $wrapper) && ($method == "options" || method_exists($this, $method))) {
201  if ($this->debug) {
202  ob_start();
203  $this->$wrapper(); // call method by name
204  $this->logContents();
205  ob_end_flush();
206  } else {
207  $this->$wrapper(); // call method by name
208 
209  }
210  } else { // method not found/implemented
211  if ($this->_SERVER["REQUEST_METHOD"] == "LOCK") {
212  $this->http_status("412 Precondition failed");
213  } else {
214  $this->http_status("405 Method not allowed");
215  header("Allow: " . join(", ", $this->_allow())); // tell client what's allowed
216 
217  }
218  }
219  }
220 
221  private function logContents()
222  {
223  $a = ob_get_contents();
224  if (substr($a, 0, 5) == '<?xml') {
225  $c = explode("\n", $a);
226  foreach ($c as $s) {
227  error_log($s);
228  }
229  }
230  }
231  // }}}
232  // {{{ abstract WebDAV methods
233  // {{{ GET()
234 
235  /**
236  * GET implementation
237  *
238  * overload this method to retrieve resources from your server
239  * <br>
240  *
241  *
242  * @abstract
243  * @param array &$params Array of input and output parameters
244  * <br><b>input</b><ul>
245  * <li> path -
246  * </ul>
247  * <br><b>output</b><ul>
248  * <li> size -
249  * </ul>
250  * @returns int HTTP-Statuscode
251  */
252  /* abstract
253  function GET(&$params)
254  {
255  // dummy entry for PHPDoc
256  }
257  */
258  // }}}
259  // {{{ PUT()
260 
261  /**
262  * PUT implementation
263  *
264  * PUT implementation
265  *
266  * @abstract
267  * @param array &$params
268  * @returns int HTTP-Statuscode
269  */
270  /* abstract
271  function PUT()
272  {
273  // dummy entry for PHPDoc
274  }
275  */
276  // }}}
277  // {{{ COPY()
278 
279  /**
280  * COPY implementation
281  *
282  * COPY implementation
283  *
284  * @abstract
285  * @param array &$params
286  * @returns int HTTP-Statuscode
287  */
288  /* abstract
289  function COPY()
290  {
291  // dummy entry for PHPDoc
292  }
293  */
294  // }}}
295  // {{{ MOVE()
296 
297  /**
298  * MOVE implementation
299  *
300  * MOVE implementation
301  *
302  * @abstract
303  * @param array &$params
304  * @returns int HTTP-Statuscode
305  */
306  /* abstract
307  function MOVE()
308  {
309  // dummy entry for PHPDoc
310  }
311  */
312  // }}}
313  // {{{ DELETE()
314 
315  /**
316  * DELETE implementation
317  *
318  * DELETE implementation
319  *
320  * @abstract
321  * @param array &$params
322  * @returns int HTTP-Statuscode
323  */
324  /* abstract
325  function DELETE()
326  {
327  // dummy entry for PHPDoc
328  }
329  */
330  // }}}
331  // {{{ PROPFIND()
332 
333  /**
334  * PROPFIND implementation
335  *
336  * PROPFIND implementation
337  *
338  * @abstract
339  * @param array &$params
340  * @returns int HTTP-Statuscode
341  */
342  /* abstract
343  function PROPFIND()
344  {
345  // dummy entry for PHPDoc
346  }
347  */
348  // }}}
349  // {{{ PROPPATCH()
350 
351  /**
352  * PROPPATCH implementation
353  *
354  * PROPPATCH implementation
355  *
356  * @abstract
357  * @param array &$params
358  * @returns int HTTP-Statuscode
359  */
360  /* abstract
361  function PROPPATCH()
362  {
363  // dummy entry for PHPDoc
364  }
365  */
366  // }}}
367  // {{{ LOCK()
368 
369  /**
370  * LOCK implementation
371  *
372  * LOCK implementation
373  *
374  * @abstract
375  * @param array &$params
376  * @returns int HTTP-Statuscode
377  */
378  /* abstract
379  function LOCK()
380  {
381  // dummy entry for PHPDoc
382  }
383  */
384  // }}}
385  // {{{ UNLOCK()
386 
387  /**
388  * UNLOCK implementation
389  *
390  * UNLOCK implementation
391  *
392  * @abstract
393  * @param array &$params
394  * @returns int HTTP-Statuscode
395  */
396  /* abstract
397  function UNLOCK()
398  {
399  // dummy entry for PHPDoc
400  }
401  */
402  // }}}
403  // }}}
404  // {{{ other abstract methods
405  // {{{ check_auth()
406 
407  /**
408  * check authentication
409  *
410  * overload this method to retrieve and confirm authentication information
411  *
412  * @abstract
413  * @param string type Authentication type, e.g. "basic" or "digest"
414  * @param string username Transmitted username
415  * @param string passwort Transmitted password
416  * @returns bool Authentication status
417  */
418  /* abstract
419  function checkAuth($type, $username, $password)
420  {
421  // dummy entry for PHPDoc
422  }
423  */
424  // }}}
425  // {{{ checklock()
426 
427  /**
428  * check lock status for a resource
429  *
430  * overload this method to return shared and exclusive locks
431  * active for this resource
432  *
433  * @abstract
434  * @param string resource Resource path to check
435  * @returns array An array of lock entries each consisting
436  * of 'type' ('shared'/'exclusive'), 'token' and 'timeout'
437  */
438  /* abstract
439  function checklock($resource)
440  {
441  // dummy entry for PHPDoc
442  }
443  */
444  // }}}
445  // }}}
446  // {{{ WebDAV HTTP method wrappers
447  // {{{ http_OPTIONS()
448 
449  /**
450  * OPTIONS method handler
451  *
452  * The OPTIONS method handler creates a valid OPTIONS reply
453  * including Dav: and Allowed: heaers
454  * based on the implemented methods found in the actual instance
455  *
456  * @param void
457  * @return void
458  */
459  function http_OPTIONS()
460  {
461  // Microsoft clients default to the Frontpage protocol
462  // unless we tell them to use WebDAV
463  header("MS-Author-Via: DAV");
464  // get allowed methods
465  $allow = $this->_allow();
466  // dav header
467  $dav = array(
468  1
469  ); // assume we are always dav class 1 compliant
470  if (isset($allow['LOCK'])) {
471  $dav[] = 2; // dav class 2 requires that locking is supported
472 
473  }
474  // tell clients what we found
475  $this->http_status("200 OK");
476  header("DAV: " . join(", ", $dav));
477  header("Allow: " . join(", ", $allow));
478 
479  header("Content-length: 0");
480  }
481  // }}}
482  // {{{ http_PROPFIND()
483 
484  /**
485  * PROPFIND method handler
486  *
487  * @param void
488  * @return void
489  */
490  function http_PROPFIND()
491  {
492  $options = Array();
493  $files = Array();
494 
495  $options["path"] = $this->path;
496  // search depth from header (default is "infinity)
497  if (isset($this->_SERVER['HTTP_DEPTH'])) {
498  $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
499  } else {
500  $options["depth"] = "infinity";
501  }
502  // analyze request payload
503  $propinfo = new _parse_propfind("php://input");
504  if (!$propinfo->success) {
505  $this->http_status("400 Error");
506  return;
507  }
508  $options['props'] = $propinfo->props;
509  // call user handler
510  if (!$this->PROPFIND($options, $files)) {
511  $files = array(
512  "files" => array()
513  );
514  if (method_exists($this, "checkLock")) {
515  // is locked?
516  $lock = $this->checkLock($this->path);
517 
518  if (is_array($lock) && count($lock)) {
519  $created = isset($lock['created']) ? $lock['created'] : time();
520  $modified = isset($lock['modified']) ? $lock['modified'] : time();
521  $files['files'][] = array(
522  "path" => $this->_slashify($this->path) ,
523  "props" => array(
524  $this->mkprop("displayname", $this->path) ,
525  $this->mkprop("creationdate", $created) ,
526  $this->mkprop("getlastmodified", $modified) ,
527  $this->mkprop("resourcetype", "") ,
528  $this->mkprop("getcontenttype", "") ,
529  $this->mkprop("getcontentlength", 0)
530  )
531  );
532  }
533  }
534 
535  if (empty($files['files'])) {
536  $this->http_status("404 Not Found");
537  return;
538  }
539  }
540  // collect namespaces here
541  $ns_hash = array();
542  // Microsoft Clients need this special namespace for date and time values
543  $ns_defs = "xmlns:ns0=\"urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/\"";
544  // now we loop over all returned file entries
545  foreach ($files["files"] as $filekey => $file) {
546  // nothing to do if no properties were returend for a file
547  if (!isset($file["props"]) || !is_array($file["props"])) {
548  continue;
549  }
550  // now loop over all returned properties
551  foreach ($file["props"] as $key => $prop) {
552  // as a convenience feature we do not require that user handlers
553  // restrict returned properties to the requested ones
554  // here we strip all unrequested entries out of the response
555  switch ($options['props']) {
556  case "all":
557  // nothing to remove
558  break;
559 
560  case "names":
561  // only the names of all existing properties were requested
562  // so we remove all values
563  unset($files["files"][$filekey]["props"][$key]["val"]);
564  break;
565 
566  default:
567  $found = false;
568  // search property name in requested properties
569  foreach ((array)$options["props"] as $reqprop) {
570  if ($reqprop["name"] == $prop["name"] && @$reqprop["xmlns"] == $prop["ns"]) {
571  $found = true;
572  break;
573  }
574  }
575  // unset property and continue with next one if not found/requested
576  if (!$found) {
577  $files["files"][$filekey]["props"][$key] = "";
578  continue(2);
579  }
580  break;
581  }
582  // namespace handling
583  if (empty($prop["ns"])) continue; // no namespace
584  $ns = $prop["ns"];
585  if ($ns == "DAV:") continue; // default namespace
586  if (isset($ns_hash[$ns])) continue; // already known
587  // register namespace
588  $ns_name = "ns" . (count($ns_hash) + 1);
589  $ns_hash[$ns] = $ns_name;
590  $ns_defs.= " xmlns:$ns_name=\"$ns\"";
591  }
592  // we also need to add empty entries for properties that were requested
593  // but for which no values where returned by the user handler
594  if (is_array($options['props'])) {
595  foreach ($options["props"] as $reqprop) {
596  if ($reqprop['name'] == "") continue; // skip empty entries
597  $found = false;
598  // check if property exists in result
599  foreach ($file["props"] as $prop) {
600  if ($reqprop["name"] == $prop["name"] && @$reqprop["xmlns"] == $prop["ns"]) {
601  $found = true;
602  break;
603  }
604  }
605 
606  if (!$found) {
607  if ($reqprop["xmlns"] === "DAV:" && $reqprop["name"] === "lockdiscovery") {
608  // lockdiscovery is handled by the base class
609  $files["files"][$filekey]["props"][] = $this->mkprop("DAV:", "lockdiscovery", $this->lockdiscovery($files["files"][$filekey]['path']));
610  } else {
611  // add empty value for this property
612  $files["files"][$filekey]["noprops"][] = $this->mkprop($reqprop["xmlns"], $reqprop["name"], "");
613  // register property namespace if not known yet
614  if ($reqprop["xmlns"] != "DAV:" && !isset($ns_hash[$reqprop["xmlns"]])) {
615  $ns_name = "ns" . (count($ns_hash) + 1);
616  $ns_hash[$reqprop["xmlns"]] = $ns_name;
617  $ns_defs.= " xmlns:$ns_name=\"$reqprop[xmlns]\"";
618  }
619  }
620  }
621  }
622  }
623  }
624  // now we generate the reply header ...
625  $this->http_status("207 Multi-Status");
626  header('Content-Type: text/xml; charset="utf-8"');
627  // ... and payload
628  echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
629  echo "<D:multistatus xmlns:D=\"DAV:\">\n";
630 
631  foreach ($files["files"] as $file) {
632  // ignore empty or incomplete entries
633  if (!is_array($file) || empty($file) || !isset($file["path"])) continue;
634  $path = $file['path'];
635  if (!is_string($path) || $path === "") continue;
636 
637  echo " <D:response $ns_defs>\n";
638  /* TODO right now the user implementation has to make sure
639  collections end in a slash, this should be done in here
640  by checking the resource attribute */
641  $href = $this->_mergePathes($this->_SERVER['SCRIPT_NAME'], $path);
642 
643  echo " <D:href>" . $this->_urlencode($href) . "</D:href>\n";
644  // report all found properties and their values (if any)
645  if (isset($file["props"]) && is_array($file["props"])) {
646  echo " <D:propstat>\n";
647  echo " <D:prop>\n";
648 
649  foreach ($file["props"] as $key => $prop) {
650 
651  if (!is_array($prop)) continue;
652  if (!isset($prop["name"])) continue;
653 
654  if (!isset($prop["val"]) || $prop["val"] === "" || $prop["val"] === false) {
655  // empty properties (cannot use empty() for check as "0" is a legal value here)
656  if ($prop["ns"] == "DAV:") {
657  echo " <D:$prop[name]/>\n";
658  } else if (!empty($prop["ns"])) {
659  echo " <" . $ns_hash[$prop["ns"]] . ":$prop[name]/>\n";
660  } else {
661  echo " <$prop[name] xmlns=\"\"/>";
662  }
663  } else if ($prop["ns"] == "DAV:") {
664  // some WebDAV properties need special treatment
665  switch ($prop["name"]) {
666  case "creationdate":
667  echo " <D:creationdate ns0:dt=\"dateTime.tz\">" . gmdate("Y-m-d\\TH:i:s\\Z", $prop['val']) . "</D:creationdate>\n";
668  break;
669 
670  case "getlastmodified":
671  echo " <D:getlastmodified ns0:dt=\"dateTime.rfc1123\">" . gmdate("D, d M Y H:i:s ", $prop['val']) . "GMT</D:getlastmodified>\n";
672  break;
673 
674  case "resourcetype":
675  echo " <D:resourcetype><D:$prop[val]/></D:resourcetype>\n";
676  break;
677 
678  case "supportedlock":
679  echo " <D:supportedlock>$prop[val]</D:supportedlock>\n";
680  break;
681 
682  case "lockdiscovery":
683  echo " <D:lockdiscovery>\n";
684  echo $prop["val"];
685  echo " </D:lockdiscovery>\n";
686  break;
687 
688  default:
689  echo " <D:$prop[name]>" . $this->_prop_encode(htmlspecialchars($prop['val'])) . "</D:$prop[name]>\n";
690  break;
691  }
692  } else {
693  // properties from namespaces != "DAV:" or without any namespace
694  if ($prop["ns"]) {
695  echo " <" . $ns_hash[$prop["ns"]] . ":$prop[name]>" . $this->_prop_encode(htmlspecialchars($prop['val'])) . "</" . $ns_hash[$prop["ns"]] . ":$prop[name]>\n";
696  } else {
697  echo " <$prop[name] xmlns=\"\">" . $this->_prop_encode(htmlspecialchars($prop['val'])) . "</$prop[name]>\n";
698  }
699  }
700  }
701 
702  echo " </D:prop>\n";
703  echo " <D:status>HTTP/1.1 200 OK</D:status>\n";
704  echo " </D:propstat>\n";
705  }
706  // now report all properties requested but not found
707  if (isset($file["noprops"])) {
708  echo " <D:propstat>\n";
709  echo " <D:prop>\n";
710 
711  foreach ($file["noprops"] as $key => $prop) {
712  if ($prop["ns"] == "DAV:") {
713  echo " <D:$prop[name]/>\n";
714  } else if ($prop["ns"] == "") {
715  echo " <$prop[name] xmlns=\"\"/>\n";
716  } else {
717  echo " <" . $ns_hash[$prop["ns"]] . ":$prop[name]/>\n";
718  }
719  }
720 
721  echo " </D:prop>\n";
722  echo " <D:status>HTTP/1.1 404 Not Found</D:status>\n";
723  echo " </D:propstat>\n";
724  }
725 
726  echo " </D:response>\n";
727  }
728 
729  echo "</D:multistatus>\n";
730  }
731  // }}}
732  // {{{ http_PROPPATCH()
733 
734  /**
735  * PROPPATCH method handler
736  *
737  * @param void
738  * @return void
739  */
740  function http_PROPPATCH()
741  {
742  if ($this->_check_lock_status($this->path)) {
743  $options = Array();
744 
745  $options["path"] = $this->path;
746 
747  $propinfo = new _parse_proppatch("php://input");
748 
749  if (!$propinfo->success) {
750  $this->http_status("400 Error");
751  return;
752  }
753 
754  $options['props'] = $propinfo->props;
755 
756  $responsedescr = $this->PROPPATCH($options);
757 
758  $this->http_status("207 Multi-Status");
759  header('Content-Type: text/xml; charset="utf-8"');
760 
761  echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
762 
763  echo "<D:multistatus xmlns:D=\"DAV:\">\n";
764  echo " <D:response>\n";
765  echo " <D:href>" . $this->_urlencode($this->_mergePathes($this->_SERVER["SCRIPT_NAME"], $this->path)) . "</D:href>\n";
766 
767  foreach ($options["props"] as $prop) {
768  echo " <D:propstat>\n";
769  echo " <D:prop><$prop[name] xmlns=\"$prop[ns]\"/></D:prop>\n";
770  echo " <D:status>HTTP/1.1 $prop[status]</D:status>\n";
771  echo " </D:propstat>\n";
772  }
773 
774  if ($responsedescr) {
775  echo " <D:responsedescription>" . $this->_prop_encode(htmlspecialchars($responsedescr)) . "</D:responsedescription>\n";
776  }
777 
778  echo " </D:response>\n";
779  echo "</D:multistatus>\n";
780  } else {
781  $this->http_status("423 Locked");
782  }
783  }
784  // }}}
785  // {{{ http_MKCOL()
786 
787  /**
788  * MKCOL method handler
789  *
790  * @param void
791  * @return void
792  */
793  function http_MKCOL()
794  {
795  $options = Array();
796 
797  $options["path"] = $this->path;
798 
799  $stat = $this->MKCOL($options);
800 
801  $this->http_status($stat);
802  }
803  // }}}
804  // {{{ http_GET()
805 
806  /**
807  * GET method handler
808  *
809  * @param void
810  * @returns void
811  */
812  function http_GET()
813  {
814  // TODO check for invalid stream
815  $options = Array();
816  $options["path"] = $this->path;
817 
818  $this->_get_ranges($options);
819 
820  if (true === ($status = $this->GET($options))) {
821  if (!headers_sent()) {
822  $status = "200 OK";
823 
824  if (!isset($options['mimetype'])) {
825  $options['mimetype'] = "application/octet-stream";
826  }
827  header("Content-type: $options[mimetype]");
828 
829  if (isset($options['mtime'])) {
830  header("Last-modified:" . gmdate("D, d M Y H:i:s ", $options['mtime']) . "GMT");
831  }
832 
833  if (isset($options['stream'])) {
834  // GET handler returned a stream
835  if (!empty($options['ranges']) && (0 === fseek($options['stream'], 0, SEEK_SET))) {
836  // partial request and stream is seekable
837  if (count($options['ranges']) === 1) {
838  $range = $options['ranges'][0];
839 
840  if (isset($range['start'])) {
841  fseek($options['stream'], $range['start'], SEEK_SET);
842  if (feof($options['stream'])) {
843  $this->http_status("416 Requested range not satisfiable");
844  return;
845  }
846 
847  if (isset($range['end'])) {
848  $size = $range['end'] - $range['start'] + 1;
849  $this->http_status("206 partial");
850  header("Content-length: $size");
851  header("Content-range: $range[start]-$range[end]/" . (isset($options['size']) ? $options['size'] : "*"));
852  while ($size && !feof($options['stream'])) {
853  $buffer = fread($options['stream'], 4096);
854  $size-= strlen($buffer);
855  echo $buffer;
856  }
857  } else {
858  $this->http_status("206 partial");
859  if (isset($options['size'])) {
860  header("Content-length: " . ($options['size'] - $range['start']));
861  header("Content-range: " . $range['start'] . "-" . $range['end'] . "/" . (isset($options['size']) ? $options['size'] : "*"));
862  }
863  fpassthru($options['stream']);
864  }
865  } else {
866  header("Content-length: " . $range['last']);
867  fseek($options['stream'], -$range['last'], SEEK_END);
868  fpassthru($options['stream']);
869  }
870  } else {
871  $this->_multipart_byterange_header(); // init multipart
872  foreach ($options['ranges'] as $range) {
873  // TODO what if size unknown? 500?
874  if (isset($range['start'])) {
875  $from = $range['start'];
876  $to = !empty($range['end']) ? $range['end'] : $options['size'] - 1;
877  } else {
878  $from = $options['size'] - $range['last'] - 1;
879  $to = $options['size'] - 1;
880  }
881  $total = isset($options['size']) ? $options['size'] : "*";
882  $size = $to - $from + 1;
883  $this->_multipart_byterange_header($options['mimetype'], $from, $to, $total);
884 
885  fseek($options['stream'], $from, SEEK_SET);
886  while ($size && !feof($options['stream'])) {
887  $buffer = fread($options['stream'], 4096);
888  $size-= strlen($buffer);
889  echo $buffer;
890  }
891  }
892  $this->_multipart_byterange_header(); // end multipart
893 
894  }
895  } else {
896  // normal request or stream isn't seekable, return full content
897  if (isset($options['size'])) {
898  header("Content-length: " . $options['size']);
899  }
900  fpassthru($options['stream']);
901  return; // no more headers
902 
903  }
904  } elseif (isset($options['data'])) {
905  if (is_array($options['data'])) {
906  // reply to partial request
907 
908  } else {
909  header("Content-length: " . strlen($options['data']));
910  echo $options['data'];
911  }
912  }
913  }
914  }
915 
916  if (!headers_sent()) {
917  if (false === $status) {
918  $this->http_status("404 not found");
919  } else {
920  // TODO: check setting of headers in various code pathes above
921  $this->http_status("$status");
922  }
923  }
924  }
925  /**
926  * parse HTTP Range: header
927  *
928  * @param array options array to store result in
929  * @return void
930  */
931  function _get_ranges(&$options)
932  {
933  // process Range: header if present
934  if (isset($this->_SERVER['HTTP_RANGE'])) {
935  // we only support standard "bytes" range specifications for now
936  if (preg_match('/bytes\s*=\s*(.+)/', $this->_SERVER['HTTP_RANGE'], $matches)) {
937  $options["ranges"] = array();
938  // ranges are comma separated
939  foreach (explode(",", $matches[1]) as $range) {
940  // ranges are either from-to pairs or just end positions
941  list($start, $end) = explode("-", $range);
942  $options["ranges"][] = ($start === "") ? array(
943  "last" => $end
944  ) : array(
945  "start" => $start,
946  "end" => $end
947  );
948  }
949  }
950  }
951  }
952  /**
953  * generate separator headers for multipart response
954  *
955  * first and last call happen without parameters to generate
956  * the initial header and closing sequence, all calls inbetween
957  * require content mimetype, start and end byte position and
958  * optionaly the total byte length of the requested resource
959  *
960  * @param string mimetype
961  * @param int start byte position
962  * @param int end byte position
963  * @param int total resource byte size
964  */
965  function _multipart_byterange_header($mimetype = false, $from = false, $to = false, $total = false)
966  {
967  if ($mimetype === false) {
968  if (!isset($this->multipart_separator)) {
969  // initial
970  // a little naive, this sequence *might* be part of the content
971  // but it's really not likely and rather expensive to check
972  $this->multipart_separator = "SEPARATOR_" . md5(microtime());
973  // generate HTTP header
974  header("Content-type: multipart/byteranges; boundary=" . $this->multipart_separator);
975  } else {
976  // final
977  // generate closing multipart sequence
978  echo "\n--{$this->multipart_separator}--";
979  }
980  } else {
981  // generate separator and header for next part
982  echo "\n--{$this->multipart_separator}\n";
983  echo "Content-type: $mimetype\n";
984  echo "Content-range: $from-$to/" . ($total === false ? "*" : $total);
985  echo "\n\n";
986  }
987  }
988  // }}}
989  // {{{ http_HEAD()
990 
991  /**
992  * HEAD method handler
993  *
994  * @param void
995  * @return void
996  */
997  function http_HEAD()
998  {
999  $status = false;
1000  $options = Array();
1001  $options["path"] = $this->path;
1002 
1003  if (method_exists($this, "HEAD")) {
1004  $status = $this->head($options);
1005  } else if (method_exists($this, "GET")) {
1006  ob_start();
1007  $status = $this->GET($options);
1008  if (!isset($options['size'])) {
1009  $options['size'] = ob_get_length();
1010  }
1011  ob_end_clean();
1012  }
1013 
1014  if (!isset($options['mimetype'])) {
1015  $options['mimetype'] = "application/octet-stream";
1016  }
1017  header("Content-type: $options[mimetype]");
1018 
1019  if (isset($options['mtime'])) {
1020  header("Last-modified:" . gmdate("D, d M Y H:i:s ", $options['mtime']) . "GMT");
1021  }
1022 
1023  if (isset($options['size'])) {
1024  header("Content-length: " . $options['size']);
1025  }
1026 
1027  if ($status === true) $status = "200 OK";
1028  if ($status === false) $status = "404 Not found";
1029 
1030  $this->http_status($status);
1031  }
1032  // }}}
1033  // {{{ http_PUT()
1034 
1035  /**
1036  * PUT method handler
1037  *
1038  * @param void
1039  * @return void
1040  */
1041  function http_PUT()
1042  {
1043  if ($this->_check_lock_status($this->path)) {
1044  $options = Array();
1045  $options["path"] = $this->path;
1046  $options["content_length"] = $this->_SERVER["CONTENT_LENGTH"];
1047  // get the Content-type
1048  if (isset($this->_SERVER["CONTENT_TYPE"])) {
1049  // for now we do not support any sort of multipart requests
1050  if (!strncmp($this->_SERVER["CONTENT_TYPE"], "multipart/", 10)) {
1051  $this->http_status("501 not implemented");
1052  echo "The service does not support mulipart PUT requests";
1053  return;
1054  }
1055  $options["content_type"] = $this->_SERVER["CONTENT_TYPE"];
1056  } else {
1057  // default content type if none given
1058  $options["content_type"] = "application/octet-stream";
1059  }
1060  /* RFC 2616 2.6 says: "The recipient of the entity MUST NOT
1061  ignore any Content-* (e.g. Content-Range) headers that it
1062  does not understand or implement and MUST return a 501
1063  (Not Implemented) response in such cases."
1064  */
1065  foreach ($this->_SERVER as $key => $val) {
1066  if (strncmp($key, "HTTP_CONTENT", 11)) continue;
1067  switch ($key) {
1068  case 'HTTP_CONTENT_ENCODING': // RFC 2616 14.11
1069  // TODO support this if ext/zlib filters are available
1070  $this->http_status("501 not implemented");
1071  echo "The service does not support '$val' content encoding";
1072  return;
1073  case 'HTTP_CONTENT_LANGUAGE': // RFC 2616 14.12
1074  // we assume it is not critical if this one is ignored
1075  // in the actual PUT implementation ...
1076  $options["content_language"] = $val;
1077  break;
1078 
1079  case 'HTTP_CONTENT_LOCATION': // RFC 2616 14.14
1080  /* The meaning of the Content-Location header in PUT
1081  or POST requests is undefined; servers are free
1082  to ignore it in those cases. */
1083  break;
1084 
1085  case 'HTTP_CONTENT_RANGE': // RFC 2616 14.16
1086  // single byte range requests are supported
1087  // the header format is also specified in RFC 2616 14.16
1088  // TODO we have to ensure that implementations support this or send 501 instead
1089  if (!preg_match('@bytes\s+(\d+)-(\d+)/((\d+)|\*)@', $val, $matches)) {
1090  $this->http_status("400 bad request");
1091  echo "The service does only support single byte ranges";
1092  return;
1093  }
1094 
1095  $range = array(
1096  "start" => $matches[1],
1097  "end" => $matches[2]
1098  );
1099  if (is_numeric($matches[3])) {
1100  $range["total_length"] = $matches[3];
1101  }
1102  $option["ranges"][] = $range;
1103  // TODO make sure the implementation supports partial PUT
1104  // this has to be done in advance to avoid data being overwritten
1105  // on implementations that do not support this ...
1106  break;
1107 
1108  case 'HTTP_CONTENT_MD5': // RFC 2616 14.15
1109  // TODO: maybe we can just pretend here?
1110  $this->http_status("501 not implemented");
1111  echo "The service does not support content MD5 checksum verification";
1112  return;
1113  default:
1114  // any other unknown Content-* headers
1115  $this->http_status("501 not implemented");
1116  echo "The service does not support '$key'";
1117  return;
1118  }
1119  }
1120 
1121  $options["stream"] = fopen("php://input", "r");
1122 
1123  $stat = $this->PUT($options);
1124 
1125  if ($stat === false) {
1126  $stat = "403 Forbidden";
1127  } else if (is_resource($stat) && get_resource_type($stat) == "stream") {
1128  $stream = $stat;
1129 
1130  $stat = $options["new"] ? "201 Created" : "204 No Content";
1131 
1132  if (!empty($options["ranges"])) {
1133  // TODO multipart support is missing (see also above)
1134  if (0 == fseek($stream, $range[0]["start"], SEEK_SET)) {
1135  $length = $range[0]["end"] - $range[0]["start"] + 1;
1136  if (!fwrite($stream, fread($options["stream"], $length))) {
1137  $stat = "403 Forbidden";
1138  }
1139  } else {
1140  $stat = "403 Forbidden";
1141  }
1142  } else {
1143  while (!feof($options["stream"])) {
1144  if (false === fwrite($stream, fread($options["stream"], 4096))) {
1145  $stat = "403 Forbidden";
1146  break;
1147  }
1148  }
1149  }
1150 
1151  fclose($stream);
1152  }
1153 
1154  $this->http_status($stat);
1155  } else {
1156  $this->http_status("423 Locked");
1157  }
1158  }
1159  // }}}
1160  // {{{ http_DELETE()
1161 
1162  /**
1163  * DELETE method handler
1164  *
1165  * @param void
1166  * @return void
1167  */
1168  function http_DELETE()
1169  {
1170  // check RFC 2518 Section 9.2, last paragraph
1171  if (isset($this->_SERVER["HTTP_DEPTH"])) {
1172  if ($this->_SERVER["HTTP_DEPTH"] != "infinity") {
1173  $this->http_status("400 Bad Request");
1174  return;
1175  }
1176  }
1177  // check lock status
1178  if ($this->_check_lock_status($this->path)) {
1179  // ok, proceed
1180  $options = Array();
1181  $options["path"] = $this->path;
1182 
1183  $stat = $this->DELETE($options);
1184 
1185  $this->http_status($stat);
1186  } else {
1187  // sorry, its locked
1188  $this->http_status("423 Locked");
1189  }
1190  }
1191  // }}}
1192  // {{{ http_COPY()
1193 
1194  /**
1195  * COPY method handler
1196  *
1197  * @param void
1198  * @return void
1199  */
1200  function http_COPY()
1201  {
1202  // no need to check source lock status here
1203  // destination lock status is always checked by the helper method
1204  $this->_copymove("copy");
1205  }
1206  // }}}
1207  // {{{ http_MOVE()
1208 
1209  /**
1210  * MOVE method handler
1211  *
1212  * @param void
1213  * @return void
1214  */
1215  function http_MOVE()
1216  {
1217  if ($this->_check_lock_status($this->path)) {
1218  // destination lock status is always checked by the helper method
1219  $this->_copymove("move");
1220  } else {
1221  $this->http_status("423 Locked");
1222  }
1223  }
1224  // }}}
1225  // {{{ http_LOCK()
1226 
1227  /**
1228  * LOCK method handler
1229  *
1230  * @param void
1231  * @return void
1232  */
1233  function http_LOCK()
1234  {
1235  $options = Array();
1236  $options["path"] = $this->path;
1237 
1238  if (isset($this->_SERVER['HTTP_DEPTH'])) {
1239  $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
1240  } else {
1241  $options["depth"] = "infinity";
1242  }
1243 
1244  if (isset($this->_SERVER["HTTP_TIMEOUT"])) {
1245  $options["timeout"] = explode(",", $this->_SERVER["HTTP_TIMEOUT"]);
1246  }
1247 
1248  if (empty($this->_SERVER['CONTENT_LENGTH']) && !empty($this->_SERVER['HTTP_IF'])) {
1249  // check if locking is possible
1250  if (!$this->_check_lock_status($this->path)) {
1251  $this->http_status("423 Locked");
1252  return;
1253  }
1254  // refresh lock
1255  $options["locktoken"] = substr($this->_SERVER['HTTP_IF'], 2, -2);
1256  $options["update"] = $options["locktoken"];
1257  // setting defaults for required fields, LOCK() SHOULD overwrite these
1258  $options['owner'] = "unknown";
1259  $options['scope'] = "exclusive";
1260  $options['type'] = "write";
1261 
1262  $stat = $this->LOCK($options);
1263  } else {
1264  // extract lock request information from request XML payload
1265  $lockinfo = new _parse_lockinfo("php://input");
1266  if (!$lockinfo->success) {
1267  $this->http_status("400 bad request");
1268  }
1269  // check if locking is possible
1270  if (!$this->_check_lock_status($this->path, $lockinfo->lockscope === "shared")) {
1271  $this->http_status("423 Locked");
1272  return;
1273  }
1274  // new lock
1275  $options["scope"] = $lockinfo->lockscope;
1276  $options["type"] = $lockinfo->locktype;
1277  $options["owner"] = $lockinfo->owner;
1278  $options["locktoken"] = $this->_new_locktoken();
1279 
1280  $stat = $this->LOCK($options);
1281  }
1282 
1283  if (is_bool($stat)) {
1284  $http_stat = $stat ? "200 OK" : "423 Locked";
1285  } else {
1286  $http_stat = $stat;
1287  }
1288  $this->http_status($http_stat);
1289 
1290  if ($http_stat{0} == 2) { // 2xx states are ok
1291  if ($options["timeout"]) {
1292  // more than a million is considered an absolute timestamp
1293  // less is more likely a relative value
1294  if ($options["timeout"] > 1000000) {
1295  $timeout = "Second-" . ($options['timeout'] - time());
1296  } else {
1297  $timeout = "Second-$options[timeout]";
1298  }
1299  } else {
1300  $timeout = "Infinite";
1301  }
1302 
1303  header('Content-Type: text/xml; charset="utf-8"');
1304  header("Lock-Token: <$options[locktoken]>");
1305  echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
1306  echo "<D:prop xmlns:D=\"DAV:\">\n";
1307  echo " <D:lockdiscovery>\n";
1308  echo " <D:activelock>\n";
1309  echo " <D:lockscope><D:$options[scope]/></D:lockscope>\n";
1310  echo " <D:locktype><D:$options[type]/></D:locktype>\n";
1311  echo " <D:depth>$options[depth]</D:depth>\n";
1312  echo " <D:owner>$options[owner]</D:owner>\n";
1313  echo " <D:timeout>$timeout</D:timeout>\n";
1314  echo " <D:locktoken><D:href>$options[locktoken]</D:href></D:locktoken>\n";
1315  echo " </D:activelock>\n";
1316  echo " </D:lockdiscovery>\n";
1317  echo "</D:prop>\n\n";
1318  }
1319  }
1320  // }}}
1321  // {{{ http_UNLOCK()
1322 
1323  /**
1324  * UNLOCK method handler
1325  *
1326  * @param void
1327  * @return void
1328  */
1329  function http_UNLOCK()
1330  {
1331  $options = Array();
1332  $options["path"] = $this->path;
1333 
1334  if (isset($this->_SERVER['HTTP_DEPTH'])) {
1335  $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
1336  } else {
1337  $options["depth"] = "infinity";
1338  }
1339  // strip surrounding <>
1340  $options["token"] = trim($this->_SERVER["HTTP_LOCK_TOKEN"], '<> ');
1341  // call user method
1342  $stat = $this->UNLOCK($options);
1343 
1344  $this->http_status($stat);
1345  }
1346  // }}}
1347  // }}}
1348  // {{{ _copymove()
1349  function _copymove($what)
1350  {
1351  $options = Array();
1352  $options["path"] = $this->path;
1353 
1354  if (isset($this->_SERVER["HTTP_DEPTH"])) {
1355  $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
1356  } else {
1357  $options["depth"] = "infinity";
1358  }
1359 
1360  extract(parse_url($this->_SERVER["HTTP_DESTINATION"]));
1361  $path = str_replace("%25", "%", $path);
1362  $path = urldecode($path);
1363  $http_host = $host;
1364  if (isset($port) && $port != 80) $http_host.= ":$port";
1365 
1366  $http_header_host = preg_replace("/:80$/", "", $this->_SERVER["HTTP_HOST"]);
1367 
1368  $basepath = dirname($_SERVER["PHP_SELF"]);
1369  if ($http_host == $http_header_host
1370  /* !strncmp($_SERVER["SCRIPT_NAME"], $path,
1371  strlen($this->__SERVER["SCRIPT_NAME"]))*/) {
1372  // $options["dest"] = substr($path, strlen($this->__SERVER["SCRIPT_NAME"]));
1373  if (strlen($basepath) > 1) {
1374  $path = substr($path, strlen($basepath));
1375  }
1376  $options["dest"] = $path;
1377 
1378  if (!$this->_check_lock_status($options["dest"])) {
1379  $this->http_status("423 Locked");
1380  return;
1381  }
1382  } else {
1383  $options["dest_url"] = $this->_SERVER["HTTP_DESTINATION"];
1384  }
1385  // see RFC 2518 Sections 9.6, 8.8.4 and 8.9.3
1386  if (isset($this->_SERVER["HTTP_OVERWRITE"])) {
1387  $options["overwrite"] = $this->_SERVER["HTTP_OVERWRITE"] == "T";
1388  } else {
1389  $options["overwrite"] = true;
1390  }
1391 
1392  $stat = $this->$what($options);
1393  $this->http_status($stat);
1394  }
1395  // }}}
1396  // {{{ _allow()
1397 
1398  /**
1399  * check for implemented HTTP methods
1400  *
1401  * @param void
1402  * @return array something
1403  */
1404  function _allow()
1405  {
1406  // OPTIONS is always there
1407  $allow = array(
1408  "OPTIONS" => "OPTIONS"
1409  );
1410  // all other METHODS need both a http_method() wrapper
1411  // and a method() implementation
1412  // the base class supplies wrappers only
1413  foreach (get_class_methods($this) as $method) {
1414  if (!strncmp("http_", $method, 5)) {
1415  $method = strtoupper(substr($method, 5));
1416  if (method_exists($this, $method)) {
1417  $allow[$method] = $method;
1418  }
1419  }
1420  }
1421  // we can emulate a missing HEAD implemetation using GET
1422  if (isset($allow["GET"])) $allow["HEAD"] = "HEAD";
1423  // no LOCK without checklok()
1424  if (!method_exists($this, "checklock")) {
1425  unset($allow["LOCK"]);
1426  unset($allow["UNLOCK"]);
1427  }
1428 
1429  return $allow;
1430  }
1431  // }}}
1432 
1433  /**
1434  * helper for property element creation
1435  *
1436  * @param string XML namespace (optional)
1437  * @param string property name
1438  * @param string property value
1439  * @return array property array
1440  */
1441  function mkprop()
1442  {
1443  $args = func_get_args();
1444  if (count($args) == 3) {
1445  return array(
1446  "ns" => $args[0],
1447  "name" => $args[1],
1448  "val" => $args[2]
1449  );
1450  } else {
1451  return array(
1452  "ns" => "DAV:",
1453  "name" => $args[0],
1454  "val" => $args[1]
1455  );
1456  }
1457  }
1458  // {{{ _check_auth
1459 
1460  /**
1461  * check authentication if check is implemented
1462  *
1463  * @param void
1464  * @return bool true if authentication succeded or not necessary
1465  */
1466  function _check_auth()
1467  {
1468  if (method_exists($this, "checkAuth")) {
1469  // PEAR style method name
1470  return $this->checkAuth(@$this->_SERVER["AUTH_TYPE"], @$this->_SERVER["PHP_AUTH_USER"], @$this->_SERVER["PHP_AUTH_PW"]);
1471  } else if (method_exists($this, "check_auth")) {
1472  // old (pre 1.0) method name
1473  return $this->check_auth(@$this->_SERVER["AUTH_TYPE"], @$this->_SERVER["PHP_AUTH_USER"], @$this->_SERVER["PHP_AUTH_PW"]);
1474  } else {
1475  // no method found -> no authentication required
1476  return true;
1477  }
1478  }
1479  // }}}
1480  // {{{ UUID stuff
1481 
1482  /**
1483  * generate Unique Universal IDentifier for lock token
1484  *
1485  * @param void
1486  * @return string a new UUID
1487  */
1488  function _new_uuid()
1489  {
1490  // use uuid extension from PECL if available
1491  if (function_exists("uuid_create")) {
1492  return uuid_create();
1493  }
1494  // fallback
1495  $uuid = md5(microtime() . getmypid()); // this should be random enough for now
1496  // set variant and version fields for 'true' random uuid
1497  $uuid{12} = "4";
1498  $n = 8 + (ord($uuid{16}) & 3);
1499  $hex = "0123456789abcdef";
1500  $uuid{16} = $hex{$n};
1501  // return formated uuid
1502  return substr($uuid, 0, 8) . "-" . substr($uuid, 8, 4) . "-" . substr($uuid, 12, 4) . "-" . substr($uuid, 16, 4) . "-" . substr($uuid, 20);
1503  }
1504  /**
1505  * create a new opaque lock token as defined in RFC2518
1506  *
1507  * @param void
1508  * @return string new RFC2518 opaque lock token
1509  */
1510  function _new_locktoken()
1511  {
1512  return "opaquelocktoken:" . $this->_new_uuid();
1513  }
1514  // }}}
1515  // {{{ WebDAV If: header parsing
1516 
1517  /**
1518  *
1519  *
1520  * @param string header string to parse
1521  * @param int current parsing position
1522  * @return array next token (type and value)
1523  */
1524  function _if_header_lexer($string, &$pos)
1525  {
1526  // skip whitespace
1527  while (ctype_space($string{$pos})) {
1528  ++$pos;
1529  }
1530  // already at end of string?
1531  if (strlen($string) <= $pos) {
1532  return false;
1533  }
1534  // get next character
1535  $c = $string{$pos++};
1536  // now it depends on what we found
1537  switch ($c) {
1538  case "<":
1539  // URIs are enclosed in <...>
1540  $pos2 = strpos($string, ">", $pos);
1541  $uri = substr($string, $pos, $pos2 - $pos);
1542  $pos = $pos2 + 1;
1543  return array(
1544  "URI",
1545  $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(
1559  $type,
1560  $etag
1561  );
1562  case "N":
1563  // "N" indicates negation
1564  $pos+= 2;
1565  return array(
1566  "NOT",
1567  "Not"
1568  );
1569  default:
1570  // anything else is passed verbatim char by char
1571  return array(
1572  "CHAR",
1573  $c
1574  );
1575  }
1576  }
1577  /**
1578  * parse If: header
1579  *
1580  * @param string header string
1581  * @return array URIs and their conditions
1582  */
1583  function _if_header_parser($str)
1584  {
1585  $pos = 0;
1586  $len = strlen($str);
1587  $uris = array();
1588  // parser loop
1589  while ($pos < $len) {
1590  // get next token
1591  $token = $this->_if_header_lexer($str, $pos);
1592  // check for URI
1593  if ($token[0] == "URI") {
1594  $uri = $token[1]; // remember URI
1595  $token = $this->_if_header_lexer($str, $pos); // get next token
1596 
1597  } else {
1598  $uri = "";
1599  }
1600  // sanity check
1601  if ($token[0] != "CHAR" || $token[1] != "(") {
1602  return false;
1603  }
1604 
1605  $list = array();
1606  $level = 1;
1607  $not = "";
1608  while ($level) {
1609  $token = $this->_if_header_lexer($str, $pos);
1610  if ($token[0] == "NOT") {
1611  $not = "!";
1612  continue;
1613  }
1614  switch ($token[0]) {
1615  case "CHAR":
1616  switch ($token[1]) {
1617  case "(":
1618  $level++;
1619  break;
1620 
1621  case ")":
1622  $level--;
1623  break;
1624 
1625  default:
1626  return false;
1627  }
1628  break;
1629 
1630  case "URI":
1631  $list[] = $not . "<$token[1]>";
1632  break;
1633 
1634  case "ETAG_WEAK":
1635  $list[] = $not . "[W/'$token[1]']>";
1636  break;
1637 
1638  case "ETAG_STRONG":
1639  $list[] = $not . "['$token[1]']>";
1640  break;
1641 
1642  default:
1643  return false;
1644  }
1645  $not = "";
1646  }
1647 
1648  if (@is_array($uris[$uri])) {
1649  $uris[$uri] = array_merge($uris[$uri], $list);
1650  } else {
1651  $uris[$uri] = $list;
1652  }
1653  }
1654 
1655  return $uris;
1656  }
1657  /**
1658  * check if conditions from "If:" headers are meat
1659  *
1660  * the "If:" header is an extension to HTTP/1.1
1661  * defined in RFC 2518 section 9.4
1662  *
1663  * @param void
1664  * @return void
1665  */
1667  {
1668  if (isset($this->_SERVER["HTTP_IF"])) {
1669  $this->_if_header_uris = $this->_if_header_parser($this->_SERVER["HTTP_IF"]);
1670 
1671  foreach ($this->_if_header_uris as $uri => $conditions) {
1672  if ($uri == "") {
1673  $uri = $this->uri;
1674  }
1675  // all must match
1676  $state = true;
1677  foreach ($conditions as $condition) {
1678  // lock tokens may be free form (RFC2518 6.3)
1679  // but if opaquelocktokens are used (RFC2518 6.4)
1680  // we have to check the format (litmus tests this)
1681  if (!strncmp($condition, "<opaquelocktoken:", strlen("<opaquelocktoken"))) {
1682  if (!preg_match('/^<opaquelocktoken:[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}>$/', $condition)) {
1683  $this->http_status("423 Locked");
1684  return false;
1685  }
1686  }
1687  if (!$this->_check_uri_condition($uri, $condition)) {
1688  $this->http_status("412 Precondition failed");
1689  $state = false;
1690  break;
1691  }
1692  }
1693  // any match is ok
1694  if ($state == true) {
1695  return true;
1696  }
1697  }
1698  return false;
1699  }
1700  return true;
1701  }
1702  /**
1703  * Check a single URI condition parsed from an if-header
1704  *
1705  * Check a single URI condition parsed from an if-header
1706  *
1707  * @abstract
1708  * @param string $uri URI to check
1709  * @param string $condition Condition to check for this URI
1710  * @returns bool Condition check result
1711  */
1712  function _check_uri_condition($uri, $condition)
1713  {
1714  // not really implemented here,
1715  // implementations must override
1716  // a lock token can never be from the DAV: scheme
1717  // litmus uses DAV:no-lock in some tests
1718  if (!strncmp("<DAV:", $condition, 5)) {
1719  return false;
1720  }
1721 
1722  return true;
1723  }
1724  /**
1725  *
1726  *
1727  * @param string path of resource to check
1728  * @param bool exclusive lock?
1729  */
1730  function _check_lock_status($path, $exclusive_only = false)
1731  {
1732  // FIXME depth -> ignored for now
1733  if (method_exists($this, "checkLock")) {
1734  // is locked?
1735  $lock = $this->checkLock($path);
1736  // ... and lock is not owned?
1737  if (is_array($lock) && count($lock)) {
1738  // FIXME doesn't check uri restrictions yet
1739  if (!isset($this->_SERVER["HTTP_IF"]) || !strstr($this->_SERVER["HTTP_IF"], $lock["token"])) {
1740  if (!$exclusive_only || ($lock["scope"] !== "shared")) return false;
1741  }
1742  }
1743  }
1744  return true;
1745  }
1746  // }}}
1747 
1748 
1749  /**
1750  * Generate lockdiscovery reply from checklock() result
1751  *
1752  * @param string resource path to check
1753  * @return string lockdiscovery response
1754  */
1756  {
1757  // no lock support without checklock() method
1758  if (!method_exists($this, "checklock")) {
1759  return "";
1760  }
1761  // collect response here
1762  $activelocks = "";
1763  // get checklock() reply
1764  $lock = $this->checklock($path);
1765  // generate <activelock> block for returned data
1766  if (is_array($lock) && count($lock)) {
1767  // check for 'timeout' or 'expires'
1768  if (!empty($lock["expires"])) {
1769  $timeout = "Second-" . ($lock["expires"] - time());
1770  } else if (!empty($lock["timeout"])) {
1771  $timeout = "Second-$lock[timeout]";
1772  } else {
1773  $timeout = "Infinite";
1774  }
1775  // genreate response block
1776  $activelocks.= "
1777  <D:activelock>
1778  <D:lockscope><D:$lock[scope]/></D:lockscope>
1779  <D:locktype><D:$lock[type]/></D:locktype>
1780  <D:depth>$lock[depth]</D:depth>
1781  <D:owner>$lock[owner]</D:owner>
1782  <D:timeout>$timeout</D:timeout>
1783  <D:locktoken><D:href>$lock[token]</D:href></D:locktoken>
1784  </D:activelock>
1785  ";
1786  }
1787  // return generated response
1788  return $activelocks;
1789  }
1790  /**
1791  * set HTTP return status and mirror it in a private header
1792  *
1793  * @param string status code and message
1794  * @return void
1795  */
1796  function http_status($status)
1797  {
1798  // simplified success case
1799  if ($status === true) {
1800  $status = "200 OK";
1801  }
1802  // remember status
1803  $this->_http_status = $status;
1804  // generate HTTP status response
1805  header("HTTP/1.1 $status");
1806  header("X-WebDAV-Status: $status", true);
1807  }
1808  /**
1809  * private minimalistic version of PHP urlencode()
1810  *
1811  * only blanks and XML special chars must be encoded here
1812  * full urlencode() encoding confuses some clients ...
1813  *
1814  * @param string URL to encode
1815  * @return string encoded URL
1816  */
1817  function _urlencode($url)
1818  {
1819  $r = rawurlencode($url);
1820  return str_replace("%2F", "/", $r);
1821  }
1822  /**
1823  * private version of PHP urldecode
1824  *
1825  * not really needed but added for completenes
1826  *
1827  * @param string URL to decode
1828  * @return string decoded URL
1829  */
1830  function _urldecode($path)
1831  {
1832  return urldecode($path);
1833  }
1834  /**
1835  * UTF-8 encode property values if not already done so
1836  *
1837  * @param string text to encode
1838  * @return string utf-8 encoded text
1839  */
1840  function _prop_encode($text)
1841  {
1842  switch (strtolower($this->_prop_encoding)) {
1843  case "utf-8":
1844  return $text;
1845  case "iso-8859-1":
1846  case "iso-8859-15":
1847  case "latin-1":
1848  default:
1849  return utf8_encode($text);
1850  }
1851  }
1852  /**
1853  * Slashify - make sure path ends in a slash
1854  *
1855  * @param string directory path
1856  * @returns string directory path wiht trailing slash
1857  */
1858  function _slashify($path)
1859  {
1860  if ($path[strlen($path) - 1] != '/') {
1861  $path = $path . "/";
1862  }
1863  return $path;
1864  }
1865  /**
1866  * Unslashify - make sure path doesn't in a slash
1867  *
1868  * @param string directory path
1869  * @returns string directory path wihtout trailing slash
1870  */
1871  function _unslashify($path)
1872  {
1873  if ($path[strlen($path) - 1] == '/') {
1874  $path = substr($path, 0, -1);
1875  }
1876  return $path;
1877  }
1878  /**
1879  * Merge two pathes, make sure there is exactly one slash between them
1880  *
1881  * @param string parent path
1882  * @param string child path
1883  * @return string merged path
1884  */
1885  function _mergePathes($parent, $child)
1886  {
1887  if ($child{0} == '/') {
1888  return $this->_unslashify($parent) . $child;
1889  } else {
1890  return $this->_slashify($parent) . $child;
1891  }
1892  }
1893 }
1894 /*
1895  * Local variables:
1896  * tab-width: 4
1897  * c-basic-offset: 4
1898  * End:
1899 */
1900 ?>
← centre documentaire © anakeen - published under CC License - Dynacase