[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/lib/pear/HTTP/WebDAV/ -> Server.php (source)

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


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1