Filesystem.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  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. */
  85. function check_auth($type, $user, $pass)
  86. {
  87. return true;
  88. }
  89. function PROPFIND($options, &$files)
  90. {
  91. // get absolute fs path to requested resource
  92. $fspath = realpath($this->base . $options["path"]);
  93. // sanity check
  94. if (!file_exists($fspath)) {
  95. return false;
  96. }
  97. // prepare property array
  98. $files["files"] = array();
  99. // store information for the requested path itself
  100. $files["files"][] = $this->fileinfo($options["path"], $options);
  101. // information for contained resources requested?
  102. if (!empty($options["depth"])) { // TODO check for is_dir() first?
  103. // make sure path ends with '/'
  104. if (substr($options["path"],-1) != "/") {
  105. $options["path"] .= "/";
  106. }
  107. // try to open directory
  108. $handle = @opendir($fspath);
  109. if ($handle) {
  110. // ok, now get all its contents
  111. while ($filename = readdir($handle)) {
  112. if ($filename != "." && $filename != "..") {
  113. $files["files"][] = $this->fileinfo ($options["path"].$filename, $options);
  114. }
  115. }
  116. // TODO recursion needed if "Depth: infinite"
  117. }
  118. }
  119. // ok, all done
  120. return true;
  121. }
  122. function fileinfo($uri, $options)
  123. {
  124. $fspath = $this->base . $uri;
  125. $file = array();
  126. $file["path"]= $uri;
  127. $file["props"][] = $this->mkprop("displayname", strtoupper($uri));
  128. $file["props"][] = $this->mkprop("creationdate", filectime($fspath));
  129. $file["props"][] = $this->mkprop("getlastmodified", filemtime($fspath));
  130. if (is_dir($fspath)) {
  131. $file["props"][] = $this->mkprop("getcontentlength", 0);
  132. $file["props"][] = $this->mkprop("resourcetype", "collection");
  133. $file["props"][] = $this->mkprop("getcontenttype", "httpd/unix-directory");
  134. } else {
  135. $file["props"][] = $this->mkprop("resourcetype", "");
  136. $file["props"][] = $this->mkprop("getcontentlength", filesize($fspath));
  137. if (is_readable($fspath)) {
  138. $file["props"][] = $this->mkprop("getcontenttype", $this->_mimetype($fspath));
  139. } else {
  140. $file["props"][] = $this->mkprop("getcontenttype", "application/x-non-readable");
  141. }
  142. }
  143. $query = "SELECT ns, name, value FROM properties WHERE path = '$uri'";
  144. $res = mysql_query($query);
  145. while($row = mysql_fetch_assoc($res)) {
  146. $file["props"][] = $this->mkprop($row["ns"], $row["name"], $row["value"]);
  147. }
  148. mysql_free_result($res);
  149. return $file;
  150. }
  151. /* @@@ */
  152. function _can_execute($name, $path=false)
  153. {
  154. if (!strncmp(PHP_OS, "WIN", 3)) {
  155. $exts = array(".exe", ".com");
  156. } else {
  157. $exts = array("");
  158. }
  159. if ($path===false) {
  160. $path = getenv("PATH");
  161. }
  162. foreach (explode(PATH_SEPARATOR, $path) as $dir) {
  163. if (!@is_dir($dir)) continue;
  164. foreach ($exts as $ext) {
  165. if (@is_executable("$dir/$name".$ext)) return true;
  166. }
  167. }
  168. }
  169. function _mimetype($fspath)
  170. {
  171. if (@is_dir($fspath)) {
  172. return "httpd/unix-directory"; // TODO what on Windows? ;>
  173. } else if (function_exists("mime_content_type")) {
  174. // use mime magic extension if available
  175. $mime_type = mime_content_type($fspath);
  176. } else if ($this->_can_execute("file")) {
  177. // it looks like we have a 'file' command,
  178. // lets see it it does have mime support
  179. $fp = popen("file -i '$fspath' 2>/dev/null", "r");
  180. $reply = fgets($fp);
  181. pclose($fp);
  182. // popen will not return an error if the binary was not found
  183. // and find may not have mime support using "-i"
  184. // so we test the format of the returned string
  185. // the reply begins with the requested filename
  186. if (!strncmp($reply, "$fspath: ", strlen($fspath)+2)) {
  187. $reply = substr($reply, strlen($fspath)+2);
  188. // followed by the mime type (maybe including options)
  189. if (ereg("^[[:alnum:]_-]+/[[:alnum:]_-]+;?.*", $reply, $matches)) {
  190. $mime_type = $matches[0];
  191. }
  192. }
  193. }
  194. if (empty($mime_type)) {
  195. // Fallback solution: try to guess the type by the file extension
  196. // TODO: add more ...
  197. // TODO: can we use the registry for this on Windows?
  198. // OTOH if the server is Windos the clients are likely to
  199. // be Windows, too, and tend do ignore the Content-Type
  200. // anyway (overriding it with information taken from
  201. // the registry)
  202. // TODO: have a seperate PEAR class for mimetype detection?
  203. switch (strtolower(strrchr(basename($fspath), "."))) {
  204. case ".html":
  205. $mime_type = "text/html";
  206. break;
  207. case ".gif":
  208. $mime_type = "image/gif";
  209. break;
  210. case ".jpg":
  211. $mime_type = "image/jpeg";
  212. break;
  213. default:
  214. $mime_type = "application/octet-stream";
  215. break;
  216. }
  217. }
  218. return $mime_type;
  219. }
  220. function GET(&$options)
  221. {
  222. $fspath = $this->base . $options["path"];
  223. if (file_exists($fspath)) {
  224. $options['mimetype'] = $this->_mimetype($fspath);
  225. // see rfc2518, section 13.7
  226. // some clients seem to treat this as a reverse rule
  227. // requiering a Last-Modified header if the getlastmodified header was set
  228. $options['mtime'] = filemtime($fspath);
  229. $options['size'] = filesize($fspath);
  230. // TODO check permissions/result
  231. $options['stream'] = fopen($fspath, "r");
  232. return true;
  233. } else {
  234. return false;
  235. }
  236. }
  237. function PUT(&$options)
  238. {
  239. $fspath = $this->base . $options["path"];
  240. if(!@is_dir(dirname($fspath))) {
  241. return "409 Conflict";
  242. }
  243. $options["new"] = ! file_exists($fspath);
  244. $fp = fopen($fspath, "w");
  245. return $fp;
  246. }
  247. function MKCOL($options)
  248. {
  249. $path = $this->base .$options["path"];
  250. $parent = dirname($path);
  251. $name = basename($path);
  252. if(!file_exists($parent)) {
  253. return "409 Conflict";
  254. }
  255. if(!is_dir($parent)) {
  256. return "403 Forbidden";
  257. }
  258. if( file_exists($parent."/".$name) ) {
  259. return "405 Method not allowed";
  260. }
  261. if(!empty($_SERVER["CONTENT_LENGTH"])) { // no body parsing yet
  262. return "415 Unsupported media type";
  263. }
  264. $stat = mkdir ($parent."/".$name,0777);
  265. if(!$stat) {
  266. return "403 Forbidden";
  267. }
  268. return ("201 Created");
  269. }
  270. function delete($options)
  271. {
  272. $path = $this->base . "/" .$options["path"];
  273. if(!file_exists($path)) return "404 Not found";
  274. if (is_dir($path)) {
  275. $query = "DELETE FROM properties WHERE path LIKE '$options[path]%'";
  276. mysql_query($query);
  277. system("rm -rf $path");
  278. } else {
  279. unlink ($path);
  280. }
  281. $query = "DELETE FROM properties WHERE path = '$options[path]'";
  282. mysql_query($query);
  283. return "204 No Content";
  284. }
  285. function move($options)
  286. {
  287. return $this->copy($options, true);
  288. }
  289. function copy($options, $del=false)
  290. {
  291. // TODO Property updates still broken (Litmus should detect this?)
  292. if(!empty($_SERVER["CONTENT_LENGTH"])) { // no body parsing yet
  293. return "415 Unsupported media type";
  294. }
  295. // no copying to different WebDAV Servers yet
  296. if(isset($options["dest_url"])) {
  297. return "502 bad gateway";
  298. }
  299. $source = $this->base .$options["path"];
  300. if(!file_exists($source)) return "404 Not found";
  301. $dest = $this->base . $options["dest"];
  302. $new = !file_exists($dest);
  303. $existing_col = false;
  304. if(!$new) {
  305. if($del && is_dir($dest)) {
  306. if(!$options["overwrite"]) {
  307. return "412 precondition failed";
  308. }
  309. $dest .= basename($source);
  310. if(file_exists($dest.basename($source))) {
  311. $options["dest"] .= basename($source);
  312. } else {
  313. $new = true;
  314. $existing_col = true;
  315. }
  316. }
  317. }
  318. if(!$new) {
  319. if($options["overwrite"]) {
  320. $stat = $this->delete(array("path" => $options["dest"]));
  321. if($stat{0} != "2") return $stat;
  322. } else {
  323. return "412 precondition failed";
  324. }
  325. }
  326. if (is_dir($source)) {
  327. // RFC 2518 Section 9.2, last paragraph
  328. if ($options["depth"] != "infinity") {
  329. error_log("---- ".$options["depth"]);
  330. return "400 Bad request";
  331. }
  332. system("cp -R $source $dest");
  333. if($del) {
  334. system("rm -rf $source");
  335. }
  336. } else {
  337. if($del) {
  338. @unlink($dest);
  339. $query = "DELETE FROM properties WHERE path = '$options[dest]'";
  340. mysql_query($query);
  341. rename($source, $dest);
  342. $query = "UPDATE properties SET path = '$options[dest]' WHERE path = '$options[path]'";
  343. mysql_query($query);
  344. } else {
  345. if(substr($dest,-1)=="/") $dest = substr($dest,0,-1);
  346. copy($source, $dest);
  347. }
  348. }
  349. return ($new && !$existing_col) ? "201 Created" : "204 No Content";
  350. }
  351. function proppatch(&$options)
  352. {
  353. global $prefs, $tab;
  354. $msg = "";
  355. $path = $options["path"];
  356. $dir = dirname($path)."/";
  357. $base = basename($path);
  358. foreach($options["props"] as $key => $prop) {
  359. if($ns == "DAV:") {
  360. $options["props"][$key][$status] = "403 Forbidden";
  361. } else {
  362. if(isset($prop["val"])) {
  363. $query = "REPLACE INTO properties SET path = '$options[path]', name = '$prop[name]', ns= '$prop[ns]', value = '$prop[val]'";
  364. } else {
  365. $query = "DELETE FROM properties WHERE path = '$options[path]' AND name = '$prop[name]' AND ns = '$prop[ns]'";
  366. }
  367. mysql_query($query);
  368. }
  369. }
  370. return "";
  371. }
  372. function lock(&$options)
  373. {
  374. if(isset($options["update"])) { // Lock Update
  375. $query = "UPDATE locks SET expires = ".(time()+300);
  376. mysql_query($query);
  377. if(mysql_affected_rows()) {
  378. $options["timeout"] = 300; // 5min hardcoded
  379. return true;
  380. } else {
  381. return false;
  382. }
  383. }
  384. $options["timeout"] = time()+300; // 5min. hardcoded
  385. $query = "INSERT INTO locks
  386. SET token = '$options[locktoken]'
  387. , path = '$options[path]'
  388. , owner = '$options[owner]'
  389. , expires = '$options[timeout]'
  390. , exclusivelock = " .($options['scope'] === "exclusive" ? "1" : "0")
  391. ;
  392. mysql_query($query);
  393. return mysql_affected_rows() > 0;
  394. return "200 OK";
  395. }
  396. function unlock(&$options)
  397. {
  398. $query = "DELETE FROM locks
  399. WHERE path = '$options[path]'
  400. AND token = '$options[token]'";
  401. mysql_query($query);
  402. return mysql_affected_rows() ? "200 OK" : "409 Conflict";
  403. }
  404. function checklock($path)
  405. {
  406. $result = false;
  407. $query = "SELECT owner, token, expires, exclusivelock
  408. FROM locks
  409. WHERE path = '$path'
  410. ";
  411. $res = mysql_query($query);
  412. if($res) {
  413. $row = mysql_fetch_array($res);
  414. mysql_free_result($res);
  415. if($row) {
  416. $result = array( "type" => "write",
  417. "scope" => $row["exclusivelock"] ? "exclusive" : "shared",
  418. "depth" => 0,
  419. "owner" => $row['owner'],
  420. "token" => $row['token'],
  421. "expires" => $row['expires']
  422. );
  423. }
  424. }
  425. return $result;
  426. }
  427. function create_database()
  428. {
  429. // TODO
  430. }
  431. }
  432. ?>