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