diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2ea0ef8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+webdav.css
+webdav.js
+lib/
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..a2b83f0
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,14 @@
+all: clean index.php
+
+deps:
+ @-mkdir -p lib/KD2/WebDAV
+ 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
+ wget -O webdav.css https://raw.githubusercontent.com/kd2org/webdav-manager.js/main/webdav.css
+
+clean:
+ rm -f index.php
+
+index.php:
+ php make.php
\ No newline at end of file
diff --git a/index.php b/index.php
index c159b4b..4bd9560 100644
--- a/index.php
+++ b/index.php
@@ -2,52 +2,9 @@
namespace KD2\WebDAV
{
- /*
- This file is part of KD2FW --
-
- Copyright (c) 2001-2022+ BohwaZ
- All rights reserved.
-
- KD2FW is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- KD2FW is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with KD2FW. If not, see .
- */
-
+
class Exception extends \RuntimeException {}
- /**
- * This is a minimal, lightweight, and self-supported WebDAV server
- * it does not require anything out of standard PHP, not even an XML library.
- * This makes it more secure by design, and also faster and lighter.
- *
- * - supports PROPFIND custom properties
- * - supports HTTP ranges for GET requests
- * - supports GZIP encoding for GET
- *
- * You have to extend the AbstractStorage class and implement all the abstract methods to
- * get a class-1 and 2 compliant server.
- *
- * By default, locking is simulated: nothing is really locked, like
- * in https://docs.rs/webdav-handler/0.2.0/webdav_handler/fakels/index.html
- *
- * You also have to implement the actual storage of properties for
- * PROPPATCH requests, by extending the 'setProperties' method.
- * But it's not required for WebDAV file storage, only for CardDAV/CalDAV.
- *
- * Differences with SabreDAV and RFC:
- * - If-Match, If-Range are not implemented
- *
- * @author BohwaZ
- */
class Server
{
// List of basic DAV properties that you should return if $requested_properties is NULL
@@ -69,30 +26,20 @@ namespace KD2\WebDAV
];
// Custom properties
- /**
- * File MD5 hash
- * Your implementation should return the hexadecimal encoded MD5 hash of the file
- */
+
const PROP_DIGEST_MD5 = 'urn:karadav:digest_md5';
- /**
- * Empty value if you want to have the property found and empty, return this constant
- */
const EMPTY_PROP_VALUE = 'DAV::empty';
const SHARED_LOCK = 'shared';
const EXCLUSIVE_LOCK = 'exclusive';
- /**
- * Base server URI (eg. "/index.php/webdav/")
- */
protected string $base_uri;
- /**
- * Original URI passed to route() before trim
- */
public string $original_uri;
+ public string $prefix = '';
+
protected AbstractStorage $storage;
public function setStorage(AbstractStorage $storage)
@@ -110,6 +57,15 @@ namespace KD2\WebDAV
$this->base_uri = rtrim($uri, '/') . '/';
}
+ protected function _prefix(string $uri): string
+ {
+ if (!$this->prefix) {
+ return $uri;
+ }
+
+ return rtrim(rtrim($this->prefix, '/') . '/' . ltrim($uri, '/'), '/');
+ }
+
protected function html_directory(string $uri, iterable $list): ?string
{
// Not a file: let's serve a directory listing if you are browsing with a web browser
@@ -180,7 +136,6 @@ namespace KD2\WebDAV
return $out;
}
-
public function http_delete(string $uri): ?string
{
// check RFC 2518 Section 9.2, last paragraph
@@ -188,6 +143,8 @@ namespace KD2\WebDAV
throw new Exception('We can only delete to infinity', 400);
}
+ $uri = $this->_prefix($uri);
+
$this->checkLock($uri);
$this->storage->delete($uri);
@@ -234,6 +191,8 @@ namespace KD2\WebDAV
$hash = bin2hex(base64_decode($_SERVER['HTTP_CONTENT_MD5']));
}
+ $uri = $this->_prefix($uri);
+
$this->checkLock($uri);
if (!empty($_SERVER['HTTP_IF_MATCH'])) {
@@ -272,6 +231,8 @@ namespace KD2\WebDAV
public function http_head(string $uri, array &$props = []): ?string
{
+ $uri = $this->_prefix($uri);
+
$requested_props = self::BASIC_PROPERTIES;
$requested_props[] = 'DAV::getetag';
@@ -326,6 +287,8 @@ namespace KD2\WebDAV
$props = [];
$this->http_head($uri, $props);
+ $uri = $this->_prefix($uri);
+
$is_collection = !empty($props['DAV::resourcetype']) && $props['DAV::resourcetype'] == 'collection';
$out = '';
@@ -418,7 +381,6 @@ namespace KD2\WebDAV
$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);
@@ -524,6 +486,8 @@ namespace KD2\WebDAV
protected function _http_copymove(string $uri, string $method): ?string
{
+ $uri = $this->_prefix($uri);
+
$destination = $_SERVER['HTTP_DESTINATION'] ?? null;
$depth = $_SERVER['HTTP_DEPTH'] ?? 1;
@@ -582,17 +546,13 @@ namespace KD2\WebDAV
throw new Exception('Unsupported body for MKCOL', 415);
}
+ $uri = $this->_prefix($uri);
$this->storage->mkcol($uri);
http_response_code(201);
return null;
}
- /**
- * Return a list of requested properties, if any.
- * We are using regexp as we don't want to depend on a XML module here.
- * Your are free to re-implement this using a XML parser if you wish
- */
protected function extractRequestedProperties(string $body): ?array
{
// We only care about properties if the client asked for it
@@ -620,7 +580,7 @@ namespace KD2\WebDAV
$dav_ns = $ns['DAV:'] . ':';
}
- $regexp = '/<(' . $dav_ns . 'prop(?!find))[^>]*?>(.*?)<\/\1\s*>/s';
+ $regexp = '/<(' . $dav_ns . 'prop(?!find))[^>]*(.*?)<\/\1\s*>/s';
if (!preg_match($regexp, $body, $match)) {
return null;
}
@@ -657,6 +617,7 @@ namespace KD2\WebDAV
// We only support depth of 0 and 1
$depth = isset($_SERVER['HTTP_DEPTH']) && empty($_SERVER['HTTP_DEPTH']) ? 0 : 1;
+ $uri = $this->_prefix($uri);
$body = file_get_contents('php://input');
if (false !== strpos($body, '';
+ $out = ' $alias) {
@@ -757,7 +718,12 @@ namespace KD2\WebDAV
foreach ($items as $uri => $item) {
$e = '';
- $path = '/' . str_replace('%2F', '/', rawurlencode(trim($this->base_uri . $uri, '/')));
+ if ($this->prefix) {
+ $uri = substr($uri, strlen($this->prefix));
+ }
+
+ $uri = trim(rtrim($this->base_uri, '/') . '/' . ltrim($uri, '/'), '/');
+ $path = '/' . str_replace('%2F', '/', rawurlencode($uri));
if (($item['DAV::resourcetype'] ?? null) == 'collection') {
$path .= '/';
@@ -930,6 +896,7 @@ namespace KD2\WebDAV
public function http_proppatch(string $uri): ?string
{
+ $uri = $this->_prefix($uri);
$this->checkLock($uri);
$body = file_get_contents('php://input');
@@ -940,7 +907,7 @@ namespace KD2\WebDAV
header('HTTP/1.1 207 Multi-Status', true);
header('Content-Type: application/xml; charset=utf-8');
- $out = '' . "\n";
+ $out = '';
$out .= '';
@@ -949,6 +916,7 @@ namespace KD2\WebDAV
public function http_lock(string $uri): ?string
{
+ $uri = $this->_prefix($uri);
// We don't use this currently, but maybe later?
//$depth = !empty($this->_SERVER['HTTP_DEPTH']) ? 1 : 0;
//$timeout = isset($_SERVER['HTTP_TIMEOUT']) ? explode(',', $_SERVER['HTTP_TIMEOUT']) : [];
@@ -1022,7 +990,7 @@ namespace KD2\WebDAV
header('Content-Type: application/xml; charset=utf-8');
header(sprintf('Lock-Token: <%s>', $token));
- $out = '' . "\n";
+ $out = '';
$out .= '';
@@ -1040,6 +1008,7 @@ namespace KD2\WebDAV
public function http_unlock(string $uri): ?string
{
+ $uri = $this->_prefix($uri);
$token = $this->getLockToken();
if (!$token) {
@@ -1056,9 +1025,6 @@ namespace KD2\WebDAV
return null;
}
- /**
- * Return current lock token supplied by client
- */
protected function getLockToken(): ?string
{
if (isset($_SERVER['HTTP_LOCK_TOKEN'])
@@ -1074,10 +1040,6 @@ namespace KD2\WebDAV
}
}
- /**
- * Check if the resource is protected
- * @throws Exception if the resource is locked
- */
protected function checkLock(string $uri, ?string $token = null): void
{
if ($token === null) {
@@ -1170,6 +1132,7 @@ namespace KD2\WebDAV
}
$uri = substr($uri, strlen($this->base_uri));
+ $uri = $this->_prefix($uri);
return $uri;
}
@@ -1261,12 +1224,9 @@ namespace KD2\WebDAV
header('Content-Type: application/xml; charset=utf-8', true);
- printf('%s', htmlspecialchars($e->getMessage(), ENT_XML1));
+ printf('%s', htmlspecialchars($e->getMessage(), ENT_XML1));
}
- /**
- * Utility function to create HMAC hash of data, useful for NextCloud and WOPI
- */
static public function hmac(array $data, string $key = '')
{
// Protect against length attacks by pre-hashing data
@@ -1278,158 +1238,49 @@ namespace KD2\WebDAV
}
- abstract class AbstractStorage
+ abstract class AbstractStorage
{
- /**
- * Return the requested resource
- *
- * @param string $uri Path to resource
- * @return null|array An array containing one of those keys:
- * path => Full filesystem path to a local file, it will be streamed directly to the client
- * resource => a PHP resource (eg. returned by fopen) that will be streamed directly to the client
- * content => a string that will be returned
- * or NULL if the resource cannot be returned (404)
- *
- * It is recommended to use X-SendFile inside this method to make things faster.
- * @see https://tn123.org/mod_xsendfile/
- */
+
abstract public function get(string $uri): ?array;
- /**
- * Return TRUE if the requested resource exists, or FALSE
- *
- * @param string $uri
- * @return bool
- */
abstract public function exists(string $uri): bool;
- /**
- * Return the requested resource properties
- *
- * This method is used for HEAD requests, for PROPFIND, and other places
- *
- * @param string $uri Path to resource
- * @param null|array $requested_properties Properties requested by the client, NULL if all available properties are requested,
- * or if specific properties are requested, each item will be a key,
- * like 'namespace_url:property_name', eg. 'DAV::getcontentlength' or 'http://owncloud.org/ns:size'
- * See Server::BASIC_PROPERTIES for default properties.
- * @param int $depth Depth, can be 0 or 1
- * @return null|array An array containing the requested properties, each item must have a key
- * of the same form as the requested properties.
- *
- * This method MUST return NULL if the resource does not exist.
- * Or it MUST return an array, where the keys are 'namespace_url:property_name' tuples,
- * and the value is the content of the property tag.
- */
abstract public function properties(string $uri, ?array $requested_properties, int $depth): ?array;
- /**
- * Store resource properties
- * @param string $uri
- * @param string $body XML PROPPATCH request, parsing it is up to you
- */
public function setProperties(string $uri, string $body): void
{
// By default, properties are not saved
}
- /**
- * Create or replace a resource
- * @param string $uri Path to resource
- * @param resource $pointer A PHP file resource containing the sent data (note that this might not always be seekable)
- * @param null|string $hash A MD5 hash of the resource to store, if it is supplied,
- * this method should fail with a 400 code WebDAV exception and not proceed to store the resource.
- * @param null|int $mtime The modification timestamp to set on the file
- * @return bool Return TRUE if the resource has been created, or FALSE it has just been updated.
- */
abstract public function put(string $uri, $pointer, ?string $hash, ?int $mtime): bool;
- /**
- * Delete a resource
- * @param string $uri
- * @return void
- */
abstract public function delete(string $uri): void;
- /**
- * Copy a resource from $uri to $destination
- * @param string $uri
- * @param string $destination
- * @return bool TRUE if the destination has been overwritten
- */
abstract public function copy(string $uri, string $destination): bool;
- /**
- * Move (rename) a resource from $uri to $destination
- * @param string $uri
- * @param string $destination
- * @return bool TRUE if the destination has been overwritten
- */
abstract public function move(string $uri, string $destination): bool;
- /**
- * Create collection of resources (eg. a directory)
- * @param string $uri
- * @return void
- */
abstract public function mkcol(string $uri): void;
- /**
- * Return a list of resources for target $uri
- *
- * @param string $uri
- * @param array $properties List of properties requested by client (see ::properties)
- * @return iterable An array or other iterable (eg. a generator)
- * where each item has a key string containing the name of the resource (eg. file name),
- * and the value being an array of properties, or NULL.
- *
- * If the array value IS NULL, then a subsequent call to properties() will be issued for each element.
- */
abstract public function list(string $uri, array $properties): iterable;
- /**
- * Lock the requested resource
- * @param string $uri Requested resource
- * @param string $token Unique token given to the client for this resource
- * @param string $scope Locking scope, either ::SHARED_LOCK or ::EXCLUSIVE_LOCK constant
- * @return void
- */
public function lock(string $uri, string $token, string $scope): void
{
// By default locking is not implemented
}
- /**
- * Unlock the requested resource
- * @param string $uri Requested resource
- * @param string $token Unique token sent by the client
- * @return void
- */
public function unlock(string $uri, string $token): void
{
// By default locking is not implemented
}
- /**
- * If $token is supplied, this method MUST return ::SHARED_LOCK or ::EXCLUSIVE_LOCK
- * if the resource is locked with this token. If the resource is unlocked, or if it is
- * locked with another token, it MUST return NULL.
- *
- * If $token is left NULL, then this method must return ::EXCLUSIVE_LOCK if there is any
- * exclusive lock on the resource. If there are no exclusive locks, but one or more
- * shared locks, it MUST return ::SHARED_LOCK. If the resource has no lock, it MUST
- * return NULL.
- *
- * @param string $uri
- * @param string|null $token
- * @return string|null
- */
public function getLock(string $uri, ?string $token = null): ?string
{
// By default locking is not implemented, so NULL is always returned
return null;
}
}
+
}
namespace NanoKaraDAV
@@ -1506,7 +1357,18 @@ namespace NanoKaraDAV
case 'DAV::resourcetype':
return is_dir($target) ? 'collection' : '';
case 'DAV::getlastmodified':
- return new \DateTime('@' . filemtime($target));
+ if (!$uri && $depth == 0 && is_dir($target)) {
+ $mtime = self::getDirectoryMTime($target);
+ }
+ else {
+ $mtime = filemtime($target);
+ }
+
+ if (!$mtime) {
+ return null;
+ }
+
+ return new \DateTime('@' . $mtime);
case 'DAV::displayname':
return basename($target);
case 'DAV::ishidden':
@@ -1715,6 +1577,30 @@ namespace NanoKaraDAV
mkdir($target, 0770);
}
+
+ static public function getDirectoryMTime(string $path): int
+ {
+ $last = 0;
+ $path = rtrim($path, '/');
+
+ foreach (glob($path . '/*', GLOB_NOSORT) as $f) {
+ if (is_dir($f)) {
+ $m = self::getDirectoryMTime($f);
+
+ if ($m > $last) {
+ $last = $m;
+ }
+ }
+
+ $m = filemtime($f);
+
+ if ($m > $last) {
+ $last = $m;
+ }
+ }
+
+ return $last;
+ }
}
class Server extends \KD2\WebDAV\Server
@@ -1765,12 +1651,12 @@ namespace {
$fp = fopen(__FILE__, 'r');
if ($relative_uri == 'webdav.js') {
- fseek($fp, 49434, SEEK_SET);
+ fseek($fp, 43642, SEEK_SET);
echo fread($fp, 24229);
}
else {
- fseek($fp, 49434 + 24229, SEEK_SET);
- echo fread($fp, 6728);
+ fseek($fp, 43642 + 24229, SEEK_SET);
+ echo fread($fp, 6752);
}
fclose($fp);
@@ -2578,6 +2464,7 @@ th, td {
padding: .5em;
text-align: left;
border: 2px solid var(--g2-color);
+ word-break: break-all;
}
td.thumb {
diff --git a/make.php b/make.php
index 25d039f..c74625a 100644
--- a/make.php
+++ b/make.php
@@ -2,8 +2,21 @@
$out = fopen('index.php', 'w');
+function clean_php_source(string $file): string
+{
+ $php = file_get_contents($file);
+ $php = preg_replace('/^namespace\s+.*;\s*$/m', '', $php);
+ $php = preg_replace('/<\?php\s*|\s*\?>/', '', $php);
+ $php = preg_replace(';/\*(?!\*/).*?\*/;s', '', $php);
+ $php = preg_replace('/^/m', "\t", $php);
+ $php = preg_replace('/^\s*$/m', "", $php);
+ return $php;
+}
+
$php = file_get_contents('server.php');
$php = strtr($php, [
+ '//__KD2\WebDAV\Server__' => clean_php_source('lib/KD2/WebDAV/Server.php'),
+ '//__KD2\WebDAV\AbstractStorage__' => clean_php_source('lib/KD2/WebDAV/AbstractStorage.php'),
'__JS_SIZE__' => filesize('webdav.js'),
'__CSS_SIZE__' => filesize('webdav.css'),
]);
diff --git a/server.php b/server.php
index bc0286e..44ffc46 100644
--- a/server.php
+++ b/server.php
@@ -2,1434 +2,9 @@
namespace KD2\WebDAV
{
- /*
- This file is part of KD2FW --
+ //__KD2\WebDAV\Server__
- Copyright (c) 2001-2022+ BohwaZ
- All rights reserved.
-
- KD2FW is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- KD2FW is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with KD2FW. If not, see .
- */
-
- class Exception extends \RuntimeException {}
-
- /**
- * This is a minimal, lightweight, and self-supported WebDAV server
- * it does not require anything out of standard PHP, not even an XML library.
- * This makes it more secure by design, and also faster and lighter.
- *
- * - supports PROPFIND custom properties
- * - supports HTTP ranges for GET requests
- * - supports GZIP encoding for GET
- *
- * You have to extend the AbstractStorage class and implement all the abstract methods to
- * get a class-1 and 2 compliant server.
- *
- * By default, locking is simulated: nothing is really locked, like
- * in https://docs.rs/webdav-handler/0.2.0/webdav_handler/fakels/index.html
- *
- * You also have to implement the actual storage of properties for
- * PROPPATCH requests, by extending the 'setProperties' method.
- * But it's not required for WebDAV file storage, only for CardDAV/CalDAV.
- *
- * Differences with SabreDAV and RFC:
- * - If-Match, If-Range are not implemented
- *
- * @author BohwaZ
- */
- class Server
- {
- // List of basic DAV properties that you should return if $requested_properties is NULL
- const BASIC_PROPERTIES = [
- 'DAV::resourcetype', // should be empty for files, and 'collection' for directories
- 'DAV::getcontenttype', // MIME type
- 'DAV::getlastmodified', // File modification date (must be \DateTimeInterface)
- 'DAV::getcontentlength', // file size
- 'DAV::displayname', // File name for display
- ];
-
- const EXTENDED_PROPERTIES = [
- 'DAV::getetag',
- 'DAV::creationdate',
- 'DAV::lastaccessed',
- 'DAV::ishidden', // Microsoft thingy
- 'DAV::quota-used-bytes',
- 'DAV::quota-available-bytes',
- ];
-
- // Custom properties
- /**
- * File MD5 hash
- * Your implementation should return the hexadecimal encoded MD5 hash of the file
- */
- const PROP_DIGEST_MD5 = 'urn:karadav:digest_md5';
-
- /**
- * Empty value if you want to have the property found and empty, return this constant
- */
- const EMPTY_PROP_VALUE = 'DAV::empty';
-
- const SHARED_LOCK = 'shared';
- const EXCLUSIVE_LOCK = 'exclusive';
-
- /**
- * Base server URI (eg. "/index.php/webdav/")
- */
- protected string $base_uri;
-
- /**
- * Original URI passed to route() before trim
- */
- public string $original_uri;
-
- protected AbstractStorage $storage;
-
- public function setStorage(AbstractStorage $storage)
- {
- $this->storage = $storage;
- }
-
- public function getStorage(): AbstractStorage
- {
- return $this->storage;
- }
-
- public function setBaseURI(string $uri): void
- {
- $this->base_uri = rtrim($uri, '/') . '/';
- }
-
- protected function html_directory(string $uri, iterable $list): ?string
- {
- // Not a file: let's serve a directory listing if you are browsing with a web browser
- if (substr($this->original_uri, -1) != '/') {
- http_response_code(301);
- header(sprintf('Location: /%s/', trim($this->base_uri . $uri, '/')), true);
- return null;
- }
-
- $out = sprintf('', '/' . ltrim($this->base_uri, '/'));
-
- $out .= sprintf('%s%1$s
', htmlspecialchars($uri ? str_replace('/', ' / ', $uri) . ' - Files' : 'Files'));
-
- if (trim($uri)) {
- $out .= 'Back |
';
- }
-
- $props = null;
-
- foreach ($list as $file => $props) {
- if (null === $props) {
- $props = $this->storage->properties(trim($uri . '/' . $file, '/'), self::BASIC_PROPERTIES, 0);
- }
-
- $collection = !empty($props['DAV::resourcetype']) && $props['DAV::resourcetype'] == 'collection';
-
- if ($collection) {
- $out .= sprintf('[DIR] | %s |
', rawurlencode($file), htmlspecialchars($file));
- }
- else {
- $size = $props['DAV::getcontentlength'];
-
- if ($size > 1024*1024) {
- $size = sprintf('%d MB', $size / 1024 / 1024);
- }
- elseif ($size) {
- $size = sprintf('%d KB', $size / 1024);
- }
-
- $date = $props['DAV::getlastmodified'];
-
- if ($date instanceof \DateTimeInterface) {
- $date = $date->format('d/m/Y H:i');
- }
-
- $out .= sprintf(' | %s | %s | %s |
',
- rawurlencode($file),
- htmlspecialchars($file),
- $size,
- $date
- );
- }
- }
-
- $out .= '
';
-
- if (null === $props) {
- $out .= 'This directory is empty.
';
- }
-
- $out .= '';
-
- return $out;
- }
-
-
- public function http_delete(string $uri): ?string
- {
- // check RFC 2518 Section 9.2, last paragraph
- if (isset($_SERVER['HTTP_DEPTH']) && $_SERVER['HTTP_DEPTH'] != 'infinity') {
- throw new Exception('We can only delete to infinity', 400);
- }
-
- $this->checkLock($uri);
-
- $this->storage->delete($uri);
-
- if ($token = $this->getLockToken()) {
- $this->storage->unlock($uri, $token);
- }
-
- http_response_code(204);
- header('Content-Length: 0', true);
- return null;
- }
-
- public function http_put(string $uri): ?string
- {
- if (!empty($_SERVER['HTTP_CONTENT_TYPE']) && !strncmp($_SERVER['HTTP_CONTENT_TYPE'], 'multipart/', 10)) {
- throw new Exception('Multipart PUT requests are not supported', 501);
- }
-
- 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);
- }
- else {
- throw new Exception('Content Encoding is not supported', 501);
- }
- }
-
- 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 (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);
- }
-
- $hash = null;
-
- // Support for checksum matching
- // https://dcache.org/old/manuals/UserGuide-6.0/webdav.shtml#checksums
- if (!empty($_SERVER['HTTP_CONTENT_MD5'])) {
- $hash = bin2hex(base64_decode($_SERVER['HTTP_CONTENT_MD5']));
- }
-
- $this->checkLock($uri);
-
- if (!empty($_SERVER['HTTP_IF_MATCH'])) {
- $etag = trim($_SERVER['HTTP_IF_MATCH'], '" ');
- $prop = $this->storage->properties($uri, ['DAV::getetag'], 0);
-
- if (!empty($prop['DAV::getetag']) && $prop['DAV::getetag'] != $etag) {
- throw new Exception('ETag did not match condition', 412);
- }
- }
-
- // Specific to NextCloud/ownCloud
- $mtime = (int)($_SERVER['HTTP_X_OC_MTIME'] ?? 0) ?: null;
-
- if ($mtime) {
- header('X-OC-MTime: accepted');
- }
-
- $created = $this->storage->put($uri, fopen('php://input', 'r'), $hash, $mtime);
-
- $prop = $this->storage->properties($uri, ['DAV::getetag'], 0);
-
- if (!empty($prop['DAV::getetag'])) {
- $value = $prop['DAV::getetag'];
-
- if (substr($value, 0, 1) != '"') {
- $value = '"' . $value . '"';
- }
-
- header(sprintf('ETag: %s', $value));
- }
-
- http_response_code($created ? 201 : 204);
- return null;
- }
-
- public function http_head(string $uri, array &$props = []): ?string
- {
- $requested_props = self::BASIC_PROPERTIES;
- $requested_props[] = 'DAV::getetag';
-
- // RFC 3230 https://www.rfc-editor.org/rfc/rfc3230.html
- if (!empty($_SERVER['HTTP_WANT_DIGEST'])) {
- $requested_props[] = self::PROP_DIGEST_MD5;
- }
-
- $props = $this->storage->properties($uri, $requested_props, 0);
-
- if (!$props) {
- throw new Exception('Resource Not Found', 404);
- }
-
- http_response_code(200);
-
- if (isset($props['DAV::getlastmodified'])
- && $props['DAV::getlastmodified'] instanceof \DateTimeInterface) {
- header(sprintf('Last-Modified: %s', $props['DAV::getlastmodified']->format(\DATE_RFC7231)));
- }
-
- if (!empty($props['DAV::getetag'])) {
- $value = $props['DAV::getetag'];
-
- if (substr($value, 0, 1) != '"') {
- $value = '"' . $value . '"';
- }
-
- header(sprintf('ETag: %s', $value));
- }
-
- if (empty($props['DAV::resourcetype']) || $props['DAV::resourcetype'] != 'collection') {
- if (!empty($props['DAV::getcontenttype'])) {
- header(sprintf('Content-Type: %s', $props['DAV::getcontenttype']));
- }
-
- if (!empty($props['DAV::getcontentlength'])) {
- header(sprintf('Content-Length: %d', $props['DAV::getcontentlength']));
- header('Accept-Ranges: bytes');
- }
- }
-
- if (!empty($props[self::PROP_DIGEST_MD5])) {
- header(sprintf('Digest: md5=%s', base64_encode(hex2bin($props[self::PROP_DIGEST_MD5]))));
- }
-
- return null;
- }
-
- public function http_get(string $uri): ?string
- {
- $props = [];
- $this->http_head($uri, $props);
-
- $is_collection = !empty($props['DAV::resourcetype']) && $props['DAV::resourcetype'] == 'collection';
- $out = '';
-
- if ($is_collection) {
- $list = $this->storage->list($uri, self::BASIC_PROPERTIES);
-
- if (!isset($_SERVER['HTTP_ACCEPT']) || false === strpos($_SERVER['HTTP_ACCEPT'], 'html')) {
- $list = is_array($list) ? $list : iterator_to_array($list);
-
- if (!count($list)) {
- return "Nothing in this collection\n";
- }
-
- return implode("\n", array_keys($list));
- }
-
- header('Content-Type: text/html; charset=utf-8', true);
-
- return $this->html_directory($uri, $list);
- }
-
- $file = $this->storage->get($uri);
-
- if (!$file) {
- throw new Exception('File Not Found', 404);
- }
-
- // If the file was returned to the client by the storage backend, stop here
- if (!empty($file['stop'])) {
- return null;
- }
-
- if (!isset($file['content']) && !isset($file['resource']) && !isset($file['path'])) {
- throw new \RuntimeException('Invalid file array returned by ::get()');
- }
-
- $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);
- }
- elseif (isset($_SERVER['HTTP_ACCEPT_ENCODING'])
- && false !== strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip')
- // Don't compress already compressed content
- && !preg_match('/\.(?:mp4|m4a|zip|docx|xlsx|ods|odt|odp|7z|gz|bz2|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, ['level' => 9]);
-
- $fp = fopen('php://memory', '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);
- }
-
- while (!feof($file['resource']) && ($end === null || $end > 0)) {
- $l = $end !== null ? min(8192, $end) : 8192;
-
- echo fread($file['resource'], $l);
- flush();
-
- if (null !== $end) {
- $end -= 8192;
- }
- }
-
- fclose($file['resource']);
-
- return null;
- }
-
- public function http_copy(string $uri): ?string
- {
- return $this->_http_copymove($uri, 'copy');
- }
-
- public function http_move(string $uri): ?string
- {
- return $this->_http_copymove($uri, 'move');
- }
-
- protected function _http_copymove(string $uri, string $method): ?string
- {
- $destination = $_SERVER['HTTP_DESTINATION'] ?? null;
- $depth = $_SERVER['HTTP_DEPTH'] ?? 1;
-
- if (!$destination) {
- throw new Exception('Destination not supplied', 400);
- }
-
- $destination = $this->getURI($destination);
-
- if (trim($destination, '/') == trim($uri, '/')) {
- throw new Exception('Cannot move file to itself', 403);
- }
-
- $overwrite = ($_SERVER['HTTP_OVERWRITE'] ?? null) == 'T';
-
- // Dolphin is removing the file name when moving to root directory
- if (empty($destination)) {
- $destination = basename($uri);
- }
-
- $this->log('<= Destination: %s', $destination);
- $this->log('<= Overwrite: %s (%s)', $overwrite ? 'Yes' : 'No', $_SERVER['HTTP_OVERWRITE'] ?? null);
-
- if (!$overwrite && $this->storage->exists($destination)) {
- throw new Exception('File already exists and overwriting is disabled', 412);
- }
-
- if ($method == 'move') {
- $this->checkLock($uri);
- }
-
- $this->checkLock($destination);
-
- // Moving/copy of directory to an existing destination and depth=0
- // should do just nothing, see 'depth_zero_copy' test in litmus
- if ($depth == 0
- && $this->storage->exists($destination)
- && current($this->storage->properties($destination, ['DAV::resourcetype'], 0)) == 'collection') {
- $overwritten = $this->storage->exists($uri);
- }
- else {
- $overwritten = $this->storage->$method($uri, $destination);
- }
-
- if ($method == 'move' && ($token = $this->getLockToken())) {
- $this->storage->unlock($uri, $token);
- }
-
- http_response_code($overwritten ? 204 : 201);
- return null;
- }
-
- public function http_mkcol(string $uri): ?string
- {
- if (!empty($_SERVER['CONTENT_LENGTH'])) {
- throw new Exception('Unsupported body for MKCOL', 415);
- }
-
- $this->storage->mkcol($uri);
-
- http_response_code(201);
- return null;
- }
-
- /**
- * Return a list of requested properties, if any.
- * We are using regexp as we don't want to depend on a XML module here.
- * Your are free to re-implement this using a XML parser if you wish
- */
- protected function extractRequestedProperties(string $body): ?array
- {
- // We only care about properties if the client asked for it
- // If not, we consider that the client just requested to get everything
- if (!preg_match('!<(?:\w+:)?propfind!', $body)) {
- return null;
- }
-
- $ns = [];
- $dav_ns = null;
- $default_ns = null;
-
- if (preg_match('/]+xmlns="DAV:"/', $body)) {
- $default_ns = 'DAV:';
- }
-
- preg_match_all('!xmlns:(\w+)\s*=\s*"([^"]+)"!', $body, $match, PREG_SET_ORDER);
-
- // Find all aliased xmlns
- foreach ($match as $found) {
- $ns[$found[2]] = $found[1];
- }
-
- if (isset($ns['DAV:'])) {
- $dav_ns = $ns['DAV:'] . ':';
- }
-
- $regexp = '/<(' . $dav_ns . 'prop(?!find))[^>]*?>(.*?)<\/\1\s*>/s';
- if (!preg_match($regexp, $body, $match)) {
- return null;
- }
-
- // Find all properties
- // Allow for empty namespace, see Litmus FAQ for propnullns
- // https://github.com/tolsen/litmus/blob/master/FAQ
- preg_match_all('!<([\w-]+)[^>]*xmlns="([^"]*)"|<(?:([\w-]+):)?([\w-]+)!', $match[2], $match, PREG_SET_ORDER);
-
- $properties = [];
-
- foreach ($match as $found) {
- if (isset($found[4])) {
- $url = array_search($found[3], $ns) ?: $default_ns;
- $name = $found[4];
- }
- else {
- $url = $found[2];
- $name = $found[1];
- }
-
- $properties[$url . ':' . $name] = [
- 'name' => $name,
- 'ns_alias' => $found[3] ?? null,
- 'ns_url' => $url,
- ];
- }
-
- return $properties;
- }
-
- public function http_propfind(string $uri): ?string
- {
- // We only support depth of 0 and 1
- $depth = isset($_SERVER['HTTP_DEPTH']) && empty($_SERVER['HTTP_DEPTH']) ? 0 : 1;
-
- $body = file_get_contents('php://input');
-
- if (false !== strpos($body, '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
- if (isset($_SERVER['HTTP_X_LITMUS'])) {
- if (false !== strpos($body, 'extractRequestedProperties($body);
- $requested_keys = $requested ? array_keys($requested) : null;
-
- // Find root element properties
- $properties = $this->storage->properties($uri, $requested_keys, $depth);
-
- if (null === $properties) {
- throw new Exception('This does not exist', 404);
- }
-
- $items = [$uri => $properties];
-
- if ($depth) {
- foreach ($this->storage->list($uri, $requested) as $file => $properties) {
- $path = trim($uri . '/' . $file, '/');
- $properties = $properties ?? $this->storage->properties($path, $requested_keys, 0);
-
- if (!$properties) {
- $this->log('!!! Cannot find "%s"', $path);
- continue;
- }
-
- $items[$path] = $properties;
- }
- }
-
- // http_response_code doesn't know the 207 status code
- header('HTTP/1.1 207 Multi-Status', true);
- $this->dav_header();
- header('Content-Type: application/xml; charset=utf-8');
-
- $root_namespaces = [
- 'DAV:' => 'd',
- // Microsoft Clients need this special namespace for date and time values (from PEAR/WebDAV)
- 'urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/' => 'ns0',
- ];
-
- $i = 0;
- $requested ??= [];
-
- foreach ($requested as $prop) {
- if ($prop['ns_url'] == 'DAV:' || !$prop['ns_url']) {
- continue;
- }
-
- if (!array_key_exists($prop['ns_url'], $root_namespaces)) {
- $root_namespaces[$prop['ns_url']] = $prop['ns_alias'] ?: 'rns' . $i++;
- }
- }
-
- foreach ($items as $properties) {
- foreach ($properties as $name => $value) {
- $pos = strrpos($name, ':');
- $ns = substr($name, 0, strrpos($name, ':'));
-
- // NULL namespace, see Litmus FAQ for propnullns
- if (!$ns) {
- continue;
- }
-
- if (!array_key_exists($ns, $root_namespaces)) {
- $root_namespaces[$ns] = 'rns' . $i++;
- }
- }
- }
-
- $out = '';
- $out .= ' $alias) {
- $out .= sprintf(' xmlns:%s="%s"', $alias, $url);
- }
-
- $out .= '>';
-
- foreach ($items as $uri => $item) {
- $e = '';
-
- $path = '/' . str_replace('%2F', '/', rawurlencode(trim($this->base_uri . $uri, '/')));
-
- if (($item['DAV::resourcetype'] ?? null) == 'collection') {
- $path .= '/';
- }
-
- $e .= sprintf('%s', htmlspecialchars($path, ENT_XML1));
- $e .= '';
-
- foreach ($item as $name => $value) {
- if (null === $value) {
- continue;
- }
-
- $pos = strrpos($name, ':');
- $ns = substr($name, 0, strrpos($name, ':'));
- $tag_name = substr($name, strrpos($name, ':') + 1);
-
- $alias = $root_namespaces[$ns] ?? null;
- $attributes = '';
-
- // The ownCloud Android app doesn't like formatted dates, it makes it crash.
- // so force it to have a timestamp
- if ($name == 'DAV::creationdate'
- && ($value instanceof \DateTimeInterface)
- && false !== stripos($_SERVER['HTTP_USER_AGENT'] ?? '', 'owncloud')) {
- $value = $value->getTimestamp();
- }
- // ownCloud app crashes if mimetype is provided for a directory
- // https://github.com/owncloud/android/issues/3768
- elseif ($name == 'DAV::getcontenttype'
- && ($item['DAV::resourcetype'] ?? null) == 'collection') {
- $value = null;
- }
-
- if ($name == 'DAV::resourcetype' && $value == 'collection') {
- $value = '';
- }
- elseif ($name == 'DAV::getetag' && strlen($value) && $value[0] != '"') {
- $value = '"' . $value . '"';
- }
- elseif ($value instanceof \DateTimeInterface) {
- // Change value to GMT
- $value = clone $value;
- $value->setTimezone(new \DateTimeZone('GMT'));
- $value = $value->format(DATE_RFC7231);
- }
- elseif (is_array($value)) {
- $attributes = $value['attributes'] ?? '';
- $value = $value['xml'] ?? null;
- }
- else {
- $value = htmlspecialchars($value, ENT_XML1);
- }
-
- // NULL namespace, see Litmus FAQ for propnullns
- if (!$ns) {
- $attributes .= ' xmlns=""';
- }
- else {
- $tag_name = $alias . ':' . $tag_name;
- }
-
- if (null === $value || self::EMPTY_PROP_VALUE === $value) {
- $e .= sprintf('<%s%s />', $tag_name, $attributes ? ' ' . $attributes : '');
- }
- else {
- $e .= sprintf('<%s%s>%s%1$s>', $tag_name, $attributes ? ' ' . $attributes : '', $value);
- }
- }
-
- $e .= 'HTTP/1.1 200 OK' . "\n";
-
- // Append missing properties
- if (!empty($requested)) {
- $missing_properties = array_diff($requested_keys, array_keys($item));
-
- if (count($missing_properties)) {
- $e .= '';
-
- foreach ($missing_properties as $name) {
- $pos = strrpos($name, ':');
- $ns = substr($name, 0, strrpos($name, ':'));
- $name = substr($name, strrpos($name, ':') + 1);
- $alias = $root_namespaces[$ns] ?? null;
-
- // NULL namespace, see Litmus FAQ for propnullns
- if (!$alias) {
- $e .= sprintf('<%s xmlns="" />', $name);
- }
- else {
- $e .= sprintf('<%s:%s />', $alias, $name);
- }
- }
-
- $e .= 'HTTP/1.1 404 Not Found';
- }
- }
-
- $e .= '' . "\n";
- $out .= $e;
- }
-
- $out .= '';
-
- return $out;
- }
-
- static public function parsePropPatch(string $body): array
- {
- if (false !== strpos($body, 'getDocNameSpaces()))) {
- $_ns = 'DAV:';
- }
-
- $out = [];
-
- // Process set/remove instructions in order (important)
- foreach ($xml->children($_ns) as $child) {
- foreach ($child->children($_ns) as $prop) {
- $prop = $prop->children();
- if ($child->getName() == 'set') {
- $ns = $prop->getNamespaces(true);
- $ns = array_flip($ns);
- $name = key($ns) . ':' . $prop->getName();
-
- $attributes = $prop->attributes();
- $attributes = $attributes === null ? null : iterator_to_array($attributes);
-
- foreach ($ns as $xmlns => $alias) {
- foreach (iterator_to_array($prop->attributes($alias)) as $key => $v) {
- $attributes[$xmlns . ':' . $key] = $value;
- }
- }
-
- if ($prop->count() > 1) {
- $text = '';
-
- foreach ($prop->children() as $c) {
- $text .= $c->asXML();
- }
- }
- else {
- $text = (string)$prop;
- }
-
- $out[$name] = ['action' => 'set', 'attributes' => $attributes ?: null, 'content' => $text ?: null];
- }
- else {
- $ns = $prop->getNamespaces();
- $name = current($ns) . ':' . $prop->getName();
- $out[$name] = ['action' => 'remove'];
- }
- }
- }
-
- return $out;
- }
-
- public function http_proppatch(string $uri): ?string
- {
- $this->checkLock($uri);
-
- $body = file_get_contents('php://input');
-
- $this->storage->setProperties($uri, $body);
-
- // http_response_code doesn't know the 207 status code
- header('HTTP/1.1 207 Multi-Status', true);
- header('Content-Type: application/xml; charset=utf-8');
-
- $out = '' . "\n";
- $out .= '';
- $out .= '';
-
- return $out;
- }
-
- public function http_lock(string $uri): ?string
- {
- // We don't use this currently, but maybe later?
- //$depth = !empty($this->_SERVER['HTTP_DEPTH']) ? 1 : 0;
- //$timeout = isset($_SERVER['HTTP_TIMEOUT']) ? explode(',', $_SERVER['HTTP_TIMEOUT']) : [];
- //$timeout = array_map('trim', $timeout);
-
- if (empty($_SERVER['CONTENT_LENGTH']) && !empty($_SERVER['HTTP_IF'])) {
- $token = $this->getLockToken();
-
- if (!$token) {
- throw new Exception('Invalid If header', 400);
- }
-
- $info = null;
- $ns = 'D';
- $scope = self::EXCLUSIVE_LOCK;
-
- $this->checkLock($uri, $token);
- $this->log('Requesting LOCK refresh: %s = %s', $uri, $scope);
- }
- else {
- $locked_scope = $this->storage->getLock($uri);
-
- if ($locked_scope == self::EXCLUSIVE_LOCK) {
- throw new Exception('Cannot acquire another lock, resource is locked for exclusive use', 423);
- }
-
- if ($locked_scope && $token = $this->getLockToken()) {
- $token = $this->getLockToken();
-
- if (!$token) {
- throw new Exception('Missing lock token', 423);
- }
-
- $this->checkLock($uri, $token);
- }
-
- $xml = file_get_contents('php://input');
-
- if (!preg_match('!<((?:(\w+):)?lockinfo)[^>]*>(.*?)\1>!is', $xml, $match)) {
- throw new Exception('Invalid XML', 400);
- }
-
- $ns = $match[2];
- $info = $match[3];
-
- // Quick and dirty UUID
- $uuid = random_bytes(16);
- $uuid[6] = chr(ord($uuid[6]) & 0x0f | 0x40); // set version to 0100
- $uuid[8] = chr(ord($uuid[8]) & 0x3f | 0x80); // set bits 6-7 to 10
- $uuid = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($uuid), 4));
-
- $token = 'opaquelocktoken:' . $uuid;
- $scope = false !== stripos($info, sprintf('<%sexclusive', $ns ? $ns . ':' : '')) ? self::EXCLUSIVE_LOCK : self::SHARED_LOCK;
-
- $this->log('Requesting LOCK: %s = %s', $uri, $scope);
- }
-
- $this->storage->lock($uri, $token, $scope);
-
- $timeout = 60*5;
- $info = sprintf('
-
-
- unknown
- %d
- Second-%d
- %s
- ', $scope, 1, $timeout, $token);
-
- http_response_code(200);
- header('Content-Type: application/xml; charset=utf-8');
- header(sprintf('Lock-Token: <%s>', $token));
-
- $out = '' . "\n";
- $out .= '';
- $out .= '';
-
- $out .= $info;
-
- $out .= '';
-
- if ($ns != 'D') {
- $out = str_replace('D:', $ns ? $ns . ':' : '', $out);
- $out = str_replace('xmlns:D', $ns ? 'xmlns:' . $ns : 'xmlns', $out);
- }
-
- return $out;
- }
-
- public function http_unlock(string $uri): ?string
- {
- $token = $this->getLockToken();
-
- if (!$token) {
- throw new Exception('Invalid Lock-Token header', 400);
- }
-
- $this->log('<= Lock Token: %s', $token);
-
- $this->checkLock($uri, $token);
-
- $this->storage->unlock($uri, $token);
-
- http_response_code(204);
- return null;
- }
-
- /**
- * Return current lock token supplied by client
- */
- protected function getLockToken(): ?string
- {
- if (isset($_SERVER['HTTP_LOCK_TOKEN'])
- && preg_match('/<(.*?)>/', trim($_SERVER['HTTP_LOCK_TOKEN']), $match)) {
- return $match[1];
- }
- elseif (isset($_SERVER['HTTP_IF'])
- && preg_match('/\(<(.*?)>\)/', trim($_SERVER['HTTP_IF']), $match)) {
- return $match[1];
- }
- else {
- return null;
- }
- }
-
- /**
- * Check if the resource is protected
- * @throws Exception if the resource is locked
- */
- protected function checkLock(string $uri, ?string $token = null): void
- {
- if ($token === null) {
- $token = $this->getLockToken();
- }
-
- // Trying to access using a parent directory
- if (isset($_SERVER['HTTP_IF'])
- && preg_match('/<([^>]+)>\s*\(<[^>]*>\)/', $_SERVER['HTTP_IF'], $match)) {
- $root = $this->getURI($match[1]);
-
- if (0 !== strpos($uri, $root)) {
- throw new Exception('Invalid "If" header path: ' . $root, 400);
- }
-
- $uri = $root;
- }
- // Try to validate token
- elseif (isset($_SERVER['HTTP_IF'])
- && preg_match('/\(<([^>]*)>\s+\["([^""]+)"\]\)/', $_SERVER['HTTP_IF'], $match)) {
- $token = $match[1];
- $request_etag = $match[2];
- $etag = current($this->storage->properties($uri, ['DAV::getetag'], 0));
-
- if ($request_etag != $etag) {
- throw new Exception('Resource is locked and etag does not match', 412);
- }
- }
-
- if ($token == 'DAV:no-lock') {
- throw new Exception('Resource is locked', 412);
- }
-
- // Token is valid
- if ($token && $this->storage->getLock($uri, $token)) {
- return;
- }
- elseif ($token) {
- throw new Exception('Invalid token', 400);
- }
- // Resource is locked
- elseif ($this->storage->getLock($uri)) {
- throw new Exception('Resource is locked', 423);
- }
- }
-
- protected function dav_header()
- {
- header('DAV: 1, 2, 3');
- }
-
- public function http_options(): void
- {
- http_response_code(200);
- $methods = 'GET HEAD PUT DELETE COPY MOVE PROPFIND MKCOL LOCK UNLOCK';
-
- $this->dav_header();
-
- header('Allow: ' . $methods);
- header('Content-length: 0');
- header('Accept-Ranges: bytes');
- header('MS-Author-Via: DAV');
- }
-
- public function log(string $message, ...$params)
- {
- if (PHP_SAPI == 'cli-server') {
- file_put_contents('php://stderr', vsprintf($message, $params) . "\n");
- }
- }
-
- protected function getURI(string $source): string
- {
- $uri = parse_url($source, PHP_URL_PATH);
- $uri = rawurldecode($uri);
- $uri = rtrim($uri, '/');
-
- if ($uri . '/' == $this->base_uri) {
- $uri .= '/';
- }
-
- if (strpos($uri, $this->base_uri) !== 0) {
- throw new Exception(sprintf('Invalid URI, "%s" is outside of scope "%s"', $uri, $this->base_uri), 400);
- }
-
- $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));
- return $uri;
- }
-
- public function route(?string $uri = null): bool
- {
- if (null === $uri) {
- $uri = $_SERVER['REQUEST_URI'] ?? '/';
- }
-
- $this->original_uri = $uri;
-
- if ($uri . '/' == $this->base_uri) {
- $uri .= '/';
- }
-
- if (0 === strpos($uri, $this->base_uri)) {
- $uri = substr($uri, strlen($this->base_uri));
- }
- else {
- $this->log('<= %s is not a managed URL', $uri);
- return false;
- }
-
- // Add some extra-logging for Litmus tests
- if (isset($_SERVER['HTTP_X_LITMUS']) || isset($_SERVER['HTTP_X_LITMUS_SECOND'])) {
- $this->log('X-Litmus: %s', $_SERVER['HTTP_X_LITMUS'] ?? $_SERVER['HTTP_X_LITMUS_SECOND']);
- }
-
- $method = $_SERVER['REQUEST_METHOD'] ?? null;
-
- header_remove('Expires');
- header_remove('Pragma');
- header_remove('Cache-Control');
- header('X-Server: KD2', true);
-
- // Stop and send reply to OPTIONS before anything else
- if ($method == 'OPTIONS') {
- $this->log('<= OPTIONS');
- $this->http_options();
- return true;
- }
-
- $uri = rawurldecode($uri);
- $uri = trim($uri, '/');
- $uri = preg_replace('!/{2,}!', '/', $uri);
-
- $this->log('<= %s /%s', $method, $uri);
-
- try {
- if (false !== strpos($uri, '..')) {
- throw new Exception(sprintf('Invalid URI: "%s"', $uri), 403);
- }
-
- // Call 'http_method' class method
- $method = 'http_' . strtolower($method);
-
- if (!method_exists($this, $method)) {
- throw new Exception('Invalid request method', 405);
- }
-
- $out = $this->$method($uri);
-
- $this->log('=> %d', http_response_code());
-
- if (null !== $out) {
- $this->log('=> %s', $out);
- }
-
- echo $out;
- }
- catch (Exception $e) {
- $this->error($e);
- }
-
- return true;
- }
-
- function error(Exception $e)
- {
- $this->log('=> %d - %s', $e->getCode(), $e->getMessage());
-
- if ($e->getCode() == 423) {
- // http_response_code doesn't know about 423 Locked
- header('HTTP/1.1 423 Locked');
- }
- else {
- http_response_code($e->getCode());
- }
-
- header('Content-Type: application/xml; charset=utf-8', true);
-
- printf('%s', htmlspecialchars($e->getMessage(), ENT_XML1));
- }
-
- /**
- * Utility function to create HMAC hash of data, useful for NextCloud and WOPI
- */
- static public function hmac(array $data, string $key = '')
- {
- // Protect against length attacks by pre-hashing data
- $data = array_map('sha1', $data);
- $data = implode(':', $data);
-
- return hash_hmac('sha1', $data, sha1($key));
- }
- }
-
-
- abstract class AbstractStorage
- {
- /**
- * Return the requested resource
- *
- * @param string $uri Path to resource
- * @return null|array An array containing one of those keys:
- * path => Full filesystem path to a local file, it will be streamed directly to the client
- * resource => a PHP resource (eg. returned by fopen) that will be streamed directly to the client
- * content => a string that will be returned
- * or NULL if the resource cannot be returned (404)
- *
- * It is recommended to use X-SendFile inside this method to make things faster.
- * @see https://tn123.org/mod_xsendfile/
- */
- abstract public function get(string $uri): ?array;
-
- /**
- * Return TRUE if the requested resource exists, or FALSE
- *
- * @param string $uri
- * @return bool
- */
- abstract public function exists(string $uri): bool;
-
- /**
- * Return the requested resource properties
- *
- * This method is used for HEAD requests, for PROPFIND, and other places
- *
- * @param string $uri Path to resource
- * @param null|array $requested_properties Properties requested by the client, NULL if all available properties are requested,
- * or if specific properties are requested, each item will be a key,
- * like 'namespace_url:property_name', eg. 'DAV::getcontentlength' or 'http://owncloud.org/ns:size'
- * See Server::BASIC_PROPERTIES for default properties.
- * @param int $depth Depth, can be 0 or 1
- * @return null|array An array containing the requested properties, each item must have a key
- * of the same form as the requested properties.
- *
- * This method MUST return NULL if the resource does not exist.
- * Or it MUST return an array, where the keys are 'namespace_url:property_name' tuples,
- * and the value is the content of the property tag.
- */
- abstract public function properties(string $uri, ?array $requested_properties, int $depth): ?array;
-
- /**
- * Store resource properties
- * @param string $uri
- * @param string $body XML PROPPATCH request, parsing it is up to you
- */
- public function setProperties(string $uri, string $body): void
- {
- // By default, properties are not saved
- }
-
- /**
- * Create or replace a resource
- * @param string $uri Path to resource
- * @param resource $pointer A PHP file resource containing the sent data (note that this might not always be seekable)
- * @param null|string $hash A MD5 hash of the resource to store, if it is supplied,
- * this method should fail with a 400 code WebDAV exception and not proceed to store the resource.
- * @param null|int $mtime The modification timestamp to set on the file
- * @return bool Return TRUE if the resource has been created, or FALSE it has just been updated.
- */
- abstract public function put(string $uri, $pointer, ?string $hash, ?int $mtime): bool;
-
- /**
- * Delete a resource
- * @param string $uri
- * @return void
- */
- abstract public function delete(string $uri): void;
-
- /**
- * Copy a resource from $uri to $destination
- * @param string $uri
- * @param string $destination
- * @return bool TRUE if the destination has been overwritten
- */
- abstract public function copy(string $uri, string $destination): bool;
-
- /**
- * Move (rename) a resource from $uri to $destination
- * @param string $uri
- * @param string $destination
- * @return bool TRUE if the destination has been overwritten
- */
- abstract public function move(string $uri, string $destination): bool;
-
- /**
- * Create collection of resources (eg. a directory)
- * @param string $uri
- * @return void
- */
- abstract public function mkcol(string $uri): void;
-
- /**
- * Return a list of resources for target $uri
- *
- * @param string $uri
- * @param array $properties List of properties requested by client (see ::properties)
- * @return iterable An array or other iterable (eg. a generator)
- * where each item has a key string containing the name of the resource (eg. file name),
- * and the value being an array of properties, or NULL.
- *
- * If the array value IS NULL, then a subsequent call to properties() will be issued for each element.
- */
- abstract public function list(string $uri, array $properties): iterable;
-
- /**
- * Lock the requested resource
- * @param string $uri Requested resource
- * @param string $token Unique token given to the client for this resource
- * @param string $scope Locking scope, either ::SHARED_LOCK or ::EXCLUSIVE_LOCK constant
- * @return void
- */
- public function lock(string $uri, string $token, string $scope): void
- {
- // By default locking is not implemented
- }
-
- /**
- * Unlock the requested resource
- * @param string $uri Requested resource
- * @param string $token Unique token sent by the client
- * @return void
- */
- public function unlock(string $uri, string $token): void
- {
- // By default locking is not implemented
- }
-
- /**
- * If $token is supplied, this method MUST return ::SHARED_LOCK or ::EXCLUSIVE_LOCK
- * if the resource is locked with this token. If the resource is unlocked, or if it is
- * locked with another token, it MUST return NULL.
- *
- * If $token is left NULL, then this method must return ::EXCLUSIVE_LOCK if there is any
- * exclusive lock on the resource. If there are no exclusive locks, but one or more
- * shared locks, it MUST return ::SHARED_LOCK. If the resource has no lock, it MUST
- * return NULL.
- *
- * @param string $uri
- * @param string|null $token
- * @return string|null
- */
- public function getLock(string $uri, ?string $token = null): ?string
- {
- // By default locking is not implemented, so NULL is always returned
- return null;
- }
- }
+ //__KD2\WebDAV\AbstractStorage__
}
namespace NanoKaraDAV
@@ -1506,7 +81,18 @@ namespace NanoKaraDAV
case 'DAV::resourcetype':
return is_dir($target) ? 'collection' : '';
case 'DAV::getlastmodified':
- return new \DateTime('@' . filemtime($target));
+ if (!$uri && $depth == 0 && is_dir($target)) {
+ $mtime = self::getDirectoryMTime($target);
+ }
+ else {
+ $mtime = filemtime($target);
+ }
+
+ if (!$mtime) {
+ return null;
+ }
+
+ return new \DateTime('@' . $mtime);
case 'DAV::displayname':
return basename($target);
case 'DAV::ishidden':
@@ -1715,6 +301,30 @@ namespace NanoKaraDAV
mkdir($target, 0770);
}
+
+ static public function getDirectoryMTime(string $path): int
+ {
+ $last = 0;
+ $path = rtrim($path, '/');
+
+ foreach (glob($path . '/*', GLOB_NOSORT) as $f) {
+ if (is_dir($f)) {
+ $m = self::getDirectoryMTime($f);
+
+ if ($m > $last) {
+ $last = $m;
+ }
+ }
+
+ $m = filemtime($f);
+
+ if ($m > $last) {
+ $last = $m;
+ }
+ }
+
+ return $last;
+ }
}
class Server extends \KD2\WebDAV\Server