PageRenderTime 70ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 1ms

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

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