2022-09-29 23:39:24 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace KaraDAV;
|
|
|
|
|
|
|
|
use KD2\WebDAV\AbstractStorage;
|
2023-09-11 14:38:07 +00:00
|
|
|
use KD2\WebDAV\TrashInterface;
|
2022-10-15 00:15:39 +00:00
|
|
|
use KD2\WebDAV\WOPI;
|
2022-09-30 13:17:47 +00:00
|
|
|
use KD2\WebDAV\Exception as WebDAV_Exception;
|
2022-09-29 23:39:24 +00:00
|
|
|
|
2023-09-11 14:38:07 +00:00
|
|
|
class Storage extends AbstractStorage implements TrashInterface
|
2022-09-29 23:39:24 +00:00
|
|
|
{
|
|
|
|
protected Users $users;
|
2022-11-04 19:52:02 +00:00
|
|
|
protected NextCloud $nextcloud;
|
2023-02-11 14:28:57 +00:00
|
|
|
protected array $properties = [];
|
2022-09-29 23:39:24 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* These file names will be ignored when doing a PUT
|
|
|
|
* as they are garbage, coming from some OS
|
|
|
|
*/
|
|
|
|
const PUT_IGNORE_PATTERN = '!^~(?:lock\.|^\._)|^(?:\.DS_Store|Thumbs\.db|desktop\.ini)$!';
|
|
|
|
|
2022-11-04 19:52:02 +00:00
|
|
|
public function __construct(Users $users, NextCloud $nextcloud)
|
2022-09-29 23:39:24 +00:00
|
|
|
{
|
|
|
|
$this->users = $users;
|
2022-11-04 19:52:02 +00:00
|
|
|
$this->nextcloud = $nextcloud;
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
|
|
|
|
2023-09-11 14:38:07 +00:00
|
|
|
protected function ensureDirectoryExists(string $path): void
|
|
|
|
{
|
|
|
|
$path = $this->users->current()->path . $path;
|
|
|
|
|
|
|
|
if (!file_exists($path)) {
|
|
|
|
@mkdir($path, @fileperms($this->users->current()->path) ?: 0770, true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-29 23:39:24 +00:00
|
|
|
public function getLock(string $uri, ?string $token = null): ?string
|
|
|
|
{
|
|
|
|
// It is important to check also for a lock on parent directory as we support depth=1
|
|
|
|
$sql = 'SELECT scope FROM locks WHERE user = ? AND (uri = ? OR uri = ?)';
|
2022-10-24 22:34:50 +00:00
|
|
|
$params = [$this->users->current()->id, $uri, dirname($uri)];
|
2022-09-29 23:39:24 +00:00
|
|
|
|
|
|
|
if ($token) {
|
|
|
|
$sql .= ' AND token = ?';
|
|
|
|
$params[] = $token;
|
|
|
|
}
|
|
|
|
|
|
|
|
$sql .= ' LIMIT 1';
|
|
|
|
|
|
|
|
return DB::getInstance()->firstColumn($sql, ...$params);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function lock(string $uri, string $token, string $scope): void
|
|
|
|
{
|
2022-10-24 22:34:50 +00:00
|
|
|
DB::getInstance()->run('REPLACE INTO locks VALUES (?, ?, ?, ?, datetime(\'now\', \'+5 minutes\'));', $this->users->current()->id, $uri, $token, $scope);
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function unlock(string $uri, string $token): void
|
|
|
|
{
|
2022-10-24 22:34:50 +00:00
|
|
|
DB::getInstance()->run('DELETE FROM locks WHERE user = ? AND uri = ? AND token = ?;', $this->users->current()->id, $uri, $token);
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function list(string $uri, ?array $properties): iterable
|
|
|
|
{
|
2022-11-15 12:10:52 +00:00
|
|
|
$dirs = self::glob($this->users->current()->path . $uri, '/*', \GLOB_ONLYDIR);
|
2022-09-29 23:39:24 +00:00
|
|
|
$dirs = array_map('basename', $dirs);
|
|
|
|
natcasesort($dirs);
|
|
|
|
|
2022-11-15 12:10:52 +00:00
|
|
|
$files = self::glob($this->users->current()->path . $uri, '/*');
|
2022-09-29 23:39:24 +00:00
|
|
|
$files = array_map('basename', $files);
|
|
|
|
$files = array_diff($files, $dirs);
|
|
|
|
natcasesort($files);
|
|
|
|
|
|
|
|
$files = array_flip(array_merge($dirs, $files));
|
|
|
|
$files = array_map(fn($a) => null, $files);
|
|
|
|
return $files;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function get(string $uri): ?array
|
|
|
|
{
|
2022-10-02 00:41:46 +00:00
|
|
|
$path = $this->users->current()->path . $uri;
|
|
|
|
|
|
|
|
if (!file_exists($path)) {
|
2022-09-29 23:39:24 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-11-21 17:18:04 +00:00
|
|
|
if (!is_readable($path)) {
|
|
|
|
throw new WebDAV_Exception('You don\'t have the right to read this file', 403);
|
|
|
|
}
|
|
|
|
|
2022-10-02 00:41:46 +00:00
|
|
|
// Recommended: Use X-SendFile to make things more efficient
|
|
|
|
// see https://tn123.org/mod_xsendfile/
|
|
|
|
// or https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/
|
|
|
|
if (ENABLE_XSENDFILE) {
|
|
|
|
header('X-SendFile: ' . $path);
|
2022-11-14 00:53:25 +00:00
|
|
|
return ['stop' => true];
|
2022-10-02 00:41:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return ['path' => $path];
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function exists(string $uri): bool
|
|
|
|
{
|
|
|
|
return file_exists($this->users->current()->path . $uri);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function get_file_property(string $uri, string $name, int $depth)
|
|
|
|
{
|
|
|
|
$target = $this->users->current()->path . $uri;
|
|
|
|
|
|
|
|
switch ($name) {
|
|
|
|
case 'DAV::getcontentlength':
|
2022-10-03 02:44:20 +00:00
|
|
|
return is_dir($target) ? null : filesize($target);
|
2022-09-29 23:39:24 +00:00
|
|
|
case 'DAV::getcontenttype':
|
2022-10-03 02:44:20 +00:00
|
|
|
// ownCloud app crashes if mimetype is provided for a directory
|
|
|
|
// https://github.com/owncloud/android/issues/3768
|
2022-11-21 17:05:55 +00:00
|
|
|
return is_dir($target) ? null : @mime_content_type($target);
|
2022-09-29 23:39:24 +00:00
|
|
|
case 'DAV::resourcetype':
|
|
|
|
return is_dir($target) ? 'collection' : '';
|
|
|
|
case 'DAV::getlastmodified':
|
2023-03-12 14:34:04 +00:00
|
|
|
if (!DISABLE_SLOW_OPERATIONS && !$uri && $depth == 0 && is_dir($target)) {
|
2022-09-30 13:17:47 +00:00
|
|
|
$mtime = self::getDirectoryMTime($target);
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
$mtime = filemtime($target);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$mtime) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return new \DateTime('@' . $mtime);
|
|
|
|
case 'DAV::displayname':
|
|
|
|
return basename($target);
|
|
|
|
case 'DAV::ishidden':
|
|
|
|
return basename($target)[0] == '.';
|
|
|
|
case 'DAV::getetag':
|
2023-03-12 14:34:04 +00:00
|
|
|
if (!DISABLE_SLOW_OPERATIONS && !$uri && !$depth) {
|
2022-09-30 13:17:47 +00:00
|
|
|
$hash = self::getDirectorySize($target) . self::getDirectoryMTime($target);
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
$hash = filemtime($target) . filesize($target);
|
|
|
|
}
|
|
|
|
|
|
|
|
return md5($hash . $target);
|
|
|
|
case 'DAV::lastaccessed':
|
|
|
|
return new \DateTime('@' . fileatime($target));
|
|
|
|
case 'DAV::creationdate':
|
|
|
|
return new \DateTime('@' . filectime($target));
|
2022-10-03 02:44:20 +00:00
|
|
|
case WebDAV::PROP_DIGEST_MD5:
|
2023-02-14 12:27:19 +00:00
|
|
|
if (!is_file($target) || is_dir($target) || !is_readable($target)) {
|
2022-10-03 02:44:20 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return md5_file($target);
|
2022-09-29 23:39:24 +00:00
|
|
|
// NextCloud stuff
|
2023-02-14 12:27:19 +00:00
|
|
|
case NextCloud::PROP_OC_CHECKSUMS:
|
|
|
|
// We are not returning OC checksums as this could slow directory listings
|
|
|
|
return null;
|
2022-10-15 00:15:39 +00:00
|
|
|
case NextCloud::PROP_NC_HAS_PREVIEW:
|
|
|
|
case NextCloud::PROP_NC_IS_ENCRYPTED:
|
2022-10-03 01:03:19 +00:00
|
|
|
return 'false';
|
2022-10-03 02:44:20 +00:00
|
|
|
case NextCloud::PROP_OC_SHARETYPES:
|
|
|
|
return WebDAV::EMPTY_PROP_VALUE;
|
2022-10-24 22:34:50 +00:00
|
|
|
case NextCloud::PROP_OC_DOWNLOADURL:
|
2023-06-11 21:43:29 +00:00
|
|
|
return $this->nextcloud->getDirectDownloadURL($uri, $this->users->current()->login);
|
2022-10-03 01:03:19 +00:00
|
|
|
case Nextcloud::PROP_NC_RICH_WORKSPACE:
|
|
|
|
if (!is_dir($target)) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
$files = ['README.md', 'Readme.md', 'readme.md'];
|
|
|
|
|
|
|
|
foreach ($files as $f) {
|
|
|
|
if (file_exists($target . '/' . $f)) {
|
|
|
|
return file_get_contents($target . '/' . $f);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return '';
|
2022-10-15 00:15:39 +00:00
|
|
|
case NextCloud::PROP_OC_ID:
|
2023-06-11 22:03:40 +00:00
|
|
|
// fileId is required by NextCloud desktop client
|
2023-09-10 22:23:14 +00:00
|
|
|
return fileinode($target);
|
2022-10-15 00:15:39 +00:00
|
|
|
case NextCloud::PROP_OC_PERMISSIONS:
|
2022-11-21 17:05:55 +00:00
|
|
|
$permissions = [];
|
|
|
|
|
|
|
|
if (is_writeable($target)) {
|
2023-09-10 23:37:05 +00:00
|
|
|
$permissions = [NextCloud::PERM_WRITE, NextCloud::PERM_DELETE, NextCloud::PERM_RENAME, NextCloud::PERM_MOVE, NextCloud::PERM_CREATE_FILES_DIRS];
|
2022-11-21 17:05:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (is_readable($target)) {
|
|
|
|
$permissions[] = NextCloud::PERM_READ;
|
|
|
|
}
|
|
|
|
|
|
|
|
return implode('', $permissions);
|
2023-09-11 14:38:07 +00:00
|
|
|
case NextCloud::PROP_NC_TRASHBIN_FILENAME:
|
|
|
|
if (0 !== strpos($uri, '.trash/')) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return basename($uri);
|
|
|
|
case NextCloud::PROP_NC_TRASHBIN_DELETION_TIME:
|
|
|
|
if (0 !== strpos($uri, '.trash/')) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->getTrashInfo(basename($uri))['DeletionDate'] ?? null;
|
|
|
|
case NextCloud::PROP_NC_TRASHBIN_ORIGINAL_LOCATION:
|
|
|
|
if (0 !== strpos($uri, '.trash/')) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->getTrashInfo(basename($uri))['Path'] ?? null;
|
2022-10-03 02:44:20 +00:00
|
|
|
case 'DAV::quota-available-bytes':
|
|
|
|
return null;
|
|
|
|
case 'DAV::quota-used-bytes':
|
|
|
|
return null;
|
2022-09-30 00:42:21 +00:00
|
|
|
case Nextcloud::PROP_OC_SIZE:
|
2023-03-12 14:34:04 +00:00
|
|
|
if (!DISABLE_SLOW_OPERATIONS && is_dir($target)) {
|
2022-09-30 13:17:47 +00:00
|
|
|
return self::getDirectorySize($target);
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
return filesize($target);
|
|
|
|
}
|
2022-10-15 00:15:39 +00:00
|
|
|
case WOPI::PROP_FILE_URL:
|
|
|
|
$id = gzcompress($uri);
|
|
|
|
$id = WOPI::base64_encode_url_safe($id);
|
|
|
|
return WWW_URL . 'wopi/files/' . $id;
|
2022-09-29 23:39:24 +00:00
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2022-10-03 01:03:19 +00:00
|
|
|
if (in_array($name, NextCloud::NC_PROPERTIES) || in_array($name, WebDAV::BASIC_PROPERTIES) || in_array($name, WebDAV::EXTENDED_PROPERTIES)) {
|
2022-09-29 23:39:24 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-09-30 13:17:47 +00:00
|
|
|
return $this->getResourceProperties($uri)->get($name);
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
|
|
|
|
2023-12-31 14:49:00 +00:00
|
|
|
public function propfind(string $uri, ?array $properties, int $depth): ?array
|
2022-09-29 23:39:24 +00:00
|
|
|
{
|
|
|
|
$target = $this->users->current()->path . $uri;
|
|
|
|
|
|
|
|
if (!file_exists($target)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (null === $properties) {
|
2022-10-03 01:03:19 +00:00
|
|
|
$properties = array_merge(WebDAV::BASIC_PROPERTIES, ['DAV::getetag', Nextcloud::PROP_OC_ID]);
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
$out = [];
|
|
|
|
|
2022-10-25 23:01:01 +00:00
|
|
|
// Generate a new token for WOPI, and provide also TTL
|
|
|
|
if (in_array(WOPI::PROP_TOKEN, $properties)) {
|
|
|
|
$out = $this->createWopiToken($uri);
|
|
|
|
unset($properties[WOPI::PROP_TOKEN], $properties[WOPI::PROP_TOKEN_TTL]);
|
|
|
|
}
|
|
|
|
|
2022-09-29 23:39:24 +00:00
|
|
|
foreach ($properties as $name) {
|
|
|
|
$v = $this->get_file_property($uri, $name, $depth);
|
|
|
|
|
|
|
|
if (null !== $v) {
|
|
|
|
$out[$name] = $v;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $out;
|
|
|
|
}
|
|
|
|
|
2023-12-31 14:49:00 +00:00
|
|
|
public function put(string $uri, $pointer, ?string $hash_algo = null, ?string $hash = null): bool
|
2022-09-29 23:39:24 +00:00
|
|
|
{
|
|
|
|
if (preg_match(self::PUT_IGNORE_PATTERN, basename($uri))) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$target = $this->users->current()->path . $uri;
|
|
|
|
$parent = dirname($target);
|
|
|
|
|
|
|
|
if (is_dir($target)) {
|
|
|
|
throw new WebDAV_Exception('Target is a directory', 409);
|
|
|
|
}
|
|
|
|
|
2023-10-15 12:58:34 +00:00
|
|
|
$this->ensureDirectoryExists($parent);
|
2022-09-29 23:39:24 +00:00
|
|
|
|
|
|
|
$new = !file_exists($target);
|
2022-11-21 17:18:04 +00:00
|
|
|
|
|
|
|
if ((!$new && !is_writeable($target)) || ($new && !is_writeable($parent))) {
|
|
|
|
throw new WebDAV_Exception('You don\'t have the rights to write to this file', 403);
|
|
|
|
}
|
|
|
|
|
2022-09-29 23:39:24 +00:00
|
|
|
$delete = false;
|
|
|
|
$size = 0;
|
|
|
|
$quota = $this->users->quota($this->users->current());
|
|
|
|
|
2023-10-15 12:58:34 +00:00
|
|
|
if ($quota->free <= 0) {
|
|
|
|
throw new WebDAV_Exception('Your quota is exhausted', 403);
|
|
|
|
}
|
|
|
|
|
2022-11-22 23:42:59 +00:00
|
|
|
$tmp_dir = sprintf(STORAGE_PATH, '_tmp');
|
|
|
|
|
|
|
|
if (!file_exists($tmp_dir)) {
|
|
|
|
@mkdir($tmp_dir, 0777, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!is_writeable($tmp_dir)) {
|
|
|
|
throw new \RuntimeException('Cannot write to temporary storage path: ' . $tmp_dir);
|
|
|
|
}
|
|
|
|
|
|
|
|
$tmp_file = $tmp_dir . sha1($target);
|
2022-09-29 23:39:24 +00:00
|
|
|
$out = fopen($tmp_file, 'w');
|
|
|
|
|
|
|
|
while (!feof($pointer)) {
|
|
|
|
$bytes = fread($pointer, 8192);
|
|
|
|
$size += strlen($bytes);
|
|
|
|
|
|
|
|
if ($size > $quota->free) {
|
|
|
|
$delete = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
fwrite($out, $bytes);
|
|
|
|
}
|
|
|
|
|
|
|
|
fclose($out);
|
|
|
|
fclose($pointer);
|
|
|
|
|
|
|
|
if ($delete) {
|
|
|
|
@unlink($tmp_file);
|
|
|
|
throw new WebDAV_Exception('Your quota is exhausted', 403);
|
|
|
|
}
|
2023-02-14 12:27:19 +00:00
|
|
|
elseif ($hash && $hash_algo == 'MD5' && md5_file($tmp_file) != $hash) {
|
2022-09-30 00:42:21 +00:00
|
|
|
@unlink($tmp_file);
|
|
|
|
throw new WebDAV_Exception('The data sent does not match the supplied MD5 hash', 400);
|
|
|
|
}
|
2023-02-14 12:27:19 +00:00
|
|
|
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);
|
|
|
|
}
|
2022-09-29 23:39:24 +00:00
|
|
|
else {
|
|
|
|
rename($tmp_file, $target);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $new;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function delete(string $uri): void
|
|
|
|
{
|
|
|
|
$target = $this->users->current()->path . $uri;
|
|
|
|
|
|
|
|
if (!file_exists($target)) {
|
|
|
|
throw new WebDAV_Exception('Target does not exist', 404);
|
|
|
|
}
|
|
|
|
|
2023-09-11 14:38:07 +00:00
|
|
|
// Move to trash
|
|
|
|
if (DEFAULT_TRASHBIN_DELAY > 0 && 0 !== strpos($uri, '.trash')) {
|
|
|
|
$this->moveToTrash($uri);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-09-29 23:39:24 +00:00
|
|
|
if (is_dir($target)) {
|
2023-02-14 11:15:15 +00:00
|
|
|
self::deleteDirectory($target);
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
|
|
|
else {
|
2023-02-14 11:15:15 +00:00
|
|
|
@unlink($target);
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
|
|
|
|
2022-09-30 13:17:47 +00:00
|
|
|
$this->getResourceProperties($uri)->clear();
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function copymove(bool $move, string $uri, string $destination): bool
|
|
|
|
{
|
|
|
|
$source = $this->users->current()->path . $uri;
|
|
|
|
$target = $this->users->current()->path . $destination;
|
|
|
|
$parent = dirname($target);
|
|
|
|
|
|
|
|
if (!file_exists($source)) {
|
|
|
|
throw new WebDAV_Exception('File not found', 404);
|
|
|
|
}
|
|
|
|
|
|
|
|
$overwritten = file_exists($target);
|
|
|
|
|
|
|
|
if (!is_dir($parent)) {
|
|
|
|
throw new WebDAV_Exception('Target parent directory does not exist', 409);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (false === $move) {
|
|
|
|
$quota = $this->users->quota($this->users->current());
|
|
|
|
|
|
|
|
if (filesize($source) > $quota->free) {
|
|
|
|
throw new WebDAV_Exception('Your quota is exhausted', 403);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($overwritten) {
|
|
|
|
$this->delete($destination);
|
|
|
|
}
|
|
|
|
|
|
|
|
$method = $move ? 'rename' : 'copy';
|
|
|
|
|
|
|
|
if ($method == 'copy' && is_dir($source)) {
|
2023-10-15 12:58:34 +00:00
|
|
|
$this->ensureDirectoryExists($parent);
|
2022-09-29 23:39:24 +00:00
|
|
|
|
2022-11-21 17:18:04 +00:00
|
|
|
if (!is_dir($target)) {
|
|
|
|
throw new WebDAV_Exception('Target directory could not be created', 409);
|
|
|
|
}
|
|
|
|
|
2022-09-29 23:39:24 +00:00
|
|
|
foreach ($iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source), \RecursiveIteratorIterator::SELF_FIRST) as $item)
|
|
|
|
{
|
|
|
|
if ($item->isDir()) {
|
|
|
|
@mkdir($target . DIRECTORY_SEPARATOR . $iterator->getSubPathname());
|
|
|
|
} else {
|
|
|
|
copy($item, $target . DIRECTORY_SEPARATOR . $iterator->getSubPathname());
|
|
|
|
}
|
2023-12-31 14:49:00 +00:00
|
|
|
touch($target . DIRECTORY_SEPARATOR . $iterator->getSubPathname(), filemtime($item));
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$method($source, $target);
|
|
|
|
|
2023-12-31 14:49:00 +00:00
|
|
|
if ($method === 'rename') {
|
|
|
|
$this->getResourceProperties($uri)->move($destination);
|
|
|
|
}
|
2023-12-31 15:23:09 +00:00
|
|
|
else {
|
|
|
|
touch($target, filemtime($source));
|
|
|
|
}
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return $overwritten;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function copy(string $uri, string $destination): bool
|
|
|
|
{
|
|
|
|
return $this->copymove(false, $uri, $destination);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function move(string $uri, string $destination): bool
|
|
|
|
{
|
|
|
|
return $this->copymove(true, $uri, $destination);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function mkcol(string $uri): void
|
|
|
|
{
|
|
|
|
if (!$this->users->current()->quota) {
|
|
|
|
throw new WebDAV_Exception('Your quota is exhausted', 403);
|
|
|
|
}
|
|
|
|
|
|
|
|
$target = $this->users->current()->path . $uri;
|
|
|
|
$parent = dirname($target);
|
|
|
|
|
|
|
|
if (file_exists($target)) {
|
|
|
|
throw new WebDAV_Exception('There is already a file with that name', 405);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!file_exists($parent)) {
|
|
|
|
throw new WebDAV_Exception('The parent directory does not exist', 409);
|
|
|
|
}
|
|
|
|
|
2022-11-21 17:18:04 +00:00
|
|
|
if (!is_writeable($parent)) {
|
|
|
|
throw new WebDAV_Exception('You don\'t have the right to create a directory here', 403);
|
|
|
|
}
|
|
|
|
|
2023-09-11 14:38:07 +00:00
|
|
|
$this->ensureDirectoryExists($uri);
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
|
|
|
|
2023-12-31 14:49:00 +00:00
|
|
|
public function touch(string $uri, \DateTimeInterface $datetime): bool
|
|
|
|
{
|
|
|
|
$target = $this->users->current()->path . $uri;
|
|
|
|
|
|
|
|
if (!file_exists($target)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!is_writeable($target)) {
|
|
|
|
throw new WebDAV_Exception('You don\'t have the right to create a directory here', 403);
|
|
|
|
}
|
|
|
|
|
|
|
|
return touch($target, $datetime->getTimestamp());
|
|
|
|
}
|
|
|
|
|
2022-09-29 23:39:24 +00:00
|
|
|
public function getResourceProperties(string $uri): Properties
|
|
|
|
{
|
|
|
|
if (!isset($this->properties[$uri])) {
|
2022-10-24 22:34:50 +00:00
|
|
|
$this->properties[$uri] = new Properties($this->users->current()->id, $uri);
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return $this->properties[$uri];
|
|
|
|
}
|
|
|
|
|
2023-12-31 14:49:00 +00:00
|
|
|
public function proppatch(string $uri, array $properties): array
|
2022-09-29 23:39:24 +00:00
|
|
|
{
|
|
|
|
$db = DB::getInstance();
|
|
|
|
|
|
|
|
$db->exec('BEGIN;');
|
2022-09-30 13:17:47 +00:00
|
|
|
|
2023-12-31 14:49:00 +00:00
|
|
|
$out = [];
|
|
|
|
|
2022-09-30 17:35:04 +00:00
|
|
|
foreach ($properties as $name => $prop) {
|
|
|
|
if ($prop['action'] == 'set') {
|
|
|
|
$this->getResourceProperties($uri)->set($name, $prop['attributes'], $prop['content']);
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
2022-09-30 17:35:04 +00:00
|
|
|
else {
|
2022-09-30 13:17:47 +00:00
|
|
|
$this->getResourceProperties($uri)->remove($name);
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
2023-12-31 14:49:00 +00:00
|
|
|
|
|
|
|
$out[$name] = 200;
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
$db->exec('END');
|
|
|
|
|
2023-12-31 14:49:00 +00:00
|
|
|
return $out;
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|
2022-09-30 13:17:47 +00:00
|
|
|
|
2022-11-15 12:10:52 +00:00
|
|
|
static protected function glob(string $path, string $pattern = '', int $flags = 0): array
|
|
|
|
{
|
|
|
|
$path = preg_replace('/[\*\?\[\]]/', '\\\\$0', $path);
|
2023-09-10 22:48:59 +00:00
|
|
|
return glob($path . $pattern, $flags) ?: [];
|
2022-11-15 12:10:52 +00:00
|
|
|
}
|
|
|
|
|
2022-09-30 13:17:47 +00:00
|
|
|
static public function getDirectorySize(string $path): int
|
|
|
|
{
|
|
|
|
$total = 0;
|
|
|
|
$path = rtrim($path, '/');
|
2023-02-14 11:15:15 +00:00
|
|
|
$path = realpath($path);
|
2022-09-30 13:17:47 +00:00
|
|
|
|
2023-02-14 11:15:15 +00:00
|
|
|
if (!$path || !file_exists($path)) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS)) as $f) {
|
|
|
|
$total += $f->getSize();
|
2022-09-30 13:17:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return $total;
|
|
|
|
}
|
|
|
|
|
2022-10-02 00:41:46 +00:00
|
|
|
static public function deleteDirectory(string $path): void
|
|
|
|
{
|
2023-02-14 11:15:15 +00:00
|
|
|
$path = rtrim($path, '/');
|
|
|
|
$path = realpath($path);
|
|
|
|
|
|
|
|
$dir = opendir($path);
|
|
|
|
|
|
|
|
while ($f = readdir($dir)) {
|
|
|
|
// Skip dots
|
|
|
|
if ($f == '.' || $f = '..') {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$f = $path . DIRECTORY_SEPARATOR . $f;
|
|
|
|
|
2022-10-02 00:41:46 +00:00
|
|
|
if (is_dir($f)) {
|
|
|
|
self::deleteDirectory($f);
|
|
|
|
@rmdir($f);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
@unlink($f);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-14 11:15:15 +00:00
|
|
|
closedir($dir);
|
|
|
|
|
2022-10-02 00:41:46 +00:00
|
|
|
@rmdir($path);
|
|
|
|
}
|
|
|
|
|
2022-09-30 13:17:47 +00:00
|
|
|
static public function getDirectoryMTime(string $path): int
|
|
|
|
{
|
|
|
|
$last = 0;
|
|
|
|
$path = rtrim($path, '/');
|
|
|
|
|
2022-11-15 12:10:52 +00:00
|
|
|
foreach (self::glob($path, '/*', GLOB_NOSORT) as $f) {
|
2022-09-30 13:17:47 +00:00
|
|
|
if (is_dir($f)) {
|
|
|
|
$m = self::getDirectoryMTime($f);
|
|
|
|
|
|
|
|
if ($m > $last) {
|
|
|
|
$last = $m;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$m = filemtime($f);
|
|
|
|
|
|
|
|
if ($m > $last) {
|
|
|
|
$last = $m;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $last;
|
|
|
|
}
|
2022-10-15 00:15:39 +00:00
|
|
|
|
2022-10-25 23:01:01 +00:00
|
|
|
protected function createWopiToken(string $uri)
|
|
|
|
{
|
|
|
|
$user = $this->users->current();
|
|
|
|
$ttl = time()+(3600*10);
|
|
|
|
|
|
|
|
// Use the user password as a server secret
|
2022-11-22 23:10:50 +00:00
|
|
|
$check = WebDAV::hmac(compact('ttl', 'uri'), $user->password);
|
2022-10-29 22:52:35 +00:00
|
|
|
$data = sprintf('%s_%s_%s', $check, $ttl, $user->login);
|
2022-10-25 23:01:01 +00:00
|
|
|
|
|
|
|
return [
|
|
|
|
WOPI::PROP_TOKEN => WOPI::base64_encode_url_safe($data),
|
|
|
|
WOPI::PROP_TOKEN_TTL => $ttl * 1000,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2022-10-15 00:15:39 +00:00
|
|
|
public function getWopiURI(string $id, string $token): ?string
|
|
|
|
{
|
|
|
|
$id = WOPI::base64_decode_url_safe($id);
|
|
|
|
$uri = gzuncompress($id);
|
|
|
|
$token_decode = WOPI::base64_decode_url_safe($token);
|
2022-10-25 23:01:01 +00:00
|
|
|
$hash = strtok($token_decode, '_');
|
2022-10-29 19:26:34 +00:00
|
|
|
$ttl = (int) strtok('_');
|
2022-10-25 23:01:01 +00:00
|
|
|
$login = strtok(false);
|
2022-10-15 00:15:39 +00:00
|
|
|
|
2022-10-25 23:01:01 +00:00
|
|
|
if ($ttl < time()) {
|
2022-10-15 00:15:39 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-10-25 23:01:01 +00:00
|
|
|
if (!$this->users->setCurrent($login)) {
|
2022-10-15 00:15:39 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-10-25 23:01:01 +00:00
|
|
|
$user = $this->users->current();
|
|
|
|
|
2022-11-22 23:10:50 +00:00
|
|
|
$check = WebDAV::hmac(compact('ttl', 'uri'), $user->password);
|
2022-10-25 23:01:01 +00:00
|
|
|
|
|
|
|
if (!hash_equals($check, $hash)) {
|
2022-10-15 00:15:39 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-10-25 23:01:01 +00:00
|
|
|
return $uri;
|
2022-10-15 00:15:39 +00:00
|
|
|
}
|
2023-09-11 14:38:07 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @see https://specifications.freedesktop.org/trash-spec/trashspec-latest.html
|
|
|
|
*/
|
|
|
|
public function moveToTrash(string $uri): void
|
|
|
|
{
|
|
|
|
$this->ensureDirectoryExists('.trash/info');
|
|
|
|
$this->ensureDirectoryExists('.trash/files');
|
|
|
|
|
|
|
|
$name = basename($uri);
|
|
|
|
|
|
|
|
$target = $this->users->current()->path . '.trash/info/' . $name . '.trashinfo';
|
|
|
|
$info = sprintf("[Trash Info]\nPath=%s\nDeletionDate=%s\n",
|
|
|
|
str_replace('%2F', '/', rawurlencode($uri)),
|
|
|
|
date(DATE_RFC3339)
|
|
|
|
);
|
|
|
|
|
|
|
|
file_put_contents($target, $info);
|
|
|
|
|
|
|
|
$this->move($uri, '.trash/files/' . $name);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function restoreFromTrash(string $uri): void
|
|
|
|
{
|
|
|
|
$src = $this->users->current()->path . '.trash/files/' . $uri;
|
|
|
|
|
|
|
|
if (!file_exists($src)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$info = $this->getTrashInfo($uri);
|
|
|
|
$dest = $info['Path'] ?? $uri;
|
|
|
|
|
|
|
|
if ($info) {
|
|
|
|
$this->delete('.trash/info/' . $uri . '.trashinfo');
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->move('.trash/files/' . $uri, $dest);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function emptyTrash(): void
|
|
|
|
{
|
|
|
|
$this->delete('.trash');
|
|
|
|
$this->ensureDirectoryExists('.trash/info');
|
|
|
|
$this->ensureDirectoryExists('.trash/files');
|
|
|
|
}
|
|
|
|
|
|
|
|
public function deleteFromTrash(string $uri): void
|
|
|
|
{
|
|
|
|
$this->delete('.trash/files/' . $uri);
|
|
|
|
$this->delete('.trash/info/' . $uri . '.trashinfo');
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function getTrashInfo(string $uri): ?array
|
|
|
|
{
|
|
|
|
$info_file = $this->users->current()->path . '.trash/info/' . $uri . '.trashinfo';
|
|
|
|
$info = @parse_ini_file($info_file, false, INI_SCANNER_RAW);
|
|
|
|
|
|
|
|
if (!isset($info['Path'], $info['DeletionDate'])) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
$info['Path'] = rawurldecode($info['Path']);
|
|
|
|
$info['DeletionDate'] = strtotime($info['DeletionDate']);
|
|
|
|
$info['InfoFilePath'] = $info_file;
|
|
|
|
return $info;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function pruneTrash(int $delete_before_timestamp): int
|
|
|
|
{
|
|
|
|
$this->ensureDirectoryExists('.trash/info');
|
|
|
|
$this->ensureDirectoryExists('.trash/files');
|
|
|
|
|
|
|
|
$info_dir = $this->users->current()->path . '.trash/info';
|
|
|
|
$count = 0;
|
|
|
|
|
|
|
|
foreach (glob($info_dir . '/*.trashinfo') as $file) {
|
|
|
|
$name = basename($file);
|
|
|
|
$name = str_replace('.trashinfo', '', $name);
|
|
|
|
$info = $this->getTrashInfo($name);
|
|
|
|
|
|
|
|
if (!$info) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($info['DeletionDate'] < $delete_before_timestamp) {
|
|
|
|
$this->delete('.trash/files/' . $name);
|
|
|
|
$this->delete('.trash/info/' . $name . '.trashinfo');
|
|
|
|
$count++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $count;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function listTrashFiles(): iterable
|
|
|
|
{
|
|
|
|
$this->pruneTrash(time() - DEFAULT_TRASHBIN_DELAY);
|
|
|
|
|
|
|
|
$this->ensureDirectoryExists('.trash/info');
|
|
|
|
$this->ensureDirectoryExists('.trash/files');
|
|
|
|
$info_dir = $this->users->current()->path . '.trash/info';
|
|
|
|
$files_dir = $this->users->current()->path . '.trash/files';
|
|
|
|
|
|
|
|
foreach (glob($info_dir . '/*.trashinfo') as $file) {
|
|
|
|
$name = basename($file);
|
|
|
|
$name = str_replace('.trashinfo', '', $name);
|
|
|
|
$target = $files_dir . '/' . $name;
|
|
|
|
|
|
|
|
if (!file_exists($target)) {
|
|
|
|
@unlink($file);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$info = $this->getTrashInfo($name);
|
|
|
|
|
|
|
|
$is_dir = is_dir($target);
|
|
|
|
$size = $is_dir ? self::getDirectorySize($target) : filesize($target);
|
|
|
|
|
|
|
|
yield $name => [
|
|
|
|
NextCloud::PROP_NC_TRASHBIN_FILENAME => $name,
|
|
|
|
NextCloud::PROP_NC_TRASHBIN_ORIGINAL_LOCATION => $info['Path'],
|
|
|
|
NextCloud::PROP_NC_TRASHBIN_DELETION_TIME => $info['DeletionDate'],
|
|
|
|
NextCloud::PROP_OC_SIZE => $size,
|
|
|
|
NextCloud::PROP_OC_ID => fileinode($file),
|
|
|
|
'DAV::getcontentlength' => $size,
|
|
|
|
'DAV::getcontenttype' => $is_dir ? null : @mime_content_type($target),
|
|
|
|
'DAV::resourcetype' => $is_dir ? 'collection' : '',
|
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|
2022-09-29 23:39:24 +00:00
|
|
|
}
|