PageRenderTime 68ms CodeModel.GetById 27ms app.highlight 30ms RepoModel.GetById 1ms app.codeStats 0ms

/administrator/components/com_extplorer/libraries/HTTP/WebDAV/Server.php

https://github.com/cavila/Astica
PHP | 2146 lines | 1054 code | 327 blank | 765 comment | 273 complexity | 0977d4aeadd1a5ac661bd26fac2d0103 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1<?php // $Id: Server.php 261205 2008-06-15 10:44:38Z hholzgra $
   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
  36require_once( dirname(__FILE__)."/Tools/_parse_propfind.php");
  37require_once( dirname(__FILE__)."/Tools/_parse_proppatch.php");
  38require_once( dirname(__FILE__)."/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 <hholzgra@php.net>
  47 * @version @package_version@
  48 */
  49class 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        // WebDAV has no concept of a query string and clients (including cadaver)
 167        // seem to pass '?' unencoded, so we need to extract the path info out
 168        // of the request URI ourselves
 169        $path_info = substr($this->_SERVER["REQUEST_URI"], strlen($this->_SERVER["SCRIPT_NAME"]));
 170
 171        // just in case the path came in empty ...
 172        if (empty($path_info)) {
 173            $path_info = "/";
 174        }
 175
 176        $this->base_uri = $uri;
 177        $this->uri      = $uri . $path_info;
 178
 179        // set path
 180        $this->path = $this->_urldecode($path_info);
 181        if (!strlen($this->path)) {
 182            if ($this->_SERVER["REQUEST_METHOD"] == "GET") {
 183                // redirect clients that try to GET a collection
 184                // WebDAV clients should never try this while
 185                // regular HTTP clients might ...
 186                header("Location: ".$this->base_uri."/");
 187                return;
 188            } else {
 189                // if a WebDAV client didn't give a path we just assume '/'
 190                $this->path = "/";
 191            }
 192        } 
 193        
 194        if (ini_get("magic_quotes_gpc")) {
 195            $this->path = stripslashes($this->path);
 196        }
 197        
 198        
 199        // identify ourselves
 200        if (empty($this->dav_powered_by)) {
 201            header("X-Dav-Powered-By: PHP class: ".get_class($this));
 202        } else {
 203            header("X-Dav-Powered-By: ".$this->dav_powered_by);
 204        }
 205
 206        // check authentication
 207        // for the motivation for not checking OPTIONS requests on / see 
 208        // http://pear.php.net/bugs/bug.php?id=5363
 209        if ( (   !(($this->_SERVER['REQUEST_METHOD'] == 'OPTIONS') && ($this->path == "/")))
 210             && (!$this->_check_auth())) {
 211            // RFC2518 says we must use Digest instead of Basic
 212            // but Microsoft Clients do not support Digest
 213            // and we don't support NTLM and Kerberos
 214            // so we are stuck with Basic here
 215            header('WWW-Authenticate: Basic realm="'.($this->http_auth_realm).'"');
 216
 217            // Windows seems to require this being the last header sent
 218            // (changed according to PECL bug #3138)
 219            $this->http_status('401 Unauthorized');
 220
 221            return;
 222        }
 223        
 224        // check 
 225        if (! $this->_check_if_header_conditions()) {
 226            return;
 227        }
 228        
 229        // detect requested method names
 230        $method  = strtolower($this->_SERVER["REQUEST_METHOD"]);
 231        $wrapper = "http_".$method;
 232        
 233        // activate HEAD emulation by GET if no HEAD method found
 234        if ($method == "head" && !method_exists($this, "head")) {
 235            $method = "get";
 236        }
 237        
 238        if (method_exists($this, $wrapper) && ($method == "options" || method_exists($this, $method))) {
 239            $this->$wrapper();  // call method by name
 240        } else { // method not found/implemented
 241            if ($this->_SERVER["REQUEST_METHOD"] == "LOCK") {
 242                $this->http_status("412 Precondition failed");
 243            } else {
 244                $this->http_status("405 Method not allowed");
 245                header("Allow: ".join(", ", $this->_allow()));  // tell client what's allowed
 246            }
 247        }
 248    }
 249
 250    // }}}
 251
 252    // {{{ abstract WebDAV methods 
 253
 254    // {{{ GET() 
 255    /**
 256     * GET implementation
 257     *
 258     * overload this method to retrieve resources from your server
 259     * <br>
 260     * 
 261     *
 262     * @abstract 
 263     * @param array &$params Array of input and output parameters
 264     * <br><b>input</b><ul>
 265     * <li> path - 
 266     * </ul>
 267     * <br><b>output</b><ul>
 268     * <li> size - 
 269     * </ul>
 270     * @returns int HTTP-Statuscode
 271     */
 272
 273    /* abstract
 274     function GET(&$params) 
 275     {
 276     // dummy entry for PHPDoc
 277     } 
 278    */
 279
 280    // }}}
 281
 282    // {{{ PUT() 
 283    /**
 284     * PUT implementation
 285     *
 286     * PUT implementation
 287     *
 288     * @abstract 
 289     * @param array &$params
 290     * @returns int HTTP-Statuscode
 291     */
 292    
 293    /* abstract
 294     function PUT() 
 295     {
 296     // dummy entry for PHPDoc
 297     } 
 298    */
 299    
 300    // }}}
 301
 302    // {{{ COPY() 
 303
 304    /**
 305     * COPY implementation
 306     *
 307     * COPY implementation
 308     *
 309     * @abstract 
 310     * @param array &$params
 311     * @returns int HTTP-Statuscode
 312     */
 313    
 314    /* abstract
 315     function COPY() 
 316     {
 317     // dummy entry for PHPDoc
 318     } 
 319    */
 320
 321    // }}}
 322
 323    // {{{ MOVE() 
 324
 325    /**
 326     * MOVE implementation
 327     *
 328     * MOVE implementation
 329     *
 330     * @abstract 
 331     * @param array &$params
 332     * @returns int HTTP-Statuscode
 333     */
 334    
 335    /* abstract
 336     function MOVE() 
 337     {
 338     // dummy entry for PHPDoc
 339     } 
 340    */
 341
 342    // }}}
 343
 344    // {{{ DELETE() 
 345
 346    /**
 347     * DELETE implementation
 348     *
 349     * DELETE implementation
 350     *
 351     * @abstract 
 352     * @param array &$params
 353     * @returns int HTTP-Statuscode
 354     */
 355    
 356    /* abstract
 357     function DELETE() 
 358     {
 359     // dummy entry for PHPDoc
 360     } 
 361    */
 362    // }}}
 363
 364    // {{{ PROPFIND() 
 365
 366    /**
 367     * PROPFIND implementation
 368     *
 369     * PROPFIND implementation
 370     *
 371     * @abstract 
 372     * @param array &$params
 373     * @returns int HTTP-Statuscode
 374     */
 375    
 376    /* abstract
 377     function PROPFIND() 
 378     {
 379     // dummy entry for PHPDoc
 380     } 
 381    */
 382
 383    // }}}
 384
 385    // {{{ PROPPATCH() 
 386
 387    /**
 388     * PROPPATCH implementation
 389     *
 390     * PROPPATCH implementation
 391     *
 392     * @abstract 
 393     * @param array &$params
 394     * @returns int HTTP-Statuscode
 395     */
 396    
 397    /* abstract
 398     function PROPPATCH() 
 399     {
 400     // dummy entry for PHPDoc
 401     } 
 402    */
 403    // }}}
 404
 405    // {{{ LOCK() 
 406
 407    /**
 408     * LOCK implementation
 409     *
 410     * LOCK implementation
 411     *
 412     * @abstract 
 413     * @param array &$params
 414     * @returns int HTTP-Statuscode
 415     */
 416    
 417    /* abstract
 418     function LOCK() 
 419     {
 420     // dummy entry for PHPDoc
 421     } 
 422    */
 423    // }}}
 424
 425    // {{{ UNLOCK() 
 426
 427    /**
 428     * UNLOCK implementation
 429     *
 430     * UNLOCK implementation
 431     *
 432     * @abstract 
 433     * @param array &$params
 434     * @returns int HTTP-Statuscode
 435     */
 436
 437    /* abstract
 438     function UNLOCK() 
 439     {
 440     // dummy entry for PHPDoc
 441     } 
 442    */
 443    // }}}
 444
 445    // }}}
 446
 447    // {{{ other abstract methods 
 448
 449    // {{{ check_auth() 
 450
 451    /**
 452     * check authentication
 453     *
 454     * overload this method to retrieve and confirm authentication information
 455     *
 456     * @abstract 
 457     * @param string type Authentication type, e.g. "basic" or "digest"
 458     * @param string username Transmitted username
 459     * @param string passwort Transmitted password
 460     * @returns bool Authentication status
 461     */
 462    
 463    /* abstract
 464     function checkAuth($type, $username, $password) 
 465     {
 466     // dummy entry for PHPDoc
 467     } 
 468    */
 469    
 470    // }}}
 471
 472    // {{{ checklock() 
 473
 474    /**
 475     * check lock status for a resource
 476     *
 477     * overload this method to return shared and exclusive locks 
 478     * active for this resource
 479     *
 480     * @abstract 
 481     * @param string resource Resource path to check
 482     * @returns array An array of lock entries each consisting
 483     *                of 'type' ('shared'/'exclusive'), 'token' and 'timeout'
 484     */
 485    
 486    /* abstract
 487     function checklock($resource) 
 488     {
 489     // dummy entry for PHPDoc
 490     } 
 491    */
 492
 493    // }}}
 494
 495    // }}}
 496
 497    // {{{ WebDAV HTTP method wrappers 
 498
 499    // {{{ http_OPTIONS() 
 500
 501    /**
 502     * OPTIONS method handler
 503     *
 504     * The OPTIONS method handler creates a valid OPTIONS reply
 505     * including Dav: and Allowed: headers
 506     * based on the implemented methods found in the actual instance
 507     *
 508     * @param  void
 509     * @return void
 510     */
 511    function http_OPTIONS() 
 512    {
 513        // Microsoft clients default to the Frontpage protocol 
 514        // unless we tell them to use WebDAV
 515        header("MS-Author-Via: DAV");
 516
 517        // get allowed methods
 518        $allow = $this->_allow();
 519
 520        // dav header
 521        $dav = array(1);        // assume we are always dav class 1 compliant
 522        if (isset($allow['LOCK'])) {
 523            $dav[] = 2;         // dav class 2 requires that locking is supported 
 524        }
 525
 526        // tell clients what we found
 527        $this->http_status("200 OK");
 528        header("DAV: "  .join(", ", $dav));
 529        header("Allow: ".join(", ", $allow));
 530
 531        header("Content-length: 0");
 532    }
 533
 534    // }}}
 535
 536
 537    // {{{ http_PROPFIND() 
 538
 539    /**
 540     * PROPFIND method handler
 541     *
 542     * @param  void
 543     * @return void
 544     */
 545    function http_PROPFIND() 
 546    {
 547        $options = Array();
 548        $files   = Array();
 549
 550        $options["path"] = $this->path;
 551        
 552        // search depth from header (default is "infinity)
 553        if (isset($this->_SERVER['HTTP_DEPTH'])) {
 554            $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
 555        } else {
 556            $options["depth"] = "infinity";
 557        }       
 558
 559        // analyze request payload
 560        $propinfo = new _parse_propfind("php://input");
 561        if (!$propinfo->success) {
 562            $this->http_status("400 Error");
 563            return;
 564        }
 565        $options['props'] = $propinfo->props;
 566
 567        // call user handler
 568        if (!$this->PROPFIND($options, $files)) {
 569            $files = array("files" => array());
 570            if (method_exists($this, "checkLock")) {
 571                // is locked?
 572                $lock = $this->checkLock($this->path);
 573
 574                if (is_array($lock) && count($lock)) {
 575                    $created          = isset($lock['created'])  ? $lock['created']  : time();
 576                    $modified         = isset($lock['modified']) ? $lock['modified'] : time();
 577                    $files['files'][] = array("path"  => $this->_slashify($this->path),
 578                                              "props" => array($this->mkprop("displayname",      $this->path),
 579                                                               $this->mkprop("creationdate",     $created),
 580                                                               $this->mkprop("getlastmodified",  $modified),
 581                                                               $this->mkprop("resourcetype",     ""),
 582                                                               $this->mkprop("getcontenttype",   ""),
 583                                                               $this->mkprop("getcontentlength", 0))
 584                                              );
 585                }
 586            }
 587
 588            if (empty($files['files'])) {
 589                $this->http_status("404 Not Found");
 590                return;
 591            }
 592        }
 593        
 594        // collect namespaces here
 595        $ns_hash = array();
 596        
 597        // Microsoft Clients need this special namespace for date and time values
 598        $ns_defs = "xmlns:ns0=\"urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/\"";    
 599    
 600        // now we loop over all returned file entries
 601        foreach ($files["files"] as $filekey => $file) {
 602            
 603            // nothing to do if no properties were returend for a file
 604            if (!isset($file["props"]) || !is_array($file["props"])) {
 605                continue;
 606            }
 607            
 608            // now loop over all returned properties
 609            foreach ($file["props"] as $key => $prop) {
 610                // as a convenience feature we do not require that user handlers
 611                // restrict returned properties to the requested ones
 612                // here we strip all unrequested entries out of the response
 613                
 614                switch($options['props']) {
 615                case "all":
 616                    // nothing to remove
 617                    break;
 618                    
 619                case "names":
 620                    // only the names of all existing properties were requested
 621                    // so we remove all values
 622                    unset($files["files"][$filekey]["props"][$key]["val"]);
 623                    break;
 624                    
 625                default:
 626                    $found = false;
 627                    
 628                    // search property name in requested properties 
 629                    foreach ((array)$options["props"] as $reqprop) {
 630                        if (!isset($reqprop["xmlns"])) {
 631                            $reqprop["xmlns"] = "";
 632                        }
 633                        if (   $reqprop["name"]  == $prop["name"] 
 634                               && $reqprop["xmlns"] == $prop["ns"]) {
 635                            $found = true;
 636                            break;
 637                        }
 638                    }
 639                    
 640                    // unset property and continue with next one if not found/requested
 641                    if (!$found) {
 642                        $files["files"][$filekey]["props"][$key]="";
 643                        continue(2);
 644                    }
 645                    break;
 646                }
 647                
 648                // namespace handling 
 649                if (empty($prop["ns"])) continue; // no namespace
 650                $ns = $prop["ns"]; 
 651                if ($ns == "DAV:") continue; // default namespace
 652                if (isset($ns_hash[$ns])) continue; // already known
 653
 654                // register namespace 
 655                $ns_name = "ns".(count($ns_hash) + 1);
 656                $ns_hash[$ns] = $ns_name;
 657                $ns_defs .= " xmlns:$ns_name=\"$ns\"";
 658            }
 659        
 660            // we also need to add empty entries for properties that were requested
 661            // but for which no values where returned by the user handler
 662            if (is_array($options['props'])) {
 663                foreach ($options["props"] as $reqprop) {
 664                    if ($reqprop['name']=="") continue; // skip empty entries
 665                    
 666                    $found = false;
 667                    
 668                    if (!isset($reqprop["xmlns"])) {
 669                        $reqprop["xmlns"] = "";
 670                    }
 671
 672                    // check if property exists in result
 673                    foreach ($file["props"] as $prop) {
 674                        if (   $reqprop["name"]  == $prop["name"]
 675                               && $reqprop["xmlns"] == $prop["ns"]) {
 676                            $found = true;
 677                            break;
 678                        }
 679                    }
 680                    
 681                    if (!$found) {
 682                        if ($reqprop["xmlns"]==="DAV:" && $reqprop["name"]==="lockdiscovery") {
 683                            // lockdiscovery is handled by the base class
 684                            $files["files"][$filekey]["props"][] 
 685                                = $this->mkprop("DAV:", 
 686                                                "lockdiscovery", 
 687                                                $this->lockdiscovery($files["files"][$filekey]['path']));
 688                        } else {
 689                            // add empty value for this property
 690                            $files["files"][$filekey]["noprops"][] =
 691                                $this->mkprop($reqprop["xmlns"], $reqprop["name"], "");
 692
 693                            // register property namespace if not known yet
 694                            if ($reqprop["xmlns"] != "DAV:" && !isset($ns_hash[$reqprop["xmlns"]])) {
 695                                $ns_name = "ns".(count($ns_hash) + 1);
 696                                $ns_hash[$reqprop["xmlns"]] = $ns_name;
 697                                $ns_defs .= " xmlns:$ns_name=\"$reqprop[xmlns]\"";
 698                            }
 699                        }
 700                    }
 701                }
 702            }
 703        }
 704        
 705        // now we generate the reply header ...
 706        $this->http_status("207 Multi-Status");
 707        header('Content-Type: text/xml; charset="utf-8"');
 708        
 709        // ... and payload
 710        echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
 711        echo "<D:multistatus xmlns:D=\"DAV:\">\n";
 712            
 713        foreach ($files["files"] as $file) {
 714            // ignore empty or incomplete entries
 715            if (!is_array($file) || empty($file) || !isset($file["path"])) continue;
 716            $path = $file['path'];                  
 717            if (!is_string($path) || $path==="") continue;
 718
 719            echo " <D:response $ns_defs>\n";
 720        
 721            /* TODO right now the user implementation has to make sure
 722             collections end in a slash, this should be done in here
 723             by checking the resource attribute */
 724            $href = $this->_mergePaths($this->_SERVER['SCRIPT_NAME'], $path);
 725
 726            /* minimal urlencoding is needed for the resource path */
 727            $href = $this->_urlencode($href);
 728        
 729            echo "  <D:href>$href</D:href>\n";
 730        
 731            // report all found properties and their values (if any)
 732            if (isset($file["props"]) && is_array($file["props"])) {
 733                echo "  <D:propstat>\n";
 734                echo "   <D:prop>\n";
 735
 736                foreach ($file["props"] as $key => $prop) {
 737                    
 738                    if (!is_array($prop)) continue;
 739                    if (!isset($prop["name"])) continue;
 740                    
 741                    if (!isset($prop["val"]) || $prop["val"] === "" || $prop["val"] === false) {
 742                        // empty properties (cannot use empty() for check as "0" is a legal value here)
 743                        if ($prop["ns"]=="DAV:") {
 744                            echo "     <D:$prop[name]/>\n";
 745                        } else if (!empty($prop["ns"])) {
 746                            echo "     <".$ns_hash[$prop["ns"]].":$prop[name]/>\n";
 747                        } else {
 748                            echo "     <$prop[name] xmlns=\"\"/>";
 749                        }
 750                    } else if ($prop["ns"] == "DAV:") {
 751                        // some WebDAV properties need special treatment
 752                        switch ($prop["name"]) {
 753                        case "creationdate":
 754                            echo "     <D:creationdate ns0:dt=\"dateTime.tz\">"
 755                                . gmdate("Y-m-d\\TH:i:s\\Z", $prop['val'])
 756                                . "</D:creationdate>\n";
 757                            break;
 758                        case "getlastmodified":
 759                            echo "     <D:getlastmodified ns0:dt=\"dateTime.rfc1123\">"
 760                                . gmdate("D, d M Y H:i:s ", $prop['val'])
 761                                . "GMT</D:getlastmodified>\n";
 762                            break;
 763                        case "resourcetype":
 764                            echo "     <D:resourcetype><D:$prop[val]/></D:resourcetype>\n";
 765                            break;
 766                        case "supportedlock":
 767                            echo "     <D:supportedlock>$prop[val]</D:supportedlock>\n";
 768                            break;
 769                        case "lockdiscovery":  
 770                            echo "     <D:lockdiscovery>\n";
 771                            echo $prop["val"];
 772                            echo "     </D:lockdiscovery>\n";
 773                            break;
 774                        // the following are non-standard Microsoft extensions to the DAV namespace
 775                        case "lastaccessed":
 776                            echo "     <D:lastaccessed ns0:dt=\"dateTime.rfc1123\">"
 777                                . gmdate("D, d M Y H:i:s ", $prop['val'])
 778                                . "GMT</D:lastaccessed>\n";
 779                            break;
 780                        case "ishidden":
 781                            echo "     <D:ishidden>"
 782                                . is_string($prop['val']) ? $prop['val'] : ($prop['val'] ? 'true' : 'false')
 783                                . "</D:ishidden>\n";
 784                            break;
 785                        default:                                    
 786                            echo "     <D:$prop[name]>"
 787                                . $this->_prop_encode(htmlspecialchars($prop['val']))
 788                                .     "</D:$prop[name]>\n";                               
 789                            break;
 790                        }
 791                    } else {
 792                        // properties from namespaces != "DAV:" or without any namespace 
 793                        if ($prop["ns"]) {
 794                            echo "     <" . $ns_hash[$prop["ns"]] . ":$prop[name]>"
 795                                . $this->_prop_encode(htmlspecialchars($prop['val']))
 796                                . "</" . $ns_hash[$prop["ns"]] . ":$prop[name]>\n";
 797                        } else {
 798                            echo "     <$prop[name] xmlns=\"\">"
 799                                . $this->_prop_encode(htmlspecialchars($prop['val']))
 800                                . "</$prop[name]>\n";
 801                        }                               
 802                    }
 803                }
 804
 805                echo "   </D:prop>\n";
 806                echo "   <D:status>HTTP/1.1 200 OK</D:status>\n";
 807                echo "  </D:propstat>\n";
 808            }
 809       
 810            // now report all properties requested but not found
 811            if (isset($file["noprops"])) {
 812                echo "  <D:propstat>\n";
 813                echo "   <D:prop>\n";
 814
 815                foreach ($file["noprops"] as $key => $prop) {
 816                    if ($prop["ns"] == "DAV:") {
 817                        echo "     <D:$prop[name]/>\n";
 818                    } else if ($prop["ns"] == "") {
 819                        echo "     <$prop[name] xmlns=\"\"/>\n";
 820                    } else {
 821                        echo "     <" . $ns_hash[$prop["ns"]] . ":$prop[name]/>\n";
 822                    }
 823                }
 824
 825                echo "   </D:prop>\n";
 826                echo "   <D:status>HTTP/1.1 404 Not Found</D:status>\n";
 827                echo "  </D:propstat>\n";
 828            }
 829            
 830            echo " </D:response>\n";
 831        }
 832        
 833        echo "</D:multistatus>\n";
 834    }
 835
 836    
 837    // }}}
 838    
 839    // {{{ http_PROPPATCH() 
 840
 841    /**
 842     * PROPPATCH method handler
 843     *
 844     * @param  void
 845     * @return void
 846     */
 847    function http_PROPPATCH() 
 848    {
 849        if ($this->_check_lock_status($this->path)) {
 850            $options = Array();
 851
 852            $options["path"] = $this->path;
 853
 854            $propinfo = new _parse_proppatch("php://input");
 855            
 856            if (!$propinfo->success) {
 857                $this->http_status("400 Error");
 858                return;
 859            }
 860            
 861            $options['props'] = $propinfo->props;
 862            
 863            $responsedescr = $this->PROPPATCH($options);
 864            
 865            $this->http_status("207 Multi-Status");
 866            header('Content-Type: text/xml; charset="utf-8"');
 867            
 868            echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
 869
 870            echo "<D:multistatus xmlns:D=\"DAV:\">\n";
 871            echo " <D:response>\n";
 872            echo "  <D:href>".$this->_urlencode($this->_mergePaths($this->_SERVER["SCRIPT_NAME"], $this->path))."</D:href>\n";
 873
 874            foreach ($options["props"] as $prop) {
 875                echo "   <D:propstat>\n";
 876                echo "    <D:prop><$prop[name] xmlns=\"$prop[ns]\"/></D:prop>\n";
 877                echo "    <D:status>HTTP/1.1 $prop[status]</D:status>\n";
 878                echo "   </D:propstat>\n";
 879            }
 880
 881            if ($responsedescr) {
 882                echo "  <D:responsedescription>".
 883                    $this->_prop_encode(htmlspecialchars($responsedescr)).
 884                    "</D:responsedescription>\n";
 885            }
 886
 887            echo " </D:response>\n";
 888            echo "</D:multistatus>\n";
 889        } else {
 890            $this->http_status("423 Locked");
 891        }
 892    }
 893    
 894    // }}}
 895
 896
 897    // {{{ http_MKCOL() 
 898
 899    /**
 900     * MKCOL method handler
 901     *
 902     * @param  void
 903     * @return void
 904     */
 905    function http_MKCOL() 
 906    {
 907        $options = Array();
 908
 909        $options["path"] = $this->path;
 910
 911        $stat = $this->MKCOL($options);
 912
 913        $this->http_status($stat);
 914    }
 915
 916    // }}}
 917
 918
 919    // {{{ http_GET() 
 920
 921    /**
 922     * GET method handler
 923     *
 924     * @param void
 925     * @returns void
 926     */
 927    function http_GET() 
 928    {
 929        // TODO check for invalid stream
 930        $options         = Array();
 931        $options["path"] = $this->path;
 932
 933        $this->_get_ranges($options);
 934
 935        if (true === ($status = $this->GET($options))) {
 936            if (!headers_sent()) {
 937                $status = "200 OK";
 938
 939                if (!isset($options['mimetype'])) {
 940                    $options['mimetype'] = "application/octet-stream";
 941                }
 942                header("Content-type: $options[mimetype]");
 943                
 944                if (isset($options['mtime'])) {
 945                    header("Last-modified:".gmdate("D, d M Y H:i:s ", $options['mtime'])."GMT");
 946                }
 947                
 948                if (isset($options['stream'])) {
 949                    // GET handler returned a stream
 950                    if (!empty($options['ranges']) && (0===fseek($options['stream'], 0, SEEK_SET))) {
 951                        // partial request and stream is seekable 
 952                        
 953                        if (count($options['ranges']) === 1) {
 954                            $range = $options['ranges'][0];
 955                            
 956                            if (isset($range['start'])) {
 957                                fseek($options['stream'], $range['start'], SEEK_SET);
 958                                if (feof($options['stream'])) {
 959                                    $this->http_status("416 Requested range not satisfiable");
 960                                    return;
 961                                }
 962
 963                                if (isset($range['end'])) {
 964                                    $size = $range['end']-$range['start']+1;
 965                                    $this->http_status("206 partial");
 966                                    header("Content-length: $size");
 967                                    header("Content-range: $range[start]-$range[end]/"
 968                                           . (isset($options['size']) ? $options['size'] : "*"));
 969                                    while ($size && !feof($options['stream'])) {
 970                                        $buffer = fread($options['stream'], 4096);
 971                                        $size  -= $this->bytes($buffer);
 972                                        echo $buffer;
 973                                    }
 974                                } else {
 975                                    $this->http_status("206 partial");
 976                                    if (isset($options['size'])) {
 977                                        header("Content-length: ".($options['size'] - $range['start']));
 978                                        header("Content-range: ".$range['start']."-".$range['end']."/"
 979                                               . (isset($options['size']) ? $options['size'] : "*"));
 980                                    }
 981                                    fpassthru($options['stream']);
 982                                }
 983                            } else {
 984                                header("Content-length: ".$range['last']);
 985                                fseek($options['stream'], -$range['last'], SEEK_END);
 986                                fpassthru($options['stream']);
 987                            }
 988                        } else {
 989                            $this->_multipart_byterange_header(); // init multipart
 990                            foreach ($options['ranges'] as $range) {
 991                                // TODO what if size unknown? 500?
 992                                if (isset($range['start'])) {
 993                                    $from = $range['start'];
 994                                    $to   = !empty($range['end']) ? $range['end'] : $options['size']-1; 
 995                                } else {
 996                                    $from = $options['size'] - $range['last']-1;
 997                                    $to   = $options['size'] -1;
 998                                }
 999                                $total = isset($options['size']) ? $options['size'] : "*"; 
1000                                $size  = $to - $from + 1;
1001                                $this->_multipart_byterange_header($options['mimetype'], $from, $to, $total);
1002
1003
1004                                fseek($options['stream'], $from, SEEK_SET);
1005                                while ($size && !feof($options['stream'])) {
1006                                    $buffer = fread($options['stream'], 4096);
1007                                    $size  -= $this->bytes($buffer);
1008                                    echo $buffer;
1009                                }
1010                            }
1011                            $this->_multipart_byterange_header(); // end multipart
1012                        }
1013                    } else {
1014                        // normal request or stream isn't seekable, return full content
1015                        if (isset($options['size'])) {
1016                            header("Content-length: ".$options['size']);
1017                        }
1018                        fpassthru($options['stream']);
1019                        return; // no more headers
1020                    }
1021                } elseif (isset($options['data'])) {
1022                    if (is_array($options['data'])) {
1023                        // reply to partial request
1024                    } else {
1025                        header("Content-length: ".$this->bytes($options['data']));
1026                        echo $options['data'];
1027                    }
1028                }
1029            } 
1030        } 
1031
1032        if (!headers_sent()) {
1033            if (false === $status) {
1034                $this->http_status("404 not found");
1035            } else {
1036                // TODO: check setting of headers in various code paths above
1037                $this->http_status("$status");
1038            }
1039        }
1040    }
1041
1042
1043    /**
1044     * parse HTTP Range: header
1045     *
1046     * @param  array options array to store result in
1047     * @return void
1048     */
1049    function _get_ranges(&$options) 
1050    {
1051        // process Range: header if present
1052        if (isset($this->_SERVER['HTTP_RANGE'])) {
1053
1054            // we only support standard "bytes" range specifications for now
1055            if (preg_match('/bytes\s*=\s*(.+)/', $this->_SERVER['HTTP_RANGE'], $matches)) {
1056                $options["ranges"] = array();
1057
1058                // ranges are comma separated
1059                foreach (explode(",", $matches[1]) as $range) {
1060                    // ranges are either from-to pairs or just end positions
1061                    list($start, $end) = explode("-", $range);
1062                    $options["ranges"][] = ($start==="") 
1063                        ? array("last"=>$end) 
1064                        : array("start"=>$start, "end"=>$end);
1065                }
1066            }
1067        }
1068    }
1069
1070    /**
1071     * generate separator headers for multipart response
1072     *
1073     * first and last call happen without parameters to generate 
1074     * the initial header and closing sequence, all calls inbetween
1075     * require content mimetype, start and end byte position and
1076     * optionaly the total byte length of the requested resource
1077     *
1078     * @param  string  mimetype
1079     * @param  int     start byte position
1080     * @param  int     end   byte position
1081     * @param  int     total resource byte size
1082     */
1083    function _multipart_byterange_header($mimetype = false, $from = false, $to=false, $total=false) 
1084    {
1085        if ($mimetype === false) {
1086            if (!isset($this->multipart_separator)) {
1087                // initial
1088
1089                // a little naive, this sequence *might* be part of the content
1090                // but it's really not likely and rather expensive to check 
1091                $this->multipart_separator = "SEPARATOR_".md5(microtime());
1092
1093                // generate HTTP header
1094                header("Content-type: multipart/byteranges; boundary=".$this->multipart_separator);
1095            } else {
1096                // final 
1097
1098                // generate closing multipart sequence
1099                echo "\n--{$this->multipart_separator}--";
1100            }
1101        } else {
1102            // generate separator and header for next part
1103            echo "\n--{$this->multipart_separator}\n";
1104            echo "Content-type: $mimetype\n";
1105            echo "Content-range: $from-$to/". ($total === false ? "*" : $total);
1106            echo "\n\n";
1107        }
1108    }
1109
1110            
1111
1112    // }}}
1113
1114    // {{{ http_HEAD() 
1115
1116    /**
1117     * HEAD method handler
1118     *
1119     * @param  void
1120     * @return void
1121     */
1122    function http_HEAD() 
1123    {
1124        $status          = false;
1125        $options         = Array();
1126        $options["path"] = $this->path;
1127        
1128        if (method_exists($this, "HEAD")) {
1129            $status = $this->head($options);
1130        } else if (method_exists($this, "GET")) {
1131            ob_start();
1132            $status = $this->GET($options);
1133            if (!isset($options['size'])) {
1134                $options['size'] = ob_get_length();
1135            }
1136            ob_end_clean();
1137        }
1138        
1139        if (!isset($options['mimetype'])) {
1140            $options['mimetype'] = "application/octet-stream";
1141        }
1142        header("Content-type: $options[mimetype]");
1143        
1144        if (isset($options['mtime'])) {
1145            header("Last-modified:".gmdate("D, d M Y H:i:s ", $options['mtime'])."GMT");
1146        }
1147                
1148        if (isset($options['size'])) {
1149            header("Content-length: ".$options['size']);
1150        }
1151
1152        if ($status === true)  $status = "200 OK";
1153        if ($status === false) $status = "404 Not found";
1154        
1155        $this->http_status($status);
1156    }
1157
1158    // }}}
1159
1160    // {{{ http_PUT() 
1161
1162    /**
1163     * PUT method handler
1164     *
1165     * @param  void
1166     * @return void
1167     */
1168    function http_PUT() 
1169    {
1170        if ($this->_check_lock_status($this->path)) {
1171            $options                   = Array();
1172            $options["path"]           = $this->path;
1173            $options["content_length"] = $this->_SERVER["CONTENT_LENGTH"];
1174
1175            // get the Content-type 
1176            if (isset($this->_SERVER["CONTENT_TYPE"])) {
1177                // for now we do not support any sort of multipart requests
1178                if (!strncmp($this->_SERVER["CONTENT_TYPE"], "multipart/", 10)) {
1179                    $this->http_status("501 not implemented");
1180                    echo "The service does not support mulipart PUT requests";
1181                    return;
1182                }
1183                $options["content_type"] = $this->_SERVER["CONTENT_TYPE"];
1184            } else {
1185                // default content type if none given
1186                $options["content_type"] = "application/octet-stream";
1187            }
1188
1189            /* RFC 2616 2.6 says: "The recipient of the entity MUST NOT 
1190             ignore any Content-* (e.g. Content-Range) headers that it 
1191             does not understand or implement and MUST return a 501 
1192             (Not Implemented) response in such cases."
1193            */ 
1194            foreach ($this->_SERVER as $key => $val) {
1195                if (strncmp($key, "HTTP_CONTENT", 11)) continue;
1196                switch ($key) {
1197                case 'HTTP_CONTENT_ENCODING': // RFC 2616 14.11
1198                    // TODO support this if ext/zlib filters are available
1199                    $this->http_status("501 not implemented"); 
1200                    echo "The service does not support '$val' content encoding";
1201                    return;
1202
1203                case 'HTTP_CONTENT_LANGUAGE': // RFC 2616 14.12
1204                    // we assume it is not critical if this one is ignored
1205                    // in the actual PUT implementation ...
1206                    $options["content_language"] = $val;
1207                    break;
1208
1209                case 'HTTP_CONTENT_LENGTH':
1210                    // defined on IIS and has the same value as CONTENT_LENGTH
1211                    break;
1212
1213                case 'HTTP_CONTENT_LOCATION': // RFC 2616 14.14
1214                    /* The meaning of the Content-Location header in PUT 
1215                     or POST requests is undefined; servers are free 
1216                     to ignore it in those cases. */
1217                    break;
1218
1219                case 'HTTP_CONTENT_RANGE':    // RFC 2616 14.16
1220                    // single byte range requests are supported
1221                    // the header format is also specified in RFC 2616 14.16
1222                    // TODO we have to ensure that implementations support this or send 501 instead
1223                    if (!preg_match('@bytes\s+(\d+)-(\d+)/((\d+)|\*)@', $val, $matches)) {
1224                        $this->http_status("400 bad request"); 
1225                        echo "The service does only support single byte ranges";
1226                        return;
1227                    }
1228                    
1229                    $range = array("start"=>$matches[1], "end"=>$matches[2]);
1230                    if (is_numeric($matches[3])) {
1231                        $range["total_length"] = $matches[3];
1232                    }
1233                    $option["ranges"][] = $range;
1234
1235                    // TODO make sure the implementation supports partial PUT
1236                    // this has to be done in advance to avoid data being overwritten
1237                    // on implementations that do not support this ...
1238                    break;
1239
1240                case 'HTTP_CONTENT_TYPE':
1241                    // defined on IIS and has the same value as CONTENT_TYPE
1242                    break;
1243
1244                case 'HTTP_CONTENT_MD5':      // RFC 2616 14.15
1245                    // TODO: maybe we can just pretend here?
1246                    $this->http_status("501 not implemented"); 
1247                    echo "The service does not support content MD5 checksum verification"; 
1248                    return;
1249
1250                default: 
1251                    // any other unknown Content-* headers
1252                    $this->http_status("501 not implemented"); 
1253                    echo "The service does not support '$key'"; 
1254                    return;
1255                }
1256            }
1257
1258            $options["stream"] = fopen("php://input", "r");
1259
1260            $stat = $this->PUT($options);
1261
1262            if ($stat === false) {
1263                $stat = "403 Forbidden";
1264            } else if (is_resource($stat) && get_resource_type($stat) == "stream") {
1265                $stream = $stat;
1266
1267                $stat = $options["new"] ? "201 Created" : "204 No Content";
1268
1269                if (!empty($options["ranges"])) {
1270                    // TODO multipart support is missing (see also above)
1271                    if (0 == fseek($stream, $range[0]["start"], SEEK_SET)) {
1272                        $length = $range[0]["end"]-$range[0]["start"]+1;
1273                        if (!fwrite($stream, fread($options["stream"], $length))) {
1274                            $stat = "403 Forbidden"; 
1275                        }
1276                    } else {
1277                        $stat = "403 Forbidden"; 
1278                    }
1279                } else {
1280                    while (!feof($options["stream"])) {
1281                        if (false === fwrite($stream, fread($options["stream"], 4096))) {
1282                            $stat = "403 Forbidden"; 
1283                            break;
1284                        }
1285                    }
1286                }
1287
1288                fclose($stream);            
1289            } 
1290
1291            $this->http_status($stat);
1292        } else {
1293            $this->http_status("423 Locked");
1294        }
1295    }
1296
1297    // }}}
1298
1299
1300    // {{{ http_DELETE() 
1301
1302    /**
1303     * DELETE method handler
1304     *
1305     * @param  void
1306     * @return void
1307     */
1308    function http_DELETE() 
1309    {
1310        // check RFC 2518 Section 9.2, last paragraph
1311        if (isset($this->_SERVER["HTTP_DEPTH"])) {
1312            if ($this->_SERVER["HTTP_DEPTH"] != "infinity") {
1313                $this->http_status("400 Bad Request");
1314                return;
1315            }
1316        }
1317
1318        // check lock status
1319        if ($this->_check_lock_status($this->path)) {
1320            // ok, proceed
1321            $options         = Array();
1322            $options["path"] = $this->path;
1323
1324            $stat = $this->DELETE($options);
1325
1326            $this->http_status($stat);
1327        } else {
1328            // sorry, its locked
1329            $this->http_status("423 Locked");
1330        }
1331    }
1332
1333    // }}}
1334
1335    // {{{ http_COPY() 
1336
1337    /**
1338     * COPY method handler
1339     *
1340     * @param  void
1341     * @return void
1342     */
1343    function http_COPY() 
1344    {
1345        // no need to check source lock status here 
1346        // destination lock status is always checked by the helper method
1347        $this->_copymove("copy");
1348    }
1349
1350    // }}}
1351
1352    // {{{ http_MOVE() 
1353
1354    /**
1355     * MOVE method handler
1356     *
1357     * @param  void
1358     * @return void
1359     */
1360    function http_MOVE() 
1361    {
1362        if ($this->_check_lock_status($this->path)) {
1363            // destination lock status is always checked by the helper method
1364            $this->_copymove("move");
1365        } else {
1366            $this->http_status("423 Locked");
1367        }
1368    }
1369
1370    // }}}
1371
1372
1373    // {{{ http_LOCK() 
1374
1375    /**
1376     * LOCK method handler
1377     *
1378     * @param  void
1379     * @return void
1380     */
1381    function http_LOCK() 
1382    {
1383        $options         = Array();
1384        $options["path"] = $this->path;
1385        
1386        if (isset($this->_SERVER['HTTP_DEPTH'])) {
1387            $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
1388        } else {
1389            $options["depth"] = "infinity";
1390        }
1391        
1392        if (isset($this->_SERVER["HTTP_TIMEOUT"])) {
1393            $options["timeout"] = explode(",", $this->_SERVER["HTTP_TIMEOUT"]);
1394        }
1395        
1396        if (empty($this->_SERVER['CONTENT_LENGTH']) && !empty($this->_SERVER['HTTP_IF'])) {
1397            // check if locking is possible
1398            if (!$this->_check_lock_status($this->path)) {
1399                $this->http_status("423 Locked");
1400                return;
1401            }
1402
1403            // refresh lock
1404            $options["locktoken"] = substr($this->_SERVER['HTTP_IF'], 2, -2);
1405            $options["update"]    = $options["locktoken"];
1406
1407            // setting defaults for required fields, LOCK() SHOULD overwrite these
1408            $options['owner']     = "unknown";
1409            $options['scope']     = "exclusive";
1410            $options['type']      = "write";
1411
1412
1413            $stat = $this->LOCK($options);
1414        } else {
1415            // extract lock request information from request XML payload
1416            $lockinfo = new _parse_lockinfo("php://input");
1417            if (!$lockinfo->success) {
1418                $this->http_status("400 bad request"); 
1419            }
1420
1421            // check if locking is possible
1422            if (!$this->_check_lock_status($this->path, $lockinfo->lockscope === "shared")) {
1423                $this->http_status("423 Locked");
1424                return;
1425            }
1426
1427            // new lock 
1428            $options["scope"]     = $lockinfo->lockscope;
1429            $options["type"]      = $lockinfo->locktype;
1430            $options["owner"]     = $lockinfo->owner;            
1431            $options["locktoken"] = $this->_new_locktoken();
1432            
1433            $stat = $this->LOCK($options);              
1434        }
1435        
1436        if (is_bool($stat)) {
1437            $http_stat = $stat ? "200 OK" : "423 Locked";
1438        } else {
1439            $http_stat = (string)$stat;
1440        }
1441        $this->http_status($http_stat);
1442        
1443        if ($http_stat{0} == 2) { // 2xx states are ok 
1444            if ($options["timeout"]) {
1445                // if multiple timeout values were given we take the first only
1446                if (is_array($options["timeout"])) {
1447                    reset($options["timeout"]);
1448                    $options["timeout"] = current($options["timeout"]);
1449                }
1450                // if the timeout is numeric only we need to reformat it
1451                if (is_numeric($options["timeout"])) {
1452                    // more than a million is considered an absolute timestamp
1453                    // less is more likely a relative value
1454                    if ($options["timeout"]>1000000) {
1455                        $timeout = "Second-".($options['timeout']-time());
1456                    } else {
1457                        $timeout = "Second-$options[timeout]";
1458                    }
1459                } else {
1460                    // non-numeric values are passed on verbatim,
1461                    // no error checking is performed here in this case
1462                    // TODO: send "Infinite" on invalid timeout strings?
1463                    $timeout = $options["timeout…

Large files files are truncated, but you can click here to view the full file