Compare commits

...

16 commits
0.1.3 ... main

Author SHA1 Message Date
bohwaz 0a0ec2297f Update dependencies, remove recursive directory time 2023-12-31 16:59:19 +01:00
bohwaz 6e431043aa Make sure we accept user if password is not set 2023-07-23 00:52:01 +02:00
bohwaz 4bfef21755 Update README, fix #10 2023-05-09 14:17:07 +02:00
BohwaZ 18ba8ffb17
Merge pull request #9 from clach04/patch-1
clarify password hashing
2023-04-02 17:56:49 +02:00
clach04 dd83f184a9
clarify password hashing
Code hashes (a best practice) passwords rather than encrypting them.
2023-04-02 08:53:08 -07:00
bohwaz 706c7ce880 Update JS dependency to avoid conflict with PHP short codes, fix #7 2023-03-26 18:58:50 +02:00
bohwaz 4ffdefa75e Fix bug #6 by throwing error if FPM is used with Transfer-Encoding: chunked and the file is empty 2023-02-18 13:57:25 +01:00
bohwaz 6856eafda8 Support for OC-Checksum header 2023-02-14 13:37:06 +01:00
bohwaz d1dd1e5778 Fix #2
Fix #4

Allow use of symlinks for index.php, generate .htaccess accordingly
2023-01-28 03:30:10 +01:00
bohwaz c6d94e0b4c Fix #5 by extending execution time 2023-01-28 03:12:57 +01:00
bohwaz 629403661a Send back displayname if requested 2022-12-23 21:25:05 +01:00
bohwaz 59669cb6c9 Limit gzip compression to smaller files, also exclude other kinds of files 2022-12-23 20:42:01 +01:00
bohwaz d8e98506c2 Try to fix issue with memory exhaustion when gzipping large files 2022-12-23 20:25:46 +01:00
bohwaz 523d58ec9b Add option to log HTTP 2022-12-23 19:40:37 +01:00
bohwaz 9b30c8da2b Fix #1 wrong class name 2022-12-22 14:15:10 +01:00
bohwaz fe645d71da Fix tempory file path 2022-11-23 00:46:06 +01:00
4 changed files with 341 additions and 173 deletions

View file

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

View file

