PageRenderTime 55ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/inc/HTTP/WebDAV/Server.php

https://github.com/chregu/fluxcms
PHP | 1881 lines | 908 code | 294 blank | 679 comment | 230 complexity | 27e29d2cf27ffd0a8308f706fe01c6bc MD5 | raw file
Possible License(s): GPL-2.0, BSD-3-Clause, Apache-2.0, LGPL-2.1

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

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

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