PageRenderTime 51ms CodeModel.GetById 12ms RepoModel.GetById 1ms app.codeStats 0ms

/components/admin/extplorer/libraries/HTTP/WebDAV/Server.php

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