@ -21,22 +21,23 @@ If you drop the [`index.php`](https://github.com/kd2org/picodav/raw/main/index.p
* Download all files of a directory
* Manage users and password with only a text file!
* Restrict users to some directories, control where they can write!
* Support for [rclone](https://rclone.org) as a NextCloud provider
## WebDAV clients
You can use any WebDAV client, but we recommend these:
* Windows/OSX: [CyberDuck](https://cyberduck.io/download/)
* Linux: Any file manager should be able to connect to WebDAV (Dolphin, Thunar, Nautilus, etc.), but you can also use [FUSE webdavfs](https://github.com/miquels/webdavfs)
* Linux: Any file manager should be able to connect to WebDAV (Dolphin, Thunar, Nautilus, etc.), but you can also use [FUSE webdavfs](https://github.com/miquels/webdavfs), or [rclone](https://rclone.org)
* Android: [RCX](https://f-droid.org/en/packages/io.github.x0b.rcx/) and [DAVx⁵](https://www.davx5.com/), see [the manual](https://manual.davx5.com/webdav_mounts.html)
## Install
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
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.
The only requirement is PHP 7.4, or more recent (8.0 and 8.1 are also supported, not tested with PHP 8.2).
The only requirement is PHP 7.4, or more recent (8.0-8.2 are also supported).
Note that by default, write access is disabled for security purposes. See below to enable write access.
@ -46,6 +47,12 @@ PicoDAV accepts a configuration file named `.picodav.ini`.
It should be in the same directory as `index.php`.
It accepts these options:
* `ANONYMOUS_READ` (boolean, see below)
* `ANONYMOUS_WRITE` (boolean, see below)
* `HTTP_LOG_FILE` (string, set to a file path to log HTTP requests for debug purposes)
### Users and passwords
By default, the WebDAV server is accessible to everyone.
@ -64,9 +71,9 @@ password = verySecret
write = true
```
Note that PicoDAV will replace this password with an encrypted version the next time it is accessed, don't worry about that, this is for extra safety, just in case the `.picodav.ini` is accessed by a hacker if you made mistake in your web server configuration.
Note that PicoDAV will replace this password with a hashed version the next time it is accessed, don't worry about that, this is for extra safety, just in case the `.picodav.ini` is accessed by a hacker if you made mistake in your web server configuration.
Here is an example of the password once it has been encrypted:
Here is an example of the password once it has been hashed:
```
password = '$2y$10$fbdabTjNPN3gMAUlaSEoR.kKHLnh0yMGneuJ7P2AOhSSNr8gUaCPu'

374
index.php
View file

@ -25,6 +25,16 @@ namespace KD2\WebDAV
'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
const PROP_DIGEST_MD5 = 'urn:karadav:digest_md5';
@ -34,6 +44,8 @@ namespace KD2\WebDAV
const SHARED_LOCK = 'shared';
const EXCLUSIVE_LOCK = 'exclusive';
protected bool $enable_gzip = true;
protected string $base_uri;
public string $original_uri;
@ -54,7 +66,18 @@ namespace KD2\WebDAV
public function setBaseURI(string $uri): void
{
$this->base_uri = rtrim($uri, '/') . '/';
$this->base_uri = '/' . ltrim($uri, '/');
$this->base_uri = rtrim($this->base_uri, '/') . '/';
}
protected function extendExecutionTime(): void
{
if (false === strpos(@ini_get('disable_functions'), 'set_time_limit')) {
@set_time_limit(3600);
}
@ini_set('max_execution_time', '3600');
@ini_set('max_input_time', '3600');
}
protected function _prefix(string $uri): string
@ -92,7 +115,7 @@ namespace KD2\WebDAV
foreach ($list as $file => $props) {
if (null === $props) {
$props = $this->storage->properties(trim($uri . '/' . $file, '/'), self::BASIC_PROPERTIES, 0);
$props = $this->storage->propfind(trim($uri . '/' . $file, '/'), self::BASIC_PROPERTIES, 0);
}
$collection = !empty($props['DAV::resourcetype']) && $props['DAV::resourcetype'] == 'collection';
@ -184,11 +207,20 @@ namespace KD2\WebDAV
}
$hash = null;
$hash_algo = 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']));
$hash_algo = 'MD5';
}
// Support for ownCloud/NextCloud checksum
// https://github.com/owncloud-archive/documentation/issues/2964
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('');
}
$uri = $this->_prefix($uri);
@ -197,23 +229,47 @@ namespace KD2\WebDAV
if (!empty($_SERVER['HTTP_IF_MATCH'])) {
$etag = trim($_SERVER['HTTP_IF_MATCH'], '" ');
$prop = $this->storage->properties($uri, ['DAV::getetag'], 0);
$prop = $this->storage->propfind($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
// Specific to NextCloud/ownCloud, to allow setting file mtime
// This expects a UNIX timestamp
$mtime = (int)($_SERVER['HTTP_X_OC_MTIME'] ?? 0) ?: null;
if ($mtime) {
header('X-OC-MTime: accepted');
$this->extendExecutionTime();
$stream = fopen('php://input', 'r');
// mod_fcgid <= 2.3.9 doesn't handle chunked transfer encoding for PUT requests
// see https://github.com/kd2org/picodav/issues/6
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));
if ($l === 0) {
throw new Exception('This server cannot accept "Transfer-Encoding: chunked" uploads (please upgrade to mod_fcgid >= 2.3.10).', 500);
}
// reset stream
fseek($stream, 0, SEEK_SET);
}
$created = $this->storage->put($uri, fopen('php://input', 'r'), $hash, $mtime);
$created = $this->storage->put($uri, $stream, $hash_algo, $hash);
$prop = $this->storage->properties($uri, ['DAV::getetag'], 0);
if ($mtime) {
$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'])) {
$value = $prop['DAV::getetag'];
@ -241,7 +297,7 @@ namespace KD2\WebDAV
$requested_props[] = self::PROP_DIGEST_MD5;
}
$props = $this->storage->properties($uri, $requested_props, 0);
$props = $this->storage->propfind($uri, $requested_props, 0);
if (!$props) {
throw new Exception('Resource Not Found', 404);
@ -322,9 +378,11 @@ namespace KD2\WebDAV
}
if (!isset($file['content']) && !isset($file['resource']) && !isset($file['path'])) {
throw new \RuntimeException('Invalid file array returned by ::get()');
throw new \RuntimeException('Invalid file array returned by ::get(): ' . print_r($file, true));
}
$this->extendExecutionTime();
$length = $start = $end = null;
$gzip = false;
@ -344,17 +402,20 @@ namespace KD2\WebDAV
$this->log('HTTP Range requested: %s-%s', $start, $end);
}
elseif (isset($_SERVER['HTTP_ACCEPT_ENCODING'])
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('/\.(?:mp4|m4a|zip|docx|xlsx|ods|odt|odp|7z|gz|bz2|rar|webm|ogg|mp3|ogm|flac|ogv|mkv|avi)$/i', $uri)) {
&& !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'))
{
if (function_exists('apache_setenv')) {
@apache_setenv('no-gzip', 1);
}
@ -436,9 +497,9 @@ namespace KD2\WebDAV
if ($gzip) {
$this->log('Using gzip output compression');
$gzip = deflate_init(ZLIB_ENCODING_GZIP, ['level' => 9]);
$gzip = deflate_init(ZLIB_ENCODING_GZIP);
$fp = fopen('php://memory', 'wb');
$fp = fopen('php://temp', 'wb');
while (!feof($file['resource'])) {
fwrite($fp, deflate_add($gzip, fread($file['resource'], 8192), ZLIB_NO_FLUSH));
@ -458,14 +519,16 @@ namespace KD2\WebDAV
header('Content-Length: ' . $length, true);
}
$block_size = 8192*4;
while (!feof($file['resource']) && ($end === null || $end > 0)) {
$l = $end !== null ? min(8192, $end) : 8192;
$l = $end !== null ? min($block_size, $end) : $block_size;
echo fread($file['resource'], $l);
flush();
if (null !== $end) {
$end -= 8192;
$end -= $block_size;
}
}
@ -525,7 +588,7 @@ namespace KD2\WebDAV
// 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') {
&& current($this->storage->propfind($destination, ['DAV::resourcetype'], 0)) == 'collection') {
$overwritten = $this->storage->exists($uri);
}
else {
@ -644,18 +707,24 @@ namespace KD2\WebDAV
$requested_keys = $requested ? array_keys($requested) : null;
// Find root element properties
$properties = $this->storage->properties($uri, $requested_keys, $depth);
$properties = $this->storage->propfind($uri, $requested_keys, $depth);
if (null === $properties) {
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];
if ($depth) {
foreach ($this->storage->list($uri, $requested) as $file => $properties) {
$path = trim($uri . '/' . $file, '/');
$properties = $properties ?? $this->storage->properties($path, $requested_keys, 0);
$properties = $properties ?? $this->storage->propfind($path, $requested_keys, 0);
if (!$properties) {
$this->log('!!! Cannot find "%s"', $path);
@ -899,19 +968,93 @@ namespace KD2\WebDAV
$uri = $this->_prefix($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');
$this->storage->setProperties($uri, $body);
$properties = $this->parsePropPatch($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
header('HTTP/1.1 207 Multi-Status', true);
header('Content-Type: application/xml; charset=utf-8');
header('Content-Type: application/xml; charset=utf-8', true);
$out = '<?xml version="1.0" encoding="utf-8"?>' . "\n";
$out .= '<d:multistatus xmlns:d="DAV:">';
$out .= '</d:multistatus>';
if (!count($properties)) {
return $prefix . $suffix;
}
return $out;
if ($set_time) {
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
@ -1062,7 +1205,7 @@ namespace KD2\WebDAV
&& preg_match('/\(<([^>]*)>\s+\["([^""]+)"\]\)/', $_SERVER['HTTP_IF'], $match)) {
$token = $match[1];
$request_etag = $match[2];
$etag = current($this->storage->properties($uri, ['DAV::getetag'], 0));
$etag = current($this->storage->propfind($uri, ['DAV::getetag'], 0));
if ($request_etag != $etag) {
throw new Exception('Resource is locked and etag does not match', 412);
@ -1115,9 +1258,10 @@ namespace KD2\WebDAV
{
$uri = parse_url($source, PHP_URL_PATH);
$uri = rawurldecode($uri);
$uri = rtrim($uri, '/');
$uri = trim($uri, '/');
$uri = '/' . $uri;
if ($uri . '/' == $this->base_uri) {
if ($uri . '/' === $this->base_uri) {
$uri .= '/';
}
@ -1142,6 +1286,7 @@ namespace KD2\WebDAV
$uri = $_SERVER['REQUEST_URI'] ?? '/';
}
$uri = '/' . ltrim($uri, '/');
$this->original_uri = $uri;
if ($uri . '/' == $this->base_uri) {
@ -1152,7 +1297,7 @@ namespace KD2\WebDAV
$uri = substr($uri, strlen($this->base_uri));
}
else {
$this->log('<= %s is not a managed URL', $uri);
$this->log('<= %s is not a managed URL (%s)', $uri, $this->base_uri);
return false;
}
@ -1245,14 +1390,14 @@ namespace KD2\WebDAV
abstract public function exists(string $uri): bool;
abstract public function properties(string $uri, ?array $requested_properties, int $depth): ?array;
abstract public function propfind(string $uri, ?array $requested_properties, int $depth): ?array;
public function setProperties(string $uri, string $body): void
public function proppatch(string $uri, array $properties): array
{
// By default, properties are not saved
}
abstract public function put(string $uri, $pointer, ?string $hash, ?int $mtime): bool;
abstract public function put(string $uri, $pointer, ?string $hash_algo, ?string $hash): bool;
abstract public function delete(string $uri): void;
@ -1264,6 +1409,8 @@ namespace KD2\WebDAV
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
{
// By default locking is not implemented
@ -1301,9 +1448,9 @@ namespace PicoDAV
public array $users = [];
public function __construct()
public function __construct(string $path)
{
$this->path = __DIR__ . '/';
$this->path = $path . '/';
}
public function auth(): bool
@ -1319,13 +1466,15 @@ namespace PicoDAV
$user = $_SERVER['PHP_AUTH_USER'] ?? null;
$password = $_SERVER['PHP_AUTH_PW'] ?? null;
$hash = $this->users[$user]['password'] ?? null;
if (!$hash) {
if (!array_key_exists($user, $this->users)) {
return false;
}
if (!password_verify($password, $hash)) {
$hash = $this->users[$user]['password'] ?? null;
// If no password is set, we accept any password as we consider that a .htaccess/.htpasswd
// access has been granted
if (null !== $hash && !password_verify($password, $hash)) {
return false;
}
@ -1473,6 +1622,8 @@ namespace PicoDAV
$target = $this->path . $uri;
switch ($name) {
case 'DAV::displayname':
return basename($uri);
case 'DAV::getcontentlength':
return is_dir($target) ? null : filesize($target);
case 'DAV::getcontenttype':
@ -1482,13 +1633,8 @@ namespace PicoDAV
case 'DAV::resourcetype':
return is_dir($target) ? 'collection' : '';
case 'DAV::getlastmodified':
if (!$uri && $depth == 0 && is_dir($target)) {
$mtime = self::getDirectoryMTime($target);
}
else {
$mtime = filemtime($target);
}
$mtime = filemtime($target);
if (!$mtime) {
return null;
}
@ -1523,7 +1669,7 @@ namespace PicoDAV
return $permissions;
case Server::PROP_DIGEST_MD5:
if (!is_file($target)) {
if (!is_file($target) || is_dir($target) || !is_readable($target)) {
return null;
}
@ -1535,7 +1681,7 @@ namespace PicoDAV
return null;
}
public function properties(string $uri, ?array $properties, int $depth): ?array
public function propfind(string $uri, ?array $properties, int $depth): ?array
{
$target = $this->path . $uri;
@ -1544,7 +1690,7 @@ namespace PicoDAV
}
if (null === $properties) {
$properties = WebDAV::BASIC_PROPERTIES;
$properties = Server::BASIC_PROPERTIES;
}
$out = [];
@ -1560,7 +1706,7 @@ namespace PicoDAV
return $out;
}
public function put(string $uri, $pointer, ?string $hash, ?int $mtime): bool
public function put(string $uri, $pointer, ?string $hash_algo, ?string $hash): bool
{
if (preg_match(self::PUT_IGNORE_PATTERN, basename($uri))) {
return false;
@ -1586,7 +1732,7 @@ namespace PicoDAV
$size = 0;
$quota = disk_free_space($this->path);
$tmp_file = '.tmp.' . sha1($target);
$tmp_file = $this->path . '.tmp.' . sha1($target);
$out = fopen($tmp_file, 'w');
while (!feof($pointer)) {
@ -1606,20 +1752,20 @@ namespace PicoDAV
if ($delete) {
@unlink($tmp_file);
throw new WebDAV_Exception('Your quota is exhausted', 403);
throw new WebDAV_Exception('Your quota is exhausted', 507);
}
elseif ($hash && md5_file($tmp_file) != $hash) {
elseif ($hash && $hash_algo == 'MD5' && md5_file($tmp_file) != $hash) {
@unlink($tmp_file);
throw new WebDAV_Exception('The data sent does not match the supplied MD5 hash', 400);
}
elseif ($hash && $hash_algo == 'SHA1' && sha1_file($tmp_file) != $hash) {
@unlink($tmp_file);
throw new WebDAV_Exception('The data sent does not match the supplied SHA1 hash', 400);
}
else {
rename($tmp_file, $target);
}
if ($mtime) {
@touch($target, $mtime);
}
return $new;
}
@ -1681,7 +1827,7 @@ namespace PicoDAV
$quota = disk_free_space($this->path);
if (filesize($source) > $quota) {
throw new WebDAV_Exception('Your quota is exhausted', 403);
throw new WebDAV_Exception('Your quota is exhausted', 507);
}
}
@ -1729,7 +1875,7 @@ namespace PicoDAV
}
if (!disk_free_space($this->path)) {
throw new WebDAV_Exception('Your quota is exhausted', 403);
throw new WebDAV_Exception('Your quota is exhausted', 507);
}
$target = $this->path . $uri;
@ -1746,28 +1892,10 @@ namespace PicoDAV
mkdir($target, 0770);
}
static public function getDirectoryMTime(string $path): int
public function touch(string $uri, \DateTimeInterface $datetime): bool
{
$last = 0;
$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;
$target = $this->path . $uri;
return @touch($target, $datetime->getTimestamp());
}
}
@ -1809,6 +1937,26 @@ namespace PicoDAV
parent::error($e);
}
protected string $_log = '';
public function log(string $message, ...$params): void
{
if (!HTTP_LOG_FILE) {
return;
}
$this->_log .= vsprintf($message, $params) . "\n";
}
public function __destruct()
{
if (!$this->_log) {
return;
}
file_put_contents(HTTP_LOG_FILE, $this->_log, \FILE_APPEND);
}
}
}
@ -1817,7 +1965,10 @@ namespace {
use PicoDAV\Storage;
$uri = strtok($_SERVER['REQUEST_URI'], '?');
$root = substr(__DIR__, strlen($_SERVER['DOCUMENT_ROOT']));
$self = $_SERVER['SCRIPT_FILENAME'];
$self_dir = dirname($self);
$root = substr(dirname($_SERVER['SCRIPT_FILENAME']), strlen($_SERVER['DOCUMENT_ROOT']));
$root = '/' . ltrim($root, '/');
if (false !== strpos($uri, '..')) {
http_response_code(404);
@ -1826,8 +1977,8 @@ namespace {
$relative_uri = ltrim(substr($uri, strlen($root)), '/');
if (!empty($_SERVER['SERVER_SOFTWARE']) && stristr($_SERVER['SERVER_SOFTWARE'], 'apache') && !file_exists(__DIR__ . '/.htaccess')) {
file_put_contents(__DIR__ . '/.htaccess', 'DirectoryIndex disabled
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
RedirectMatch 404 \\.picodav\\.ini
@ -1841,7 +1992,7 @@ RewriteBase /
#RewriteCond %{REQUEST_METHOD} !GET
RewriteRule ^.*$ /index.php [END]
');
'));
}
if ($relative_uri == '.webdav/webdav.js' || $relative_uri == '.webdav/webdav.css') {
@ -1863,11 +2014,11 @@ RewriteRule ^.*$ /index.php [END]
$fp = fopen(__FILE__, 'r');
if ($relative_uri == '.webdav/webdav.js') {
fseek($fp, 50022, SEEK_SET);
echo fread($fp, 27769);
fseek($fp, 55024, SEEK_SET);
echo fread($fp, 27891);
}
else {
fseek($fp, 50022 + 27769, SEEK_SET);
fseek($fp, 55024 + 27891, SEEK_SET);
echo fread($fp, 7004);
}
@ -1876,20 +2027,20 @@ RewriteRule ^.*$ /index.php [END]
exit;
}
const CONFIG_FILE = __DIR__ . '/.picodav.ini';
define('PicoDAV\INTERNAL_FILES', ['.picodav.ini', basename(__FILE__), '.webdav/webdav.js', '.webdav/webdav.css']);
$config_file = $self_dir . '/.picodav.ini';
define('PicoDAV\INTERNAL_FILES', ['.picodav.ini', $self_dir, '.webdav/webdav.js', '.webdav/webdav.css']);
const DEFAULT_CONFIG = [
'ANONYMOUS_READ' => true,
'ANONYMOUS_WRITE' => false,
'HTTP_LOG_FILE' => null,
];
$config = [];
$storage = new Storage;
$storage = new Storage($self_dir);
if (file_exists(CONFIG_FILE)) {
$config = parse_ini_file(CONFIG_FILE, true);
if (file_exists($config_file)) {
$config = parse_ini_file($config_file, true);
$users = array_filter($config, 'is_array');
$config = array_diff_key($config, $users);
$config = array_change_key_case($config, \CASE_UPPER);
@ -1903,7 +2054,7 @@ RewriteRule ^.*$ /index.php [END]
}
if (count($replace)) {
$lines = file(CONFIG_FILE);
$lines = file($config_file);
$current = null;
foreach ($lines as &$line) {
@ -1919,7 +2070,7 @@ RewriteRule ^.*$ /index.php [END]
unset($line, $current);
file_put_contents(CONFIG_FILE, implode('', $lines));
file_put_contents($config_file, implode('', $lines));
}
$storage->users = $users;
@ -1938,14 +2089,14 @@ RewriteRule ^.*$ /index.php [END]
}
$dav = new Server;
$dav = new Server();
$dav->setStorage($storage);
$dav->setBaseURI($root);
if (!$dav->route($uri)) {
http_response_code(404);
die('Invalid URL, sorry');
die('Unknown URL, sorry.');
}
exit;
@ -1958,7 +2109,7 @@ const WebDAVNavigator = (url, options) => {
// https://github.com/commit-intl/micro-down
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+"</"+n+">"}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"<li>"+g(e+"\n"+(t=r||"").replace(new RegExp("^"+(t.match(/^\s+/)||"")[0],"gm"),"").replace(o,c))+"</li>";var t})+"</"+e+">":""}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,[/<!--((.|\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,'<a name="$1"></a>',/^(#+) +(.*)(?:$)/gm,function(n,e,r){return l("h"+e.length,g(r))},/^(===+|---+)(?=\s*$)/gm,"<hr>"],p,u)}var i=this,a=function(n){return n?n.replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;"):""},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<e&&(r=l("strong",r)),e%2&&(r=l("em",r)),r},/(~{1,3})((.|\n)+?)\1/g,function(n,e,r){return l([,"u","s","del"][e.length],t(r))},/ \n|\n /g,"<br>"],t)},f=function(n,e,r,t){for(var u,c=0;c<e.length;){if(u=e[c++].exec(n))return r(n.slice(0,u.index),t)+("string"==typeof e[c]?e[c].replace(/\$(\d)/g,function(n,e){return u[e]}):e[c].apply(i,u))+r(n.slice(u.index+u[0].length),t);c++}return n},p=function(n,e){n=n.replace(/[\r\v\b\f]/g,"").replace(/\\./g,function(n){return"&#"+n.charCodeAt(1)+";"});var r=t(n,e);return r!==n||r.match(/^[\s\n]*$/i)||(r=g(r).replace(/((.|\n)+?)(\n\n+|$)/g,function(n,e){return l("p",e)})),r.replace(/&#(\d+);/g,function(n,e){return String.fromCharCode(parseInt(e))})};return{parse:p,block:t,inline:r,inlineBlock:g}}();
const PREVIEW_TYPES = /^image\/(png|webp|svg|jpeg|jpg|gif|png)|^application\/pdf|^text\/|^audio\/|^video\//;
const PREVIEW_TYPES = /^image\/(png|webp|svg|jpeg|jpg|gif|png)|^application\/pdf|^text\/|^audio\/|^video\/|application\/x-empty/;
const _ = key => typeof lang_strings != 'undefined' && key in lang_strings ? lang_strings[key] : key;
@ -2003,14 +2154,14 @@ const WebDAVNavigator = (url, options) => {
const dir_row_tpl = `<tr data-permissions="%permissions%"><td class="thumb"><span class="icon dir"><b>%icon%</b></span></td><th colspan="2"><a href="%uri%">%name%</a></th><td>%modified%</td><td class="buttons"><div></div></td></tr>`;
const file_row_tpl = `<tr data-permissions="%permissions%" data-mime="%mime%" data-size="%size%"><td class="thumb"><span class="icon %icon%"><b>%icon%</b></span></td><th><a href="%uri%">%name%</a></th><td class="size">%size_bytes%</td><td>%modified%</td><td class="buttons"><div><a href="%uri%" download class="btn">${_('Download')}</a></div></td></tr>`;
const propfind_tpl = `<?xml version="1.0" encoding="UTF-8"?>
const propfind_tpl = '<'+ `?xml version="1.0" encoding="UTF-8"?>
<D:propfind xmlns:D="DAV:" xmlns:oc="http://owncloud.org/ns">
<D:prop>
<D:getlastmodified/><D:getcontenttype/><D:getcontentlength/><D:resourcetype/><D:displayname/><oc:permissions/>
</D:prop>
</D:propfind>`;
const wopi_propfind_tpl = `<?xml version="1.0" encoding="UTF-8"?>
const wopi_propfind_tpl = '<' + `?xml version="1.0" encoding="UTF-8"?>
<D:propfind xmlns:D="DAV:" xmlns:W="https://interoperability.blob.core.windows.net/files/MS-WOPI/">
<D:prop>
<W:file-url/><W:token/><W:token-ttl/>
@ -2110,12 +2261,14 @@ const WebDAVNavigator = (url, options) => {
};
const wopi_init = async () => {
if (!wopi_discovery_url) {
try {
var d = await reqXML('GET', wopi_discovery_url);
}
catch (e) {
reloadListing();
return;
}
var d = await reqXML('GET', wopi_discovery_url);
d.querySelectorAll('app').forEach(app => {
var mime = (a = app.getAttribute('name').match(/^.*\/.*$/)) ? a[0] : null;
wopi_mimes[mime] = {};
@ -2304,6 +2457,10 @@ const WebDAVNavigator = (url, options) => {
};
const formatDate = (date) => {
if (isNaN(date)) {
return '';
}
var now = new Date;
var nb_hours = (+(now) - +(date)) / 3600 / 1000;
@ -2494,7 +2651,7 @@ const WebDAVNavigator = (url, options) => {
$('.download_all').onclick = download_all;
}
if (!root_permissions || root_permissions.indexOf('CK') != -1) {
if (!root_permissions || root_permissions.indexOf('C') != -1 || root_permissions.indexOf('K') != -1) {
$('.upload').insertAdjacentHTML('afterbegin', create_buttons);
$('.mkdir').onclick = () => {
@ -2657,7 +2814,7 @@ const WebDAVNavigator = (url, options) => {
}
if (!permissions || permissions.indexOf('W') != -1) {
if ( mime.match(/^text\/|application\/x-empty/)) {
if (mime.match(/^text\/|application\/x-empty/)) {
buttons.insertAdjacentHTML('beforeend', edit_button);
$$('.edit').onclick = (e) => {
@ -2729,11 +2886,10 @@ const WebDAVNavigator = (url, options) => {
document.querySelector('html').innerHTML = html_tpl;
// Wait for WOPI discovery before creating the list
if (wopi_discovery_url) {
// Wait for WOPI discovery before creating the list
wopi_init();
}
else {
} else {
reloadListing();
}

View file

@ -25,9 +25,9 @@ namespace PicoDAV
public array $users = [];
public function __construct()
public function __construct(string $path)
{
$this->path = __DIR__ . '/';
$this->path = $path . '/';
}
public function auth(): bool
@ -43,13 +43,15 @@ namespace PicoDAV
$user = $_SERVER['PHP_AUTH_USER'] ?? null;
$password = $_SERVER['PHP_AUTH_PW'] ?? null;
$hash = $this->users[$user]['password'] ?? null;
if (!$hash) {
if (!array_key_exists($user, $this->users)) {
return false;
}
if (!password_verify($password, $hash)) {
$hash = $this->users[$user]['password'] ?? null;
// If no password is set, we accept any password as we consider that a .htaccess/.htpasswd
// access has been granted
if (null !== $hash && !password_verify($password, $hash)) {
return false;
}
@ -197,6 +199,8 @@ namespace PicoDAV
$target = $this->path . $uri;
switch ($name) {
case 'DAV::displayname':
return basename($uri);
case 'DAV::getcontentlength':
return is_dir($target) ? null : filesize($target);
case 'DAV::getcontenttype':
@ -206,12 +210,7 @@ namespace PicoDAV
case 'DAV::resourcetype':
return is_dir($target) ? 'collection' : '';
case 'DAV::getlastmodified':
if (!$uri && $depth == 0 && is_dir($target)) {
$mtime = self::getDirectoryMTime($target);
}
else {
$mtime = filemtime($target);
}
$mtime = filemtime($target);
if (!$mtime) {
return null;
@ -247,7 +246,7 @@ namespace PicoDAV
return $permissions;
case Server::PROP_DIGEST_MD5:
if (!is_file($target)) {
if (!is_file($target) || is_dir($target) || !is_readable($target)) {
return null;
}
@ -259,7 +258,7 @@ namespace PicoDAV
return null;
}
public function properties(string $uri, ?array $properties, int $depth): ?array
public function propfind(string $uri, ?array $properties, int $depth): ?array
{
$target = $this->path . $uri;
@ -268,7 +267,7 @@ namespace PicoDAV
}
if (null === $properties) {
$properties = WebDAV::BASIC_PROPERTIES;
$properties = Server::BASIC_PROPERTIES;
}
$out = [];
@ -284,7 +283,7 @@ namespace PicoDAV
return $out;
}
public function put(string $uri, $pointer, ?string $hash, ?int $mtime): bool
public function put(string $uri, $pointer, ?string $hash_algo, ?string $hash): bool
{
if (preg_match(self::PUT_IGNORE_PATTERN, basename($uri))) {
return false;
@ -310,7 +309,7 @@ namespace PicoDAV
$size = 0;
$quota = disk_free_space($this->path);
$tmp_file = '.tmp.' . sha1($target);
$tmp_file = $this->path . '.tmp.' . sha1($target);
$out = fopen($tmp_file, 'w');
while (!feof($pointer)) {
@ -330,20 +329,20 @@ namespace PicoDAV
if ($delete) {
@unlink($tmp_file);
throw new WebDAV_Exception('Your quota is exhausted', 403);
throw new WebDAV_Exception('Your quota is exhausted', 507);
}
elseif ($hash && md5_file($tmp_file) != $hash) {
elseif ($hash && $hash_algo == 'MD5' && md5_file($tmp_file) != $hash) {
@unlink($tmp_file);
throw new WebDAV_Exception('The data sent does not match the supplied MD5 hash', 400);
}
elseif ($hash && $hash_algo == 'SHA1' && sha1_file($tmp_file) != $hash) {
@unlink($tmp_file);
throw new WebDAV_Exception('The data sent does not match the supplied SHA1 hash', 400);
}
else {
rename($tmp_file, $target);
}
if ($mtime) {
@touch($target, $mtime);
}
return $new;
}
@ -405,7 +404,7 @@ namespace PicoDAV
$quota = disk_free_space($this->path);
if (filesize($source) > $quota) {
throw new WebDAV_Exception('Your quota is exhausted', 403);
throw new WebDAV_Exception('Your quota is exhausted', 507);
}
}
@ -453,7 +452,7 @@ namespace PicoDAV
}
if (!disk_free_space($this->path)) {
throw new WebDAV_Exception('Your quota is exhausted', 403);
throw new WebDAV_Exception('Your quota is exhausted', 507);
}
$target = $this->path . $uri;
@ -470,28 +469,10 @@ namespace PicoDAV
mkdir($target, 0770);
}
static public function getDirectoryMTime(string $path): int
public function touch(string $uri, \DateTimeInterface $datetime): bool
{
$last = 0;
$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;
$target = $this->path . $uri;
return @touch($target, $datetime->getTimestamp());
}
}
@ -533,6 +514,26 @@ namespace PicoDAV
parent::error($e);
}
protected string $_log = '';
public function log(string $message, ...$params): void
{
if (!HTTP_LOG_FILE) {
return;
}
$this->_log .= vsprintf($message, $params) . "\n";
}
public function __destruct()
{
if (!$this->_log) {
return;
}
file_put_contents(HTTP_LOG_FILE, $this->_log, \FILE_APPEND);
}
}
}
@ -541,7 +542,10 @@ namespace {
use PicoDAV\Storage;
$uri = strtok($_SERVER['REQUEST_URI'], '?');
$root = substr(__DIR__, strlen($_SERVER['DOCUMENT_ROOT']));
$self = $_SERVER['SCRIPT_FILENAME'];
$self_dir = dirname($self);
$root = substr(dirname($_SERVER['SCRIPT_FILENAME']), strlen($_SERVER['DOCUMENT_ROOT']));
$root = '/' . ltrim($root, '/');
if (false !== strpos($uri, '..')) {
http_response_code(404);
@ -550,8 +554,8 @@ namespace {
$relative_uri = ltrim(substr($uri, strlen($root)), '/');
if (!empty($_SERVER['SERVER_SOFTWARE']) && stristr($_SERVER['SERVER_SOFTWARE'], 'apache') && !file_exists(__DIR__ . '/.htaccess')) {
file_put_contents(__DIR__ . '/.htaccess', /*__HTACCESS__*/);
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), /*__HTACCESS__*/));
}
if ($relative_uri == '.webdav/webdav.js' || $relative_uri == '.webdav/webdav.css') {
@ -586,20 +590,20 @@ namespace {
exit;
}
const CONFIG_FILE = __DIR__ . '/.picodav.ini';
define('PicoDAV\INTERNAL_FILES', ['.picodav.ini', basename(__FILE__), '.webdav/webdav.js', '.webdav/webdav.css']);
$config_file = $self_dir . '/.picodav.ini';
define('PicoDAV\INTERNAL_FILES', ['.picodav.ini', $self_dir, '.webdav/webdav.js', '.webdav/webdav.css']);
const DEFAULT_CONFIG = [
'ANONYMOUS_READ' => true,
'ANONYMOUS_WRITE' => false,
'HTTP_LOG_FILE' => null,
];
$config = [];
$storage = new Storage;
$storage = new Storage($self_dir);
if (file_exists(CONFIG_FILE)) {
$config = parse_ini_file(CONFIG_FILE, true);
if (file_exists($config_file)) {
$config = parse_ini_file($config_file, true);
$users = array_filter($config, 'is_array');
$config = array_diff_key($config, $users);
$config = array_change_key_case($config, \CASE_UPPER);
@ -613,7 +617,7 @@ namespace {
}
if (count($replace)) {
$lines = file(CONFIG_FILE);
$lines = file($config_file);
$current = null;
foreach ($lines as &$line) {
@ -629,7 +633,7 @@ namespace {
unset($line, $current);
file_put_contents(CONFIG_FILE, implode('', $lines));
file_put_contents($config_file, implode('', $lines));
}
$storage->users = $users;
@ -648,14 +652,14 @@ namespace {
}
$dav = new Server;
$dav = new Server();
$dav->setStorage($storage);
$dav->setBaseURI($root);
if (!$dav->route($uri)) {
http_response_code(404);
die('Invalid URL, sorry');
die('Unknown URL, sorry.');
}
exit;