PageRenderTime 840ms CodeModel.GetById 71ms app.highlight 482ms RepoModel.GetById 52ms app.codeStats 4ms

/ext/lxcr/HTTP/WebDAV/Server.php

http://github.com/symfony-cmf/phpcrbrowser
PHP | 2048 lines | 1002 code | 315 blank | 731 comment | 260 complexity | 73ff2951d52c73e1d6c7ea3426f8b003 MD5 | raw file

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

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

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