PageRenderTime 64ms CodeModel.GetById 30ms RepoModel.GetById 0ms app.codeStats 1ms

/include/HTTP_WebDAV_Server/Server.php

https://bitbucket.org/cviolette/sugarcrm
PHP | 1873 lines | 906 code | 292 blank | 675 comment | 228 complexity | 85e6613006abbd02d316572254f9f97f MD5 | raw file
Possible License(s): LGPL-2.1, MPL-2.0-no-copyleft-exception, BSD-3-Clause

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

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

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