PageRenderTime 45ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

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

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