1<?php // $Id: Server.php 17189 2007-11-17 08:53:36Z bharat $ 2/* 3 +----------------------------------------------------------------------+ 4 | Copyright (c) 2002-2007 Christian Stocker, Hartmut Holzgraefe | 5 | All rights reserved | 6 | | 7 | Redistribution and use in source and binary forms, with or without | 8 | modification, are permitted provided that the following conditions | 9 | are met: | 10 | | 11 | 1. Redistributions of source code must retain the above copyright | 12 | notice, this list of conditions and the following disclaimer. | 13 | 2. Redistributions in binary form must reproduce the above copyright | 14 | notice, this list of conditions and the following disclaimer in | 15 | the documentation and/or other materials provided with the | 16 | distribution. | 17 | 3. The names of the authors may not be used to endorse or promote | 18 | products derived from this software without specific prior | 19 | written permission. | 20 | | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS | 24 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE | 25 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, | 26 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, | 27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | 28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT | 30 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN | 31 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | 32 | POSSIBILITY OF SUCH DAMAGE. | 33 +----------------------------------------------------------------------+ 34*/ 35 36require_once(dirname(__FILE__) . '/Tools/_parse_propfind.php'); 37require_once(dirname(__FILE__) . '/Tools/_parse_proppatch.php'); 38require_once(dirname(__FILE__) . '/Tools/_parse_lockinfo.php'); 39 40define('HTTP_WEBDAV_SERVER_DATATYPE_NAMESPACE', 41 'urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882'); 42 43/** 44 * Virtual base class for implementing WebDAV servers 45 * 46 * WebDAV server base class, needs to be extended to do useful work 47 * 48 * @package HTTP_WebDAV_Server 49 * @author Hartmut Holzgraefe <hholzgra@php.net> 50 * @version 0.99.1dev 51 */ 52class HTTP_WebDAV_Server 53{ 54 // {{{ Member Variables 55 56 /** 57 * URL path for this request 58 * 59 * @var string 60 */ 61 var $path; 62 63 /** 64 * Base URL for this request 65 * 66 * See PHP parse_url structure 67 * 68 * @var array 69 */ 70 var $baseUrl; 71 72 /** 73 * Realm string to be used in authentification popups 74 * 75 * @var string 76 */ 77 var $http_auth_realm = 'PHP WebDAV'; 78 79 /** 80 * String to be used in "X-Dav-Powered-By" header 81 * 82 * @var string 83 */ 84 var $dav_powered_by = ''; 85 86 /** 87 * Remember parsed If: (RFC2518 9.4) header conditions 88 * 89 * @var array 90 */ 91 var $_if_header_uris = array(); 92 93 /** 94 * HTTP response headers 95 * 96 * @var array 97 */ 98 var $headers = array(); 99 100 /** 101 * Encoding of property values passed in 102 * 103 * @var string 104 */ 105 var $_prop_encoding = 'utf-8'; 106 107 // }}} 108 109 // {{{ handleRequest 110 111 /** 112 * Handle WebDAV request 113 * 114 * Dispatch WebDAV request to the apropriate method wrapper 115 * 116 * @param void 117 * @return void 118 */ 119 function handleRequest() 120 { 121 // identify ourselves 122 if (empty($this->dav_powered_by)) { 123 $this->dav_powered_by = 'PHP class: ' . get_class($this); 124 } 125 $this->setResponseHeader('X-Dav-Powered-By: ' . $this->dav_powered_by); 126 127 // set path 128 if (empty($this->path)) { 129 $this->path = $this->_urldecode($_SERVER['PATH_INFO']); 130 $this->path = trim($this->path, '/'); 131 } 132 133 if (ini_get('magic_quotes_gpc')) { 134 $this->path = stripslashes($this->path); 135 } 136 137 // set base URL 138 if (empty($this->baseUrl)) { 139 $this->baseUrl = parse_url( 140 "http://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"); 141 $this->baseUrl['path'] = substr($this->baseUrl['path'], 0, 142 strlen($this->baseUrl['path']) - strlen($_SERVER['PATH_INFO'])); 143 } 144 145 // check authentication 146 if (!$this->check_auth_wrapper()) { 147 148 // RFC2518 says we must use Digest instead of Basic but Microsoft 149 // clients do not support Digest and we don't support NTLM or 150 // Kerberos so we are stuck with Basic here 151 $this->setResponseHeader('WWW-Authenticate: Basic realm="' 152 . $this->http_auth_realm . '"'); 153 154 // Windows seems to require this being the last header sent 155 // (changed according to PECL bug #3138) 156 $this->setResponseStatus('401 Authentication Required'); 157 return; 158 } 159 160 // check 161 if (!$this->_check_if_header_conditions()) { 162 $this->setResponseStatus('412 Precondition Failed'); 163 return; 164 } 165 166 // detect requested method names 167 $method = strtolower($_SERVER['REQUEST_METHOD']); 168 $wrapper = $method . '_wrapper'; 169 170 // emulate HEAD using GET if no HEAD method found 171 if ($wrapper == 'head_wrapper' && 172 !method_exists($this, 'head')) { 173 $method = 'get'; 174 } 175 176 if (method_exists($this, $method) && 177 method_exists($this, $wrapper) || 178 $method == 'options') { 179 $this->$wrapper(); 180 return; 181 } 182 183 // method not found/implemented 184 if ($method == 'lock') { 185 $this->setResponseStatus('412 Precondition Failed'); 186 return; 187 } 188 189 // tell client what's allowed 190 $this->setResponseStatus('405 Method Not Allowed'); 191 $this->setResponseHeader('Allow: ' . implode(', ', $this->_allow())); 192 } 193 194 // }}} 195 196 // {{{ abstract WebDAV methods 197 198 // {{{ PROPFIND 199 200 /** 201 * PROPFIND implementation 202 * 203 * @abstract 204 * @param array &$params 205 * @returns int HTTP-Statuscode 206 */ 207 208 /* abstract 209 function propfind() 210 { 211 // dummy entry for PHPDoc 212 } 213 */ 214 215 // }}} 216 217 // {{{ PROPPATCH 218 219 /** 220 * PROPPATCH implementation 221 * 222 * @abstract 223 * @param array &$params 224 * @returns int HTTP-Statuscode 225 */ 226 227 /* abstract 228 function proppatch() 229 { 230 // dummy entry for PHPDoc 231 } 232 */ 233 234 // }}} 235 236 // {{{ MKCOL 237 238 /** 239 * MKCOL implementation 240 * 241 * @abstract 242 * @param array &$params 243 * @returns int HTTP-Statuscode 244 */ 245 246 /* abstract 247 function mkcol() 248 { 249 // dummy entry for PHPDoc 250 } 251 */ 252 253 // }}} 254 255 // {{{ GET 256 257 /** 258 * GET implementation 259 * 260 * Overload this method to retrieve resources from your server 261 * 262 * @abstract 263 * @param array &$params array of input and output parameters 264 * <br><b>input</b><ul> 265 * <li> path - 266 * </ul> 267 * <br><b>output</b><ul> 268 * <li> size - 269 * </ul> 270 * @returns int HTTP-Statuscode 271 */ 272 273 /* abstract 274 function get() 275 { 276 // dummy entry for PHPDoc 277 } 278 */ 279 280 // }}} 281 282 // {{{ DELETE 283 284 /** 285 * DELETE implementation 286 * 287 * @abstract 288 * @param array &$params 289 * @returns int HTTP-Statuscode 290 */ 291 292 /* abstract 293 function delete() 294 { 295 // dummy entry for PHPDoc 296 } 297 */ 298 299 // }}} 300 301 // {{{ PUT 302 303 /** 304 * PUT implementation 305 * 306 * @abstract 307 * @param array &$params 308 * @returns int HTTP-Statuscode 309 */ 310 311 /* abstract 312 function put() 313 { 314 // dummy entry for PHPDoc 315 } 316 */ 317 318 // }}} 319 320 // {{{ COPY 321 322 /** 323 * COPY implementation 324 * 325 * @abstract 326 * @param array &$params 327 * @returns int HTTP-Statuscode 328 */ 329 330 /* abstract 331 function copy() 332 { 333 // dummy entry for PHPDoc 334 } 335 */ 336 337 // }}} 338 339 // {{{ MOVE 340 341 /** 342 * MOVE implementation 343 * 344 * @abstract 345 * @param array &$params 346 * @returns int HTTP-Statuscode 347 */ 348 349 /* abstract 350 function move() 351 { 352 // dummy entry for PHPDoc 353 } 354 */ 355 356 // }}} 357 358 // {{{ LOCK 359 360 /** 361 * LOCK implementation 362 * 363 * @abstract 364 * @param array &$params 365 * @returns int HTTP-Statuscode 366 */ 367 368 /* abstract 369 function lock() 370 { 371 // dummy entry for PHPDoc 372 } 373 */ 374 375 // }}} 376 377 // {{{ UNLOCK 378 379 /** 380 * UNLOCK implementation 381 * 382 * @abstract 383 * @param array &$params 384 * @returns int HTTP-Statuscode 385 */ 386 387 /* abstract 388 function unlock() 389 { 390 // dummy entry for PHPDoc 391 } 392 */ 393 394 // }}} 395 396 // }}} 397 398 // {{{ other abstract methods 399 400 // {{{ checkAuth 401 402 /** 403 * Check authentication 404 * 405 * Overload this method to retrieve and confirm authentication information 406 * 407 * @abstract 408 * @param string type Authentication type, e.g. "basic" or "digest" 409 * @param string username Transmitted username 410 * @param string passwort Transmitted password 411 * @returns bool Authentication status 412 */ 413 414 /* abstract 415 function checkAuth($type, $username, $password) 416 { 417 // dummy entry for PHPDoc 418 } 419 */ 420 421 // }}} 422 423 // {{{ getLocks 424 425 /** 426 * Get lock entries for a resource 427 * 428 * Overload this method to return shared and exclusive locks active for 429 * this resource 430 * 431 * @abstract 432 * @param string resource path to check 433 * @returns array of lock entries each consisting 434 * of 'type' ('shared'/'exclusive'), 'token' and 'timeout' 435 */ 436 437 /* abstract 438 function getLocks($path) 439 { 440 // dummy entry for PHPDoc 441 } 442 */ 443 444 // }}} 445 446 // }}} 447 448 // {{{ WebDAV HTTP method wrappers 449 450 // {{{ options 451 452 /** 453 * OPTIONS method handler 454 * 455 * The OPTIONS method handler creates a valid OPTIONS reply including Dav: 456 * and Allowed: heaers based on the implemented methods found in the actual 457 * instance 458 * 459 * @param void 460 * @return void 461 */ 462 function options() 463 { 464 // get allowed methods 465 $allow = $this->_allow(); 466 467 // dav header 468 $dav = array(1); // assume we are always dav class 1 compliant 469 if (in_array('LOCK', $allow) && in_array('UNLOCK', $allow)) { 470 $dav[] = 2; // dav class 2 requires that locking is supported 471 } 472 473 // tell clients what we found 474 $this->setResponseHeader('Allow: ' . implode(', ', $allow)); 475 $this->setResponseHeader('DAV: ' . implode(',', $dav)); 476 $this->setResponseHeader('Content-Length: 0'); 477 478 // Microsoft clients default to the Frontpage protocol unless we tell 479 // them to use WebDAV 480 $this->setResponseHeader('MS-Author-Via: DAV'); 481 482 $this->setResponseStatus('200 OK'); 483 } 484 485 // }}} 486 487 // {{{ propfind_request_helper 488 489 /** 490 * PROPFIND request helper - prepares data-structures from PROPFIND requests 491 * 492 * @param options 493 * @return void 494 */ 495 function propfind_request_helper(&$options) 496 { 497 $options = array(); 498 $options['path'] = $this->path; 499 500 // get depth from header (default is 'infinity') 501 $options['depth'] = 'infinity'; 502 if ($this->_hasNonEmptyDepthRequestHeader()) { 503 $options['depth'] = $_SERVER['HTTP_DEPTH']; 504 } 505 506 // analyze request payload 507 $parser = new _parse_propfind($this->openRequestBody()); 508 if (!$parser->success) { 509 $this->setResponseStatus('400 Bad Request'); 510 return; 511 } 512 513 $options['props'] = $parser->props; 514 515 return true; 516 } 517 518 // }}} 519 520 // {{{ propfind_response_helper 521 522 /** 523 * PROPFIND response helper - format PROPFIND response 524 * 525 * @param options 526 * @param files 527 * @return void 528 */ 529 function propfind_response_helper($options, $files) 530 { 531 $responses = array(); 532 533 // now loop over all returned files 534 foreach ($files as $file) { 535 $response = array(); 536 537 if (empty($file['href'])) { 538 $response['href'] = $this->getHref($file['path']); 539 } else { 540 $response['href'] = $file['href']; 541 } 542 543 $response['propstat'] = array(); 544 545 // collect namespaces here 546 $response['namespaces'] = array(); 547 if (!empty($options['namespaces'])) { 548 $response['namespaces'] = $options['namespaces']; 549 } 550 551 // Microsoft needs this special namespace for date and time values 552 $response['namespaces'][HTTP_WEBDAV_SERVER_DATATYPE_NAMESPACE] = 553 'ns' . count($response['namespaces']); 554 555 if (is_array($options['props'])) { 556 557 // loop over all requested properties 558 foreach ($options['props'] as $reqprop) { 559 $status = '200 OK'; 560 $prop = $this->getProp($reqprop, $file, $options); 561 562 if (!empty($prop['status'])) { 563 $status = $prop['status']; 564 } 565 566 if (empty($response['propstat'][$status])) { 567 $response['propstat'][$status] = array(); 568 } 569 570 $response['propstat'][$status][] = $prop; 571 572 // namespace handling 573 if (empty($prop['ns']) || // empty namespace 574 $prop['ns'] == 'DAV:' || // default namespace 575 !empty($response['namespaces'][$prop['ns']])) { // already known 576 continue; 577 } 578 579 // register namespace 580 $response['namespaces'][$prop['ns']] = 'ns' . count($response['namespaces']); 581 } 582 } else if (is_array($file['props'])) { 583 584 // loop over all returned properties 585 foreach ($file['props'] as $prop) { 586 $status = '200 OK'; 587 588 if (!empty($prop['status'])) { 589 $status = $prop['status']; 590 } 591 592 if (empty($response['propstat'][$status])) { 593 $response['propstat'][$status] = array(); 594 } 595 596 if ($options['props'] == 'propname') { 597 598 // only names of all existing properties were requested 599 // so remove values 600 unset($prop['value']); 601 } 602 603 $response['propstat'][$status][] = $prop; 604 unset($prop['value']); 605 606 // namespace handling 607 if (empty($prop['ns']) || // empty namespace 608 $prop['ns'] == 'DAV:' || // default namespace 609 !empty($response['namespaces'][$prop['ns']])) { // already known 610 continue; 611 } 612 613 // register namespace 614 $response['namespaces'][$prop['ns']] = 'ns' . count($response['namespaces']); 615 } 616 } 617 618 $responses[] = $response; 619 } 620 621 $this->_multistatusResponseHelper($responses); 622 } 623 624 // }}} 625 626 // {{{ propfind_wrapper 627 628 /** 629 * PROPFIND method wrapper 630 * 631 * @param void 632 * @return void 633 */ 634 function propfind_wrapper() 635 { 636 // prepare data-structure from PROPFIND request 637 if (!$this->propfind_request_helper($options)) { 638 return; 639 } 640 641 // call user handler 642 if (!$this->propfind($options, $files)) { 643 return; 644 } 645 646 // format PROPFIND response 647 $this->propfind_response_helper($options, $files); 648 } 649 650 // }}} 651 652 // {{{ proppatch_request_helper 653 654 /** 655 * PROPPATCH request helper - prepares data-structures from PROPPATCH requests 656 * 657 * @param options 658 * @return void 659 */ 660 function proppatch_request_helper(&$options) 661 { 662 $options = array(); 663 $options['path'] = $this->path; 664 665 $propinfo = new _parse_proppatch($this->openRequestBody()); 666 667 if (!$propinfo->success) { 668 $this->setResponseStatus('400 Bad Request'); 669 return; 670 } 671 672 $options['props'] = $propinfo->props; 673 674 return true; 675 } 676 677 // }}} 678 679 // {{{ proppatch_response_helper 680 681 /** 682 * PROPPATCH response helper - format PROPPATCH response 683 * 684 * @param options 685 * @param responsedescr 686 * @return void 687 */ 688 function proppatch_response_helper($options, $responsedescription=null) 689 { 690 $response = array(); 691 692 if (empty($options['href'])) { 693 $response['href'] = $this->getHref($options['path']); 694 } else { 695 $response['href'] = $options['href']; 696 } 697 698 $response['propstat'] = array(); 699 700 // collect namespaces here 701 $response['namespaces'] = array(); 702 if (!empty($options['namespaces'])) { 703 $response['namespaces'] = $options['namespaces']; 704 } 705 706 if (!empty($options['props']) && is_array($options['props'])) { 707 foreach ($options['props'] as $prop) { 708 $status = '200 OK'; 709 if (!empty($prop['status'])) { 710 $status = $prop['status']; 711 } 712 713 if (empty($response['propstat'][$status])) { 714 $response['propstat'][$status] = array(); 715 } 716 717 $response['propstat'][$status][] = $prop; 718 719 // namespace handling 720 if (empty($prop['ns']) || // empty namespace 721 $prop['ns'] == 'DAV:' || // default namespace 722 !empty($response['namespaces'][$prop['ns']])) { // already known 723 continue; 724 } 725 726 // register namespace 727 $response['namespaces'][$prop['ns']] = 'ns' . count($response['namespaces']); 728 } 729 } 730 731 $response['responsedescription'] = $responsedescription; 732 733 $this->_multistatusResponseHelper(array($response)); 734 } 735 736 // }}} 737 738 // {{{ proppatch_wrapper 739 740 /** 741 * PROPPATCH method wrapper 742 * 743 * @param void 744 * @return void 745 */ 746 function proppatch_wrapper() 747 { 748 // check resource is not locked 749 if (!$this->check_locks_wrapper($this->path)) { 750 $this->setResponseStatus('423 Locked'); 751 return; 752 } 753 754 // perpare data-structure from PROPATCH request 755 if (!$this->proppatch_request_helper($options)) { 756 return; 757 } 758 759 // call user handler 760 $responsedescription = $this->proppatch($options); 761 762 // format PROPPATCH response 763 $this->proppatch_response_helper($options, $responsedescription); 764 } 765 766 // }}} 767 768 // {{{ mkcol_wrapper 769 770 /** 771 * MKCOL method wrapper 772 * 773 * @param void 774 * @return void 775 */ 776 function mkcol_wrapper() 777 { 778 $options = array(); 779 $options['path'] = $this->path; 780 781 $status = $this->mkcol($options); 782 783 $this->setResponseStatus($status); 784 } 785 786 // }}} 787 788 // {{{ get_request_helper 789 790 /** 791 * GET request helper - prepares data-structures from GET requests 792 * 793 * @param options 794 * @return void 795 */ 796 function get_request_helper(&$options) 797 { 798 // TODO check for invalid stream 799 800 $options = array(); 801 $options['path'] = $this->path; 802 803 $this->_get_ranges($options); 804 805 return true; 806 } 807 808 /** 809 * Parse HTTP Range: header 810 * 811 * @param array options array to store result in 812 * @return void 813 */ 814 function _get_ranges(&$options) 815 { 816 // process Range: header if present 817 if (!empty($_SERVER['HTTP_RANGE'])) { 818 819 // we only support standard 'bytes' range specifications for now 820 if (ereg('bytes[[:space:]]*=[[:space:]]*(.+)', $_SERVER['HTTP_RANGE'], $matches)) { 821 $options['ranges'] = array(); 822 823 // ranges are comma separated 824 foreach (explode(',', $matches[1]) as $range) { 825 // ranges are either from-to pairs or just end positions 826 list($start, $end) = explode('-', $range); 827 $options['ranges'][] = ($start === '') ? array('last' => $end) : array('start' => $start, 'end' => $end); 828 } 829 } 830 } 831 } 832 833 // }}} 834 835 // {{{ get_response_helper 836 837 /** 838 * GET response helper - format GET response 839 * 840 * @param options 841 * @param status 842 * @return void 843 */ 844 function get_response_helper($options, $status) 845 { 846 if (empty($status)) { 847 $status = '404 Not Found'; 848 } 849 850 // set headers before we start printing 851 $this->setResponseStatus($status); 852 853 if ($status !== true) { 854 return; 855 } 856 857 if (empty($options['mimetype'])) { 858 $options['mimetype'] = 'application/octet-stream'; 859 } 860 $this->setResponseHeader("Content-Type: $options[mimetype]"); 861 862 if (!empty($options['mtime'])) { 863 $this->setResponseHeader('Last-Modified:' 864 . gmdate('D, d M Y H:i:s', $options['mtime']) . 'GMT'); 865 } 866 867 if ($options['stream']) { 868 // GET handler returned a stream 869 870 if (!empty($options['ranges']) && 871 (fseek($options['stream'], 0, SEEK_SET) === 0)) { 872 // partial request and stream is seekable 873 874 if (count($options['ranges']) === 1) { 875 $range = $options['ranges'][0]; 876 877 if (!empty($range['start'])) { 878 fseek($options['stream'], $range['start'], SEEK_SET); 879 if (feof($options['stream'])) { 880 $this->setResponseStatus( 881 '416 Requested Range Not Satisfiable'); 882 return; 883 } 884 885 if (!empty($range['end'])) { 886 $size = $range['end'] - $range['start'] + 1; 887 $this->setResponseStatus('206 Partial'); 888 $this->setResponseHeader("Content-Length: $size"); 889 $this->setResponseHeader( 890 "Content-Range: $range[start]-$range[end]/" 891 . (!empty($options['size']) ? $options['size'] : '*')); 892 while ($size && !feof($options['stream'])) { 893 $buffer = fread($options['stream'], 4096); 894 $size -= strlen($buffer); 895 echo $buffer; 896 } 897 } else { 898 $this->setResponseStatus('206 Partial'); 899 if (!empty($options['size'])) { 900 $this->setResponseHeader("Content-Length: " 901 . ($options['size'] - $range['start'])); 902 $this->setResponseHeader( 903 "Content-Range: $range[start]-$range[end]/" 904 . (!empty($options['size']) ? $options['size'] : '*')); 905 } 906 fpassthru($options['stream']); 907 } 908 } else { 909 $this->setResponseHeader("Content-Length: $range[last]"); 910 fseek($options['stream'], -$range['last'], SEEK_END); 911 fpassthru($options['stream']); 912 } 913 } else { 914 $this->_multipart_byterange_header(); // init multipart 915 foreach ($options['ranges'] as $range) { 916 917 // TODO what if size unknown? 500? 918 if (!empty($range['start'])) { 919 $from = $range['start']; 920 $to = !empty($range['end']) ? $range['end'] : $options['size'] - 1; 921 } else { 922 $from = $options['size'] - $range['last'] - 1; 923 $to = $options['size'] - 1; 924 } 925 $total = !empty($options['size']) ? $options['size'] : '*'; 926 $size = $to - $from + 1; 927 $this->_multipart_byterange_header($options['mimetype'], 928 $from, $to, $total); 929 930 fseek($options['stream'], $start, SEEK_SET); 931 while ($size && !feof($options['stream'])) { 932 $buffer = fread($options['stream'], 4096); 933 $size -= strlen($buffer); 934 echo $buffer; 935 } 936 } 937 938 // end multipart 939 $this->_multipart_byterange_header(); 940 } 941 } else { 942 // normal request or stream isn't seekable, return full content 943 if (!empty($options['size'])) { 944 $this->setResponseHeader("Content-Length: $options[size]"); 945 } 946 947 fpassthru($options['stream']); 948 } 949 } else if (!empty($options['data'])) { 950 if (is_array($options['data'])) { 951 // reply to partial request 952 } else { 953 $this->setResponseHeader("Content-Length: " 954 . strlen($options['data'])); 955 echo $options['data']; 956 } 957 } 958 } 959 960 /** 961 * Generate separator headers for multipart response 962 * 963 * First and last call happen without parameters to generate the initial 964 * header and closing sequence, all calls inbetween require content 965 * mimetype, start and end byte position and optionaly the total byte 966 * length of the requested resource 967 * 968 * @param string mimetype 969 * @param int start byte position 970 * @param int end byte position 971 * @param int total resource byte size 972 */ 973 function _multipart_byterange_header($mimetype = false, $from = false, 974 $to = false, $total = false) 975 { 976 if ($mimetype === false) { 977 if (empty($this->multipart_separator)) { 978 // init 979 // a little naive, this sequence *might* be part of the content 980 // but it's really not likely and rather expensive to check 981 $this->multipart_separator = 'SEPARATOR_' . md5(microtime()); 982 983 // generate HTTP header 984 $this->setResponseHeader( 985 'Content-Type: multipart/byteranges; boundary=' 986 . $this->multipart_separator); 987 return; 988 } 989 990 // end 991 // generate closing multipart sequence 992 echo "\n--{$this->multipart_separator}--"; 993 return; 994 } 995 996 // generate separator and header for next part 997 echo "\n--{$this->multipart_separator}\n"; 998 echo "Content-Type: $mimetype\n"; 999 echo "Content-Range: $from-$to/" 1000 . ($total === false ? "*" : $total) . "\n\n"; 1001 } 1002 1003 // }}} 1004 1005 // {{{ get_wrapper 1006 1007 /** 1008 * GET method wrapper 1009 * 1010 * @param void 1011 * @return void 1012 */ 1013 function get_wrapper() 1014 { 1015 // perpare data-structure from GET request 1016 if (!$this->get_request_helper($options)) { 1017 return; 1018 } 1019 1020 // call user handler 1021 $status = $this->get($options); 1022 1023 // format GET response 1024 $this->get_response_helper($options, $status); 1025 } 1026 1027 // }}} 1028 1029 // {{{ head_response_helper 1030 1031 /** 1032 * HEAD response helper - format HEAD response 1033 * 1034 * @param options 1035 * @param status 1036 * @return void 1037 */ 1038 function head_response_helper($options, $status) 1039 { 1040 if (empty($status)) { 1041 $status = '404 Not Found'; 1042 } 1043 1044 // set headers before we start printing 1045 $this->setResponseStatus($status); 1046 1047 if ($status !== true) { 1048 return; 1049 } 1050 1051 if (empty($options['mimetype'])) { 1052 $options['mimetype'] = 'application/octet-stream'; 1053 } 1054 $this->setResponseHeader("Content-Type: $options[mimetype]"); 1055 1056 if (!empty($options['mtime'])) { 1057 $this->setResponseHeader('Last-Modified:' 1058 . gmdate('D, d M Y H:i:s', $options['mtime']) . 'GMT'); 1059 } 1060 1061 if (!empty($options['stream'])) { 1062 // GET handler returned a stream 1063 1064 if (!empty($options['ranges']) 1065 && (fseek($options['stream'], 0, SEEK_SET) === 0)) { 1066 // partial request and stream is seekable 1067 1068 if (count($options['ranges']) === 1) { 1069 $range = $options['ranges'][0]; 1070 1071 if (!empty($range['start'])) { 1072 fseek($options['stream'], $range['start'], SEEK_SET); 1073 if (feof($options['stream'])) { 1074 $this->setResponseStatus( 1075 '416 Requested Range Not Satisfiable'); 1076 return; 1077 } 1078 1079 if (!empty($range['end'])) { 1080 $size = $range['end'] - $range['start'] + 1; 1081 $this->setResponseStatus('206 Partial'); 1082 $this->setResponseHeader("Content-Length: $size"); 1083 $this->setResponseHeader( 1084 "Content-Range: $range[start]-$range[end]/" 1085 . (!empty($options['size']) ? $options['size'] : '*')); 1086 } else { 1087 $this->setResponseStatus('206 Partial'); 1088 if (!empty($options['size'])) { 1089 $this->setResponseHeader("Content-Length: " 1090 . ($options['size'] - $range['start'])); 1091 $this->setResponseHeader( 1092 "Content-Range: $start-$end/" 1093 . (!empty($options['size']) ? $options['size'] : '*')); 1094 } 1095 } 1096 } else { 1097 $this->setResponseHeader( 1098 "Content-Length: $range[last]"); 1099 fseek($options['stream'], -$range['last'], SEEK_END); 1100 } 1101 } else { 1102 $this->_multipart_byterange_header(); // init multipart 1103 foreach ($options['ranges'] as $range) { 1104 1105 // TODO what if size unknown? 500? 1106 if (!empty($range['start'])) { 1107 $from = $range['start']; 1108 $to = !empty($range['end']) ? $range['end'] : 1109 $options['size'] - 1; 1110 } else { 1111 $from = $options['size'] - $range['last'] - 1; 1112 $to = $options['size'] - 1; 1113 } 1114 $total = !empty($options['size']) ? $options['size'] : 1115 '*'; 1116 $size = $to - $from + 1; 1117 $this->_multipart_byterange_header($options['mimetype'], 1118 $from, $to, $total); 1119 1120 fseek($options['stream'], $start, SEEK_SET); 1121 } 1122 $this->_multipart_byterange_header(); // end multipart 1123 } 1124 } else { 1125 // normal request or stream isn't seekable, return full content 1126 if (!empty($options['size'])) { 1127 $this->setResponseHeader("Content-Length: $options[size]"); 1128 } 1129 } 1130 } else if (!empty($options['data'])) { 1131 if (is_array($options['data'])) { 1132 // reply to partial request 1133 } else { 1134 $this->setResponseHeader("Content-Length: " 1135 . strlen($options['data'])); 1136 } 1137 } 1138 } 1139 1140 // }}} 1141 1142 // {{{ head_wrapper 1143 1144 /** 1145 * HEAD method wrapper 1146 * 1147 * @param void 1148 * @return void 1149 */ 1150 function head_wrapper() 1151 { 1152 $options = array(); 1153 $options['path'] = $this->path; 1154 1155 // call user handler 1156 if (method_exists($this, 'head')) { 1157 $status = $this->head($options); 1158 } else { 1159 1160 // can emulate HEAD using GET 1161 ob_start(); 1162 $status = $this->get($options); 1163 ob_end_clean(); 1164 } 1165 1166 // format HEAD response 1167 $this->head_response_helper($options, $status); 1168 } 1169 1170 // }}} 1171 1172 // {{{ put_request_helper 1173 1174 /** 1175 * PUT request helper - prepares data-structures from PUT requests 1176 * 1177 * @param options 1178 * @return void 1179 */ 1180 function put_request_helper(&$options) 1181 { 1182 $options = array(); 1183 $options['path'] = $this->path; 1184 1185 /* Content-Length may be zero */ 1186 if (!isset($_SERVER['CONTENT_LENGTH'])) { 1187 return; 1188 } 1189 $options['content_length'] = $_SERVER['CONTENT_LENGTH']; 1190 1191 // default content type if none given 1192 $options['content_type'] = 'application/unknown'; 1193 1194 // get the content-type 1195 if (!empty($_SERVER['CONTENT_TYPE'])) { 1196 1197 // for now we do not support any sort of multipart requests 1198 if (!strncmp($_SERVER['CONTENT_TYPE'], 'multipart/', 10)) { 1199 $this->setResponseStatus('501 Not Implemented'); 1200 echo 'The service does not support mulipart PUT requests'; 1201 return; 1202 } 1203 1204 $options['content_type'] = $_SERVER['CONTENT_TYPE']; 1205 } 1206 1207 // RFC2616 2.6: The recipient of the entity MUST NOT ignore any 1208 // Content-* (e.g. Content-Range) headers that it does not understand 1209 // or implement and MUST return a 501 (Not Implemented) response in 1210 // such cases. 1211 foreach ($_SERVER as $key => $value) { 1212 if (strncmp($key, 'HTTP_CONTENT', 11)) { 1213 continue; 1214 } 1215 1216 switch ($key) { 1217 case 'HTTP_CONTENT_ENCODING': // RFC2616 14.11 1218 1219 // TODO support this if ext/zlib filters are available 1220 $this->setResponseStatus('501 Not Implemented'); 1221 echo "The service does not support '$value' content encoding"; 1222 return; 1223 1224 case 'HTTP_CONTENT_LANGUAGE': // RFC2616 14.12 1225 1226 // we assume it is not critical if this one is ignored in the 1227 // actual PUT implementation 1228 $options['content_language'] = $value; 1229 break; 1230 1231 case 'HTTP_CONTENT_LENGTH': 1232 1233 // defined on IIS and has the same value as CONTENT_LENGTH 1234 break; 1235 1236 case 'HTTP_CONTENT_LOCATION': // RFC2616 14.14 1237 1238 // meaning of the Content-Location header in PUT or POST 1239 // requests is undefined; servers are free to ignore it in 1240 // those cases 1241 break; 1242 1243 case 'HTTP_CONTENT_RANGE': // RFC2616 14.16 1244 1245 // single byte range requests are supported 1246 // the header format is also specified in RFC2616 14.16 1247 // TODO we have to ensure that implementations support this or send 501 instead 1248 if (!preg_match('@bytes\s+(\d+)-(\d+)/((\d+)|\*)@', $value, $matches)) { 1249 $this->setResponseStatus('400 Bad Request'); 1250 echo 'The service does only support single byte ranges'; 1251 return; 1252 } 1253 1254 $range = array('start' => $matches[1], 'end' => $matches[2]); 1255 if (is_numeric($matches[3])) { 1256 $range['total_length'] = $matches[3]; 1257 } 1258 $option['ranges'][] = $range; 1259 1260 // TODO make sure the implementation supports partial PUT 1261 // this has to be done in advance to avoid data being overwritten 1262 // on implementations that do not support this... 1263 break; 1264 1265 case 'HTTP_CONTENT_MD5': // RFC2616 14.15 1266 1267 // TODO maybe we can just pretend here? 1268 $this->setResponseStatus('501 Not Implemented'); 1269 echo 'The service does not support content MD5 checksum verification'; 1270 return; 1271 1272 case 'HTTP_CONTENT_TYPE': 1273 1274 // defined on IIS and has the same value as CONTENT_TYPE 1275 break; 1276 1277 default: 1278 1279 // any other unknown Content-* headers 1280 $this->setResponseStatus('501 Not Implemented'); 1281 echo "The service does not support '$key'"; 1282 return; 1283 } 1284 } 1285 1286 $options['stream'] = $this->openRequestBody(); 1287 1288 return true; 1289 } 1290 1291 // }}} 1292 1293 // {{{ put_response_helper 1294 1295 /** 1296 * PUT response helper - format PUT response 1297 * 1298 * @param options 1299 * @param status 1300 * @return void 1301 */ 1302 function put_response_helper($options, $status) 1303 { 1304 if (empty($status)) { 1305 $status = '403 Forbidden'; 1306 } else if (is_resource($status) 1307 && get_resource_type($status) == 'stream') { 1308 $stream = $status; 1309 $status = isset($options['new']) && $options['new'] === false ? '204 No Content' : '201 Created'; 1310 1311 if (!empty($options['ranges'])) { 1312 1313 // TODO multipart support is missing (see also above) 1314 if (0 == fseek($stream, $range[0]['start'], SEEK_SET)) { 1315 $length = $range[0]['end'] - $range[0]['start'] + 1; 1316 if (!fwrite($stream, fread($options['stream'], $length))) { 1317 $status = '403 Forbidden'; 1318 } 1319 } else { 1320 $status = '403 Forbidden'; 1321 } 1322 } else { 1323 while (!feof($options['stream'])) { 1324 $buf = fread($options['stream'], 4096); 1325 if (fwrite($stream, $buf) != 4096) { 1326 break; 1327 } 1328 } 1329 } 1330 1331 fclose($stream); 1332 } 1333 1334 $this->setResponseStatus($status); 1335 } 1336 1337 // }}} 1338 1339 // {{{ put_wrapper 1340 1341 /** 1342 * PUT method wrapper 1343 * 1344 * @param void 1345 * @return void 1346 */ 1347 function put_wrapper() 1348 { 1349 // check resource is not locked 1350 if (!$this->check_locks_wrapper($this->path)) { 1351 $this->setResponseStatus('423 Locked'); 1352 return; 1353 } 1354 1355 // perpare data-structure from PUT request 1356 if (!$this->put_request_helper($options)) { 1357 return; 1358 } 1359 1360 // call user handler 1361 $status = $this->put($options); 1362 1363 // format PUT response 1364 $this->put_response_helper($options, $status); 1365 } 1366 1367 // }}} 1368 1369 // {{{ delete_wrapper 1370 1371 /** 1372 * DELETE method wrapper 1373 * 1374 * @param void 1375 * @return void 1376 */ 1377 function delete_wrapper() 1378 { 1379 // RFC2518 9.2 last paragraph 1380 if (isset($_SERVER['HTTP_DEPTH']) 1381 && $_SERVER['HTTP_DEPTH'] !== 'infinity') { 1382 $this->setResponseStatus('400 Bad Request'); 1383 return; 1384 } 1385 1386 // check resource is not locked 1387 if (!$this->check_locks_wrapper($this->path)) { 1388 $this->setResponseStatus('423 Locked'); 1389 return; 1390 } 1391 1392 $options = array(); 1393 $options['path'] = $this->path; 1394 1395 // call user handler 1396 $status = $this->delete($options); 1397 if ($status === true) { 1398 $status = '204 No Content'; 1399 } 1400 1401 $this->setResponseStatus($status); 1402 } 1403 1404 // }}} 1405 1406 // {{{ copymove_request_helper 1407 1408 /** 1409 * COPY/MOVE request helper - prepares data-structures from COPY/MOVE 1410 * requests 1411 * 1412 * @param options 1413 * @return void 1414 */ 1415 function copymove_request_helper(&$options) 1416 { 1417 $options = array(); 1418 $options['path'] = $this->path; 1419 1420 $options['depth'] = 'infinity'; 1421 if ($this->_hasNonEmptyDepthRequestHeader()) { 1422 $options['depth'] = $_SERVER['HTTP_DEPTH']; 1423 } 1424 1425 // RFC2518 9.6, 8.8.4 and 8.9.3 1426 $options['overwrite'] = true; 1427 if (!empty($_SERVER['HTTP_OVERWRITE'])) { 1428 $options['overwrite'] = $_SERVER['HTTP_OVERWRITE'] == 'T'; 1429 } 1430 1431 $url = parse_url($_SERVER['HTTP_DESTINATION']); 1432 1433 // does the destination resource belong on this server? 1434 if ($url['host'] == $this->baseUrl['host'] 1435 && (empty($url['port']) ? 80 : $url['port']) == (empty($this->baseUrl['port']) ? 80 : $this->baseUrl['port']) 1436 && !strncmp($url['path'], $this->baseUrl['path'], strlen($this->baseUrl['path']))) { 1437 if (!empty($this->baseurl['query'])) { 1438 foreach (explode('&', $this->baseUrl['query']) as $queryComponent) { 1439 if (!in_array($queryComponent, explode('&', $url['query']))) { 1440 $options['dest_url'] = $_SERVER['HTTP_DESTINATION']; 1441 1442 return true; 1443 } 1444 } 1445 } 1446 1447 $options['dest'] = 1448 substr($url['path'], strlen($this->baseUrl['path'])); 1449 1450 $options['dest'] = $this->_urldecode($options['dest']); 1451 $options['dest'] = trim($options['dest'], '/'); 1452 1453 // check source and destination are not the same - data could be lost 1454 // if overwrite is true - RFC2518 8.8.5 1455 if ($options['dest'] == $this->path) { 1456 $this->setResponseStatus('403 Forbidden'); 1457 return; 1458 } 1459 1460 return true; 1461 } 1462 1463 $options['dest_url'] = $_SERVER['HTTP_DESTINATION']; 1464 1465 return true; 1466 } 1467 1468 // }}} 1469 1470 // {{{ copy_wrapper 1471 1472 /** 1473 * COPY method wrapper 1474 * 1475 * @param void 1476 * @return void 1477 */ 1478 function copy_wrapper() 1479 { 1480 // no need to check source is not locked 1481 1482 // perpare data-structure from COPY request 1483 if (!$this->copymove_request_helper($options)) { 1484 return; 1485 } 1486 1487 // check destination is not locked 1488 if (!empty($options['dest']) && 1489 !$this->check_locks_wrapper($options['dest'])) { 1490 $this->setResponseStatus('423 Locked'); 1491 return; 1492 } 1493 1494 // call user handler 1495 $status = $this->copy($options); 1496 if ($status === true) { 1497 $status = $options['new'] === false ? '204 No Content' : 1498 '201 Created'; 1499 } 1500 1501 $this->setResponseStatus($status); 1502 } 1503 1504 // }}} 1505 1506 // {{{ move_wrapper 1507 1508 /** 1509 * MOVE method wrapper 1510 * 1511 * @param void 1512 * @return void 1513 */ 1514 function move_wrapper() 1515 { 1516 // check resource is not locked 1517 if (!$this->check_locks_wrapper($this->path)) { 1518 $this->setResponseStatus('423 Locked'); 1519 return; 1520 } 1521 1522 // perpare data-structure from MOVE request 1523 if (!$this->copymove_request_helper($options)) { 1524 return; 1525 } 1526 1527 // check destination is not locked 1528 if (!empty($options['dest']) && 1529 !$this->check_locks_wrapper($options['dest'])) { 1530 $this->setResponseStatus('423 Locked'); 1531 return; 1532 } 1533 1534 // call user handler 1535 $status = $this->move($options); 1536 if ($status === true) { 1537 $status = $options['new'] === false ? '204 No Content' : 1538 '201 Created'; 1539 } 1540 1541 $this->setResponseStatus($status); 1542 } 1543 1544 // }}} 1545 1546 // {{{ lock_request_helper 1547 1548 /** 1549 * LOCK request helper - prepares data-structures from LOCK requests 1550 * 1551 * @param options 1552 * @return void 1553 */ 1554 function lock_request_helper(&$options) 1555 { 1556 $options = array(); 1557 $options['path'] = $this->path; 1558 1559 // a LOCK request with an If header but without a body is used to 1560 // refresh a lock. Content-Lenght may be unset or zero. 1561 if (empty($_SERVER['CONTENT_LENGTH']) && !empty($_SERVER['HTTP_IF'])) { 1562 1563 // FIXME: Refresh multiple locks? 1564 $options['update'] = substr($_SERVER['HTTP_IF'], 2, -2); 1565 1566 return true; 1567 } 1568 1569 $options['depth'] = 'infinity'; 1570 if ($this->_hasNonEmptyDepthRequestHeader()) { 1571 $options['depth'] = $_SERVER['HTTP_DEPTH']; 1572 } 1573 1574 if (!empty($_SERVER['HTTP_TIMEOUT'])) { 1575 $options['timeout'] = explode(',', $_SERVER['HTTP_TIMEOUT']); 1576 } 1577 1578 // extract lock request information from request XML payload 1579 $lockinfo = new _parse_lockinfo($this->openRequestBody()); 1580 if (!$lockinfo->success) { 1581 $this->setResponseStatus('400 Bad Request'); 1582 return; 1583 } 1584 1585 // new lock 1586 $options['scope'] = $lockinfo->lockscope; 1587 $options['type'] = $lockinfo->locktype; 1588 $options['owner'] = $lockinfo->owner; 1589 1590 $options['token'] = $this->_new_locktoken(); 1591 1592 return true; 1593 } 1594 1595 // }}} 1596 1597 // {{{ lock_response_helper 1598 1599 /** 1600 * LOCK response helper - format LOCK response 1601 * 1602 * @param options 1603 * @param status 1604 * @return void 1605 */ 1606 function lock_response_helper($options, $status) 1607 { 1608 if (!empty($options['locks']) && is_array($options['locks'])) { 1609 $this->setResponseStatus('409 Conflict'); 1610 1611 $responses = array(); 1612 foreach ($options['locks'] as $lock) { 1613 $response = array(); 1614 1615 if (empty($lock['href'])) { 1616 $response['href'] = $this->getHref($lock['path']); 1617 } else { 1618 $response['href'] = $lock['href']; 1619 } 1620 1621 $response['status'] = '423 Locked'; 1622 1623 $responses[] = $response; 1624 } 1625 1626 $this->_multistatusResponseHelper($responses); 1627 1628 return; 1629 } 1630 1631 if ($status === true) { 1632 $status = '200 OK'; 1633 } else if ($status === false) { 1634 $status = '423 Locked'; 1635 } 1636 1637 // set headers before we start printing 1638 $this->setResponseStatus($status); 1639 1640 if ($status{0} == 2) { // 2xx states are ok 1641 $this->setResponseHeader('Content-Type: text/xml; charset="utf-8"'); 1642 1643 // RFC2518 8.10.1: In order to indicate the lock token associated 1644 // with a newly created lock, a Lock-Token response header MUST be 1645 // included in the response for every successful LOCK request for a 1646 // new lock. Note that the Lock-Token header would not be returned 1647 // in the response for a successful refresh LOCK request because a 1648 // new lock was not created. 1649 if (empty($options['update']) || !empty($options['token'])) { 1650 $this->setResponseHeader("Lock-Token: <$options[token]>"); 1651 } 1652 1653 $lock = array(); 1654 foreach (array('scope', 'type', 'depth', 'owner') as $key) { 1655 $lock[$key] = $options[$key]; 1656 } 1657 1658 if (!empty($options['expires'])) { 1659 $lock['expires'] = $options['expires']; 1660 } else { 1661 $lock['timeout'] = $options['timeout']; 1662 } 1663 1664 if (!empty($options['update'])) { 1665 $lock['token'] = $options['update']; 1666 } else { 1667 $lock['token'] = $options['token']; 1668 } 1669 1670 echo "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"; 1671 echo "<D:prop xmlns:D=\"DAV:\">\n"; 1672 echo " <D:lockdiscovery>\n"; 1673 echo ' ' . $this->_activelocksResponseHelper(array($lock)) 1674 . "\n"; 1675 echo " </D:lockdiscovery>\n"; 1676 echo "</D:prop>\n"; 1677 } 1678 } 1679 1680 // }}} 1681 1682 // {{{ lock_wrapper 1683 1684 /** 1685 * LOCK method wrapper 1686 * 1687 * @param void 1688 * @return void 1689 */ 1690 function lock_wrapper() 1691 { 1692 // perpare data-structure from LOCK request 1693 if (!$this->lock_request_helper($options)) { 1694 return; 1695 } 1696 1697 // check resource is not locked 1698 if (!empty($options['update']) 1699 && !$this->check_locks_wrapper( 1700 $this->path, $options['scope'] == 'shared')) { 1701 $this->setResponseStatus('423 Locked'); 1702 return; 1703 } 1704 1705 $options['locks'] = $this->getDescendentsLocks($this->path); 1706 if (empty($options['locks'])) { 1707 1708 // call user handler 1709 $status = $this->lock($options); 1710 } 1711 1712 // format LOCK response 1713 $this->lock_response_helper($options, $status); 1714 } 1715 1716 // }}} 1717 1718 // {{{ unlock_request_helper 1719 1720 /** 1721 * UNLOCK request helper - prepares data-structures from UNLOCK requests 1722 * 1723 * @param options 1724 * @return void 1725 */ 1726 function unlock_request_helper(&$options) 1727 { 1728 $options = array(); 1729 $options['path'] = $this->path; 1730 1731 if (empty($_SERVER['HTTP_LOCK_TOKEN'])) { 1732 return; 1733 } 1734 1735 // strip surrounding <> 1736 $options['token'] = substr(trim($_SERVER['HTTP_LOCK_TOKEN']), 1, -1); 1737 1738 return true; 1739 } 1740 1741 // }}} 1742 1743 // {{{ unlock_wrapper 1744 1745 /** 1746 * UNLOCK method wrapper 1747 * 1748 * @param void 1749 * @return void 1750 */ 1751 function unlock_wrapper() 1752 { 1753 // perpare data-structure from DELETE request 1754 if (!$this->unlock_request_helper($options)) { 1755 return; 1756 } 1757 1758 // call user handler 1759 $status = $this->unlock($options); 1760 1761 // RFC2518 8.11.1 1762 if ($status === true) { 1763 $status = '204 No Content'; 1764 } 1765 1766 $this->setResponseStatus($status); 1767 } 1768 1769 // }}} 1770 1771 function _multistatusResponseHelper($responses) 1772 { 1773 // now we generate the response header... 1774 $this->setResponseStatus('207 Multi-Status', false); 1775 $this->setResponseHeader('Content-Type: text/xml; charset="utf-8"'); 1776 1777 // ...& payload 1778 echo "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"; 1779 echo "<D:multistatus xmlns:D=\"DAV:\">\n"; 1780 1781 foreach ($responses as $response) { 1782 1783 // ignore empty or incomplete entries 1784 if (!is_array($response) || empty($response)) { 1785 continue; 1786 } 1787 1788 $namespaces = ''; 1789 if (!empty($response['namespaces'])) { 1790 foreach ($response['namespaces'] as $name => $prefix) { 1791 $namespaces .= " xmlns:$prefix=\"$name\""; 1792 } 1793 } 1794 echo " <D:response$namespaces>\n"; 1795 echo " <D:href>$response[href]</D:href>\n"; 1796 1797 // report all found properties and their values (if any) 1798 // nothing to do if no properties were returend for a file 1799 if (!empty($response['propstat']) && 1800 is_array($response['propstat'])) { 1801 1802 foreach ($response['propstat'] as $status => $props) { 1803 echo " <D:propstat>\n"; 1804 echo " <D:prop>\n"; 1805 1806 foreach ($props as $prop) { 1807 if (!is_array($prop) || empty($prop['name'])) { 1808 continue; 1809 } 1810 1811 // empty properties (cannot use empty for check as '0' 1812 // is a legal value here) 1813 if (empty($prop['value']) && (!isset($prop['value']) 1814 || $prop['value'] !== 0)) { 1815 if ($prop['ns'] == 'DAV:') { 1816 echo " <D:$prop[name]/>\n"; 1817 continue; 1818 } 1819 1820 if (!empty($prop['ns'])) { 1821 echo ' <' . $response['namespaces'][$prop['ns']] . ":$prop[name]/>\n"; 1822 continue; 1823 } 1824 1825 echo " <$prop[name] xmlns=\"\"/>"; 1826 continue; 1827 } 1828 1829 // some WebDAV properties need special treatment 1830 if ($prop['ns'] == 'DAV:') { 1831 1832 switch ($prop['name']) { 1833 case 'creationdate': 1834 echo ' <D:creationdate ' . $response['namespaces'][HTTP_WEBDAV_SERVER_DATATYPE_NAMESPACE] . ':dt="dateTime.tz">' . gmdate('Y-m-d\TH:i:s\Z', $prop['value']) . "</D:creationdate>\n"; 1835 break; 1836 1837 case 'getlastmodified': 1838 echo ' <D:getlastmodified ' . $response['namespaces'][HTTP_WEBDAV_SERVER_DATATYPE_NAMESPACE] . ':dt="dateTime.rfc1123">' . gmdate('D, d M Y H:i:s', $prop['value']) . " UTC</D:getlastmodified>\n"; 1839 break; 1840 1841 case 'resourcetype': 1842 echo " <D:resourcetype><D:$prop[value]/></D:resourcetype>\n"; 1843 break; 1844 1845 case 'supportedlock': 1846 1847 if (is_array($prop['value'])) { 1848 $prop['value'] = 1849 $this->_lockentriesResponseHelper( 1850 $prop['value']); 1851 } 1852 echo " <D:supportedlock>\n"; 1853 echo " $prop[value]\n"; 1854 echo " </D:supportedlock>\n"; 1855 break; 1856 1857 case 'lockdiscovery': 1858 1859 if (is_array($prop['value'])) { 1860 $prop['value'] = 1861 $this->_activelocksResponseHelper( 1862 $prop['value']); 1863 } 1864 echo " <D:lockdiscovery>\n"; 1865 echo " $prop[value]\n"; 1866 echo " </D:lockdiscovery>\n"; 1867 break; 1868 1869 default: 1870 echo " <D:$prop[name]>" . $this->_prop_encode(htmlspecialchars($prop['value'])) . "</D:$prop[name]>\n"; 1871 } 1872 1873 continue; 1874 } 1875 1876 if (!empty($prop['ns'])) { 1877 echo ' <' . $response['namespaces'][$prop['ns']] . ":$prop[name]>" . $this->_prop_encode(htmlspecialchars($prop['value'])) . '</' . $response['namespaces'][$prop['ns']] . ":$prop[name]>\n"; 1878 1879 continue; 1880 } 1881 1882 echo " <$prop[name] xmlns=\"\">" . $this->_prop_encode(htmlspecialchars($prop['value'])) . "</$prop[name]>\n"; 1883 } 1884 1885 echo " </D:prop>\n"; 1886 echo " <D:status>HTTP/1.1 $status</D:status>\n"; 1887 echo " </D:propstat>\n"; 1888 } 1889 } 1890 1891 if (!empty($response['status'])) { 1892 echo " <D:status>HTTP/1.1 $response[status]</D:status>\n"; 1893 } 1894 1895 if (!empty($response['responsedescription'])) { 1896 echo ' <D:responsedescription>' . $this->_prop_encode(htmlspecialchars($response['responsedescription'])) . "</D:responsedescription>\n"; 1897 } 1898 1899 echo " </D:response>\n"; 1900 } 1901 1902 echo "</D:multistatus>\n"; 1903 } 1904 1905 function _activelocksResponseHelper($locks) 1906 { 1907 if (!is_array($locks) || empty($locks)) { 1908 return ''; 1909 } 1910 1911 foreach ($locks as $key => $lock) { 1912 if (!is_array($lock) || empty($lock)) { 1913 continue; 1914 } 1915 1916 // check for 'timeout' or 'expires' 1917 $timeout = 'Infinite'; 1918 if (!empty($lock['expires'])) { 1919 $timeout = 'Second-' . ($lock['expires'] - time()); 1920 } else if (!empty($lock['timeout'])) { 1921 1922 // more than a million is considered an absolute timestamp 1923 // less is more likely a relative value 1924 $timeout = "Second-$lock[timeout]"; 1925 if ($lock['timeout'] > 1000000) { 1926 $timeout = 'Second-' . ($lock['timeout'] - time()); 1927 } 1928 } 1929 1930 // genreate response block 1931 $locks[$key] = "<D:activelock> 1932 <D:lockscope><D:$lock[scope]/></D:lockscope> 1933 <D:locktype><D:$lock[type]/></D:locktype> 1934 <D:depth>" . ($lock['depth'] == 'infinity' ? 'Infinity' : $lock['depth']) . "</D:depth> 1935 <D:owner>$lock[owner]</D:owner> 1936 <D:timeout>$timeout</D:timeout> 1937 <D:locktoken><D:href>$lock[token]</D:href></D:locktoken> 1938 </D:activelock>"; 1939 } 1940 1941 return implode('', $locks); 1942 } 1943 1944 function _lockentriesResponseHelper($locks) 1945 { 1946 if (!is_array($locks) || empty($locks)) { 1947 return ''; 1948 } 1949 1950 foreach ($locks as $key => $lock) { 1951 if (!is_array($lock) || empty($lock)) { 1952 continue; 1953 } 1954 1955 $locks[$key] = "<D:lockentry> 1956 <D:lockscope><D:$lock[scope]/></D:lockscope> 1957 <D:locktype><D:$lock[type]/></D:locktype> 1958 </D:lockentry>"; 1959 } 1960 1961 return implode('', $locks); 1962 } 1963 1964 function getHref($path) 1965 { 1966 return $this->baseUrl['path'] . '/' . $path; 1967 } 1968 1969 function getProp($reqprop, $file, $options) 1970 { 1971 // check if property exists in response 1972 foreach ($file['props'] as $prop) { 1973 if ($reqprop['name'] == $prop['name'] 1974 && $reqprop['ns'] == $prop['ns']) { 1975 return $prop; 1976 } 1977 } 1978 1979 if ($reqprop['name'] == 'lockdiscovery' 1980 && $reqprop['ns'] == 'DAV:' 1981 && method_exists($this, 'getLocks')) { 1982 return $this->mkprop('DAV:', 'lockdiscovery', 1983 $this->getLocks($file['path'])); 1984 } 1985 1986 // incase the requested property had a value, like calendar-data 1987 unset($reqprop['value']); 1988 $reqprop['status'] = '404 Not Found'; 1989 1990 return $reqprop; 1991 } 1992 1993 function getDescendentsLocks($path) 1994 { 1995 $options = array(); 1996 $options['path'] = $path; 1997 $options['depth'] = 'infinity'; 1998 $options['props'] = array(); 1999 $options['props'][] = $this->mkprop('DAV:', 'lockdiscovery', null); 2000 2001 // call user handler 2002 if (!$this->propfind($options, $files)) { 2003 return; 2004 } 2005 2006 return $files; 2007 } 2008 2009 // {{{ _allow() 2010 2011 /** 2012 * List implemented methods 2013 * 2014 * @param void 2015 * @return array something 2016 */ 2017 function _allow() 2018 { 2019 // OPTIONS is always there 2020 $allow = array('OPTIONS'); 2021 2022 // all other methods need both a method_wrapper() and a method() 2023 // implementation 2024 // the base class defines only wrappers 2025 foreach(get_class_methods($this) as $method) { 2026 2027 // strncmp breaks with negative len - 2028 // http://bugs.php.net/bug.php?id=36944 2029 //if (!strncmp('_wrapper', $method, -8)) { 2030 if (!strcmp(substr($method, -8), '_wrapper')) { 2031 $method = strtolower(substr($method, 0, -8)); 2032 if (method_exists($this, $method) && 2033 ($method != 'lock' && $method != 'unlock' || 2034 method_exists($this, 'getLocks'))) { 2035 $allow[] = $method; 2036 } 2037 } 2038 } 2039 2040 // we can emulate a missing HEAD implemetation using GET 2041 if (in_array('GET', $allow)) { 2042 $allow[] = 'HEAD'; 2043 } 2044 2045 return $allow; 2046 } 2047 2048 // }}} 2049 2050 // {{{ mkprop 2051 2052 /** 2053 * Helper for property element creation 2054 * 2055 * @param string XML namespace (optional) 2056 * @param string property name 2057 * @param string property value 2058 * @return array property array 2059 */ 2060 function mkprop() 2061 { 2062 $args = func_get_args(); 2063 2064 $prop = array(); 2065 $prop['ns'] = 'DAV:'; 2066 if (count($args) > 2) { 2067 $prop['ns'] = array_shift($args); 2068 } 2069 2070 $prop['name'] = array_shift($args); 2071 $prop['value'] = array_shift($args); 2072 $prop['status'] = array_shift($args); 2073 2074 return $prop; 2075 } 2076 2077 // }}} 2078 2079 // {{{ check_auth_wrapper 2080 2081 /** 2082 * Check authentication if implemented 2083 * 2084 * @param void 2085 * @return boolean true if authentication succeded or not necessary 2086 */ 2087 function check_auth_wrapper() 2088 { 2089 if (method_exists($this, 'checkAuth')) { 2090 2091 // PEAR style method name 2092 return $this->checkAuth(@$_SERVER['AUTH_TYPE'], 2093 @$_SERVER['PHP_AUTH_USER'], 2094 @$_SERVER['PHP_AUTH_PW']); 2095 } 2096 2097 if (method_exists($this, 'check_auth')) { 2098 2099 // old (pre 1.0) method name 2100 return $this->check_auth(@$_SERVER['AUTH_TYPE'], 2101 @$_SERVER['PHP_AUTH_USER'], 2102 @$_SERVER['PHP_AUTH_PW']); 2103 } 2104 2105 // no method found -> no authentication required 2106 return true; 2107 } 2108 2109 // }}} 2110 2111 // {{{ UUID stuff 2112 2113 /** 2114 * Generate Unique Universal IDentifier for lock token 2115 * 2116 * @param void 2117 * @return string a new UUID 2118 */ 2119 function _new_uuid() 2120 { 2121 // use uuid extension from PECL if available 2122 if (function_exists('uuid_create')) { 2123 return uuid_create(); 2124 } 2125 2126 // fallback 2127 $uuid = md5(microtime() . getmypid()); // this should be random enough for now 2128 2129 // set variant and version fields for 'true' random uuid 2130 $uuid{12} = '4'; 2131 $n = 8 + (ord($uuid{16}) & 3); 2132 $hex = '0123456789abcdef'; 2133 $uuid{16} = $hex{$n}; 2134 2135 // return formated uuid 2136 return substr($uuid, 0, 8) 2137 . '-' . substr($uuid, 8, 4) 2138 . '-' . substr($uuid, 12, 4) 2139 . '-' . substr($uuid, 16, 4) 2140 . '-' . substr($uuid, 20); 2141 } 2142 2143 /** 2144 * Create a new opaque lock token as defined in RFC2518 2145 * 2146 * @param void 2147 * @return string new RFC2518 opaque lock token 2148 */ 2149 function _new_locktoken() 2150 { 2151 return 'opaquelocktoken:' . $this->_new_uuid(); 2152 } 2153 2154 // }}} 2155 2156 // {{{ WebDAV If: header parsing 2157 2158 /** 2159 * 2160 * 2161 * @param string header string to parse 2162 * @param int current parsing position 2163 * @return array next token (type and value) 2164 */ 2165 function _if_header_lexer($string, &$pos) 2166 { 2167 // skip whitespace 2168 while (ctype_space($string{$pos})) { 2169 ++$pos; 2170 } 2171 2172 // already at end of string? 2173 if (strlen($string) <= $pos) { 2174 return; 2175 } 2176 2177 // get next character 2178 $c = $string{$pos++}; 2179 2180 // now it depends on what we found 2181 switch ($c) { 2182 2183 // URLs are enclosed in <...> 2184 case '<': 2185 $pos2 = strpos($string, '>', $pos); 2186 $uri = substr($string, $pos, $pos2 - $pos); 2187 $pos = $pos2 + 1; 2188 return array('URI', $uri); 2189 2190 // ETags are enclosed in [...] 2191 case '[': 2192 $type = 'ETAG_STRONG'; 2193 if ($string{$pos} == 'W') { 2194 $type = 'ETAG_WEAK'; 2195 $pos += 2; 2196 } 2197 2198 $pos2 = strpos($string, ']', $pos); 2199 $etag = substr($string, $pos + 1, $pos2 - $pos - 2); 2200 $pos = $pos2 + 1; 2201 return array($type, $etag); 2202 2203 // 'N' indicates negation 2204 case 'N': 2205 $pos += 2; 2206 return array('NOT', 'Not'); 2207 2208 // anything else is passed verbatim char by char 2209 default: 2210 return array('CHAR', $c); 2211 } 2212 } 2213 2214 /** 2215 * Parse If: header 2216 * 2217 * @param string header string 2218 * @return array URLs and their conditions 2219 */ 2220 function _if_header_parser($str) 2221 { 2222 $pos = 0; 2223 $len = strlen($str); 2224 2225 $uris = array(); 2226 2227 // parser loop 2228 while ($pos < $len) { 2229 2230 // get next token 2231 $token = $this->_if_header_lexer($str, $pos); 2232 2233 // check for URL 2234 $uri = ''; 2235 if ($token[0] == 'URI') { 2236 $uri = $token[1]; // remember URL 2237 $token = $this->_if_header_lexer($str, $pos); // get next token 2238 } 2239 2240 // sanity check 2241 if ($token[0] != 'CHAR' || $token[1] != '(') { 2242 return; 2243 } 2244 2245 $list = array(); 2246 $level = 1; 2247 while ($level) { 2248 $token = $this->_if_header_lexer($str, $pos); 2249 2250 $not = ''; 2251 if ($token[0] == 'NOT') { 2252 $not = '!'; 2253 $token = $this->_if_header_lexer($str, $pos); 2254 } 2255 2256 switch ($token[0]) { 2257 case 'CHAR': 2258 switch ($token[1]) { 2259 case '(': 2260 $level++; 2261 break; 2262 2263 case ')': 2264 $level--; 2265 break; 2266 2267 default: 2268 return; 2269 } 2270 break; 2271 2272 case 'URI': 2273 $list[] = $not . "<$token[1]>"; 2274 break; 2275 2276 case 'ETAG_WEAK': 2277 $list[] = $not . "[W/'$token[1]']"; 2278 break; 2279 2280 case 'ETAG_STRONG': 2281 $list[] = $not . "['$token[1]']"; 2282 break; 2283 2284 default: 2285 return; 2286 } 2287 } 2288 2289 // RFC2518 9.4.1: The No-tag-list production describes a series of 2290 // state tokens and ETags. If multiple No-tag-list productions are 2291 // used then one only needs to match the state of the resource for 2292 // the method to be allowed to continue. 2293 // 2294 // FIXME: Since only one list of conditions must be satisfied, it 2295 // is a mistake to merge all lists of conditions. Instead, a URL 2296 // should reference an array of arrays of conditions, the inner 2297 // array representing a conjunction and the outer array 2298 // representing a disjunction, or $uris shouldn't be associative, 2299 // but be an array of productions array('href' => $href, 2300 // 'conditions' => array($condition, ...)) 2301 if (!empty($uris[$uri])) { 2302 $uris[$uri] = array_merge($uris[$uri], $list); 2303 continue; 2304 } 2305 $uris[$uri] = $list; 2306 } 2307 2308 return $uris; 2309 } 2310 2311 /** 2312 * Check if conditions from If: headers are met 2313 * 2314 * The If: header is an extension to HTTP/1.1 defined in RFC2518 9.4 2315 * 2316 * @param void 2317 * @return boolean 2318 */ 2319 function _check_if_header_conditions() 2320 { 2321 if (empty($_SERVER['HTTP_IF'])) { 2322 return true; 2323 } 2324 2325 $this->_if_header_uris = 2326 $this->_if_header_parser($_SERVER['HTTP_IF']); 2327 2328 // any match is ok 2329 foreach ($this->_if_header_uris as $uri => $conditions) { 2330 2331 // RFC2518 9.4.1: If a method, due to the presence of a Depth or 2332 // Destination header, is applied to multiple resources then the 2333 // No-tag-list production MUST be applied to each resource the 2334 // method is applied to. 2335 if (empty($uri)) { 2336 $uri = $this->getHref($this->path); 2337 } 2338 2339 // all must match 2340 foreach ($conditions as $condition) { 2341 2342 // lock tokens may be free form (RFC2518 6.3) 2343 // but if opaquelocktokens are used (RFC2518 6.4) 2344 // we have to check the format (litmus tests this) 2345 if (!strncmp($condition, '<opaquelocktoken:', strlen('<opaquelocktoken'))) { 2346 if (!ereg('^<opaquelocktoken:[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}>$', $condition)) { 2347 return; 2348 } 2349 } 2350 2351 if (!$this->_check_uri_condition($uri, $condition)) { 2352 continue 2; 2353 } 2354 } 2355 2356 return true; 2357 } 2358 } 2359 2360 /** 2361 * Check a single URL condition parsed from an if-header 2362 * 2363 * @abstract 2364 * @param string URL to check 2365 * @param string condition to check for this URL 2366 * @return boolean condition check result 2367 */ 2368 function _check_uri_condition($uri, $condition) 2369 { 2370 // not really implemented here, 2371 // implementations must override 2372 return true; 2373 } 2374 2375 /** 2376 * For each lock on the requested resource, check that the lock token is in 2377 * the If header. If requesting a shared lock, check only exclusive locks 2378 * on the requested resource. 2379 * 2380 * @param array of locks 2381 * @param string path of resource to check 2382 * @param boolean exclusive lock? 2383 * @return boolean true if the request is allowed 2384 */ 2385 function check_locks_helper($locks, $path, $exclusive_only=false) 2386 { 2387 $conditions = array(); 2388 if (!empty($_SERVER['HTTP_IF'])) { 2389 $conditions = $this->_if_header_parser($_SERVER['HTTP_IF']); 2390 } 2391 2392 foreach ($locks as $lock) { 2393 if ($exclusive_only && ($lock['scope'] == 'shared')) { 2394 continue; 2395 } 2396 2397 // for both Tagged-list and No-tag-list productions 2398 foreach (array($this->getHref($path), '') as $href) { 2399 if (!empty($conditions[$href]) 2400 && in_array("<$lock[token]>", $conditions[$href])) { 2401 continue 2; 2402 } 2403 } 2404 2405 return false; 2406 } 2407 2408 return true; 2409 } 2410 2411 /** 2412 * @param string path of resource to check 2413 * @param boolean exclusive lock? 2414 */ 2415 function check_locks_wrapper($path, $exclusive_only=false) 2416 { 2417 if (!method_exists($this, 'getLocks')) { 2418 return true; 2419 } 2420 2421 return $this->check_locks_helper( 2422 $this->getLocks($path), $path, $exclusive_only); 2423 } 2424 2425 // }}} 2426 2427 /** 2428 * Open request body input stream 2429 * 2430 * Because it's not possible to write to php://input (unlike the potential 2431 * to set request variables) and not possible until PHP 5.1 to register 2432 * alternative stream wrappers with php:// URLs, this function enables 2433 * sub-classes to override the request body. Gallery uses this for unit 2434 * testing. This function also collects all instances of opening the 2435 * request body in one place. 2436 * 2437 * @return resource a file descriptor 2438 */ 2439 function openRequestBody() 2440 { 2441 return fopen('php://input', 'rb'); 2442 } 2443 2444 /** 2445 * Set HTTP response header 2446 * 2447 * This function enables sub-classes to override header-setting. Gallery 2448 * uses this to avoid replacing headers elsewhere in the application, and 2449 * for testability. 2450 * 2451 * @param string status code and message 2452 * @return void 2453 */ 2454 function setResponseHeader($header, $replace=true) 2455 { 2456 $key = 'status'; 2457 if (strncasecmp($header, 'HTTP/', 5) !== 0) { 2458 $key = strtolower(substr($header, 0, strpos($header, ':'))); 2459 } 2460 2461 if ($replace || empty($this->headers[$key])) { 2462 header($header); 2463 $this->headers[$key] = $header; 2464 } 2465 } 2466 2467 /** 2468 * Set HTTP response status and mirror it in a private header 2469 * 2470 * @param string status code and message 2471 * @return void 2472 */ 2473 function setResponseStatus($status, $replace=true) 2474 { 2475 // simplified success case 2476 if ($status === true) { 2477 $status = '200 OK'; 2478 } 2479 2480 // didn't set a more specific status code 2481 if (empty($status)) { 2482 $status = '500 Internal Server Error'; 2483 } 2484 2485 // generate HTTP status response 2486 $this->setResponseHeader("HTTP/1.1 $status", $replace); 2487 $this->setResponseHeader("X-WebDAV-Status: $status", $replace); 2488 } 2489 2490 /** 2491 * Private minimalistic version of PHP urlencode 2492 * 2493 * Only blanks and XML special chars must be encoded here. Full urlencode 2494 * encoding confuses some clients. 2495 * 2496 * @param string URL to encode 2497 * @return string encoded URL 2498 */ 2499 function _urlencode($url) 2500 { 2501 return strtr($url, array(' ' => '%20', 2502 '&' => '%26', 2503 '<' => '%3C', 2504 '>' => '%3E')); 2505 } 2506 2507 /** 2508 * Private version of PHP urldecode 2509 * 2510 * Not really needed but added for completenes. 2511 * 2512 * @param string URL to decode 2513 * @return string decoded URL 2514 */ 2515 function _urldecode($path) 2516 { 2517 return urldecode($path); 2518 } 2519 2520 /** 2521 * UTF-8 encode property values if not already done so 2522 * 2523 * @param string text to encode 2524 * @return string UTF-8 encoded text 2525 */ 2526 function _prop_encode($text) 2527 { 2528 switch (strtolower($this->_prop_encoding)) { 2529 case 'utf-8': 2530 return $text; 2531 case 'iso-8859-1': 2532 case 'iso-8859-15': 2533 case 'latin-1': 2534 default: 2535 return utf8_encode($text); 2536 } 2537 } 2538 2539 /** 2540 * Make sure path ends in a slash 2541 * 2542 * @param string directory path 2543 * @return string directory path with trailing slash 2544 */ 2545 function _slashify($path) 2546 { 2547 if (substr($path, -1) != '/') { 2548 $path .= '/'; 2549 } 2550 2551 return $path; 2552 } 2553 2554 /** 2555 * Verify that the Depth request header is set and has a non-empty value. 2556 * Doesn't verify for allowed values though. 2557 * @return boolean true if set and not empty 2558 */ 2559 function _hasNonEmptyDepthRequestHeader() { 2560 return isset($_SERVER['HTTP_DEPTH']) && strlen(trim((string)$_SERVER['HTTP_DEPTH'])); 2561 } 2562} 2563 2564// Local variables: 2565// tab-width: 4 2566// c-basic-offset: 4 2567// End: 2568?> 2569