PageRenderTime 100ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/kloxo/httpdocs/webdisk/HTTP/WebDAV/Server/Filesystem.php

https://bitbucket.org/Nemcio/kloxo-mr
PHP | 804 lines | 429 code | 123 blank | 252 comment | 96 complexity | c6eeee2df1efa998542ebfdfdd1437e9 MD5 | raw file
  1. <?php
  2. require_once "HTTP/WebDAV/Server.php";
  3. require_once "System.php";
  4. /**
  5. * Filesystem access using WebDAV
  6. *
  7. * @access public
  8. * @author Hartmut Holzgraefe <hartmut@php.net>
  9. * @version @package-version@
  10. */
  11. class HTTP_WebDAV_Server_Filesystem extends HTTP_WebDAV_Server
  12. {
  13. /**
  14. * Root directory for WebDAV access
  15. *
  16. * Defaults to webserver document root (set by ServeRequest)
  17. *
  18. * @access private
  19. * @var string
  20. */
  21. var $base = "";
  22. /**
  23. * MySQL Host where property and locking information is stored
  24. *
  25. * @access private
  26. * @var string
  27. */
  28. var $db_host = "localhost";
  29. /**
  30. * MySQL database for property/locking information storage
  31. *
  32. * @access private
  33. * @var string
  34. */
  35. var $db_name = "webdav";
  36. /**
  37. * MySQL table name prefix
  38. *
  39. * @access private
  40. * @var string
  41. */
  42. var $db_prefix = "";
  43. /**
  44. * MySQL user for property/locking db access
  45. *
  46. * @access private
  47. * @var string
  48. */
  49. var $db_user = "root";
  50. /**
  51. * MySQL password for property/locking db access
  52. *
  53. * @access private
  54. * @var string
  55. */
  56. var $db_passwd = "";
  57. /**
  58. * Serve a webdav request
  59. *
  60. * @access public
  61. * @param string
  62. */
  63. function ServeRequest($base = false)
  64. {
  65. // special treatment for litmus compliance test
  66. // reply on its identifier header
  67. // not needed for the test itself but eases debugging
  68. /*
  69. foreach (apache_request_headers() as $key => $value) {
  70. if (stristr($key, "litmus")) {
  71. error_log("Litmus test $value");
  72. header("X-Litmus-reply: ".$value);
  73. }
  74. }
  75. */
  76. // set root directory, defaults to webserver document root if not set
  77. if ($base) {
  78. $this->base = realpath($base); // TODO throw if not a directory
  79. } else if (!$this->base) {
  80. $this->base = $this->_SERVER['DOCUMENT_ROOT'];
  81. }
  82. // establish connection to property/locking db
  83. @mysql_connect($this->db_host, $this->db_user, $this->db_passwd) or die(mysql_error());
  84. mysql_select_db($this->db_name) or die(mysql_error());
  85. // TODO throw on connection problems
  86. // let the base class do all the work
  87. parent::ServeRequest();
  88. }
  89. /**
  90. * No authentication is needed here
  91. *
  92. * @access private
  93. * @param string HTTP Authentication type (Basic, Digest, ...)
  94. * @param string Username
  95. * @param string Password
  96. * @return bool true on successful authentication
  97. */
  98. function check_auth($type, $user, $pass)
  99. {
  100. return true;
  101. }
  102. /**
  103. * PROPFIND method handler
  104. *
  105. * @param array general parameter passing array
  106. * @param array return array for file properties
  107. * @return bool true on success
  108. */
  109. function PROPFIND(&$options, &$files)
  110. {
  111. // get absolute fs path to requested resource
  112. $fspath = $this->base . $options["path"];
  113. // sanity check
  114. if (!file_exists($fspath)) {
  115. return false;
  116. }
  117. // prepare property array
  118. $files["files"] = array();
  119. // store information for the requested path itself
  120. $files["files"][] = $this->fileinfo($options["path"]);
  121. // information for contained resources requested?
  122. if (!empty($options["depth"])) { // TODO check for is_dir() first?
  123. // make sure path ends with '/'
  124. $options["path"] = $this->_slashify($options["path"]);
  125. // try to open directory
  126. $handle = @opendir($fspath);
  127. if ($handle) {
  128. // ok, now get all its contents
  129. while ($filename = readdir($handle)) {
  130. if ($filename != "." && $filename != "..") {
  131. $files["files"][] = $this->fileinfo($options["path"].$filename);
  132. }
  133. }
  134. // TODO recursion needed if "Depth: infinite"
  135. }
  136. }
  137. // ok, all done
  138. return true;
  139. }
  140. /**
  141. * Get properties for a single file/resource
  142. *
  143. * @param string resource path
  144. * @return array resource properties
  145. */
  146. function fileinfo($path)
  147. {
  148. // map URI path to filesystem path
  149. $fspath = $this->base . $path;
  150. // create result array
  151. $info = array();
  152. // TODO remove slash append code when base clase is able to do it itself
  153. $info["path"] = is_dir($fspath) ? $this->_slashify($path) : $path;
  154. $info["props"] = array();
  155. // no special beautified displayname here ...
  156. $info["props"][] = $this->mkprop("displayname", strtoupper($path));
  157. // creation and modification time
  158. $info["props"][] = $this->mkprop("creationdate", filectime($fspath));
  159. $info["props"][] = $this->mkprop("getlastmodified", filemtime($fspath));
  160. // type and size (caller already made sure that path exists)
  161. if (is_dir($fspath)) {
  162. // directory (WebDAV collection)
  163. $info["props"][] = $this->mkprop("resourcetype", "collection");
  164. $info["props"][] = $this->mkprop("getcontenttype", "httpd/unix-directory");
  165. } else {
  166. // plain file (WebDAV resource)
  167. $info["props"][] = $this->mkprop("resourcetype", "");
  168. if (is_readable($fspath)) {
  169. $info["props"][] = $this->mkprop("getcontenttype", $this->_mimetype($fspath));
  170. } else {
  171. $info["props"][] = $this->mkprop("getcontenttype", "application/x-non-readable");
  172. }
  173. $info["props"][] = $this->mkprop("getcontentlength", filesize($fspath));
  174. }
  175. // get additional properties from database
  176. $query = "SELECT ns, name, value
  177. FROM {$this->db_prefix}properties
  178. WHERE path = '$path'";
  179. $res = mysql_query($query);
  180. while ($row = mysql_fetch_assoc($res)) {
  181. $info["props"][] = $this->mkprop($row["ns"], $row["name"], $row["value"]);
  182. }
  183. mysql_free_result($res);
  184. return $info;
  185. }
  186. /**
  187. * detect if a given program is found in the search PATH
  188. *
  189. * helper function used by _mimetype() to detect if the
  190. * external 'file' utility is available
  191. *
  192. * @param string program name
  193. * @param string optional search path, defaults to $PATH
  194. * @return bool true if executable program found in path
  195. */
  196. function _can_execute($name, $path = false)
  197. {
  198. // path defaults to PATH from environment if not set
  199. if ($path === false) {
  200. $path = getenv("PATH");
  201. }
  202. // check method depends on operating system
  203. if (!strncmp(PHP_OS, "WIN", 3)) {
  204. // on Windows an appropriate COM or EXE file needs to exist
  205. $exts = array(".exe", ".com");
  206. $check_fn = "file_exists";
  207. } else {
  208. // anywhere else we look for an executable file of that name
  209. $exts = array("");
  210. $check_fn = "is_executable";
  211. }
  212. // now check the directories in the path for the program
  213. foreach (explode(PATH_SEPARATOR, $path) as $dir) {
  214. // skip invalid path entries
  215. if (!file_exists($dir)) continue;
  216. if (!is_dir($dir)) continue;
  217. // and now look for the file
  218. foreach ($exts as $ext) {
  219. if ($check_fn("$dir/$name".$ext)) return true;
  220. }
  221. }
  222. return false;
  223. }
  224. /**
  225. * try to detect the mime type of a file
  226. *
  227. * @param string file path
  228. * @return string guessed mime type
  229. */
  230. function _mimetype($fspath)
  231. {
  232. if (@is_dir($fspath)) {
  233. // directories are easy
  234. return "httpd/unix-directory";
  235. } else if (function_exists("mime_content_type")) {
  236. // use mime magic extension if available
  237. $mime_type = mime_content_type($fspath);
  238. } else if ($this->_can_execute("file")) {
  239. // it looks like we have a 'file' command,
  240. // lets see it it does have mime support
  241. $fp = popen("file -i '$fspath' 2>/dev/null", "r");
  242. $reply = fgets($fp);
  243. pclose($fp);
  244. // popen will not return an error if the binary was not found
  245. // and find may not have mime support using "-i"
  246. // so we test the format of the returned string
  247. // the reply begins with the requested filename
  248. if (!strncmp($reply, "$fspath: ", strlen($fspath)+2)) {
  249. $reply = substr($reply, strlen($fspath)+2);
  250. // followed by the mime type (maybe including options)
  251. if (preg_match('|^[[:alnum:]_-]+/[[:alnum:]_-]+;?.*|', $reply, $matches)) {
  252. $mime_type = $matches[0];
  253. }
  254. }
  255. }
  256. if (empty($mime_type)) {
  257. // Fallback solution: try to guess the type by the file extension
  258. // TODO: add more ...
  259. // TODO: it has been suggested to delegate mimetype detection
  260. // to apache but this has at least three issues:
  261. // - works only with apache
  262. // - needs file to be within the document tree
  263. // - requires apache mod_magic
  264. // TODO: can we use the registry for this on Windows?
  265. // OTOH if the server is Windos the clients are likely to
  266. // be Windows, too, and tend do ignore the Content-Type
  267. // anyway (overriding it with information taken from
  268. // the registry)
  269. // TODO: have a seperate PEAR class for mimetype detection?
  270. switch (strtolower(strrchr(basename($fspath), "."))) {
  271. case ".html":
  272. $mime_type = "text/html";
  273. break;
  274. case ".gif":
  275. $mime_type = "image/gif";
  276. break;
  277. case ".jpg":
  278. $mime_type = "image/jpeg";
  279. break;
  280. default:
  281. $mime_type = "application/octet-stream";
  282. break;
  283. }
  284. }
  285. return $mime_type;
  286. }
  287. /**
  288. * GET method handler
  289. *
  290. * @param array parameter passing array
  291. * @return bool true on success
  292. */
  293. function GET(&$options)
  294. {
  295. // get absolute fs path to requested resource
  296. $fspath = $this->base . $options["path"];
  297. // sanity check
  298. if (!file_exists($fspath)) return false;
  299. // is this a collection?
  300. if (is_dir($fspath)) {
  301. return $this->GetDir($fspath, $options);
  302. }
  303. // detect resource type
  304. $options['mimetype'] = $this->_mimetype($fspath);
  305. // detect modification time
  306. // see rfc2518, section 13.7
  307. // some clients seem to treat this as a reverse rule
  308. // requiering a Last-Modified header if the getlastmodified header was set
  309. $options['mtime'] = filemtime($fspath);
  310. // detect resource size
  311. $options['size'] = filesize($fspath);
  312. // no need to check result here, it is handled by the base class
  313. $options['stream'] = fopen($fspath, "r");
  314. return true;
  315. }
  316. /**
  317. * GET method handler for directories
  318. *
  319. * This is a very simple mod_index lookalike.
  320. * See RFC 2518, Section 8.4 on GET/HEAD for collections
  321. *
  322. * @param string directory path
  323. * @return void function has to handle HTTP response itself
  324. */
  325. function GetDir($fspath, &$options)
  326. {
  327. $path = $this->_slashify($options["path"]);
  328. if ($path != $options["path"]) {
  329. header("Location: ".$this->base_uri.$path);
  330. exit;
  331. }
  332. // fixed width directory column format
  333. $format = "%15s %-19s %-s\n";
  334. $handle = @opendir($fspath);
  335. if (!$handle) {
  336. return false;
  337. }
  338. echo "<html><head><title>Index of ".htmlspecialchars($options['path'])."</title></head>\n";
  339. echo "<h1>Index of ".htmlspecialchars($options['path'])."</h1>\n";
  340. echo "<pre>";
  341. printf($format, "Size", "Last modified", "Filename");
  342. echo "<hr>";
  343. while ($filename = readdir($handle)) {
  344. if ($filename != "." && $filename != "..") {
  345. $fullpath = $fspath."/".$filename;
  346. $name = htmlspecialchars($filename);
  347. printf($format,
  348. number_format(filesize($fullpath)),
  349. strftime("%Y-%m-%d %H:%M:%S", filemtime($fullpath)),
  350. "<a href='$name'>$name</a>");
  351. }
  352. }
  353. echo "</pre>";
  354. closedir($handle);
  355. echo "</html>\n";
  356. exit;
  357. }
  358. /**
  359. * PUT method handler
  360. *
  361. * @param array parameter passing array
  362. * @return bool true on success
  363. */
  364. function PUT(&$options)
  365. {
  366. $fspath = $this->base . $options["path"];
  367. if (!@is_dir(dirname($fspath))) {
  368. return "409 Conflict";
  369. }
  370. $options["new"] = ! file_exists($fspath);
  371. $fp = fopen($fspath, "w");
  372. return $fp;
  373. }
  374. /**
  375. * MKCOL method handler
  376. *
  377. * @param array general parameter passing array
  378. * @return bool true on success
  379. */
  380. function MKCOL($options)
  381. {
  382. $path = $this->base .$options["path"];
  383. $parent = dirname($path);
  384. $name = basename($path);
  385. if (!file_exists($parent)) {
  386. return "409 Conflict";
  387. }
  388. if (!is_dir($parent)) {
  389. return "403 Forbidden";
  390. }
  391. if ( file_exists($parent."/".$name) ) {
  392. return "405 Method not allowed";
  393. }
  394. if (!empty($this->_SERVER["CONTENT_LENGTH"])) { // no body parsing yet
  395. return "415 Unsupported media type";
  396. }
  397. $stat = mkdir($parent."/".$name, 0777);
  398. if (!$stat) {
  399. return "403 Forbidden";
  400. }
  401. return ("201 Created");
  402. }
  403. /**
  404. * DELETE method handler
  405. *
  406. * @param array general parameter passing array
  407. * @return bool true on success
  408. */
  409. function DELETE($options)
  410. {
  411. $path = $this->base . "/" .$options["path"];
  412. if (!file_exists($path)) {
  413. return "404 Not found";
  414. }
  415. if (is_dir($path)) {
  416. $query = "DELETE FROM {$this->db_prefix}properties
  417. WHERE path LIKE '".$this->_slashify($options["path"])."%'";
  418. mysql_query($query);
  419. System::rm("-rf $path");
  420. } else {
  421. unlink($path);
  422. }
  423. $query = "DELETE FROM {$this->db_prefix}properties
  424. WHERE path = '$options[path]'";
  425. mysql_query($query);
  426. return "204 No Content";
  427. }
  428. /**
  429. * MOVE method handler
  430. *
  431. * @param array general parameter passing array
  432. * @return bool true on success
  433. */
  434. function MOVE($options)
  435. {
  436. return $this->COPY($options, true);
  437. }
  438. /**
  439. * COPY method handler
  440. *
  441. * @param array general parameter passing array
  442. * @return bool true on success
  443. */
  444. function COPY($options, $del=false)
  445. {
  446. // TODO Property updates still broken (Litmus should detect this?)
  447. if (!empty($this->_SERVER["CONTENT_LENGTH"])) { // no body parsing yet
  448. return "415 Unsupported media type";
  449. }
  450. // no copying to different WebDAV Servers yet
  451. if (isset($options["dest_url"])) {
  452. return "502 bad gateway";
  453. }
  454. $source = $this->base .$options["path"];
  455. if (!file_exists($source)) return "404 Not found";
  456. $dest = $this->base . $options["dest"];
  457. $new = !file_exists($dest);
  458. $existing_col = false;
  459. if (!$new) {
  460. if ($del && is_dir($dest)) {
  461. if (!$options["overwrite"]) {
  462. return "412 precondition failed";
  463. }
  464. $dest .= basename($source);
  465. if (file_exists($dest)) {
  466. $options["dest"] .= basename($source);
  467. } else {
  468. $new = true;
  469. $existing_col = true;
  470. }
  471. }
  472. }
  473. if (!$new) {
  474. if ($options["overwrite"]) {
  475. $stat = $this->DELETE(array("path" => $options["dest"]));
  476. if (($stat{0} != "2") && (substr($stat, 0, 3) != "404")) {
  477. return $stat;
  478. }
  479. } else {
  480. return "412 precondition failed";
  481. }
  482. }
  483. if (is_dir($source) && ($options["depth"] != "infinity")) {
  484. // RFC 2518 Section 9.2, last paragraph
  485. return "400 Bad request";
  486. }
  487. if ($del) {
  488. if (!rename($source, $dest)) {
  489. return "500 Internal server error";
  490. }
  491. $destpath = $this->_unslashify($options["dest"]);
  492. if (is_dir($source)) {
  493. $query = "UPDATE {$this->db_prefix}properties
  494. SET path = REPLACE(path, '".$options["path"]."', '".$destpath."')
  495. WHERE path LIKE '".$this->_slashify($options["path"])."%'";
  496. mysql_query($query);
  497. }
  498. $query = "UPDATE {$this->db_prefix}properties
  499. SET path = '".$destpath."'
  500. WHERE path = '".$options["path"]."'";
  501. mysql_query($query);
  502. } else {
  503. if (is_dir($source)) {
  504. $files = System::find($source);
  505. $files = array_reverse($files);
  506. } else {
  507. $files = array($source);
  508. }
  509. if (!is_array($files) || empty($files)) {
  510. return "500 Internal server error";
  511. }
  512. foreach ($files as $file) {
  513. if (is_dir($file)) {
  514. $file = $this->_slashify($file);
  515. }
  516. $destfile = str_replace($source, $dest, $file);
  517. if (is_dir($file)) {
  518. if (!is_dir($destfile)) {
  519. // TODO "mkdir -p" here? (only natively supported by PHP 5)
  520. if (!@mkdir($destfile)) {
  521. return "409 Conflict";
  522. }
  523. }
  524. } else {
  525. if (!@copy($file, $destfile)) {
  526. return "409 Conflict";
  527. }
  528. }
  529. }
  530. $query = "INSERT INTO {$this->db_prefix}properties
  531. SELECT *
  532. FROM {$this->db_prefix}properties
  533. WHERE path = '".$options['path']."'";
  534. }
  535. return ($new && !$existing_col) ? "201 Created" : "204 No Content";
  536. }
  537. /**
  538. * PROPPATCH method handler
  539. *
  540. * @param array general parameter passing array
  541. * @return bool true on success
  542. */
  543. function PROPPATCH(&$options)
  544. {
  545. global $prefs, $tab;
  546. $msg = "";
  547. $path = $options["path"];
  548. $dir = dirname($path)."/";
  549. $base = basename($path);
  550. foreach ($options["props"] as $key => $prop) {
  551. if ($prop["ns"] == "DAV:") {
  552. $options["props"][$key]['status'] = "403 Forbidden";
  553. } else {
  554. if (isset($prop["val"])) {
  555. $query = "REPLACE INTO {$this->db_prefix}properties
  556. SET path = '$options[path]'
  557. , name = '$prop[name]'
  558. , ns= '$prop[ns]'
  559. , value = '$prop[val]'";
  560. } else {
  561. $query = "DELETE FROM {$this->db_prefix}properties
  562. WHERE path = '$options[path]'
  563. AND name = '$prop[name]'
  564. AND ns = '$prop[ns]'";
  565. }
  566. mysql_query($query);
  567. }
  568. }
  569. return "";
  570. }
  571. /**
  572. * LOCK method handler
  573. *
  574. * @param array general parameter passing array
  575. * @return bool true on success
  576. */
  577. function LOCK(&$options)
  578. {
  579. // get absolute fs path to requested resource
  580. $fspath = $this->base . $options["path"];
  581. // TODO recursive locks on directories not supported yet
  582. if (is_dir($fspath) && !empty($options["depth"])) {
  583. return "409 Conflict";
  584. }
  585. $options["timeout"] = time()+300; // 5min. hardcoded
  586. if (isset($options["update"])) { // Lock Update
  587. $where = "WHERE path = '$options[path]' AND token = '$options[update]'";
  588. $query = "SELECT owner, exclusivelock FROM {$this->db_prefix}locks $where";
  589. $res = mysql_query($query);
  590. $row = mysql_fetch_assoc($res);
  591. mysql_free_result($res);
  592. if (is_array($row)) {
  593. $query = "UPDATE {$this->db_prefix}locks
  594. SET expires = '$options[timeout]'
  595. , modified = ".time()."
  596. $where";
  597. mysql_query($query);
  598. $options['owner'] = $row['owner'];
  599. $options['scope'] = $row["exclusivelock"] ? "exclusive" : "shared";
  600. $options['type'] = $row["exclusivelock"] ? "write" : "read";
  601. return true;
  602. } else {
  603. return false;
  604. }
  605. }
  606. $query = "INSERT INTO {$this->db_prefix}locks
  607. SET token = '$options[locktoken]'
  608. , path = '$options[path]'
  609. , created = ".time()."
  610. , modified = ".time()."
  611. , owner = '$options[owner]'
  612. , expires = '$options[timeout]'
  613. , exclusivelock = " .($options['scope'] === "exclusive" ? "1" : "0")
  614. ;
  615. mysql_query($query);
  616. return mysql_affected_rows() ? "200 OK" : "409 Conflict";
  617. }
  618. /**
  619. * UNLOCK method handler
  620. *
  621. * @param array general parameter passing array
  622. * @return bool true on success
  623. */
  624. function UNLOCK(&$options)
  625. {
  626. $query = "DELETE FROM {$this->db_prefix}locks
  627. WHERE path = '$options[path]'
  628. AND token = '$options[token]'";
  629. mysql_query($query);
  630. return mysql_affected_rows() ? "204 No Content" : "409 Conflict";
  631. }
  632. /**
  633. * checkLock() helper
  634. *
  635. * @param string resource path to check for locks
  636. * @return bool true on success
  637. */
  638. function checkLock($path)
  639. {
  640. $result = false;
  641. $query = "SELECT owner, token, created, modified, expires, exclusivelock
  642. FROM {$this->db_prefix}locks
  643. WHERE path = '$path'
  644. ";
  645. $res = mysql_query($query);
  646. if ($res) {
  647. $row = mysql_fetch_array($res);
  648. mysql_free_result($res);
  649. if ($row) {
  650. $result = array( "type" => "write",
  651. "scope" => $row["exclusivelock"] ? "exclusive" : "shared",
  652. "depth" => 0,
  653. "owner" => $row['owner'],
  654. "token" => $row['token'],
  655. "created" => $row['created'],
  656. "modified" => $row['modified'],
  657. "expires" => $row['expires']
  658. );
  659. }
  660. }
  661. return $result;
  662. }
  663. /**
  664. * create database tables for property and lock storage
  665. *
  666. * @param void
  667. * @return bool true on success
  668. */
  669. function create_database()
  670. {
  671. // TODO
  672. return false;
  673. }
  674. }
  675. /*
  676. * Local variables:
  677. * tab-width: 4
  678. * c-basic-offset: 4
  679. * indent-tabs-mode:nil
  680. * End:
  681. */