1
0

server.php 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. <?php
  2. namespace KD2\WebDAV
  3. {
  4. //__KD2\WebDAV\Server__
  5. //__KD2\WebDAV\AbstractStorage__
  6. }
  7. namespace NanoKaraDAV
  8. {
  9. use KD2\WebDAV\AbstractStorage;
  10. use KD2\WebDAV\Exception as WebDAV_Exception;
  11. class Storage extends AbstractStorage
  12. {
  13. /**
  14. * These file names will be ignored when doing a PUT
  15. * as they are garbage, coming from some OS
  16. */
  17. const PUT_IGNORE_PATTERN = '!^~(?:lock\.|^\._)|^(?:\.DS_Store|Thumbs\.db|desktop\.ini)$!';
  18. protected string $path;
  19. public function __construct()
  20. {
  21. $this->path = __DIR__ . '/';
  22. }
  23. public function list(string $uri, ?array $properties): iterable
  24. {
  25. $dirs = glob($this->path . $uri . '/*', \GLOB_ONLYDIR);
  26. $dirs = array_map('basename', $dirs);
  27. natcasesort($dirs);
  28. $files = glob($this->path . $uri . '/*');
  29. $files = array_map('basename', $files);
  30. $files = array_diff($files, $dirs);
  31. // Remove PHP files from listings
  32. $files = array_filter($files, fn($a) => !preg_match('/\.(?:php\d?|phtml|phps)$/i', $a));
  33. if (!$uri) {
  34. $files = array_diff($files, ['webdav.js', 'webdav.css']);
  35. }
  36. natcasesort($files);
  37. $files = array_flip(array_merge($dirs, $files));
  38. $files = array_map(fn($a) => null, $files);
  39. return $files;
  40. }
  41. public function get(string $uri): ?array
  42. {
  43. $path = $this->path . $uri;
  44. if (!file_exists($path)) {
  45. return null;
  46. }
  47. return ['path' => $path];
  48. }
  49. public function exists(string $uri): bool
  50. {
  51. return file_exists($this->path . $uri);
  52. }
  53. public function get_file_property(string $uri, string $name, int $depth)
  54. {
  55. $target = $this->path . $uri;
  56. switch ($name) {
  57. case 'DAV::getcontentlength':
  58. return is_dir($target) ? null : filesize($target);
  59. case 'DAV::getcontenttype':
  60. // ownCloud app crashes if mimetype is provided for a directory
  61. // https://github.com/owncloud/android/issues/3768
  62. return is_dir($target) ? null : mime_content_type($target);
  63. case 'DAV::resourcetype':
  64. return is_dir($target) ? 'collection' : '';
  65. case 'DAV::getlastmodified':
  66. if (!$uri && $depth == 0 && is_dir($target)) {
  67. $mtime = self::getDirectoryMTime($target);
  68. }
  69. else {
  70. $mtime = filemtime($target);
  71. }
  72. if (!$mtime) {
  73. return null;
  74. }
  75. return new \DateTime('@' . $mtime);
  76. case 'DAV::displayname':
  77. return basename($target);
  78. case 'DAV::ishidden':
  79. return basename($target)[0] == '.';
  80. case 'DAV::getetag':
  81. $hash = filemtime($target) . filesize($target);
  82. return md5($hash . $target);
  83. case 'DAV::lastaccessed':
  84. return new \DateTime('@' . fileatime($target));
  85. case 'DAV::creationdate':
  86. return new \DateTime('@' . filectime($target));
  87. case WebDAV::PROP_DIGEST_MD5:
  88. if (!is_file($target)) {
  89. return null;
  90. }
  91. return md5_file($target);
  92. default:
  93. break;
  94. }
  95. return null;
  96. }
  97. public function properties(string $uri, ?array $properties, int $depth): ?array
  98. {
  99. $target = $this->path . $uri;
  100. if (!file_exists($target)) {
  101. return null;
  102. }
  103. if (null === $properties) {
  104. $properties = WebDAV::BASIC_PROPERTIES;
  105. }
  106. $out = [];
  107. foreach ($properties as $name) {
  108. $v = $this->get_file_property($uri, $name, $depth);
  109. if (null !== $v) {
  110. $out[$name] = $v;
  111. }
  112. }
  113. return $out;
  114. }
  115. public function put(string $uri, $pointer, ?string $hash, ?int $mtime): bool
  116. {
  117. if (preg_match(self::PUT_IGNORE_PATTERN, basename($uri))) {
  118. return false;
  119. }
  120. $target = $this->path . $uri;
  121. $parent = dirname($target);
  122. if (is_dir($target)) {
  123. throw new WebDAV_Exception('Target is a directory', 409);
  124. }
  125. if (!file_exists($parent)) {
  126. mkdir($parent, 0770, true);
  127. }
  128. $new = !file_exists($target);
  129. $delete = false;
  130. $size = 0;
  131. $quota = disk_free_space($this->path);
  132. $tmp_file = '.tmp.' . sha1($target);
  133. $out = fopen($tmp_file, 'w');
  134. while (!feof($pointer)) {
  135. $bytes = fread($pointer, 8192);
  136. $size += strlen($bytes);
  137. if ($size > $quota) {
  138. $delete = true;
  139. break;
  140. }
  141. fwrite($out, $bytes);
  142. }
  143. fclose($out);
  144. fclose($pointer);
  145. if ($delete) {
  146. @unlink($tmp_file);
  147. throw new WebDAV_Exception('Your quota is exhausted', 403);
  148. }
  149. elseif ($hash && md5_file($tmp_file) != $hash) {
  150. @unlink($tmp_file);
  151. throw new WebDAV_Exception('The data sent does not match the supplied MD5 hash', 400);
  152. }
  153. else {
  154. rename($tmp_file, $target);
  155. }
  156. if ($mtime) {
  157. @touch($target, $mtime);
  158. }
  159. return $new;
  160. }
  161. public function delete(string $uri): void
  162. {
  163. $target = $this->path . $uri;
  164. if (!file_exists($target)) {
  165. throw new WebDAV_Exception('Target does not exist', 404);
  166. }
  167. if (is_dir($target)) {
  168. foreach (glob($target . '/*') as $file) {
  169. $this->delete(substr($file, strlen($this->path)));
  170. }
  171. rmdir($target);
  172. }
  173. else {
  174. unlink($target);
  175. }
  176. }
  177. public function copymove(bool $move, string $uri, string $destination): bool
  178. {
  179. $source = $this->path . $uri;
  180. $target = $this->path . $destination;
  181. $parent = dirname($target);
  182. if (!file_exists($source)) {
  183. throw new WebDAV_Exception('File not found', 404);
  184. }
  185. $overwritten = file_exists($target);
  186. if (!is_dir($parent)) {
  187. throw new WebDAV_Exception('Target parent directory does not exist', 409);
  188. }
  189. if (false === $move) {
  190. $quota = disk_free_space($this->path);
  191. if (filesize($source) > $quota) {
  192. throw new WebDAV_Exception('Your quota is exhausted', 403);
  193. }
  194. }
  195. if ($overwritten) {
  196. $this->delete($destination);
  197. }
  198. $method = $move ? 'rename' : 'copy';
  199. if ($method == 'copy' && is_dir($source)) {
  200. @mkdir($target, 0770, true);
  201. foreach ($iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source), \RecursiveIteratorIterator::SELF_FIRST) as $item)
  202. {
  203. if ($item->isDir()) {
  204. @mkdir($target . DIRECTORY_SEPARATOR . $iterator->getSubPathname());
  205. } else {
  206. copy($item, $target . DIRECTORY_SEPARATOR . $iterator->getSubPathname());
  207. }
  208. }
  209. }
  210. else {
  211. $method($source, $target);
  212. $this->getResourceProperties($uri)->move($destination);
  213. }
  214. return $overwritten;
  215. }
  216. public function copy(string $uri, string $destination): bool
  217. {
  218. return $this->copymove(false, $uri, $destination);
  219. }
  220. public function move(string $uri, string $destination): bool
  221. {
  222. return $this->copymove(true, $uri, $destination);
  223. }
  224. public function mkcol(string $uri): void
  225. {
  226. if (!disk_free_space($this->path)) {
  227. throw new WebDAV_Exception('Your quota is exhausted', 403);
  228. }
  229. $target = $this->path . $uri;
  230. $parent = dirname($target);
  231. if (file_exists($target)) {
  232. throw new WebDAV_Exception('There is already a file with that name', 405);
  233. }
  234. if (!file_exists($parent)) {
  235. throw new WebDAV_Exception('The parent directory does not exist', 409);
  236. }
  237. mkdir($target, 0770);
  238. }
  239. static public function getDirectoryMTime(string $path): int
  240. {
  241. $last = 0;
  242. $path = rtrim($path, '/');
  243. foreach (glob($path . '/*', GLOB_NOSORT) as $f) {
  244. if (is_dir($f)) {
  245. $m = self::getDirectoryMTime($f);
  246. if ($m > $last) {
  247. $last = $m;
  248. }
  249. }
  250. $m = filemtime($f);
  251. if ($m > $last) {
  252. $last = $m;
  253. }
  254. }
  255. return $last;
  256. }
  257. }
  258. class Server extends \KD2\WebDAV\Server
  259. {
  260. protected function html_directory(string $uri, iterable $list): ?string
  261. {
  262. $out = parent::html_directory($uri, $list);
  263. if (null !== $out) {
  264. $out = str_replace('<body>', sprintf('<body style="opacity: 0"><script type="text/javascript" src="%s/webdav.js"></script>', rtrim($this->base_uri, '/')), $out);
  265. }
  266. return $out;
  267. }
  268. }
  269. }
  270. namespace {
  271. use NanoKaraDAV\Server;
  272. use NanoKaraDAV\Storage;
  273. $uri = strtok($_SERVER['REQUEST_URI'], '?');
  274. $root = substr(__DIR__, strlen($_SERVER['DOCUMENT_ROOT']));
  275. if (false !== strpos($uri, '..')) {
  276. http_response_code(404);
  277. die('Invalid URL');
  278. }
  279. $relative_uri = ltrim(substr($uri, strlen($root)), '/');
  280. if ($relative_uri == 'webdav.js' || $relative_uri == 'webdav.css') {
  281. http_response_code(200);
  282. if ($relative_uri == 'webdav.js') {
  283. header('Content-Type: text/javascript', true);
  284. }
  285. else {
  286. header('Content-Type: text/css', true);
  287. }
  288. $seconds_to_cache = 3600 * 24 * 365;
  289. $ts = gmdate("D, d M Y H:i:s", time() + $seconds_to_cache) . " GMT";
  290. header("Expires: " . $ts);
  291. header("Pragma: cache");
  292. header("Cache-Control: max-age=" . $seconds_to_cache);
  293. $fp = fopen(__FILE__, 'r');
  294. if ($relative_uri == 'webdav.js') {
  295. fseek($fp, __PHP_SIZE__, SEEK_SET);
  296. echo fread($fp, __JS_SIZE__);
  297. }
  298. else {
  299. fseek($fp, __PHP_SIZE__ + __JS_SIZE__, SEEK_SET);
  300. echo fread($fp, __CSS_SIZE__);
  301. }
  302. fclose($fp);
  303. exit;
  304. }
  305. $dav = new Server;
  306. $dav->setStorage(new Storage);
  307. $dav->setBaseURI($root);
  308. if (!$dav->route($uri)) {
  309. http_response_code(404);
  310. die('Invalid URL, sorry');
  311. }
  312. exit;
  313. }