PageRenderTime 53ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/inc/HTTP/WebDAV/Server/Filesystem.php

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