You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2140 lines
68 KiB

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