diff --git a/Makefile b/Makefile index d884e92..94b87fa 100644 --- a/Makefile +++ b/Makefile @@ -3,9 +3,6 @@ KD2FW_URL := https://fossil.kd2.org/kd2fw/doc/trunk/src/lib/KD2/ deps: @-mkdir -p lib/KD2/WebDAV - @-mkdir -p lib/KD2/HTTP - wget -O lib/KD2/HTTP/Server.php '${KD2FW_URL}HTTP/Server.php' - wget -O lib/KD2/WebDAV/Exception.php '${KD2FW_URL}WebDAV/Exception.php' wget -O lib/KD2/WebDAV/Server.php '${KD2FW_URL}WebDAV/Server.php' wget -O lib/KD2/WebDAV/AbstractStorage.php '${KD2FW_URL}WebDAV/AbstractStorage.php' wget -O webdav.js https://raw.githubusercontent.com/kd2org/webdav-manager.js/main/webdav.js diff --git a/README.md b/README.md index 30bc7f0..252ba01 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ If you drop the [`index.php`](https://fossil.kd2.org/picodav/doc/trunk/index.php) file in a directory of your web-server, it will make the contents of this directory available via WebDAV, and will also provide a nice web UI to manage the files, using [WebDAV Manager.js](https://fossil.kd2.org/webdav-manager/). -![Web UI screenshot](scr.webp) +![Web UI screenshot](https://raw.githubusercontent.com/kd2org/webdav-manager.js/main/scr_desktop.png) -* Single-file WebDAV server! Only 110 KB! +* Single-file WebDAV server! Only 85 KB! * No database! * Very fast and lightweight! * Compatible with tons of apps! diff --git a/index.php b/index.php index 6f44439..fb6e1b0 100644 --- a/index.php +++ b/index.php @@ -1,242 +1,10 @@ $size) { - throw new \LogicException('End range cannot be satisfied', 416); - } - } - elseif ($allow_gzip - && isset($_SERVER['HTTP_ACCEPT_ENCODING']) - && false !== strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') - && isset($size, $name) - // Don't compress if size is larger than 8 MiB - && $size < 8*1024*1024 - // Don't compress already compressed content - && !preg_match('/\.(?:cbz|cbr|cb7|mp4|m4a|zip|docx|xlsx|pptx|ods|odt|odp|7z|gz|bz2|lzma|lz|xz|apk|dmg|jar|rar|webm|ogg|mp3|ogm|flac|ogv|mkv|avi)$/i', $name)) { - $gzip = true; - header('Content-Encoding: gzip', true); - } - - // Try to avoid common issues with output buffering and stuff - if (function_exists('apache_setenv')) { - @apache_setenv('no-gzip', 1); - } - - @ini_set('zlib.output_compression', 'Off'); - - // Clean buffer, just in case - if (@ob_get_length()) { - @ob_clean(); - } - - if (isset($content)) { - $length = strlen($content); - - if ($start || $end) { - if (null !== $end && $end > $length) { - header('Content-Range: bytes */' . $length, true); - throw new \LogicException('End range cannot be satisfied', 416); - } - - if ($start === null) { - $start = $length - $end; - $end = $start + $end; - } - elseif ($end === null) { - $end = $length; - } - - http_response_code(206); - header(sprintf('Content-Range: bytes %s-%s/%s', $start, $end - 1, $length)); - $content = substr($content, $start, $end - $start); - $length = $end - $start; - } - - if ($gzip) { - $content = gzencode($content, 9); - $length = strlen($content); - } - - if (!$disable_length) { - header('Content-Length: ' . $length, true); - header('Accept-Ranges: bytes'); - } - - echo $content; - return; - } - - if (isset($path)) { - $resource = fopen($path, 'rb'); - } - - $seek = fseek($resource, 0, SEEK_END); - - if ($seek === 0) { - $length = ftell($resource); - fseek($resource, 0, SEEK_SET); - } - - http_response_code(200); - - if (($start || $end) && $seek === 0) { - if (null !== $end && $end > $length) { - header('Content-Range: bytes */' . $length, true); - throw new \LogicException('End range cannot be satisfied', 416); - } - - if ($start === null) { - $start = $length - $end; - $end = $start + $end; - } - elseif ($end === null) { - $end = $length; - } - - fseek($resource, $start, SEEK_SET); - - http_response_code(206); - header(sprintf('Content-Range: bytes %s-%s/%s', $start, $end - 1, $length), true); - - $length = $end - $start; - $end -= $start; - } - elseif (null === $length && isset($path)) { - $end = $length = filesize($path); - } - - if ($gzip) { - $gzip = deflate_init(ZLIB_ENCODING_GZIP); - - $fp = fopen('php://temp', 'wb'); - - while (!feof($resource)) { - fwrite($fp, deflate_add($gzip, fread($resource, 8192), ZLIB_NO_FLUSH)); - } - - fwrite($fp, deflate_add($gzip, '', ZLIB_FINISH)); - $length = ftell($fp); - rewind($fp); - unset($resource); - - $resource = $fp; - unset($fp); - } - - if (null !== $length && !$disable_length) { - header('Content-Length: ' . $length, true); - header('Accept-Ranges: bytes'); - } - - $block_size = 8192*4; - - while (!feof($resource) && ($end === null || $end > 0)) { - $l = $end !== null ? min($block_size, $end) : $block_size; - - echo fread($resource, $l); - flush(); - - if (null !== $end) { - $end -= $block_size; - } - } - } - } - -} - namespace KD2\WebDAV { class Exception extends \RuntimeException {} - - - use KD2\HTTP\Server as HTTP_Server; - class Server { // List of basic DAV properties that you should return if $requested_properties is NULL @@ -257,6 +25,8 @@ namespace KD2\WebDAV 'DAV::quota-available-bytes', ]; + const PROP_NAMESPACE_MICROSOFT = 'urn:schemas-microsoft-com:'; + const MODIFICATION_TIME_PROPERTIES = [ 'DAV::lastmodified', 'DAV::creationdate', @@ -274,8 +44,6 @@ namespace KD2\WebDAV const SHARED_LOCK = 'shared'; const EXCLUSIVE_LOCK = 'exclusive'; - const DATE_RFC7231 = "D, d M Y H:i:s \\G\\M\\T"; - protected bool $enable_gzip = true; protected string $base_uri; @@ -286,24 +54,6 @@ namespace KD2\WebDAV protected AbstractStorage $storage; - protected array $headers; - - public function __construct() - { - $this->headers = apache_request_headers(); - $this->headers = array_change_key_case($this->headers, \CASE_LOWER); - } - - public function getHeader(string $name): ?string - { - return $this->headers[strtolower($name)] ?? null; - } - - public function setHeader(string $name, string $value): void - { - $this->headers[strtolower($name)] = $value; - } - public function setStorage(AbstractStorage $storage) { $this->storage = $storage; @@ -433,16 +183,12 @@ namespace KD2\WebDAV public function http_put(string $uri): ?string { - $content_type = $this->getHeader('Content-Type'); - - if ($content_type && !strncmp($content_type, 'multipart/', 10)) { + if (!empty($_SERVER['HTTP_CONTENT_TYPE']) && !strncmp($_SERVER['HTTP_CONTENT_TYPE'], 'multipart/', 10)) { throw new Exception('Multipart PUT requests are not supported', 501); } - $content_encoding = $this->getHeader('Content-Encoding'); - - if ($content_encoding) { - if (false !== strpos($content_encoding, 'gzip')) { + if (!empty($_SERVER['HTTP_CONTENT_ENCODING'])) { + if (false !== strpos($_SERVER['HTTP_CONTENT_ENCODING'], 'gzip')) { // Might be supported later? throw new Exception('Content Encoding is not supported', 501); } @@ -451,12 +197,12 @@ namespace KD2\WebDAV } } - if ($this->getHeader('Content-Range')) { + if (!empty($_SERVER['HTTP_CONTENT_RANGE'])) { throw new Exception('Content Range is not supported', 501); } // See SabreDAV CorePlugin for reason why OS/X Finder is buggy - if ($this->getHeader('X-Expected-Entity-Length')) { + if (isset($_SERVER['HTTP_X_EXPECTED_ENTITY_LENGTH'])) { throw new Exception('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.', 403); } @@ -465,14 +211,14 @@ namespace KD2\WebDAV // Support for checksum matching // https://dcache.org/old/manuals/UserGuide-6.0/webdav.shtml#checksums - if ($hash = $this->getHeader('Content-MD5')) { - $hash = bin2hex(base64_decode($hash)); + if (!empty($_SERVER['HTTP_CONTENT_MD5'])) { + $hash = bin2hex(base64_decode($_SERVER['HTTP_CONTENT_MD5'])); $hash_algo = 'MD5'; } // Support for ownCloud/NextCloud checksum // https://github.com/owncloud-archive/documentation/issues/2964 - elseif (($checksum = $this->getHeader('OC-Checksum')) - && preg_match('/MD5:[a-f0-9]{32}|SHA1:[a-f0-9]{40}/', $checksum, $match)) { + elseif (!empty($_SERVER['HTTP_OC_CHECKSUM']) + && preg_match('/MD5:[a-f0-9]{32}|SHA1:[a-f0-9]{40}/', $_SERVER['HTTP_OC_CHECKSUM'], $match)) { $hash_algo = strtok($match[0], ':'); $hash = strtok(''); } @@ -481,8 +227,8 @@ namespace KD2\WebDAV $this->checkLock($uri); - if ($match = $this->getHeader('If-Match')) { - $etag = trim($match, '" '); + if (!empty($_SERVER['HTTP_IF_MATCH'])) { + $etag = trim($_SERVER['HTTP_IF_MATCH'], '" '); $prop = $this->storage->propfind($uri, ['DAV::getetag'], 0); if (!empty($prop['DAV::getetag']) && $prop['DAV::getetag'] != $etag) { @@ -490,19 +236,9 @@ namespace KD2\WebDAV } } - if ($date = $this->getHeader('If-Unmodified-Since')) { - $date = \DateTime::createFromFormat(self::DATE_RFC7231, $date); - $prop = $this->storage->propfind($uri, ['DAV::getlastmodified'], 0); - if ($date && $prop && $prop instanceof \DateTimeInterface) { - if ($date != $prop) { - throw new Exception('File was modified since "If-Unmodified-Since" condition', 412); - } - } - } - // Specific to NextCloud/ownCloud, to allow setting file mtime // This expects a UNIX timestamp - $mtime = intval($this->getHeader('X-OC-MTime')) ?: null; + $mtime = (int)($_SERVER['HTTP_X_OC_MTIME'] ?? 0) ?: null; $this->extendExecutionTime(); @@ -510,7 +246,7 @@ namespace KD2\WebDAV // mod_fcgid <= 2.3.9 doesn't handle chunked transfer encoding for PUT requests // see https://github.com/kd2org/picodav/issues/6 - if (strstr($this->getHeader('Transfer-Encoding') ?? '', 'chunked') && PHP_SAPI == 'fpm-fcgi') { + if (strstr($_SERVER['HTTP_TRANSFER_ENCODING'] ?? '', 'chunked') && PHP_SAPI == 'fpm-fcgi') { // We can't seek here // see https://github.com/php/php-src/issues/9441 $l = strlen(fread($stream, 1)); @@ -571,7 +307,7 @@ namespace KD2\WebDAV if (isset($props['DAV::getlastmodified']) && $props['DAV::getlastmodified'] instanceof \DateTimeInterface) { - header(sprintf('Last-Modified: %s', $props['DAV::getlastmodified']->format(self::DATE_RFC7231))); + header(sprintf('Last-Modified: %s', $props['DAV::getlastmodified']->format(\DATE_RFC7231))); } if (!empty($props['DAV::getetag'])) { @@ -610,6 +346,7 @@ namespace KD2\WebDAV $uri = $this->_prefix($uri); $is_collection = !empty($props['DAV::resourcetype']) && $props['DAV::resourcetype'] == 'collection'; + $out = ''; if ($is_collection) { $list = $this->storage->list($uri, self::BASIC_PROPERTIES); @@ -640,29 +377,163 @@ namespace KD2\WebDAV return null; } - try { - HTTP_Server::serveFile( - $file['content'] ?? null, - $file['path'] ?? null, - $file['resource'] ?? null, - [ - 'gzip' => $this->enable_gzip, - 'ranges' => true, - 'xsendfile' => false, - 'name' => $uri, - 'size' => $props['DAV::getcontentlength'], - ] - ); + if (!isset($file['content']) && !isset($file['resource']) && !isset($file['path'])) { + throw new \RuntimeException('Invalid file array returned by ::get(): ' . print_r($file, true)); } - catch (\LogicException $e) { - throw new Exception($e->getMessage(), $e->getCode()); + + $this->extendExecutionTime(); + + $length = $start = $end = null; + $gzip = false; + + if (isset($_SERVER['HTTP_RANGE']) + && preg_match('/^bytes=(\d*)-(\d*)$/i', $_SERVER['HTTP_RANGE'], $match) + && $match[1] . $match[2] !== '') { + $start = $match[1] === '' ? null : (int) $match[1]; + $end = $match[2] === '' ? null : (int) $match[2]; + + if (null !== $start && $start < 0) { + throw new Exception('Start range cannot be satisfied', 416); + } + + if (isset($props['DAV::getcontentlength']) && $start > $props['DAV::getcontentlength']) { + throw new Exception('End range cannot be satisfied', 416); + } + + $this->log('HTTP Range requested: %s-%s', $start, $end); } - finally { - if (isset($file['resource'])) { - fclose($file['resource']); + elseif ($this->enable_gzip + && isset($_SERVER['HTTP_ACCEPT_ENCODING']) + && false !== strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') + && isset($props['DAV::getcontentlength']) + // Don't compress if size is larger than 8 MiB + && $props['DAV::getcontentlength'] < 8*1024*1024 + // Don't compress already compressed content + && !preg_match('/\.(?:cbz|cbr|cb7|mp4|m4a|zip|docx|xlsx|pptx|ods|odt|odp|7z|gz|bz2|lzma|lz|xz|apk|dmg|jar|rar|webm|ogg|mp3|ogm|flac|ogv|mkv|avi)$/i', $uri)) { + $gzip = true; + header('Content-Encoding: gzip', true); + } + + // Try to avoid common issues with output buffering and stuff + if (function_exists('apache_setenv')) { + @apache_setenv('no-gzip', 1); + } + + @ini_set('zlib.output_compression', 'Off'); + + if (@ob_get_length()) { + @ob_clean(); + } + + if (isset($file['content'])) { + $length = strlen($file['content']); + + if ($start || $end) { + if (null !== $end && $end > $length) { + header('Content-Range: bytes */' . $length, true); + throw new Exception('End range cannot be satisfied', 416); + } + + if ($start === null) { + $start = $length - $end; + $end = $start + $end; + } + elseif ($end === null) { + $end = $length; + } + + http_response_code(206); + header(sprintf('Content-Range: bytes %s-%s/%s', $start, $end - 1, $length)); + $file['content'] = substr($file['content'], $start, $end - $start); + $length = $end - $start; + } + + if ($gzip) { + $file['content'] = gzencode($file['content'], 9); + $length = strlen($file['content']); + } + + header('Content-Length: ' . $length, true); + echo $file['content']; + return null; + } + + if (isset($file['path'])) { + $file['resource'] = fopen($file['path'], 'rb'); + } + + $seek = fseek($file['resource'], 0, SEEK_END); + + if ($seek === 0) { + $length = ftell($file['resource']); + fseek($file['resource'], 0, SEEK_SET); + } + + if (($start || $end) && $seek === 0) { + if (null !== $end && $end > $length) { + header('Content-Range: bytes */' . $length, true); + throw new Exception('End range cannot be satisfied', 416); + } + + if ($start === null) { + $start = $length - $end; + $end = $start + $end; + } + elseif ($end === null) { + $end = $length; + } + + fseek($file['resource'], $start, SEEK_SET); + + http_response_code(206); + header(sprintf('Content-Range: bytes %s-%s/%s', $start, $end - 1, $length), true); + + $length = $end - $start; + $end -= $start; + } + elseif (null === $length && isset($file['path'])) { + $end = $length = filesize($file['path']); + } + + if ($gzip) { + $this->log('Using gzip output compression'); + $gzip = deflate_init(ZLIB_ENCODING_GZIP); + + $fp = fopen('php://temp', 'wb'); + + while (!feof($file['resource'])) { + fwrite($fp, deflate_add($gzip, fread($file['resource'], 8192), ZLIB_NO_FLUSH)); + } + + fwrite($fp, deflate_add($gzip, '', ZLIB_FINISH)); + $length = ftell($fp); + rewind($fp); + fclose($file['resource']); + + $file['resource'] = $fp; + unset($fp); + } + + if (null !== $length) { + $this->log('Length: %s', $length); + header('Content-Length: ' . $length, true); + } + + $block_size = 8192*4; + + while (!feof($file['resource']) && ($end === null || $end > 0)) { + $l = $end !== null ? min($block_size, $end) : $block_size; + + echo fread($file['resource'], $l); + flush(); + + if (null !== $end) { + $end -= $block_size; } } + fclose($file['resource']); + return null; } @@ -816,7 +687,7 @@ namespace KD2\WebDAV throw new Exception('Invalid XML', 400); } - $this->log('<= Requested depth: %s', $depth); + $this->log('Requested depth: %s', $depth); // We don't really care about having a correct XML string, // but we can get better WebDAV compliance if we do @@ -856,7 +727,7 @@ namespace KD2\WebDAV $properties = $properties ?? $this->storage->propfind($path, $requested_keys, 0); if (!$properties) { - $this->log('!! Cannot find "%s"', $path); + $this->log('!!! Cannot find "%s"', $path); continue; } @@ -936,18 +807,17 @@ namespace KD2\WebDAV } $pos = strrpos($name, ':'); - $ns = substr($name, 0, $pos); - $tag_name = substr($name, $pos + 1); + $ns = substr($name, 0, strrpos($name, ':')); + $tag_name = substr($name, strrpos($name, ':') + 1); $alias = $root_namespaces[$ns] ?? null; $attributes = ''; - // The ownCloud/OpenCloud Android app doesn't like formatted dates, it makes it crash. + // The ownCloud Android app doesn't like formatted dates, it makes it crash. // so force it to have a timestamp - // see https://github.com/opencloud-eu/android/issues/74 if ($name == 'DAV::creationdate' && ($value instanceof \DateTimeInterface) - && false !== preg_match('/owncloud|opencloud/', $_SERVER['HTTP_USER_AGENT'] ?? '')) { + && false !== stripos($_SERVER['HTTP_USER_AGENT'] ?? '', 'owncloud')) { $value = $value->getTimestamp(); } // ownCloud app crashes if mimetype is provided for a directory @@ -967,7 +837,7 @@ namespace KD2\WebDAV // Change value to GMT $value = clone $value; $value->setTimezone(new \DateTimeZone('GMT')); - $value = $value->format(self::DATE_RFC7231); + $value = $value->format(DATE_RFC7231); } elseif (is_array($value)) { $attributes = $value['attributes'] ?? ''; @@ -1004,8 +874,8 @@ namespace KD2\WebDAV foreach ($missing_properties as $name) { $pos = strrpos($name, ':'); - $ns = substr($name, 0, $pos); - $name = substr($name, $pos + 1); + $ns = substr($name, 0, strrpos($name, ':')); + $name = substr($name, strrpos($name, ':') + 1); $alias = $root_namespaces[$ns] ?? null; // NULL namespace, see Litmus FAQ for propnullns @@ -1039,7 +909,7 @@ namespace KD2\WebDAV $xml = @simplexml_load_string($body); if (false === $xml) { - throw new Exception('Invalid XML', 400); + throw new WebDAV_Exception('Invalid XML', 400); } $_ns = null; @@ -1133,11 +1003,11 @@ namespace KD2\WebDAV $ts = '@' . $ts; } - $set_time = new \DateTime($ts); + $set_time = new \DateTime($value['content']); $set_time_name = $name; } - $prefix .= sprintf(">\n\n %s\n", htmlspecialchars($uri, ENT_XML1)); + $prefix .= sprintf(">\n\n %s\n", htmlspecialchars($url, ENT_XML1)); // http_response_code doesn't know the 207 status code header('HTTP/1.1 207 Multi-Status', true); @@ -1153,7 +1023,7 @@ namespace KD2\WebDAV $return = $this->storage->proppatch($uri, $properties); - if ($set_time && $this->storage->touch($uri, $set_time)) { + if ($set_time && $this->touch($uri, $set_time)) { $return[$set_time_name] = 200; } @@ -1202,6 +1072,7 @@ namespace KD2\WebDAV throw new Exception('Invalid If header', 400); } + $info = null; $ns = 'D'; $scope = self::EXCLUSIVE_LOCK; @@ -1215,7 +1086,7 @@ namespace KD2\WebDAV throw new Exception('Cannot acquire another lock, resource is locked for exclusive use', 423); } - if ($locked_scope) { + if ($locked_scope && $token = $this->getLockToken()) { $token = $this->getLockToken(); if (!$token) { @@ -1383,19 +1254,6 @@ namespace KD2\WebDAV } } - public function validateURI(string $uri): string - { - $uri = preg_replace('!/{2,}!', '/', $uri); - $uri = str_replace('\\', '/', $uri); - - // Protect against path traversal - if (preg_match('!(?:^|/)\.\.(?:$|/)!', $uri)) { - throw new Exception(sprintf('Invalid URI: "%s"', $uri), 403); - } - - return $uri; - } - protected function getURI(string $source): string { $uri = parse_url($source, PHP_URL_PATH); @@ -1411,7 +1269,11 @@ namespace KD2\WebDAV throw new Exception(sprintf('Invalid URI, "%s" is outside of scope "%s"', $uri, $this->base_uri), 400); } - $uri = $this->validateURI($uri); + $uri = preg_replace('!/{2,}!', '/', $uri); + + if (false !== strpos($uri, '..')) { + throw new Exception(sprintf('Invalid URI: "%s"', $uri), 403); + } $uri = substr($uri, strlen($this->base_uri)); $uri = $this->_prefix($uri); @@ -1435,7 +1297,7 @@ namespace KD2\WebDAV $uri = substr($uri, strlen($this->base_uri)); } else { - $this->log('=> %s is not a managed URL (%s)', $uri, $this->base_uri); + $this->log('<= %s is not a managed URL (%s)', $uri, $this->base_uri); return false; } @@ -1465,7 +1327,9 @@ namespace KD2\WebDAV $this->log('<= %s /%s', $method, $uri); try { - $uri = $this->validateURI($uri); + if (false !== strpos($uri, '..')) { + throw new Exception(sprintf('Invalid URI: "%s"', $uri), 403); + } // Call 'http_method' class method $method = 'http_' . strtolower($method); @@ -1522,13 +1386,8 @@ namespace KD2\WebDAV abstract class AbstractStorage { - const PUT_IGNORE_PATTERN = '!^~|~$|^~.*tmp$|^Thumbs\.db$|^desktop\.ini$|\.unison$|^My Saved Places' - . '|^\.(lock\.|_|DS_Store|DocumentRevisions|directory|Trash|Temp|fseventsd|apdisk|synkron|sync|symform|fuse|nfs)!i'; - abstract public function get(string $uri): ?array; - abstract public function fetch(string $uri): ?string; - abstract public function exists(string $uri): bool; abstract public function propfind(string $uri, ?array $requested_properties, int $depth): ?array; @@ -1536,13 +1395,6 @@ namespace KD2\WebDAV public function proppatch(string $uri, array $properties): array { // By default, properties are not saved - $out = []; - - foreach ($properties as $key => $value) { - $out[$key] = 200; - } - - return $out; } abstract public function put(string $uri, $pointer, ?string $hash_algo, ?string $hash): bool; @@ -1557,7 +1409,7 @@ namespace KD2\WebDAV abstract public function list(string $uri, array $properties): iterable; - abstract public function touch(string $uri, \DateTimeInterface $datetime): bool; + abstract public function touch(string $uri, \DateTimeInterface $timestamp): bool; public function lock(string $uri, string $token, string $scope): void { @@ -1630,28 +1482,10 @@ namespace PicoDAV return true; } - static protected function glob(string $path, int $flags = 0): array + static protected function glob(string $path, string $pattern = '', int $flags = 0): array { - if (file_exists($path) && !is_dir($path)) { - return [$path]; - } - - $dir = dir($path); - $out = []; - - while ($file = $dir->read()) { - if ($file === '.' || $file === '..') { - continue; - } - - if (($flags & GLOB_ONLYDIR) && !is_dir($path . '/' . $file)) { - continue; - } - - $out[] = $path . '/' . $file; - } - - return $out; + $path = preg_replace('/[\*\?\[\]]/', '\\\\$0', $path); + return glob($path . $pattern, $flags); } public function canRead(string $uri): bool @@ -1743,12 +1577,12 @@ namespace PicoDAV //throw new WebDAV_Exception('Access forbidden', 403); } - $dirs = self::glob($this->path . $uri, \GLOB_ONLYDIR); + $dirs = self::glob($this->path . $uri, '/*', \GLOB_ONLYDIR); $dirs = array_map('basename', $dirs); $dirs = array_filter($dirs, fn($a) => $this->canRead(ltrim($uri . '/' . $a, '/') . '/')); natcasesort($dirs); - $files = self::glob($this->path . $uri); + $files = self::glob($this->path . $uri, '/*'); $files = array_map('basename', $files); $files = array_diff($files, $dirs); @@ -1778,23 +1612,6 @@ namespace PicoDAV return ['path' => $path]; } - public function fetch(string $uri): ?string - { - $r = $this->get($uri); - - if (!$r) { - return null; - } - - $r = file_get_contents($r['path']); - - if ($r === false) { - return null; - } - - return $r; - } - public function exists(string $uri): bool { return file_exists($this->path . $uri); @@ -1817,7 +1634,7 @@ namespace PicoDAV return is_dir($target) ? 'collection' : ''; case 'DAV::getlastmodified': $mtime = filemtime($target); - + if (!$mtime) { return null; } @@ -1973,7 +1790,7 @@ namespace PicoDAV } if (is_dir($target)) { - foreach (self::glob($target) as $file) { + foreach (self::glob($target, '/*') as $file) { $this->delete(substr($file, strlen($this->path))); } @@ -2034,6 +1851,8 @@ namespace PicoDAV } else { $method($source, $target); + + $this->getResourceProperties($uri)->move($destination); } return $overwritten; @@ -2161,14 +1980,6 @@ namespace { if (!empty($_SERVER['SERVER_SOFTWARE']) && stristr($_SERVER['SERVER_SOFTWARE'], 'apache') && !file_exists($self_dir . '/.htaccess')) { file_put_contents($self_dir . '/.htaccess', str_replace('index.php', basename($self), 'DirectoryIndex disabled - - = 2.4.13> - # Force Apache to pass the Authorization header to PHP - # See https://github.com/kd2org/picodav/issues/14 - CGIPassAuth On - - - RedirectMatch 404 \\.picodav\\.ini RewriteEngine On @@ -2203,12 +2014,12 @@ RewriteRule ^.*$ /index.php [END] $fp = fopen(__FILE__, 'r'); if ($relative_uri == '.webdav/webdav.js') { - fseek($fp, 58962, SEEK_SET); - echo fread($fp, 33651); + fseek($fp, 55024, SEEK_SET); + echo fread($fp, 27891); } else { - fseek($fp, 58962 + 33651, SEEK_SET); - echo fread($fp, 16767); + fseek($fp, 55024 + 27891, SEEK_SET); + echo fread($fp, 7004); } fclose($fp); @@ -2299,74 +2110,49 @@ const WebDAVNavigator = (url, options) => { const microdown=function(){function l(n,e,r){return"<"+n+(r?" "+Object.keys(r).map(function(n){return r[n]?n+'="'+(a(r[n])||"")+'"':""}).join(" "):"")+">"+e+""}function c(n,e){return e=n.match(/^[+-]/m)?"ul":"ol",n?"<"+e+">"+n.replace(/(?:[+-]|\d+\.) +(.*)\n?(([ \t].*\n?)*)/g,function(n,e,r){return"
  • "+g(e+"\n"+(t=r||"").replace(new RegExp("^"+(t.match(/^\s+/)||"")[0],"gm"),"").replace(o,c))+"
  • ";var t})+"":""}function e(r,t,u,c){return function(n,e){return n=n.replace(t,u),l(r,c?c(n):n)}}function t(n,u){return f(n,[//g,"\x3c!--$1--\x3e",/^("""|```)(.*)\n((.*\n)*?)\1/gm,function(n,e,r,t){return'"""'===e?l("div",p(t,u),{class:r}):u&&u.preCode?l("pre",l("code",a(t),{class:r})):l("pre",a(t),{class:r})},/(^>.*\n?)+/gm,e("blockquote",/^> ?(.*)$/gm,"$1",r),/((^|\n)\|.+)+/g,e("table",/^.*(\n\|---.*?)?$/gm,function(n,t){return e("tr",/\|(-?)([^|]*)\1(\|$)?/gm,function(n,e,r){return l(e||t?"th":"td",g(r))})(n.slice(0,n.length-(t||"").length))}),o,c,/#\[([^\]]+?)]/g,'',/^(#+) +(.*)(?:$)/gm,function(n,e,r){return l("h"+e.length,g(r))},/^(===+|---+)(?=\s*$)/gm,"
    "],p,u)}var i=this,a=function(n){return n?n.replace(/"/g,""").replace(//g,">"):""},o=/(?:(^|\n)([+-]|\d+\.) +(.*(\n[ \t]+.*)*))+/g,g=function c(n,i){var o=[];return n=(n||"").trim().replace(/`([^`]*)`/g,function(n,e){return"\\"+o.push(l("code",a(e)))}).replace(/[!&]?\[([!&]?\[.*?\)|[^\]]*?)]\((.*?)( .*?)?\)|(\w+:\/\/[$\-.+!*'()/,\w]+)/g,function(n,e,r,t,u){return u?i?n:"\\"+o.push(l("a",u,{href:u})):"&"==n[0]?(e=e.match(/^(.+),(.+),([^ \]]+)( ?.+?)?$/),"\\"+o.push(l("iframe","",{width:e[1],height:e[2],frameborder:e[3],class:e[4],src:r,title:t}))):"\\"+o.push("!"==n[0]?l("img","",{src:r,alt:e,title:t}):l("a",c(e,1),{href:r,title:t}))}),n=function r(n){return n.replace(/\\(\d+)/g,function(n,e){return r(o[Number.parseInt(e)-1])})}(i?n:r(n))},r=function t(n){return f(n,[/([*_]{1,3})((.|\n)+?)\1/g,function(n,e,r){return e=e.length,r=t(r),1"],t)},f=function(n,e,r,t){for(var u,c=0;c typeof lang_strings != 'undefined' && key in lang_strings ? lang_strings[key] : key; - const rename_button = ``; - const delete_button = ``; + const rename_button = ``; + const delete_button = ``; - const edit_button = ``; + const edit_button = ``; const mkdir_dialog = ``; const mkfile_dialog = ``; const rename_dialog = ``; const paste_upload_dialog = `

    Upload this file?

    `; const edit_dialog = ``; - const markdown_dialog = `
    `; + const markdown_dialog = `
    `; const delete_dialog = `

    ${_('Confirm delete?')}

    `; - const wopi_dialog = ``; const dialog_tpl = `

    %s
    %b
    `; const html_tpl = ` - + Files
    `; - const body_tpl = ` -
    -
    - - -
    + const body_tpl = `

    %title%

    +
    + +
    - - - - - - - - - - - %table% -
    `; + %table%
    `; - const create_buttons = ` - - - `; + const create_buttons = ` + + + `; - const dir_row_tpl = ` - - %thumb% %name% - %modified% -
    - `; - - const file_row_tpl = ` - - %thumb% %name% - %size_bytes% - %modified% - - `; - - const icon_tpl = `%icon%`; - const root_url = url.replace(/(?`; + const dir_row_tpl = `%icon%%name%%modified%
    `; + const file_row_tpl = `%icon%%name%%size_bytes%%modified%`; const propfind_tpl = '<'+ `?xml version="1.0" encoding="UTF-8"?> @@ -2378,7 +2164,7 @@ const WebDAVNavigator = (url, options) => { const wopi_propfind_tpl = '<' + `?xml version="1.0" encoding="UTF-8"?> - + `; @@ -2395,27 +2181,21 @@ const WebDAVNavigator = (url, options) => { }).then(str => new window.DOMParser().parseFromString(str, "text/xml")); }; - const reqHandler = (r, c) => { - if (!r.ok) { - return r.text().then(t => { - var message; - if (a = t.match(/<((?:\w+:)?message)>(.*)<\/\1>/)) { - message = "\n" + a[2]; - } - - throw new Error(r.status + ' ' + r.statusText + message); - }); - } - window.setTimeout(c, 200); - return r; - }; - const reqAndReload = (method, url, body, headers) => { animateLoading(); - req(method, url, body, headers).then(r => reqHandler(r, () => { + req(method, url, body, headers).then(r => { stopLoading(); + if (!r.ok) { + return r.text().then(t => { + var message; + if (a = t.match(/<((?:\w+:)?message)>(.*)<\/\1>/)) { + message = "\n" + a[2]; + } + + throw new Error(r.status + ' ' + r.statusText + message); }); + } reloadListing(); - })).catch(e => { + }).catch(e => { console.error(e); alert(e); }); @@ -2462,32 +2242,6 @@ const WebDAVNavigator = (url, options) => { return p; }; - const uploadFiles = (files) => { - animateLoading(); - - (async () => { - for (var i = 0; i < files.length; i++) { - var f = files[i]; - await reqOrError('PUT', current_url + encodeURIComponent(f.name), f); - } - - window.setTimeout(() => { - stopLoading(); - reloadListing(); - }, 500); - })(); - }; - - const reqOrError = (method, url, body) => { - return req(method, url, body).then(reqHandler).catch(e => { - console.error(e); - alert(e); - stopLoading(); - reloadListing(); - throw e; - }); - } - const get_url = async (url) => { var progress = (e) => { var p = $('progress'); @@ -2568,7 +2322,7 @@ const WebDAVNavigator = (url, options) => { const wopi_open = async (document_url, wopi_url) => { var properties = await reqXML('PROPFIND', document_url, wopi_propfind_tpl, {'Depth': '0'}); - var src = (a = properties.querySelector('wopi-url')) ? a.textContent : null; + var src = (a = properties.querySelector('file-url')) ? a.textContent : null; var token = (a = properties.querySelector('token')) ? a.textContent : null; var token_ttl = (a = properties.querySelector('token-ttl')) ? a.textContent : +(new Date(Date.now() + 3600 * 1000)); @@ -2655,18 +2409,14 @@ const WebDAVNavigator = (url, options) => { window.onbeforeunload = null; }; - const download_selected = async () => { - var items = document.querySelectorAll('tbody input[type=checkbox]:checked'); + const download_all = async () => { for (var i = 0; i < items.length; i++) { - var input = items[i]; - var row = input.parentNode.parentNode; - - // Skip directories - if (!row.dataset.mime) { - return; + var item = items[i]; + if (item.is_dir) { + continue; } - await download(row.dataset.name, row.dataset.size, row.querySelector('th a').href); + await download(item.name, item.size, item.uri) } }; @@ -2726,7 +2476,7 @@ const WebDAVNavigator = (url, options) => { return _('Yesterday, %s').replace(/%s/, date.toLocaleTimeString()); } - return date.toLocaleString([], {year: 'numeric', month: 'numeric', day: 'numeric'}); + return date.toLocaleString(); }; const openListing = (uri, push) => { @@ -2788,13 +2538,12 @@ const WebDAVNavigator = (url, options) => { const buildListing = (uri, xml) => { uri = normalizeURL(uri); - items = []; + items = [[], []]; var title = null; var root_permissions = null; xml.querySelectorAll('response').forEach((node) => { - var path = node.querySelector('href').textContent; - var item_uri = normalizeURL(path); + var item_uri = normalizeURL(node.querySelector('href').textContent); var props = null; node.querySelectorAll('propstat').forEach((propstat) => { @@ -2821,11 +2570,10 @@ const WebDAVNavigator = (url, options) => { } var is_dir = node.querySelector('resourcetype collection') ? true : false; - var index = is_dir ? 0 : 1; + var index = sort_order == 'name' && is_dir ? 0 : 1; - items.push({ + items[index].push({ 'uri': item_uri, - 'path': item_uri.substring(base_url.length), 'name': name, 'size': !is_dir && (prop = node.querySelector('getcontentlength')) ? parseInt(prop.textContent, 10) : null, 'mime': !is_dir && (prop = node.querySelector('getcontenttype')) ? prop.textContent : null, @@ -2835,35 +2583,39 @@ const WebDAVNavigator = (url, options) => { }); }); - items.sort((a, b) => { - if (sort_order === 'date') { - return a.modified - b.modified; + if (sort_order == 'name') { + items[0].sort((a, b) => a.name.localeCompare(b.name)); + } + + items[1].sort((a, b) => { + if (sort_order == 'date') { + return b.modified - a.modified; } - else if (sort_order === 'size') { - return a.size - b.size; + else if (sort_order == 'size') { + return b.size - a.size; } else { return a.name.localeCompare(b.name); } }); - if (sort_order !== 'date') { + if (sort_order == 'name') { // Sort with directories first - items.sort((a, b) => b.is_dir - a.is_dir); + items = items[0].concat(items[1]); + } + else { + items = items[1]; } - if (sort_order_desc) { - items = items.reverse(); - } var table = ''; var parent = uri.replace(/\/+$/, '').split('/').slice(0, -1).join('/') + '/'; if (parent.length >= base_url.length) { - table += template(dir_row_tpl, {'name': _('Back'), 'uri': parent, 'class': 'parent', 'thumb': template(icon_tpl, {})}); + table += template(dir_row_tpl, {'name': _('Back'), 'uri': parent, 'icon': '↲'}); } else { - title = _('My files'); + title = 'My files'; } items.forEach(item => { @@ -2875,93 +2627,32 @@ const WebDAVNavigator = (url, options) => { var row = item.is_dir ? dir_row_tpl : file_row_tpl; item.size_bytes = item.size !== null ? formatBytes(item.size).replace(/ /g, ' ') : null; - - if (!item.is_dir && (pos = item.uri.lastIndexOf('.'))) { - var ext = item.uri.substr(pos+1).toUpperCase(); - - if (ext.length > 4) { - ext = ''; - } - } - - item.icon = ext || ''; - item.class = item.is_dir ? 'dir' : 'file'; + item.icon = item.is_dir ? '📁' : (item.uri.indexOf('.') > 0 ? item.uri.replace(/^.*\.(\w+)$/, '$1').toUpperCase() : ''); item.modified = item.modified !== null ? formatDate(item.modified) : null; item.name = html(item.name); - - if (item.mime && item.mime.match(/^image\//) && options.nc_thumbnails) { - item.thumb = template(image_thumb_tpl, item); - } - else { - item.thumb = template(icon_tpl, item); - } - table += template(row, item); }); document.title = title; document.querySelector('main').innerHTML = template(body_tpl, {'title': html(document.title), 'base_url': base_url, 'table': table}); - var parent_check = document.querySelector('tbody tr.parent .check'); - - if (parent_check) { - parent_check.innerHTML = ''; - } - - var column = document.querySelector('thead td[data-sort="' + sort_order + '"]').className += ' selected ' + (sort_order_desc ? 'desc' : 'asc'); - - document.querySelectorAll('thead td[data-sort] button').forEach(elm => elm.onclick = (e) => { - var new_sort_order = e.target.parentNode.dataset.sort; - - if (sort_order == new_sort_order) { - sort_order_desc = !sort_order_desc; - } - - sort_order = new_sort_order; - - window.localStorage.setItem('sort_order', new_sort_order); - window.localStorage.setItem('sort_order_desc', sort_order_desc ? '1' : '0'); + var select = $('.sortorder'); + select.value = sort_order; + select.onchange = () => { + sort_order = select.value; + window.localStorage.setItem('sort_order', sort_order); reloadListing(); - }); - - document.querySelector('thead td.check input').onchange = (e) => { - document.querySelectorAll('tbody td.check input').forEach(i => i.checked = e.target.checked); }; if (!items.length) { - $('div.buttons .download').disabled = true; + $('.download_all').disabled = true; } else { - $('div.buttons .download').onclick = download_selected; + $('.download_all').onclick = download_all; } - $('div.buttons .delete').onclick = () => { - var l = document.querySelectorAll('input[name=delete]:checked'); - - if (!l.length) { - alert(_('No file is selected')); - return; - } - - openDialog(delete_dialog); - document.forms[0].onsubmit = () => { - animateLoading(); - - for (var i = 0; i < l.length; i++) { - reqOrError('DELETE', l[i].value); - } - - // Don't reload too fast - window.setTimeout(() => { - stopLoading(); - reloadListing(); - }, 500); - }; - - }; - if (!root_permissions || root_permissions.indexOf('C') != -1 || root_permissions.indexOf('K') != -1) { - $('.buttons').insertAdjacentHTML('beforeend', create_buttons); + $('.upload').insertAdjacentHTML('afterbegin', create_buttons); $('.mkdir').onclick = () => { openDialog(mkdir_dialog); @@ -2996,19 +2687,24 @@ const WebDAVNavigator = (url, options) => { var fi = $('input[type=file]'); - $('.upload').onclick = () => fi.click(); + $('.uploadfile').onclick = () => fi.click(); fi.onchange = () => { if (!fi.files.length) return; - uploadFiles(fi.files); + var body = new Blob(fi.files); + var name = fi.files[0].name; + + name = encodeURIComponent(name); + + return reqAndReload('PUT', current_url + name, body); }; } - document.querySelectorAll('table tbody tr').forEach(tr => { + Array.from($('table').rows).forEach((tr) => { var $$ = (a) => tr.querySelector(a); var file_url = $$('a').href; - var file_name = tr.dataset.name; + var file_name = $$('a').innerText; var dir = $$('[colspan]'); var mime = !dir ? tr.getAttribute('data-mime') : 'dir'; var buttons = $$('td.buttons div'); @@ -3082,26 +2778,16 @@ const WebDAVNavigator = (url, options) => { } var view_url, edit_url; - var allow_preview = false; - - if (mime.match(PREVIEW_TYPES) - || file_name.match(PREVIEW_EXTENSIONS)) { - allow_preview = true; - } // Don't preview PDF in mobile - if ((mime == 'application/pdf' || file_name.match(/\.pdf/i)) - && window.navigator.userAgent.match(/Mobi|Tablet|Android|iPad|iPhone/)) { - allow_preview = false; - } - - if (allow_preview) { + if (mime.match(PREVIEW_TYPES) + && !(mime == 'application/pdf' && window.navigator.userAgent.match(/Mobi|Tablet|Android|iPad|iPhone/))) { $$('a').onclick = () => { if (file_url.match(/\.md$/)) { openDialog('
    ', false); $('dialog').className = 'preview'; req('GET', file_url).then(r => r.text()).then(t => { - $('.md_preview').innerHTML = microdown.parse(t); + $('.md_preview').innerHTML = microdown.parse(html(t)); }); return false; } @@ -3123,7 +2809,7 @@ const WebDAVNavigator = (url, options) => { else if (user && password && !dir) { $$('a').onclick = () => { download(file_name, size, file_url); return false; }; } - else if (!dir) { + else { $$('a').download = file_name; } @@ -3134,97 +2820,16 @@ const WebDAVNavigator = (url, options) => { $$('.edit').onclick = (e) => { req('GET', file_url).then((r) => r.text().then((t) => { let md = file_url.match(/\.md$/); - var tpl = dialog_tpl.replace(/%b/, ''); - $('body').classList.add('dialog'); - $('body').insertAdjacentHTML('beforeend', tpl.replace(/%s/, md ? markdown_dialog : edit_dialog)); - - var tb = $('.close'); - tb.className = 'toolbar'; - tb.innerHTML = ` - - - `; - + openDialog(md ? markdown_dialog : edit_dialog); var txt = $('textarea[name=edit]'); txt.value = t; - var saved_status = $('.toolbar .status'); - var close_btn = $('.toolbar .close'); - var save_btn = $('.toolbar .save'); - var autosave = $('.toolbar .autosave'); - - var c = localStorage.getItem('autosave') ?? options.autosave; - autosave.checked = c == 1 || c === true; - autosave.onchange = () => { - localStorage.setItem('autosave', autosave.checked ? 1 : 0); - }; - - var preventClose = (e) => { - if (txt.value == t) { - return; - } - - e.preventDefault(); - e.returnValue = ''; - return true; - }; - - var close = () => { - if (txt.value !== t) { - if (!confirm(_('Your changes have not been saved. Do you want to cancel WITHOUT saving?'))) { - return; - } - } - - window.removeEventListener('beforeunload', preventClose, {capture: true}); - closeDialog(); - }; - - var save = () => { - reqOrError('PUT', file_url, txt.value); - t = txt.value; - updateSaveStatus(); - }; - - var updateSaveStatus = () => { - saved_status.innerHTML = txt.value !== t ? '⚠️ ' + _('Modified') : '✔️ ' + _('Saved'); - }; - - save_btn.onclick = () => { save(); close(); }; - close_btn.onclick = close; - - // Prevent close of tab if content has changed and is not saved - window.addEventListener('beforeunload', preventClose, { capture: true }); - - txt.onkeydown = (e) => { - if (e.ctrlKey && e.key == 's') { - save(); - e.preventDefault(); - return false; - } - else if (e.key === 'Escape') { - close(); - e.preventDefault(); - return false; - } - }; - - txt.onkeyup = (e) => { - updateSaveStatus(); - }; - - window.setInterval(() => { - if (autosave.checked && t != txt.value) { - save(); - } - }, 10000); - // Markdown editor if (md) { - let pre = $('.md_preview'); + let pre = $('#md'); txt.oninput = () => { - pre.innerHTML = microdown.parse(txt.value); + pre.innerHTML = microdown.parse(html(txt.value)); }; txt.oninput(); @@ -3275,11 +2880,9 @@ const WebDAVNavigator = (url, options) => { var evt, paste_upload, popstate_evt, temp_object_url; var sort_order = window.localStorage.getItem('sort_order') || 'name'; - var sort_order_desc = !!parseInt(window.localStorage.getItem('sort_order_desc'), 10); var wopi_mimes = {}, wopi_extensions = {}; const wopi_discovery_url = options.wopi_discovery_url || null; - options.autosave = options.autosave || false; document.querySelector('html').innerHTML = html_tpl; @@ -3354,48 +2957,52 @@ const WebDAVNavigator = (url, options) => { document.body.classList.remove('dragging'); dragcounter = 0; - var files = [...e.dataTransfer.items].map(item => item.getAsFile()); - - files = files.filter(f => f !== null); + const files = [...e.dataTransfer.items].map(item => item.getAsFile()); if (!files.length) return; - uploadFiles(files); + animateLoading(); + + (async () => { + for (var i = 0; i < files.length; i++) { + var f = files[i] + await req('PUT', current_url + encodeURIComponent(f.name), f); + } + + window.setTimeout(() => { + stopLoading(); + reloadListing(); + }, 500); + })(); }); }; if (url = document.querySelector('html').getAttribute('data-webdav-url')) { WebDAVNavigator(url, { 'wopi_discovery_url': document.querySelector('html').getAttribute('data-wopi-discovery-url'), - 'nc_thumbnails': document.querySelector('html').getAttribute('data-nc-thumbnails') ? true : false }); } :root { --bg-color: #fff; --fg-color: #000; - --g1-color: #f5f7f7; - --g2-color: #ccd8d6; - --g3-color: #7c9696; - --link-color: #2e3c3a; - --visited-color: #2e3c3a; - --active-color: #c47508; -} - -* { - margin: 0; - padding: 0; + --g1-color: #eee; + --g2-color: #ccc; + --g3-color: #999; + --link-color: blue; + --visited-color: purple; + --active-color: darkred; } body { text-align: center; - font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; + font-size: 1.1em; + font-family: Arial, Helvetica, sans-serif; background: var(--bg-color); color: var(--fg-color); } a:link { color: var(--link-color); - text-decoration: none; } a:visited { @@ -3404,130 +3011,31 @@ a:visited { a:hover { color: var(--active-color); - text-decoration: underline; } table { - margin: 0 auto; + margin: 2em auto; border-collapse: collapse; - width: 100%; + width: 90%; } -thead td { - height: 2em; - vertical-align: bottom; -} - -thead button { - width: 100%; - border: none; - text-align: inherit; - font: inherit; - background: none; - cursor: pointer; - font-size: .9em; - color: #999; - padding: .4em; -} - -thead .selected button::after { - content: "🠇"; - margin-left: .5em; -} - -thead .selected.desc button::after { - content: "🠅"; -} - -td.name, tbody th, td.date { +th, td { + padding: .5em; text-align: left; + border: 2px solid var(--g2-color); } -tbody tr:has(.check input:checked) { - background: #fdefdc; -} - -tbody th, tbody td { - padding: 0 .5em; - text-align: left; - border-top: 1px solid var(--g2-color); - border-bottom: 1px solid var(--g2-color); - vertical-align: middle; -} - -tbody th { +th { word-break: break-all; - font-weight: normal; } -tbody th a { - display: flex; - align-items: center; - justify-content: flex-start; - gap: 1em; -} - -tbody th img { - width: 2em; - height: 2em; - border-radius: .2em; -} - -tbody th .icon { - width: 2em; - height: 2em; - border-radius: .2em; - overflow: hidden; - background: no-repeat center center; - background-size: 100%; - position: relative; -} - -th .icon b { - font-size: 7pt; - font-weight: normal; - text-decoration: none; - display: inline-block; - background: #eee; - border-radius: .3em; - padding: 0 .2em; - color: #2e3c3a; - position: absolute; - bottom: 0; - right: 0; - border: 1px solid #2e3c3a; -} - -th .icon b:empty { - display: none; -} - -td.check { - width: 1.5em; - text-align: center; -} - -td.check input { - cursor: pointer; - width: 1.5em; - height: 1.5em; - border: 2px solid #999; - border-radius: .2em; - background: none; - appearance: none; - outline: 0; -} - -td.check input:checked { - box-shadow: 0px 0px 5px darkorange; - background: no-repeat center center; - background-image: url('data:image/svg+xml;utf8,'); - background-size: contain; +td.thumb { + width: 5%; } td.buttons { text-align: right; - width: 5em; + width: 20em; } td.buttons div { @@ -3535,68 +3043,63 @@ td.buttons div { flex-direction: row-reverse; } -tbody tr:nth-child(even) { +table tr:nth-child(even) { background: var(--g1-color); } -tbody th .icon { - background-image: url('data:image/svg+xml;utf8,'); +.icon { + width: 2.6em; + height: 2.6em; + display: block; + border-radius: .2em; + background:var(--g3-color); + overflow: hidden; + color: var(--bg-color); + text-align: center; } -th .icon.JPEG, th .icon.PNG, th .icon.JPG, th .icon.GIF, th .icon.SVG, th .icon.WEBP { - background-image: url('data:image/svg+xml;utf8,'); +.icon b { + font-weight: normal; + display: inline-block; + transform: rotate(-30deg); + line-height: 2.6rem; } -th .icon.TXT, th .icon.MD { - background-image: url('data:image/svg+xml;utf8,'); +.icon.JPEG, .icon.PNG, .icon.JPG, .icon.GIF, .icon.SVG, .icon.WEBP { + background: #966; } -th .icon.MP4, th .icon.MKV, th .icon.MP3, th .icon.M4A, th .icon.WAV, th .icon.FLAC, th .icon.OGG, th .icon.OGV, th .icon.AAC, th .icon.WEBM { - background-image: url('data:image/svg+xml;utf8,'); +.icon.TXT, .icon.MD { + background: var(--fg-color); } -th .icon.document { - background-image: url('data:image/svg+xml;utf8,'); +.icon.MP4, .icon.MKV, .icon.MP3, .icon.M4A, .icon.WAV, .icon.FLAC, .icon.OGG, .icon.OGV, .icon.AAC, .icon.WEBM { + background: #669; } -th .icon.document b { - position: relative; - background: none; - border: none; - color: #fff; - width: 100%; - height: 100%; - padding: 0; - display: inline-flex; - justify-content: center; - align-items: center; +.icon.document { + background: #696; } -.dir th .icon { - background-image: url('data:image/svg+xml;utf8,'); +.icon.PDF { + background: #969; } -.parent th .icon { - background-image: url('data:image/svg+xml;utf8,'); +.icon.dir { + background: var(--g2-color); + color: var(--fg-color); } -.dir th .icon b, .parent th .icon b { - display: none; +.icon.dir b { + font-size: 2em; + transform: none; } -td.size { +.size { text-align: right; - font-size: .9em; - color: #666; } -td.date { - font-size: .9em; - color: #666; - min-width: 9em; -} - -input[type=button], input[type=submit], .btn, a.btn { +input[type=button], input[type=submit], .btn { font-size: 1.2em; padding: .3em .5em; margin: .2em .3em; @@ -3605,7 +3108,7 @@ input[type=button], input[type=submit], .btn, a.btn { border-radius: .2em; cursor: pointer; text-decoration: none; - color: var(--fg-color); + color: var(--fg-color) !important; font-family: inherit; } @@ -3631,7 +3134,7 @@ input:focus, textarea:focus { input[type=button]:hover, input[type=submit]:hover, .btn:hover { color: var(--active-color); text-decoration: underline; - background-color: var(--bg-color); + background: var(--bg-color); box-shadow: 0px 0px 5px var(--fg-color); } @@ -3640,34 +3143,10 @@ input[type=button]:hover, input[type=submit]:hover, .btn:hover { margin: 0; } -input.close { +.close input { font-size: .8em; } -.toolbar { - margin: 0; - display: flex; - justify-content: space-between; - align-items: center; -} - -.toolbar label { - padding: .5em; - background: var(--bg-color); - cursor: pointer; - border-radius: .5em; -} - -.toolbar .save { - background: DarkSlateBlue; - color: #fff; -} - -.toolbar .status { - width: 10em; - display: block; -} - input[type=submit] { float: right; } @@ -3691,31 +3170,8 @@ dialog form div { text-align: center; } -div.buttons { - display: flex; - justify-content: flex-end; - margin: .5em; - margin-bottom: 0; -} - -div.buttons .selected { - margin-right: auto; - display: none; -} - -body:has(tbody .check input:checked) div.buttons .selected { - display: block; -} - -div.buttons input { - overflow: visible; - text-indent: 0; - width: auto; - height: auto; - background-position: .2em center; - padding: .3em .5em; - padding-left: 2em; - background-size: 1.3em; +.upload { + margin: 1em 0; } #mdp div, #mdp textarea { @@ -3727,7 +3183,7 @@ div.buttons input { margin: 0; } -#mdp .md_preview { +#md { overflow: hidden; overflow-x: auto; } @@ -3737,11 +3193,7 @@ div.buttons input { grid-template-columns: 1fr 1fr; grid-gap: .2em; background: var(--g1-color); - height: 90vh; -} - -.md_preview img, .md_preview video, .md_preview iframe, .md_preview embed, .md_preview object { - max-width: 100%; + height: 82vh; } dialog.preview { @@ -3757,7 +3209,7 @@ dialog.preview { overflow: hidden; } -iframe, dialog.preview .md_preview { +iframe, .md_preview { overflow: auto; position: absolute; top: 0; @@ -3798,21 +3250,11 @@ iframe, iframe body, .md_preview { max-height: 100%; } -dialog.preview .md_preview { +.md_preview { width: calc(100vw - 2em); - height: calc(100vh - 4em); + height: calc(100vh - 2em); padding: 1em; text-align: left; - padding-bottom: 2em; -} - -.md_preview ul, .md_preview ol, .md_preview p, .md_preview blockquote, .md_preview pre, -.md_preview h1, .md_preview h2, .md_preview h3, .md_preview h4, .md_preview h5, .md_preview h6 { - margin: 1rem 0; -} - -.md_preview ul, .md_preview ol, .md_preview blockquote { - margin-left: 2em; } .preview .close { @@ -3901,19 +3343,6 @@ input[name=rename], input[name=paste_name] { dialog { transition: all .3s; - margin: auto; - padding: 1em; -} - -dialog:has(#mdp) { - margin: 0; - padding: .5em; - border-radius: none; - top: 0; - left: 0; - right: 0; - bottom: 0; - height: 100vh; } progress { @@ -3921,45 +3350,6 @@ progress { width: 90%; } -input.icon, .buttons a[download] { - background: #e0e6e6 no-repeat center center; - background-size: contain; - width: 1.8em; - height: 1.8em; - padding: 0; - overflow: hidden; - text-indent: 100em; - display: inline-block; -} - -input.delete { - background-image: url('data:image/svg+xml;utf8,'); -} - -input.rename { - background-image: url('data:image/svg+xml;utf8,'); -} - -.buttons a[download], input.download { - background-image: url('data:image/svg+xml;utf8,'); -} - -input.edit { - background-image: url('data:image/svg+xml;utf8,'); -} - -input.mkdir { - background-image: url('data:image/svg+xml;utf8,'); -} - -input.mkfile { - background-image: url('data:image/svg+xml;utf8,'); -} - -input.upload { - background-image: url('data:image/svg+xml;utf8,'); -} - @keyframes spin { to { transform: rotate(360deg); } } @media screen and (max-width: 800px) { @@ -3972,99 +3362,74 @@ input.upload { body { margin: 0; - } - - div.buttons { - flex-wrap: wrap; - justify-content: center; - flex-direction: row; - font-size: .9em; - } - - div.buttons .selected { - margin: 0; - position: fixed; - top: 0; - left: 0; - right: 0; - padding: 1em; - background: var(--bg-color); - box-shadow: 0px 0px 2px 5px var(--g3-color); + font-size: 1em; } table { + margin: 2em 0; width: 100%; display: flex; flex-direction: column; } - table tbody tr { - display: grid; - grid-template-columns: 3rem 1fr .3fr; - grid-template-areas: - "name name check" - "size date buttons"; - padding: 0; - } - - table tbody td, table tbody th { - border: none; + table tr { display: block; - padding: .2em .5em; - width: unset; + border-top: 5px solid var(--bg-color); + padding: 0; + padding-left: 2em; + position: relative; + text-align: left; + min-height: 2.5em; } - table tbody th { - grid-area: name; - } - - table tbody td.check { - grid-area: check; - text-align: right; - width: unset; - } - - table tbody td.buttons { - grid-area: buttons; - width: unset; - } - - table tbody td.date { - grid-area: date; + table td, table th { + border: none; display: inline-block; - min-width: unset; + padding: .2em .5em; } - table tbody td.size { - grid-area: size; - font-size: .8em; - width: 3rem; - overflow: hidden; - text-align: center; + table td.buttons { + display: block; + width: auto; + text-align: left; + } + + td.buttons div { + display: inline-block; + } + + table td.thumb { + padding: 0; + width: 2em; + position: absolute; + left: 0; + top: 0; + bottom: 0; + } + + table th { + display: block; + } + + .icon { + font-size: 12px; + height: 100%; + border-radius: 0; + } + + .icon:not(.dir) b { + line-height: 3em; + display: block; + transform: translateX(-50%) translateY(-50%) rotate(-90deg); + font-size: 2em; + height: 3em; + position: absolute; + top: 50%; + left: 50%; } table th a { font-size: 1.2em; - gap: .5em; - } - - table thead tr { - display: block; - text-align: left; - } - - table thead tr td { - display: inline-block; - vertical-align: top; - } - - table thead td.check { - float: right; - margin-right: .4em; - } - - table thead button { - font-size: 1rem; } input[name=rename], input[name=paste_name] { diff --git a/make.php b/make.php index 07a6295..8bb818f 100644 --- a/make.php +++ b/make.php @@ -15,8 +15,6 @@ function clean_php_source(string $file): string $php = file_get_contents('server.php'); $php = strtr($php, [ - '//__KD2\HTTP\Server__' => clean_php_source('lib/KD2/HTTP/Server.php'), - '//__KD2\WebDAV\Exception__' => clean_php_source('lib/KD2/WebDAV/Exception.php'), '//__KD2\WebDAV\Server__' => clean_php_source('lib/KD2/WebDAV/Server.php'), '//__KD2\WebDAV\AbstractStorage__' => clean_php_source('lib/KD2/WebDAV/AbstractStorage.php'), '/*__HTACCESS__*/' => var_export(file_get_contents('.htaccess'), true), diff --git a/scr.webp b/scr.webp deleted file mode 100644 index 3770a69..0000000 Binary files a/scr.webp and /dev/null differ diff --git a/server.php b/server.php index d821788..44e149c 100644 --- a/server.php +++ b/server.php @@ -1,14 +1,7 @@ $path]; } - public function fetch(string $uri): ?string - { - $r = $this->get($uri); - - if (!$r) { - return null; - } - - $r = file_get_contents($r['path']); - - if ($r === false) { - return null; - } - - return $r; - } - public function exists(string $uri): bool { return file_exists($this->path . $uri); @@ -470,6 +446,8 @@ namespace PicoDAV } else { $method($source, $target); + + $this->getResourceProperties($uri)->move($destination); } return $overwritten;