PageRenderTime 58ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/pear/HTTP/WebDAV/Server.php

https://bitbucket.org/ceu/moodle_demo
PHP | 2130 lines | 1047 code | 324 blank | 759 comment | 272 complexity | 098c06b47c713514f43b5aeda58594b6 MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.0, LGPL-2.1

Large files files are truncated, but you can click here to view the full file

  1. <?php // $Id: Server.php,v 1.1.2.3 2008/02/27 03:02:05 martinlanghoff Exp $
  2. /*
  3. +----------------------------------------------------------------------+
  4. | Copyright (c) 2002-2007 Christian Stocker, Hartmut Holzgraefe |
  5. | All rights reserved |
  6. | |
  7. | Redistribution and use in source and binary forms, with or without |
  8. | modification, are permitted provided that the following conditions |
  9. | are met: |
  10. | |
  11. | 1. Redistributions of source code must retain the above copyright |
  12. | notice, this list of conditions and the following disclaimer. |
  13. | 2. Redistributions in binary form must reproduce the above copyright |
  14. | notice, this list of conditions and the following disclaimer in |
  15. | the documentation and/or other materials provided with the |
  16. | distribution. |
  17. | 3. The names of the authors may not be used to endorse or promote |
  18. | products derived from this software without specific prior |
  19. | written permission. |
  20. | |
  21. | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
  22. | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
  23. | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS |
  24. | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE |
  25. | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, |
  26. | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
  27. | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
  28. | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
  29. | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
  30. | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
  31. | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
  32. | POSSIBILITY OF SUCH DAMAGE. |
  33. +----------------------------------------------------------------------+
  34. */
  35. require_once "HTTP/WebDAV/Tools/_parse_propfind.php";
  36. require_once "HTTP/WebDAV/Tools/_parse_proppatch.php";
  37. require_once "HTTP/WebDAV/Tools/_parse_lockinfo.php";
  38. /**
  39. * Virtual base class for implementing WebDAV servers
  40. *
  41. * WebDAV server base class, needs to be extended to do useful work
  42. *
  43. * @package HTTP_WebDAV_Server
  44. * @author Hartmut Holzgraefe <hholzgra@php.net>
  45. * @version @package_version@
  46. */
  47. class HTTP_WebDAV_Server
  48. {
  49. // {{{ Member Variables
  50. /**
  51. * complete URI for this request
  52. *
  53. * @var string
  54. */
  55. var $uri;
  56. /**
  57. * base URI for this request
  58. *
  59. * @var string
  60. */
  61. var $base_uri;
  62. /**
  63. * URI path for this request
  64. *
  65. * @var string
  66. */
  67. var $path;
  68. /**
  69. * Realm string to be used in authentification popups
  70. *
  71. * @var string
  72. */
  73. var $http_auth_realm = "PHP WebDAV";
  74. /**
  75. * String to be used in "X-Dav-Powered-By" header
  76. *
  77. * @var string
  78. */
  79. var $dav_powered_by = "";
  80. /**
  81. * Remember parsed If: (RFC2518/9.4) header conditions
  82. *
  83. * @var array
  84. */
  85. var $_if_header_uris = array();
  86. /**
  87. * HTTP response status/message
  88. *
  89. * @var string
  90. */
  91. var $_http_status = "200 OK";
  92. /**
  93. * encoding of property values passed in
  94. *
  95. * @var string
  96. */
  97. var $_prop_encoding = "utf-8";
  98. /**
  99. * Copy of $_SERVER superglobal array
  100. *
  101. * Derived classes may extend the constructor to
  102. * modify its contents
  103. *
  104. * @var array
  105. */
  106. var $_SERVER;
  107. // }}}
  108. // {{{ Constructor
  109. /**
  110. * Constructor
  111. *
  112. * @param void
  113. */
  114. function HTTP_WebDAV_Server()
  115. {
  116. // PHP messages destroy XML output -> switch them off
  117. ini_set("display_errors", 0);
  118. // copy $_SERVER variables to local _SERVER array
  119. // so that derived classes can simply modify these
  120. $this->_SERVER = $_SERVER;
  121. }
  122. // }}}
  123. // {{{ ServeRequest()
  124. /**
  125. * Serve WebDAV HTTP request
  126. *
  127. * dispatch WebDAV HTTP request to the apropriate method handler
  128. *
  129. * @param void
  130. * @return void
  131. */
  132. function ServeRequest()
  133. {
  134. // prevent warning in litmus check 'delete_fragment'
  135. if (strstr($this->_SERVER["REQUEST_URI"], '#')) {
  136. $this->http_status("400 Bad Request");
  137. return;
  138. }
  139. // default uri is the complete request uri
  140. $uri = "http";
  141. if (isset($this->_SERVER["HTTPS"]) && $this->_SERVER["HTTPS"] === "on") {
  142. $uri = "https";
  143. }
  144. $uri.= "://".$this->_SERVER["HTTP_HOST"].$this->_SERVER["SCRIPT_NAME"];
  145. $path_info = empty($this->_SERVER["PATH_INFO"]) ? "/" : $this->_SERVER["PATH_INFO"];
  146. $this->base_uri = $uri;
  147. $this->uri = $uri . $path_info;
  148. // set path
  149. $this->path = $this->_urldecode($path_info);
  150. if (!strlen($this->path)) {
  151. if ($this->_SERVER["REQUEST_METHOD"] == "GET") {
  152. // redirect clients that try to GET a collection
  153. // WebDAV clients should never try this while
  154. // regular HTTP clients might ...
  155. header("Location: ".$this->base_uri."/");
  156. return;
  157. } else {
  158. // if a WebDAV client didn't give a path we just assume '/'
  159. $this->path = "/";
  160. }
  161. }
  162. if (ini_get("magic_quotes_gpc")) {
  163. $this->path = stripslashes($this->path);
  164. }
  165. // identify ourselves
  166. if (empty($this->dav_powered_by)) {
  167. header("X-Dav-Powered-By: PHP class: ".get_class($this));
  168. } else {
  169. header("X-Dav-Powered-By: ".$this->dav_powered_by);
  170. }
  171. // check authentication
  172. // for the motivation for not checking OPTIONS requests on / see
  173. // http://pear.php.net/bugs/bug.php?id=5363
  174. if ( ( !(($this->_SERVER['REQUEST_METHOD'] == 'OPTIONS') && ($this->path == "/")))
  175. && (!$this->_check_auth())) {
  176. // RFC2518 says we must use Digest instead of Basic
  177. // but Microsoft Clients do not support Digest
  178. // and we don't support NTLM and Kerberos
  179. // so we are stuck with Basic here
  180. header('WWW-Authenticate: Basic realm="'.($this->http_auth_realm).'"');
  181. // Windows seems to require this being the last header sent
  182. // (changed according to PECL bug #3138)
  183. $this->http_status('401 Unauthorized');
  184. return;
  185. }
  186. // check
  187. if (! $this->_check_if_header_conditions()) {
  188. return;
  189. }
  190. // detect requested method names
  191. $method = strtolower($this->_SERVER["REQUEST_METHOD"]);
  192. $wrapper = "http_".$method;
  193. // activate HEAD emulation by GET if no HEAD method found
  194. if ($method == "head" && !method_exists($this, "head")) {
  195. $method = "get";
  196. }
  197. if (method_exists($this, $wrapper) && ($method == "options" || method_exists($this, $method))) {
  198. $this->$wrapper(); // call method by name
  199. } else { // method not found/implemented
  200. if ($this->_SERVER["REQUEST_METHOD"] == "LOCK") {
  201. $this->http_status("412 Precondition failed");
  202. } else {
  203. $this->http_status("405 Method not allowed");
  204. header("Allow: ".join(", ", $this->_allow())); // tell client what's allowed
  205. }
  206. }
  207. }
  208. // }}}
  209. // {{{ abstract WebDAV methods
  210. // {{{ GET()
  211. /**
  212. * GET implementation
  213. *
  214. * overload this method to retrieve resources from your server
  215. * <br>
  216. *
  217. *
  218. * @abstract
  219. * @param array &$params Array of input and output parameters
  220. * <br><b>input</b><ul>
  221. * <li> path -
  222. * </ul>
  223. * <br><b>output</b><ul>
  224. * <li> size -
  225. * </ul>
  226. * @returns int HTTP-Statuscode
  227. */
  228. /* abstract
  229. function GET(&$params)
  230. {
  231. // dummy entry for PHPDoc
  232. }
  233. */
  234. // }}}
  235. // {{{ PUT()
  236. /**
  237. * PUT implementation
  238. *
  239. * PUT implementation
  240. *
  241. * @abstract
  242. * @param array &$params
  243. * @returns int HTTP-Statuscode
  244. */
  245. /* abstract
  246. function PUT()
  247. {
  248. // dummy entry for PHPDoc
  249. }
  250. */
  251. // }}}
  252. // {{{ COPY()
  253. /**
  254. * COPY implementation
  255. *
  256. * COPY implementation
  257. *
  258. * @abstract
  259. * @param array &$params
  260. * @returns int HTTP-Statuscode
  261. */
  262. /* abstract
  263. function COPY()
  264. {
  265. // dummy entry for PHPDoc
  266. }
  267. */
  268. // }}}
  269. // {{{ MOVE()
  270. /**
  271. * MOVE implementation
  272. *
  273. * MOVE implementation
  274. *
  275. * @abstract
  276. * @param array &$params
  277. * @returns int HTTP-Statuscode
  278. */
  279. /* abstract
  280. function MOVE()
  281. {
  282. // dummy entry for PHPDoc
  283. }
  284. */
  285. // }}}
  286. // {{{ DELETE()
  287. /**
  288. * DELETE implementation
  289. *
  290. * DELETE implementation
  291. *
  292. * @abstract
  293. * @param array &$params
  294. * @returns int HTTP-Statuscode
  295. */
  296. /* abstract
  297. function DELETE()
  298. {
  299. // dummy entry for PHPDoc
  300. }
  301. */
  302. // }}}
  303. // {{{ PROPFIND()
  304. /**
  305. * PROPFIND implementation
  306. *
  307. * PROPFIND implementation
  308. *
  309. * @abstract
  310. * @param array &$params
  311. * @returns int HTTP-Statuscode
  312. */
  313. /* abstract
  314. function PROPFIND()
  315. {
  316. // dummy entry for PHPDoc
  317. }
  318. */
  319. // }}}
  320. // {{{ PROPPATCH()
  321. /**
  322. * PROPPATCH implementation
  323. *
  324. * PROPPATCH implementation
  325. *
  326. * @abstract
  327. * @param array &$params
  328. * @returns int HTTP-Statuscode
  329. */
  330. /* abstract
  331. function PROPPATCH()
  332. {
  333. // dummy entry for PHPDoc
  334. }
  335. */
  336. // }}}
  337. // {{{ LOCK()
  338. /**
  339. * LOCK implementation
  340. *
  341. * LOCK implementation
  342. *
  343. * @abstract
  344. * @param array &$params
  345. * @returns int HTTP-Statuscode
  346. */
  347. /* abstract
  348. function LOCK()
  349. {
  350. // dummy entry for PHPDoc
  351. }
  352. */
  353. // }}}
  354. // {{{ UNLOCK()
  355. /**
  356. * UNLOCK implementation
  357. *
  358. * UNLOCK implementation
  359. *
  360. * @abstract
  361. * @param array &$params
  362. * @returns int HTTP-Statuscode
  363. */
  364. /* abstract
  365. function UNLOCK()
  366. {
  367. // dummy entry for PHPDoc
  368. }
  369. */
  370. // }}}
  371. // }}}
  372. // {{{ other abstract methods
  373. // {{{ check_auth()
  374. /**
  375. * check authentication
  376. *
  377. * overload this method to retrieve and confirm authentication information
  378. *
  379. * @abstract
  380. * @param string type Authentication type, e.g. "basic" or "digest"
  381. * @param string username Transmitted username
  382. * @param string passwort Transmitted password
  383. * @returns bool Authentication status
  384. */
  385. /* abstract
  386. function checkAuth($type, $username, $password)
  387. {
  388. // dummy entry for PHPDoc
  389. }
  390. */
  391. // }}}
  392. // {{{ checklock()
  393. /**
  394. * check lock status for a resource
  395. *
  396. * overload this method to return shared and exclusive locks
  397. * active for this resource
  398. *
  399. * @abstract
  400. * @param string resource Resource path to check
  401. * @returns array An array of lock entries each consisting
  402. * of 'type' ('shared'/'exclusive'), 'token' and 'timeout'
  403. */
  404. /* abstract
  405. function checklock($resource)
  406. {
  407. // dummy entry for PHPDoc
  408. }
  409. */
  410. // }}}
  411. // }}}
  412. // {{{ WebDAV HTTP method wrappers
  413. // {{{ http_OPTIONS()
  414. /**
  415. * OPTIONS method handler
  416. *
  417. * The OPTIONS method handler creates a valid OPTIONS reply
  418. * including Dav: and Allowed: heaers
  419. * based on the implemented methods found in the actual instance
  420. *
  421. * @param void
  422. * @return void
  423. */
  424. function http_OPTIONS()
  425. {
  426. // Microsoft clients default to the Frontpage protocol
  427. // unless we tell them to use WebDAV
  428. header("MS-Author-Via: DAV");
  429. // get allowed methods
  430. $allow = $this->_allow();
  431. // dav header
  432. $dav = array(1); // assume we are always dav class 1 compliant
  433. if (isset($allow['LOCK'])) {
  434. $dav[] = 2; // dav class 2 requires that locking is supported
  435. }
  436. // tell clients what we found
  437. $this->http_status("200 OK");
  438. header("DAV: " .join(", ", $dav));
  439. header("Allow: ".join(", ", $allow));
  440. header("Content-length: 0");
  441. }
  442. // }}}
  443. // {{{ http_PROPFIND()
  444. /**
  445. * PROPFIND method handler
  446. *
  447. * @param void
  448. * @return void
  449. */
  450. function http_PROPFIND()
  451. {
  452. $options = Array();
  453. $files = Array();
  454. $options["path"] = $this->path;
  455. // search depth from header (default is "infinity)
  456. if (isset($this->_SERVER['HTTP_DEPTH'])) {
  457. $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
  458. } else {
  459. $options["depth"] = "infinity";
  460. }
  461. // analyze request payload
  462. $propinfo = new _parse_propfind("php://input");
  463. if (!$propinfo->success) {
  464. $this->http_status("400 Error");
  465. return;
  466. }
  467. $options['props'] = $propinfo->props;
  468. // call user handler
  469. if (!$this->PROPFIND($options, $files)) {
  470. $files = array("files" => array());
  471. if (method_exists($this, "checkLock")) {
  472. // is locked?
  473. $lock = $this->checkLock($this->path);
  474. if (is_array($lock) && count($lock)) {
  475. $created = isset($lock['created']) ? $lock['created'] : time();
  476. $modified = isset($lock['modified']) ? $lock['modified'] : time();
  477. $files['files'][] = array("path" => $this->_slashify($this->path),
  478. "props" => array($this->mkprop("displayname", $this->path),
  479. $this->mkprop("creationdate", $created),
  480. $this->mkprop("getlastmodified", $modified),
  481. $this->mkprop("resourcetype", ""),
  482. $this->mkprop("getcontenttype", ""),
  483. $this->mkprop("getcontentlength", 0))
  484. );
  485. }
  486. }
  487. if (empty($files['files'])) {
  488. $this->http_status("404 Not Found");
  489. return;
  490. }
  491. }
  492. // collect namespaces here
  493. $ns_hash = array();
  494. // Microsoft Clients need this special namespace for date and time values
  495. $ns_defs = "xmlns:ns0=\"urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/\"";
  496. // now we loop over all returned file entries
  497. foreach ($files["files"] as $filekey => $file) {
  498. // nothing to do if no properties were returend for a file
  499. if (!isset($file["props"]) || !is_array($file["props"])) {
  500. continue;
  501. }
  502. // now loop over all returned properties
  503. foreach ($file["props"] as $key => $prop) {
  504. // as a convenience feature we do not require that user handlers
  505. // restrict returned properties to the requested ones
  506. // here we strip all unrequested entries out of the response
  507. switch($options['props']) {
  508. case "all":
  509. // nothing to remove
  510. break;
  511. case "names":
  512. // only the names of all existing properties were requested
  513. // so we remove all values
  514. unset($files["files"][$filekey]["props"][$key]["val"]);
  515. break;
  516. default:
  517. $found = false;
  518. // search property name in requested properties
  519. foreach ((array)$options["props"] as $reqprop) {
  520. if (!isset($reqprop["xmlns"])) {
  521. $reqprop["xmlns"] = "";
  522. }
  523. if ( $reqprop["name"] == $prop["name"]
  524. && $reqprop["xmlns"] == $prop["ns"]) {
  525. $found = true;
  526. break;
  527. }
  528. }
  529. // unset property and continue with next one if not found/requested
  530. if (!$found) {
  531. $files["files"][$filekey]["props"][$key]="";
  532. continue(2);
  533. }
  534. break;
  535. }
  536. // namespace handling
  537. if (empty($prop["ns"])) continue; // no namespace
  538. $ns = $prop["ns"];
  539. if ($ns == "DAV:") continue; // default namespace
  540. if (isset($ns_hash[$ns])) continue; // already known
  541. // register namespace
  542. $ns_name = "ns".(count($ns_hash) + 1);
  543. $ns_hash[$ns] = $ns_name;
  544. $ns_defs .= " xmlns:$ns_name=\"$ns\"";
  545. }
  546. // we also need to add empty entries for properties that were requested
  547. // but for which no values where returned by the user handler
  548. if (is_array($options['props'])) {
  549. foreach ($options["props"] as $reqprop) {
  550. if ($reqprop['name']=="") continue; // skip empty entries
  551. $found = false;
  552. if (!isset($reqprop["xmlns"])) {
  553. $reqprop["xmlns"] = "";
  554. }
  555. // check if property exists in result
  556. foreach ($file["props"] as $prop) {
  557. if ( $reqprop["name"] == $prop["name"]
  558. && $reqprop["xmlns"] == $prop["ns"]) {
  559. $found = true;
  560. break;
  561. }
  562. }
  563. if (!$found) {
  564. if ($reqprop["xmlns"]==="DAV:" && $reqprop["name"]==="lockdiscovery") {
  565. // lockdiscovery is handled by the base class
  566. $files["files"][$filekey]["props"][]
  567. = $this->mkprop("DAV:",
  568. "lockdiscovery",
  569. $this->lockdiscovery($files["files"][$filekey]['path']));
  570. } else {
  571. // add empty value for this property
  572. $files["files"][$filekey]["noprops"][] =
  573. $this->mkprop($reqprop["xmlns"], $reqprop["name"], "");
  574. // register property namespace if not known yet
  575. if ($reqprop["xmlns"] != "DAV:" && !isset($ns_hash[$reqprop["xmlns"]])) {
  576. $ns_name = "ns".(count($ns_hash) + 1);
  577. $ns_hash[$reqprop["xmlns"]] = $ns_name;
  578. $ns_defs .= " xmlns:$ns_name=\"$reqprop[xmlns]\"";
  579. }
  580. }
  581. }
  582. }
  583. }
  584. }
  585. // now we generate the reply header ...
  586. $this->http_status("207 Multi-Status");
  587. header('Content-Type: text/xml; charset="utf-8"');
  588. // ... and payload
  589. echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
  590. echo "<D:multistatus xmlns:D=\"DAV:\">\n";
  591. foreach ($files["files"] as $file) {
  592. // ignore empty or incomplete entries
  593. if (!is_array($file) || empty($file) || !isset($file["path"])) continue;
  594. $path = $file['path'];
  595. if (!is_string($path) || $path==="") continue;
  596. echo " <D:response $ns_defs>\n";
  597. /* TODO right now the user implementation has to make sure
  598. collections end in a slash, this should be done in here
  599. by checking the resource attribute */
  600. $href = $this->_mergePathes($this->_SERVER['SCRIPT_NAME'], $path);
  601. /* minimal urlencoding is needed for the resource path */
  602. $href = $this->_urlencode($href);
  603. echo " <D:href>$href</D:href>\n";
  604. // report all found properties and their values (if any)
  605. if (isset($file["props"]) && is_array($file["props"])) {
  606. echo " <D:propstat>\n";
  607. echo " <D:prop>\n";
  608. foreach ($file["props"] as $key => $prop) {
  609. if (!is_array($prop)) continue;
  610. if (!isset($prop["name"])) continue;
  611. if (!isset($prop["val"]) || $prop["val"] === "" || $prop["val"] === false) {
  612. // empty properties (cannot use empty() for check as "0" is a legal value here)
  613. if ($prop["ns"]=="DAV:") {
  614. echo " <D:$prop[name]/>\n";
  615. } else if (!empty($prop["ns"])) {
  616. echo " <".$ns_hash[$prop["ns"]].":$prop[name]/>\n";
  617. } else {
  618. echo " <$prop[name] xmlns=\"\"/>";
  619. }
  620. } else if ($prop["ns"] == "DAV:") {
  621. // some WebDAV properties need special treatment
  622. switch ($prop["name"]) {
  623. case "creationdate":
  624. echo " <D:creationdate ns0:dt=\"dateTime.tz\">"
  625. . gmdate("Y-m-d\\TH:i:s\\Z", $prop['val'])
  626. . "</D:creationdate>\n";
  627. break;
  628. case "getlastmodified":
  629. echo " <D:getlastmodified ns0:dt=\"dateTime.rfc1123\">"
  630. . gmdate("D, d M Y H:i:s ", $prop['val'])
  631. . "GMT</D:getlastmodified>\n";
  632. break;
  633. case "resourcetype":
  634. echo " <D:resourcetype><D:$prop[val]/></D:resourcetype>\n";
  635. break;
  636. case "supportedlock":
  637. echo " <D:supportedlock>$prop[val]</D:supportedlock>\n";
  638. break;
  639. case "lockdiscovery":
  640. echo " <D:lockdiscovery>\n";
  641. echo $prop["val"];
  642. echo " </D:lockdiscovery>\n";
  643. break;
  644. // the following are non-standard Microsoft extensions to the DAV namespace
  645. case "lastaccessed":
  646. echo " <D:lastaccessed ns0:dt=\"dateTime.rfc1123\">"
  647. . gmdate("D, d M Y H:i:s ", $prop['val'])
  648. . "GMT</D:lastaccessed>\n";
  649. break;
  650. case "ishidden":
  651. echo " <D:ishidden>"
  652. . is_string($prop['val']) ? $prop['val'] : ($prop['val'] ? 'true' : 'false')
  653. . "</D:ishidden>\n";
  654. break;
  655. default:
  656. echo " <D:$prop[name]>"
  657. . $this->_prop_encode(htmlspecialchars($prop['val']))
  658. . "</D:$prop[name]>\n";
  659. break;
  660. }
  661. } else {
  662. // properties from namespaces != "DAV:" or without any namespace
  663. if ($prop["ns"]) {
  664. echo " <" . $ns_hash[$prop["ns"]] . ":$prop[name]>"
  665. . $this->_prop_encode(htmlspecialchars($prop['val']))
  666. . "</" . $ns_hash[$prop["ns"]] . ":$prop[name]>\n";
  667. } else {
  668. echo " <$prop[name] xmlns=\"\">"
  669. . $this->_prop_encode(htmlspecialchars($prop['val']))
  670. . "</$prop[name]>\n";
  671. }
  672. }
  673. }
  674. echo " </D:prop>\n";
  675. echo " <D:status>HTTP/1.1 200 OK</D:status>\n";
  676. echo " </D:propstat>\n";
  677. }
  678. // now report all properties requested but not found
  679. if (isset($file["noprops"])) {
  680. echo " <D:propstat>\n";
  681. echo " <D:prop>\n";
  682. foreach ($file["noprops"] as $key => $prop) {
  683. if ($prop["ns"] == "DAV:") {
  684. echo " <D:$prop[name]/>\n";
  685. } else if ($prop["ns"] == "") {
  686. echo " <$prop[name] xmlns=\"\"/>\n";
  687. } else {
  688. echo " <" . $ns_hash[$prop["ns"]] . ":$prop[name]/>\n";
  689. }
  690. }
  691. echo " </D:prop>\n";
  692. echo " <D:status>HTTP/1.1 404 Not Found</D:status>\n";
  693. echo " </D:propstat>\n";
  694. }
  695. echo " </D:response>\n";
  696. }
  697. echo "</D:multistatus>\n";
  698. }
  699. // }}}
  700. // {{{ http_PROPPATCH()
  701. /**
  702. * PROPPATCH method handler
  703. *
  704. * @param void
  705. * @return void
  706. */
  707. function http_PROPPATCH()
  708. {
  709. if ($this->_check_lock_status($this->path)) {
  710. $options = Array();
  711. $options["path"] = $this->path;
  712. $propinfo = new _parse_proppatch("php://input");
  713. if (!$propinfo->success) {
  714. $this->http_status("400 Error");
  715. return;
  716. }
  717. $options['props'] = $propinfo->props;
  718. $responsedescr = $this->PROPPATCH($options);
  719. $this->http_status("207 Multi-Status");
  720. header('Content-Type: text/xml; charset="utf-8"');
  721. echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
  722. echo "<D:multistatus xmlns:D=\"DAV:\">\n";
  723. echo " <D:response>\n";
  724. echo " <D:href>".$this->_urlencode($this->_mergePathes($this->_SERVER["SCRIPT_NAME"], $this->path))."</D:href>\n";
  725. foreach ($options["props"] as $prop) {
  726. echo " <D:propstat>\n";
  727. echo " <D:prop><$prop[name] xmlns=\"$prop[ns]\"/></D:prop>\n";
  728. echo " <D:status>HTTP/1.1 $prop[status]</D:status>\n";
  729. echo " </D:propstat>\n";
  730. }
  731. if ($responsedescr) {
  732. echo " <D:responsedescription>".
  733. $this->_prop_encode(htmlspecialchars($responsedescr)).
  734. "</D:responsedescription>\n";
  735. }
  736. echo " </D:response>\n";
  737. echo "</D:multistatus>\n";
  738. } else {
  739. $this->http_status("423 Locked");
  740. }
  741. }
  742. // }}}
  743. // {{{ http_MKCOL()
  744. /**
  745. * MKCOL method handler
  746. *
  747. * @param void
  748. * @return void
  749. */
  750. function http_MKCOL()
  751. {
  752. $options = Array();
  753. $options["path"] = $this->path;
  754. $stat = $this->MKCOL($options);
  755. $this->http_status($stat);
  756. }
  757. // }}}
  758. // {{{ http_GET()
  759. /**
  760. * GET method handler
  761. *
  762. * @param void
  763. * @returns void
  764. */
  765. function http_GET()
  766. {
  767. // TODO check for invalid stream
  768. $options = Array();
  769. $options["path"] = $this->path;
  770. $this->_get_ranges($options);
  771. if (true === ($status = $this->GET($options))) {
  772. if (!headers_sent()) {
  773. $status = "200 OK";
  774. if (!isset($options['mimetype'])) {
  775. $options['mimetype'] = "application/octet-stream";
  776. }
  777. header("Content-type: $options[mimetype]");
  778. if (isset($options['mtime'])) {
  779. header("Last-modified:".gmdate("D, d M Y H:i:s ", $options['mtime'])."GMT");
  780. }
  781. if (isset($options['stream'])) {
  782. // GET handler returned a stream
  783. if (!empty($options['ranges']) && (0===fseek($options['stream'], 0, SEEK_SET))) {
  784. // partial request and stream is seekable
  785. if (count($options['ranges']) === 1) {
  786. $range = $options['ranges'][0];
  787. if (isset($range['start'])) {
  788. fseek($options['stream'], $range['start'], SEEK_SET);
  789. if (feof($options['stream'])) {
  790. $this->http_status("416 Requested range not satisfiable");
  791. return;
  792. }
  793. if (isset($range['end'])) {
  794. $size = $range['end']-$range['start']+1;
  795. $this->http_status("206 partial");
  796. header("Content-length: $size");
  797. header("Content-range: $range[start]-$range[end]/"
  798. . (isset($options['size']) ? $options['size'] : "*"));
  799. while ($size && !feof($options['stream'])) {
  800. $buffer = fread($options['stream'], 4096);
  801. $size -= $this->bytes($buffer);
  802. echo $buffer;
  803. }
  804. } else {
  805. $this->http_status("206 partial");
  806. if (isset($options['size'])) {
  807. header("Content-length: ".($options['size'] - $range['start']));
  808. header("Content-range: ".$range['start']."-".$range['end']."/"
  809. . (isset($options['size']) ? $options['size'] : "*"));
  810. }
  811. fpassthru($options['stream']);
  812. }
  813. } else {
  814. header("Content-length: ".$range['last']);
  815. fseek($options['stream'], -$range['last'], SEEK_END);
  816. fpassthru($options['stream']);
  817. }
  818. } else {
  819. $this->_multipart_byterange_header(); // init multipart
  820. foreach ($options['ranges'] as $range) {
  821. // TODO what if size unknown? 500?
  822. if (isset($range['start'])) {
  823. $from = $range['start'];
  824. $to = !empty($range['end']) ? $range['end'] : $options['size']-1;
  825. } else {
  826. $from = $options['size'] - $range['last']-1;
  827. $to = $options['size'] -1;
  828. }
  829. $total = isset($options['size']) ? $options['size'] : "*";
  830. $size = $to - $from + 1;
  831. $this->_multipart_byterange_header($options['mimetype'], $from, $to, $total);
  832. fseek($options['stream'], $from, SEEK_SET);
  833. while ($size && !feof($options['stream'])) {
  834. $buffer = fread($options['stream'], 4096);
  835. $size -= $this->bytes($buffer);
  836. echo $buffer;
  837. }
  838. }
  839. $this->_multipart_byterange_header(); // end multipart
  840. }
  841. } else {
  842. // normal request or stream isn't seekable, return full content
  843. if (isset($options['size'])) {
  844. header("Content-length: ".$options['size']);
  845. }
  846. fpassthru($options['stream']);
  847. return; // no more headers
  848. }
  849. } elseif (isset($options['data'])) {
  850. if (is_array($options['data'])) {
  851. // reply to partial request
  852. } else {
  853. header("Content-length: ".$this->bytes($options['data']));
  854. echo $options['data'];
  855. }
  856. }
  857. }
  858. }
  859. if (!headers_sent()) {
  860. if (false === $status) {
  861. $this->http_status("404 not found");
  862. } else {
  863. // TODO: check setting of headers in various code pathes above
  864. $this->http_status("$status");
  865. }
  866. }
  867. }
  868. /**
  869. * parse HTTP Range: header
  870. *
  871. * @param array options array to store result in
  872. * @return void
  873. */
  874. function _get_ranges(&$options)
  875. {
  876. // process Range: header if present
  877. if (isset($this->_SERVER['HTTP_RANGE'])) {
  878. // we only support standard "bytes" range specifications for now
  879. if (preg_match('/bytes\s*=\s*(.+)/', $this->_SERVER['HTTP_RANGE'], $matches)) {
  880. $options["ranges"] = array();
  881. // ranges are comma separated
  882. foreach (explode(",", $matches[1]) as $range) {
  883. // ranges are either from-to pairs or just end positions
  884. list($start, $end) = explode("-", $range);
  885. $options["ranges"][] = ($start==="")
  886. ? array("last"=>$end)
  887. : array("start"=>$start, "end"=>$end);
  888. }
  889. }
  890. }
  891. }
  892. /**
  893. * generate separator headers for multipart response
  894. *
  895. * first and last call happen without parameters to generate
  896. * the initial header and closing sequence, all calls inbetween
  897. * require content mimetype, start and end byte position and
  898. * optionaly the total byte length of the requested resource
  899. *
  900. * @param string mimetype
  901. * @param int start byte position
  902. * @param int end byte position
  903. * @param int total resource byte size
  904. */
  905. function _multipart_byterange_header($mimetype = false, $from = false, $to=false, $total=false)
  906. {
  907. if ($mimetype === false) {
  908. if (!isset($this->multipart_separator)) {
  909. // initial
  910. // a little naive, this sequence *might* be part of the content
  911. // but it's really not likely and rather expensive to check
  912. $this->multipart_separator = "SEPARATOR_".md5(microtime());
  913. // generate HTTP header
  914. header("Content-type: multipart/byteranges; boundary=".$this->multipart_separator);
  915. } else {
  916. // final
  917. // generate closing multipart sequence
  918. echo "\n--{$this->multipart_separator}--";
  919. }
  920. } else {
  921. // generate separator and header for next part
  922. echo "\n--{$this->multipart_separator}\n";
  923. echo "Content-type: $mimetype\n";
  924. echo "Content-range: $from-$to/". ($total === false ? "*" : $total);
  925. echo "\n\n";
  926. }
  927. }
  928. // }}}
  929. // {{{ http_HEAD()
  930. /**
  931. * HEAD method handler
  932. *
  933. * @param void
  934. * @return void
  935. */
  936. function http_HEAD()
  937. {
  938. $status = false;
  939. $options = Array();
  940. $options["path"] = $this->path;
  941. if (method_exists($this, "HEAD")) {
  942. $status = $this->head($options);
  943. } else if (method_exists($this, "GET")) {
  944. ob_start();
  945. $status = $this->GET($options);
  946. if (!isset($options['size'])) {
  947. $options['size'] = ob_get_length();
  948. }
  949. ob_end_clean();
  950. }
  951. if (!isset($options['mimetype'])) {
  952. $options['mimetype'] = "application/octet-stream";
  953. }
  954. header("Content-type: $options[mimetype]");
  955. if (isset($options['mtime'])) {
  956. header("Last-modified:".gmdate("D, d M Y H:i:s ", $options['mtime'])."GMT");
  957. }
  958. if (isset($options['size'])) {
  959. header("Content-length: ".$options['size']);
  960. }
  961. if ($status === true) $status = "200 OK";
  962. if ($status === false) $status = "404 Not found";
  963. $this->http_status($status);
  964. }
  965. // }}}
  966. // {{{ http_PUT()
  967. /**
  968. * PUT method handler
  969. *
  970. * @param void
  971. * @return void
  972. */
  973. function http_PUT()
  974. {
  975. if ($this->_check_lock_status($this->path)) {
  976. $options = Array();
  977. $options["path"] = $this->path;
  978. $options["content_length"] = $this->_SERVER["CONTENT_LENGTH"];
  979. // get the Content-type
  980. if (isset($this->_SERVER["CONTENT_TYPE"])) {
  981. // for now we do not support any sort of multipart requests
  982. if (!strncmp($this->_SERVER["CONTENT_TYPE"], "multipart/", 10)) {
  983. $this->http_status("501 not implemented");
  984. echo "The service does not support mulipart PUT requests";
  985. return;
  986. }
  987. $options["content_type"] = $this->_SERVER["CONTENT_TYPE"];
  988. } else {
  989. // default content type if none given
  990. $options["content_type"] = "application/octet-stream";
  991. }
  992. /* RFC 2616 2.6 says: "The recipient of the entity MUST NOT
  993. ignore any Content-* (e.g. Content-Range) headers that it
  994. does not understand or implement and MUST return a 501
  995. (Not Implemented) response in such cases."
  996. */
  997. foreach ($this->_SERVER as $key => $val) {
  998. if (strncmp($key, "HTTP_CONTENT", 11)) continue;
  999. switch ($key) {
  1000. case 'HTTP_CONTENT_ENCODING': // RFC 2616 14.11
  1001. // TODO support this if ext/zlib filters are available
  1002. $this->http_status("501 not implemented");
  1003. echo "The service does not support '$val' content encoding";
  1004. return;
  1005. case 'HTTP_CONTENT_LANGUAGE': // RFC 2616 14.12
  1006. // we assume it is not critical if this one is ignored
  1007. // in the actual PUT implementation ...
  1008. $options["content_language"] = $val;
  1009. break;
  1010. case 'HTTP_CONTENT_LOCATION': // RFC 2616 14.14
  1011. /* The meaning of the Content-Location header in PUT
  1012. or POST requests is undefined; servers are free
  1013. to ignore it in those cases. */
  1014. break;
  1015. case 'HTTP_CONTENT_RANGE': // RFC 2616 14.16
  1016. // single byte range requests are supported
  1017. // the header format is also specified in RFC 2616 14.16
  1018. // TODO we have to ensure that implementations support this or send 501 instead
  1019. if (!preg_match('@bytes\s+(\d+)-(\d+)/((\d+)|\*)@', $val, $matches)) {
  1020. $this->http_status("400 bad request");
  1021. echo "The service does only support single byte ranges";
  1022. return;
  1023. }
  1024. $range = array("start"=>$matches[1], "end"=>$matches[2]);
  1025. if (is_numeric($matches[3])) {
  1026. $range["total_length"] = $matches[3];
  1027. }
  1028. $option["ranges"][] = $range;
  1029. // TODO make sure the implementation supports partial PUT
  1030. // this has to be done in advance to avoid data being overwritten
  1031. // on implementations that do not support this ...
  1032. break;
  1033. case 'HTTP_CONTENT_MD5': // RFC 2616 14.15
  1034. // TODO: maybe we can just pretend here?
  1035. $this->http_status("501 not implemented");
  1036. echo "The service does not support content MD5 checksum verification";
  1037. return;
  1038. default:
  1039. // any other unknown Content-* headers
  1040. $this->http_status("501 not implemented");
  1041. echo "The service does not support '$key'";
  1042. return;
  1043. }
  1044. }
  1045. $options["stream"] = fopen("php://input", "r");
  1046. $stat = $this->PUT($options);
  1047. if ($stat === false) {
  1048. $stat = "403 Forbidden";
  1049. } else if (is_resource($stat) && get_resource_type($stat) == "stream") {
  1050. $stream = $stat;
  1051. $stat = $options["new"] ? "201 Created" : "204 No Content";
  1052. if (!empty($options["ranges"])) {
  1053. // TODO multipart support is missing (see also above)
  1054. if (0 == fseek($stream, $range[0]["start"], SEEK_SET)) {
  1055. $length = $range[0]["end"]-$range[0]["start"]+1;
  1056. if (!fwrite($stream, fread($options["stream"], $length))) {
  1057. $stat = "403 Forbidden";
  1058. }
  1059. } else {
  1060. $stat = "403 Forbidden";
  1061. }
  1062. } else {
  1063. while (!feof($options["stream"])) {
  1064. if (false === fwrite($stream, fread($options["stream"], 4096))) {
  1065. $stat = "403 Forbidden";
  1066. break;
  1067. }
  1068. }
  1069. }
  1070. fclose($stream);
  1071. }
  1072. $this->http_status($stat);
  1073. } else {
  1074. $this->http_status("423 Locked");
  1075. }
  1076. }
  1077. // }}}
  1078. // {{{ http_DELETE()
  1079. /**
  1080. * DELETE method handler
  1081. *
  1082. * @param void
  1083. * @return void
  1084. */
  1085. function http_DELETE()
  1086. {
  1087. // check RFC 2518 Section 9.2, last paragraph
  1088. if (isset($this->_SERVER["HTTP_DEPTH"])) {
  1089. if ($this->_SERVER["HTTP_DEPTH"] != "infinity") {
  1090. $this->http_status("400 Bad Request");
  1091. return;
  1092. }
  1093. }
  1094. // check lock status
  1095. if ($this->_check_lock_status($this->path)) {
  1096. // ok, proceed
  1097. $options = Array();
  1098. $options["path"] = $this->path;
  1099. $stat = $this->DELETE($options);
  1100. $this->http_status($stat);
  1101. } else {
  1102. // sorry, its locked
  1103. $this->http_status("423 Locked");
  1104. }
  1105. }
  1106. // }}}
  1107. // {{{ http_COPY()
  1108. /**
  1109. * COPY method handler
  1110. *
  1111. * @param void
  1112. * @return void
  1113. */
  1114. function http_COPY()
  1115. {
  1116. // no need to check source lock status here
  1117. // destination lock status is always checked by the helper method
  1118. $this->_copymove("copy");
  1119. }
  1120. // }}}
  1121. // {{{ http_MOVE()
  1122. /**
  1123. * MOVE method handler
  1124. *
  1125. * @param void
  1126. * @return void
  1127. */
  1128. function http_MOVE()
  1129. {
  1130. if ($this->_check_lock_status($this->path)) {
  1131. // destination lock status is always checked by the helper method
  1132. $this->_copymove("move");
  1133. } else {
  1134. $this->http_status("423 Locked");
  1135. }
  1136. }
  1137. // }}}
  1138. // {{{ http_LOCK()
  1139. /**
  1140. * LOCK method handler
  1141. *
  1142. * @param void
  1143. * @return void
  1144. */
  1145. function http_LOCK()
  1146. {
  1147. $options = Array();
  1148. $options["path"] = $this->path;
  1149. if (isset($this->_SERVER['HTTP_DEPTH'])) {
  1150. $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
  1151. } else {
  1152. $options["depth"] = "infinity";
  1153. }
  1154. if (isset($this->_SERVER["HTTP_TIMEOUT"])) {
  1155. $options["timeout"] = explode(",", $this->_SERVER["HTTP_TIMEOUT"]);
  1156. }
  1157. if (empty($this->_SERVER['CONTENT_LENGTH']) && !empty($this->_SERVER['HTTP_IF'])) {
  1158. // check if locking is possible
  1159. if (!$this->_check_lock_status($this->path)) {
  1160. $this->http_status("423 Locked");
  1161. return;
  1162. }
  1163. // refresh lock
  1164. $options["locktoken"] = substr($this->_SERVER['HTTP_IF'], 2, -2);
  1165. $options["update"] = $options["locktoken"];
  1166. // setting defaults for required fields, LOCK() SHOULD overwrite these
  1167. $options['owner'] = "unknown";
  1168. $options['scope'] = "exclusive";
  1169. $options['type'] = "write";
  1170. $stat = $this->LOCK($options);
  1171. } else {
  1172. // extract lock request information from request XML payload
  1173. $lockinfo = new _parse_lockinfo("php://input");
  1174. if (!$lockinfo->success) {
  1175. $this->http_status("400 bad request");
  1176. }
  1177. // check if locking is possible
  1178. if (!$this->_check_lock_status($this->path, $lockinfo->lockscope === "shared")) {
  1179. $this->http_status("423 Locked");
  1180. return;
  1181. }
  1182. // new lock
  1183. $options["scope"] = $lockinfo->lockscope;
  1184. $options["type"] = $lockinfo->locktype;
  1185. $options["owner"] = $lockinfo->owner;
  1186. $options["locktoken"] = $this->_new_locktoken();
  1187. $stat = $this->LOCK($options);
  1188. }
  1189. if (is_bool($stat)) {
  1190. $http_stat = $stat ? "200 OK" : "423 Locked";
  1191. } else {
  1192. $http_stat = $stat;
  1193. }
  1194. $this->http_status($http_stat);
  1195. if ($http_stat{0} == 2) { // 2xx states are ok
  1196. if ($options["timeout"]) {
  1197. // if multiple timeout values were given we take the first only
  1198. if (is_array($options["timeout"])) {
  1199. reset($options["timeout"]);
  1200. $options["timeout"] = current($options["timeout"]);
  1201. }

Large files files are truncated, but you can click here to view the full file