Compare commits

..

No commits in common. "main" and "0.1.9" have entirely different histories.
main ... 0.1.9

4 changed files with 105 additions and 152 deletions

View file

@ -1,5 +1,4 @@
all: clean index.php all: clean index.php
KD2FW_URL := https://fossil.kd2.org/kd2fw/doc/trunk/src/lib/KD2/
deps: deps:
@-mkdir -p lib/KD2/WebDAV @-mkdir -p lib/KD2/WebDAV

View file

@ -2,7 +2,7 @@
## Single-file WebDAV server in PHP, just drop it in a directory! ## Single-file WebDAV server in PHP, just drop it in a directory!
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/). If you drop the [`index.php`](https://github.com/kd2org/picodav/raw/main/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://github.com/kd2org/webdav-manager.js).
![Web UI screenshot](https://raw.githubusercontent.com/kd2org/webdav-manager.js/main/scr_desktop.png) ![Web UI screenshot](https://raw.githubusercontent.com/kd2org/webdav-manager.js/main/scr_desktop.png)
@ -23,11 +23,6 @@ If you drop the [`index.php`](https://fossil.kd2.org/picodav/doc/trunk/index.php
* Restrict users to some directories, control where they can write! * Restrict users to some directories, control where they can write!
* Support for [rclone](https://rclone.org) as a NextCloud provider * Support for [rclone](https://rclone.org) as a NextCloud provider
## Development
* Main Fossil repository: <https://fossil.kd2.org/picodav/>
* Git mirror: <https://github.com/kd2org/picodav/> (issues and PR accepted)
## WebDAV clients ## WebDAV clients
You can use any WebDAV client, but we recommend these: You can use any WebDAV client, but we recommend these:
@ -38,7 +33,7 @@ You can use any WebDAV client, but we recommend these:
## Install ## Install
It's really as simple as it says: just upload the [`index.php`](https://fossil.kd2.org/picodav/doc/trunk/index.php) file to a directory on your web-server, and it will now be available via WebDAV! It's really as simple as it says: just upload the [`index.php`](https://github.com/kd2org/picodav/raw/main/index.php) file to a directory on your web-server, and it will now be available via WebDAV!
If you are using Apache (version 2.3.9 or later is required), a .htaccess file will be created if it does not exist, to redirect requests to `index.php`. If not, you can use the provided `.htaccess` as a basis for your server configuration. If you are using Apache (version 2.3.9 or later is required), a .htaccess file will be created if it does not exist, to redirect requests to `index.php`. If not, you can use the provided `.htaccess` as a basis for your server configuration.
@ -159,7 +154,7 @@ This is designed to work best with Apache web servers. If you are using another
## See also: KaraDAV ## See also: KaraDAV
[KaraDAV](https://fossil.kd2.org/karadav/) is another WebDAV server built by me, using the same library and the same web UI to manage files. [KaraDAV](https://github.com/kd2org/karadav/) is another WebDAV server built by me, using the same library and the same web UI to manage files.
How KaraDAV is different? Well, KaraDAV: How KaraDAV is different? Well, KaraDAV:

202
index.php
View file

@ -25,16 +25,6 @@ namespace KD2\WebDAV
'DAV::quota-available-bytes', 'DAV::quota-available-bytes',
]; ];
const PROP_NAMESPACE_MICROSOFT = 'urn:schemas-microsoft-com:';
const MODIFICATION_TIME_PROPERTIES = [
'DAV::lastmodified',
'DAV::creationdate',
'DAV::getlastmodified',
'urn:schemas-microsoft-com::Win32LastModifiedTime',
'urn:schemas-microsoft-com::Win32CreationTime',
];
// Custom properties // Custom properties
const PROP_DIGEST_MD5 = 'urn:karadav:digest_md5'; const PROP_DIGEST_MD5 = 'urn:karadav:digest_md5';
@ -66,8 +56,7 @@ namespace KD2\WebDAV
public function setBaseURI(string $uri): void public function setBaseURI(string $uri): void
{ {
$this->base_uri = '/' . ltrim($uri, '/'); $this->base_uri = rtrim($uri, '/') . '/';
$this->base_uri = rtrim($this->base_uri, '/') . '/';
} }
protected function extendExecutionTime(): void protected function extendExecutionTime(): void
@ -115,7 +104,7 @@ namespace KD2\WebDAV
foreach ($list as $file => $props) { foreach ($list as $file => $props) {
if (null === $props) { if (null === $props) {
$props = $this->storage->propfind(trim($uri . '/' . $file, '/'), self::BASIC_PROPERTIES, 0); $props = $this->storage->properties(trim($uri . '/' . $file, '/'), self::BASIC_PROPERTIES, 0);
} }
$collection = !empty($props['DAV::resourcetype']) && $props['DAV::resourcetype'] == 'collection'; $collection = !empty($props['DAV::resourcetype']) && $props['DAV::resourcetype'] == 'collection';
@ -220,7 +209,7 @@ namespace KD2\WebDAV
elseif (!empty($_SERVER['HTTP_OC_CHECKSUM']) elseif (!empty($_SERVER['HTTP_OC_CHECKSUM'])
&& preg_match('/MD5:[a-f0-9]{32}|SHA1:[a-f0-9]{40}/', $_SERVER['HTTP_OC_CHECKSUM'], $match)) { && preg_match('/MD5:[a-f0-9]{32}|SHA1:[a-f0-9]{40}/', $_SERVER['HTTP_OC_CHECKSUM'], $match)) {
$hash_algo = strtok($match[0], ':'); $hash_algo = strtok($match[0], ':');
$hash = strtok(''); $hash = strtok(false);
} }
$uri = $this->_prefix($uri); $uri = $this->_prefix($uri);
@ -229,7 +218,7 @@ namespace KD2\WebDAV
if (!empty($_SERVER['HTTP_IF_MATCH'])) { if (!empty($_SERVER['HTTP_IF_MATCH'])) {
$etag = trim($_SERVER['HTTP_IF_MATCH'], '" '); $etag = trim($_SERVER['HTTP_IF_MATCH'], '" ');
$prop = $this->storage->propfind($uri, ['DAV::getetag'], 0); $prop = $this->storage->properties($uri, ['DAV::getetag'], 0);
if (!empty($prop['DAV::getetag']) && $prop['DAV::getetag'] != $etag) { if (!empty($prop['DAV::getetag']) && $prop['DAV::getetag'] != $etag) {
throw new Exception('ETag did not match condition', 412); throw new Exception('ETag did not match condition', 412);
@ -240,6 +229,10 @@ namespace KD2\WebDAV
// This expects a UNIX timestamp // This expects a UNIX timestamp
$mtime = (int)($_SERVER['HTTP_X_OC_MTIME'] ?? 0) ?: null; $mtime = (int)($_SERVER['HTTP_X_OC_MTIME'] ?? 0) ?: null;
if ($mtime) {
header('X-OC-MTime: accepted');
}
$this->extendExecutionTime(); $this->extendExecutionTime();
$stream = fopen('php://input', 'r'); $stream = fopen('php://input', 'r');
@ -259,17 +252,9 @@ namespace KD2\WebDAV
fseek($stream, 0, SEEK_SET); fseek($stream, 0, SEEK_SET);
} }
$created = $this->storage->put($uri, $stream, $hash_algo, $hash); $created = $this->storage->put($uri, $stream, $hash_algo, $hash, $mtime);
if ($mtime) { $prop = $this->storage->properties($uri, ['DAV::getetag'], 0);
$mtime = new \DateTime('@' . $mtime);
if ($this->storage->touch($uri, $mtime)) {
header('X-OC-MTime: accepted');
}
}
$prop = $this->storage->propfind($uri, ['DAV::getetag'], 0);
if (!empty($prop['DAV::getetag'])) { if (!empty($prop['DAV::getetag'])) {
$value = $prop['DAV::getetag']; $value = $prop['DAV::getetag'];
@ -297,7 +282,7 @@ namespace KD2\WebDAV
$requested_props[] = self::PROP_DIGEST_MD5; $requested_props[] = self::PROP_DIGEST_MD5;
} }
$props = $this->storage->propfind($uri, $requested_props, 0); $props = $this->storage->properties($uri, $requested_props, 0);
if (!$props) { if (!$props) {
throw new Exception('Resource Not Found', 404); throw new Exception('Resource Not Found', 404);
@ -378,7 +363,7 @@ namespace KD2\WebDAV
} }
if (!isset($file['content']) && !isset($file['resource']) && !isset($file['path'])) { if (!isset($file['content']) && !isset($file['resource']) && !isset($file['path'])) {
throw new \RuntimeException('Invalid file array returned by ::get(): ' . print_r($file, true)); throw new \RuntimeException('Invalid file array returned by ::get()');
} }
$this->extendExecutionTime(); $this->extendExecutionTime();
@ -588,7 +573,7 @@ namespace KD2\WebDAV
// should do just nothing, see 'depth_zero_copy' test in litmus // should do just nothing, see 'depth_zero_copy' test in litmus
if ($depth == 0 if ($depth == 0
&& $this->storage->exists($destination) && $this->storage->exists($destination)
&& current($this->storage->propfind($destination, ['DAV::resourcetype'], 0)) == 'collection') { && current($this->storage->properties($destination, ['DAV::resourcetype'], 0)) == 'collection') {
$overwritten = $this->storage->exists($uri); $overwritten = $this->storage->exists($uri);
} }
else { else {
@ -707,24 +692,18 @@ namespace KD2\WebDAV
$requested_keys = $requested ? array_keys($requested) : null; $requested_keys = $requested ? array_keys($requested) : null;
// Find root element properties // Find root element properties
$properties = $this->storage->propfind($uri, $requested_keys, $depth); $properties = $this->storage->properties($uri, $requested_keys, $depth);
if (null === $properties) { if (null === $properties) {
throw new Exception('This does not exist', 404); throw new Exception('This does not exist', 404);
} }
if (isset($properties['DAV::getlastmodified'])) {
foreach (self::MODIFICATION_TIME_PROPERTIES as $name) {
$properties[$name] = $properties['DAV::getlastmodified'];
}
}
$items = [$uri => $properties]; $items = [$uri => $properties];
if ($depth) { if ($depth) {
foreach ($this->storage->list($uri, $requested) as $file => $properties) { foreach ($this->storage->list($uri, $requested) as $file => $properties) {
$path = trim($uri . '/' . $file, '/'); $path = trim($uri . '/' . $file, '/');
$properties = $properties ?? $this->storage->propfind($path, $requested_keys, 0); $properties = $properties ?? $this->storage->properties($path, $requested_keys, 0);
if (!$properties) { if (!$properties) {
$this->log('!!! Cannot find "%s"', $path); $this->log('!!! Cannot find "%s"', $path);
@ -968,93 +947,19 @@ namespace KD2\WebDAV
$uri = $this->_prefix($uri); $uri = $this->_prefix($uri);
$this->checkLock($uri); $this->checkLock($uri);
$prefix = '<?xml version="1.0" encoding="utf-8"?>' . "\n";
$prefix.= '<d:multistatus xmlns:d="DAV:"';
$suffix = "</d:multistatus>\n";
$body = file_get_contents('php://input'); $body = file_get_contents('php://input');
$properties = $this->parsePropPatch($body); $this->storage->setProperties($uri, $body);
$root_namespaces = [];
$i = 0;
$set_time = null;
$set_time_name = null;
foreach ($properties as $name => $value) {
$pos = strrpos($name, ':');
$ns = substr($name, 0, $pos);
if (!array_key_exists($ns, $root_namespaces)) {
$alias = 'rns' . $i++;
$root_namespaces[$ns] = $alias;
$prefix .= sprintf(' xmlns:%s="%s"', $alias, htmlspecialchars($ns, ENT_XML1));
}
}
// See if the client wants to set the modification time
foreach (self::MODIFICATION_TIME_PROPERTIES as $name) {
if (!array_key_exists($name, $properties) || $value['action'] !== 'set' || empty($value['content'])) {
continue;
}
$ts = $value['content'];
if (ctype_digit($ts)) {
$ts = '@' . $ts;
}
$set_time = new \DateTime($value['content']);
$set_time_name = $name;
}
$prefix .= sprintf(">\n<d:response>\n <d:href>%s</d:href>\n", htmlspecialchars($url, ENT_XML1));
// http_response_code doesn't know the 207 status code // http_response_code doesn't know the 207 status code
header('HTTP/1.1 207 Multi-Status', true); header('HTTP/1.1 207 Multi-Status', true);
header('Content-Type: application/xml; charset=utf-8', true); header('Content-Type: application/xml; charset=utf-8');
if (!count($properties)) { $out = '<?xml version="1.0" encoding="utf-8"?>' . "\n";
return $prefix . $suffix; $out .= '<d:multistatus xmlns:d="DAV:">';
} $out .= '</d:multistatus>';
if ($set_time) { return $out;
unset($properties[$set_time_name]);
}
$return = $this->storage->proppatch($uri, $properties);
if ($set_time && $this->touch($uri, $set_time)) {
$return[$set_time_name] = 200;
}
$out = '';
static $messages = [
200 => 'OK',
403 => 'Forbidden',
409 => 'Conflict',
427 => 'Failed Dependency',
507 => 'Insufficient Storage',
];
foreach ($return as $name => $status) {
$pos = strrpos($name, ':');
$ns = substr($name, 0, $pos);
$name = substr($name, $pos + 1);
$out .= " <d:propstat>\n <d:prop>";
$out .= sprintf("<%s:%s /></d:prop>\n <d:status>HTTP/1.1 %d %s</d:status>",
$root_namespaces[$ns],
$name,
$status,
$messages[$status] ?? ''
);
$out .= "\n </d:propstat>\n";
}
$out .= "</d:response>\n";
return $prefix . $out . $suffix;
} }
public function http_lock(string $uri): ?string public function http_lock(string $uri): ?string
@ -1205,7 +1110,7 @@ namespace KD2\WebDAV
&& preg_match('/\(<([^>]*)>\s+\["([^""]+)"\]\)/', $_SERVER['HTTP_IF'], $match)) { && preg_match('/\(<([^>]*)>\s+\["([^""]+)"\]\)/', $_SERVER['HTTP_IF'], $match)) {
$token = $match[1]; $token = $match[1];
$request_etag = $match[2]; $request_etag = $match[2];
$etag = current($this->storage->propfind($uri, ['DAV::getetag'], 0)); $etag = current($this->storage->properties($uri, ['DAV::getetag'], 0));
if ($request_etag != $etag) { if ($request_etag != $etag) {
throw new Exception('Resource is locked and etag does not match', 412); throw new Exception('Resource is locked and etag does not match', 412);
@ -1258,10 +1163,9 @@ namespace KD2\WebDAV
{ {
$uri = parse_url($source, PHP_URL_PATH); $uri = parse_url($source, PHP_URL_PATH);
$uri = rawurldecode($uri); $uri = rawurldecode($uri);
$uri = trim($uri, '/'); $uri = rtrim($uri, '/');
$uri = '/' . $uri;
if ($uri . '/' === $this->base_uri) { if ($uri . '/' == $this->base_uri) {
$uri .= '/'; $uri .= '/';
} }
@ -1286,7 +1190,6 @@ namespace KD2\WebDAV
$uri = $_SERVER['REQUEST_URI'] ?? '/'; $uri = $_SERVER['REQUEST_URI'] ?? '/';
} }
$uri = '/' . ltrim($uri, '/');
$this->original_uri = $uri; $this->original_uri = $uri;
if ($uri . '/' == $this->base_uri) { if ($uri . '/' == $this->base_uri) {
@ -1297,7 +1200,7 @@ namespace KD2\WebDAV
$uri = substr($uri, strlen($this->base_uri)); $uri = substr($uri, strlen($this->base_uri));
} }
else { else {
$this->log('<= %s is not a managed URL (%s)', $uri, $this->base_uri); $this->log('<= %s is not a managed URL', $uri);
return false; return false;
} }
@ -1390,14 +1293,14 @@ namespace KD2\WebDAV
abstract public function exists(string $uri): bool; abstract public function exists(string $uri): bool;
abstract public function propfind(string $uri, ?array $requested_properties, int $depth): ?array; abstract public function properties(string $uri, ?array $requested_properties, int $depth): ?array;
public function proppatch(string $uri, array $properties): array public function setProperties(string $uri, string $body): void
{ {
// By default, properties are not saved // By default, properties are not saved
} }
abstract public function put(string $uri, $pointer, ?string $hash_algo, ?string $hash): bool; abstract public function put(string $uri, $pointer, ?string $hash_algo, ?string $hash, ?int $mtime): bool;
abstract public function delete(string $uri): void; abstract public function delete(string $uri): void;
@ -1409,8 +1312,6 @@ namespace KD2\WebDAV
abstract public function list(string $uri, array $properties): iterable; abstract public function list(string $uri, array $properties): iterable;
abstract public function touch(string $uri, \DateTimeInterface $timestamp): bool;
public function lock(string $uri, string $token, string $scope): void public function lock(string $uri, string $token, string $scope): void
{ {
// By default locking is not implemented // By default locking is not implemented
@ -1633,7 +1534,12 @@ namespace PicoDAV
case 'DAV::resourcetype': case 'DAV::resourcetype':
return is_dir($target) ? 'collection' : ''; return is_dir($target) ? 'collection' : '';
case 'DAV::getlastmodified': case 'DAV::getlastmodified':
if (!$uri && $depth == 0 && is_dir($target)) {
$mtime = self::getDirectoryMTime($target);
}
else {
$mtime = filemtime($target); $mtime = filemtime($target);
}
if (!$mtime) { if (!$mtime) {
return null; return null;
@ -1681,7 +1587,7 @@ namespace PicoDAV
return null; return null;
} }
public function propfind(string $uri, ?array $properties, int $depth): ?array public function properties(string $uri, ?array $properties, int $depth): ?array
{ {
$target = $this->path . $uri; $target = $this->path . $uri;
@ -1706,7 +1612,7 @@ namespace PicoDAV
return $out; return $out;
} }
public function put(string $uri, $pointer, ?string $hash_algo, ?string $hash): bool public function put(string $uri, $pointer, ?string $hash_algo, ?string $hash, ?int $mtime): bool
{ {
if (preg_match(self::PUT_IGNORE_PATTERN, basename($uri))) { if (preg_match(self::PUT_IGNORE_PATTERN, basename($uri))) {
return false; return false;
@ -1766,6 +1672,10 @@ namespace PicoDAV
rename($tmp_file, $target); rename($tmp_file, $target);
} }
if ($mtime) {
@touch($target, $mtime);
}
return $new; return $new;
} }
@ -1892,10 +1802,28 @@ namespace PicoDAV
mkdir($target, 0770); mkdir($target, 0770);
} }
public function touch(string $uri, \DateTimeInterface $datetime): bool static public function getDirectoryMTime(string $path): int
{ {
$target = $this->path . $uri; $last = 0;
return @touch($target, $datetime->getTimestamp()); $path = rtrim($path, '/');
foreach (self::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;
} }
} }
@ -2014,11 +1942,11 @@ RewriteRule ^.*$ /index.php [END]
$fp = fopen(__FILE__, 'r'); $fp = fopen(__FILE__, 'r');
if ($relative_uri == '.webdav/webdav.js') { if ($relative_uri == '.webdav/webdav.js') {
fseek($fp, 55024, SEEK_SET); fseek($fp, 52782, SEEK_SET);
echo fread($fp, 27891); echo fread($fp, 28039);
} }
else { else {
fseek($fp, 55024 + 27891, SEEK_SET); fseek($fp, 52782 + 28039, SEEK_SET);
echo fread($fp, 7004); echo fread($fp, 7004);
} }
@ -2873,6 +2801,9 @@ const WebDAVNavigator = (url, options) => {
if (location.pathname.indexOf(base_url) === 0) { if (location.pathname.indexOf(base_url) === 0) {
current_url = location.pathname; current_url = location.pathname;
} }
else if (options.start_url) {
current_url = options.start_url;
}
if (!base_url.match(/^https?:/)) { if (!base_url.match(/^https?:/)) {
base_url = location.href.replace(/^(https?:\/\/[^\/]+\/).*$/, '$1') + base_url.replace(/^\/+/, ''); base_url = location.href.replace(/^(https?:\/\/[^\/]+\/).*$/, '$1') + base_url.replace(/^\/+/, '');
@ -2979,6 +2910,7 @@ const WebDAVNavigator = (url, options) => {
if (url = document.querySelector('html').getAttribute('data-webdav-url')) { if (url = document.querySelector('html').getAttribute('data-webdav-url')) {
WebDAVNavigator(url, { WebDAVNavigator(url, {
'start_url' : document.querySelector('html').getAttribute('data-start-url'),
'wopi_discovery_url': document.querySelector('html').getAttribute('data-wopi-discovery-url'), 'wopi_discovery_url': document.querySelector('html').getAttribute('data-wopi-discovery-url'),
}); });
} }

View file

@ -210,7 +210,12 @@ namespace PicoDAV
case 'DAV::resourcetype': case 'DAV::resourcetype':
return is_dir($target) ? 'collection' : ''; return is_dir($target) ? 'collection' : '';
case 'DAV::getlastmodified': case 'DAV::getlastmodified':
if (!$uri && $depth == 0 && is_dir($target)) {
$mtime = self::getDirectoryMTime($target);
}
else {
$mtime = filemtime($target); $mtime = filemtime($target);
}
if (!$mtime) { if (!$mtime) {
return null; return null;
@ -258,7 +263,7 @@ namespace PicoDAV
return null; return null;
} }
public function propfind(string $uri, ?array $properties, int $depth): ?array public function properties(string $uri, ?array $properties, int $depth): ?array
{ {
$target = $this->path . $uri; $target = $this->path . $uri;
@ -283,7 +288,7 @@ namespace PicoDAV
return $out; return $out;
} }
public function put(string $uri, $pointer, ?string $hash_algo, ?string $hash): bool public function put(string $uri, $pointer, ?string $hash_algo, ?string $hash, ?int $mtime): bool
{ {
if (preg_match(self::PUT_IGNORE_PATTERN, basename($uri))) { if (preg_match(self::PUT_IGNORE_PATTERN, basename($uri))) {
return false; return false;
@ -343,6 +348,10 @@ namespace PicoDAV
rename($tmp_file, $target); rename($tmp_file, $target);
} }
if ($mtime) {
@touch($target, $mtime);
}
return $new; return $new;
} }
@ -469,10 +478,28 @@ namespace PicoDAV
mkdir($target, 0770); mkdir($target, 0770);
} }
public function touch(string $uri, \DateTimeInterface $datetime): bool static public function getDirectoryMTime(string $path): int
{ {
$target = $this->path . $uri; $last = 0;
return @touch($target, $datetime->getTimestamp()); $path = rtrim($path, '/');
foreach (self::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;
} }
} }