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