aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/lib/Connector/Sabre
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav/lib/Connector/Sabre')
-rw-r--r--apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php1
-rw-r--r--apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php5
-rw-r--r--apps/dav/lib/Connector/Sabre/Auth.php65
-rw-r--r--apps/dav/lib/Connector/Sabre/BearerAuth.php37
-rw-r--r--apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php31
-rw-r--r--apps/dav/lib/Connector/Sabre/CachingTree.php3
-rw-r--r--apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php17
-rw-r--r--apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php13
-rw-r--r--apps/dav/lib/Connector/Sabre/DavAclPlugin.php21
-rw-r--r--apps/dav/lib/Connector/Sabre/Directory.php117
-rw-r--r--apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php7
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/BadGateway.php1
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/FileLocked.php5
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/Forbidden.php12
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php12
-rw-r--r--apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php12
-rw-r--r--apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php16
-rw-r--r--apps/dav/lib/Connector/Sabre/File.php193
-rw-r--r--apps/dav/lib/Connector/Sabre/FilesPlugin.php226
-rw-r--r--apps/dav/lib/Connector/Sabre/FilesReportPlugin.php94
-rw-r--r--apps/dav/lib/Connector/Sabre/MaintenancePlugin.php11
-rw-r--r--apps/dav/lib/Connector/Sabre/MtimeSanitizer.php1
-rw-r--r--apps/dav/lib/Connector/Sabre/Node.php110
-rw-r--r--apps/dav/lib/Connector/Sabre/ObjectTree.php24
-rw-r--r--apps/dav/lib/Connector/Sabre/Principal.php78
-rw-r--r--apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php78
-rw-r--r--apps/dav/lib/Connector/Sabre/PublicAuth.php70
-rw-r--r--apps/dav/lib/Connector/Sabre/QuotaPlugin.php45
-rw-r--r--apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php8
-rw-r--r--apps/dav/lib/Connector/Sabre/Server.php202
-rw-r--r--apps/dav/lib/Connector/Sabre/ServerFactory.php197
-rw-r--r--apps/dav/lib/Connector/Sabre/ShareTypeList.php15
-rw-r--r--apps/dav/lib/Connector/Sabre/ShareeList.php9
-rw-r--r--apps/dav/lib/Connector/Sabre/SharesPlugin.php6
-rw-r--r--apps/dav/lib/Connector/Sabre/TagList.php15
-rw-r--r--apps/dav/lib/Connector/Sabre/TagsPlugin.php86
-rw-r--r--apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php193
37 files changed, 1340 insertions, 696 deletions
diff --git a/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php b/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php
index b39dc7197b0..0e2b1c58748 100644
--- a/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php b/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php
index 8f5195e926b..9cff113140a 100644
--- a/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -58,8 +59,8 @@ class AppleQuirksPlugin extends ServerPlugin {
* This method handles HTTP REPORT requests.
*
* @param string $reportName
- * @param mixed $report
- * @param mixed $path
+ * @param mixed $report
+ * @param mixed $path
*
* @return bool
*/
diff --git a/apps/dav/lib/Connector/Sabre/Auth.php b/apps/dav/lib/Connector/Sabre/Auth.php
index 9b67d960107..a174920946a 100644
--- a/apps/dav/lib/Connector/Sabre/Auth.php
+++ b/apps/dav/lib/Connector/Sabre/Auth.php
@@ -13,10 +13,13 @@ use OC\Authentication\TwoFactorAuth\Manager;
use OC\User\Session;
use OCA\DAV\Connector\Sabre\Exception\PasswordLoginForbidden;
use OCA\DAV\Connector\Sabre\Exception\TooManyRequests;
+use OCP\AppFramework\Http;
+use OCP\Defaults;
use OCP\IRequest;
use OCP\ISession;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\Bruteforce\MaxDelayReached;
+use OCP\Server;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Auth\Backend\AbstractBasic;
use Sabre\DAV\Exception\NotAuthenticated;
@@ -26,29 +29,20 @@ use Sabre\HTTP\ResponseInterface;
class Auth extends AbstractBasic {
public const DAV_AUTHENTICATED = 'AUTHENTICATED_TO_DAV_BACKEND';
-
- private ISession $session;
- private Session $userSession;
- private IRequest $request;
private ?string $currentUser = null;
- private Manager $twoFactorManager;
- private IThrottler $throttler;
-
- public function __construct(ISession $session,
- Session $userSession,
- IRequest $request,
- Manager $twoFactorManager,
- IThrottler $throttler,
- string $principalPrefix = 'principals/users/') {
- $this->session = $session;
- $this->userSession = $userSession;
- $this->twoFactorManager = $twoFactorManager;
- $this->request = $request;
- $this->throttler = $throttler;
+
+ public function __construct(
+ private ISession $session,
+ private Session $userSession,
+ private IRequest $request,
+ private Manager $twoFactorManager,
+ private IThrottler $throttler,
+ string $principalPrefix = 'principals/users/',
+ ) {
$this->principalPrefix = $principalPrefix;
// setup realm
- $defaults = new \OCP\Defaults();
+ $defaults = new Defaults();
$this->realm = $defaults->getName() ?: 'Nextcloud';
}
@@ -61,8 +55,8 @@ class Auth extends AbstractBasic {
* @see https://github.com/owncloud/core/issues/13245
*/
public function isDavAuthenticated(string $username): bool {
- return !is_null($this->session->get(self::DAV_AUTHENTICATED)) &&
- $this->session->get(self::DAV_AUTHENTICATED) === $username;
+ return !is_null($this->session->get(self::DAV_AUTHENTICATED))
+ && $this->session->get(self::DAV_AUTHENTICATED) === $username;
}
/**
@@ -77,8 +71,8 @@ class Auth extends AbstractBasic {
* @throws PasswordLoginForbidden
*/
protected function validateUserPass($username, $password) {
- if ($this->userSession->isLoggedIn() &&
- $this->isDavAuthenticated($this->userSession->getUser()->getUID())
+ if ($this->userSession->isLoggedIn()
+ && $this->isDavAuthenticated($this->userSession->getUser()->getUID())
) {
$this->session->close();
return true;
@@ -115,7 +109,7 @@ class Auth extends AbstractBasic {
} catch (Exception $e) {
$class = get_class($e);
$msg = $e->getMessage();
- \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
+ Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
throw new ServiceUnavailable("$class: $msg");
}
}
@@ -124,8 +118,9 @@ class Auth extends AbstractBasic {
* Checks whether a CSRF check is required on the request
*/
private function requiresCSRFCheck(): bool {
- // GET requires no check at all
- if ($this->request->getMethod() === 'GET') {
+
+ $methodsWithoutCsrf = ['GET', 'HEAD', 'OPTIONS'];
+ if (in_array($this->request->getMethod(), $methodsWithoutCsrf)) {
return false;
}
@@ -149,8 +144,8 @@ class Auth extends AbstractBasic {
}
// If logged-in AND DAV authenticated no check is required
- if ($this->userSession->isLoggedIn() &&
- $this->isDavAuthenticated($this->userSession->getUser()->getUID())) {
+ if ($this->userSession->isLoggedIn()
+ && $this->isDavAuthenticated($this->userSession->getUser()->getUID())) {
return false;
}
@@ -164,13 +159,13 @@ class Auth extends AbstractBasic {
private function auth(RequestInterface $request, ResponseInterface $response): array {
$forcedLogout = false;
- if (!$this->request->passesCSRFCheck() &&
- $this->requiresCSRFCheck()) {
+ if (!$this->request->passesCSRFCheck()
+ && $this->requiresCSRFCheck()) {
// In case of a fail with POST we need to recheck the credentials
if ($this->request->getMethod() === 'POST') {
$forcedLogout = true;
} else {
- $response->setStatus(401);
+ $response->setStatus(Http::STATUS_UNAUTHORIZED);
throw new \Sabre\DAV\Exception\NotAuthenticated('CSRF check not passed.');
}
}
@@ -183,10 +178,10 @@ class Auth extends AbstractBasic {
}
if (
//Fix for broken webdav clients
- ($this->userSession->isLoggedIn() && is_null($this->session->get(self::DAV_AUTHENTICATED))) ||
+ ($this->userSession->isLoggedIn() && is_null($this->session->get(self::DAV_AUTHENTICATED)))
//Well behaved clients that only send the cookie are allowed
- ($this->userSession->isLoggedIn() && $this->session->get(self::DAV_AUTHENTICATED) === $this->userSession->getUser()->getUID() && empty($request->getHeader('Authorization'))) ||
- \OC_User::handleApacheAuth()
+ || ($this->userSession->isLoggedIn() && $this->session->get(self::DAV_AUTHENTICATED) === $this->userSession->getUser()->getUID() && empty($request->getHeader('Authorization')))
+ || \OC_User::handleApacheAuth()
) {
$user = $this->userSession->getUser()->getUID();
$this->currentUser = $user;
@@ -203,7 +198,7 @@ class Auth extends AbstractBasic {
} elseif (in_array('XMLHttpRequest', explode(',', $request->getHeader('X-Requested-With') ?? ''))) {
// For ajax requests use dummy auth name to prevent browser popup in case of invalid creditials
$response->addHeader('WWW-Authenticate', 'DummyBasic realm="' . $this->realm . '"');
- $response->setStatus(401);
+ $response->setStatus(Http::STATUS_UNAUTHORIZED);
throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls');
}
return $data;
diff --git a/apps/dav/lib/Connector/Sabre/BearerAuth.php b/apps/dav/lib/Connector/Sabre/BearerAuth.php
index 8caae8dced9..23453ae8efb 100644
--- a/apps/dav/lib/Connector/Sabre/BearerAuth.php
+++ b/apps/dav/lib/Connector/Sabre/BearerAuth.php
@@ -1,10 +1,14 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Connector\Sabre;
+use OCP\AppFramework\Http;
+use OCP\Defaults;
+use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUserSession;
@@ -13,22 +17,15 @@ use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class BearerAuth extends AbstractBearer {
- private IUserSession $userSession;
- private ISession $session;
- private IRequest $request;
- private string $principalPrefix;
-
- public function __construct(IUserSession $userSession,
- ISession $session,
- IRequest $request,
- $principalPrefix = 'principals/users/') {
- $this->userSession = $userSession;
- $this->session = $session;
- $this->request = $request;
- $this->principalPrefix = $principalPrefix;
-
+ public function __construct(
+ private IUserSession $userSession,
+ private ISession $session,
+ private IRequest $request,
+ private IConfig $config,
+ private string $principalPrefix = 'principals/users/',
+ ) {
// setup realm
- $defaults = new \OCP\Defaults();
+ $defaults = new Defaults();
$this->realm = $defaults->getName() ?: 'Nextcloud';
}
@@ -63,6 +60,14 @@ class BearerAuth extends AbstractBearer {
* @param ResponseInterface $response
*/
public function challenge(RequestInterface $request, ResponseInterface $response): void {
- $response->setStatus(401);
+ // Legacy ownCloud clients still authenticate via OAuth2
+ $enableOcClients = $this->config->getSystemValueBool('oauth2.enable_oc_clients', false);
+ $userAgent = $request->getHeader('User-Agent');
+ if ($enableOcClients && $userAgent !== null && str_contains($userAgent, 'mirall')) {
+ parent::challenge($request, $response);
+ return;
+ }
+
+ $response->setStatus(Http::STATUS_UNAUTHORIZED);
}
}
diff --git a/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php b/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php
index c4e579bef0b..21358406a4a 100644
--- a/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php
@@ -7,6 +7,7 @@
*/
namespace OCA\DAV\Connector\Sabre;
+use OCA\Theming\ThemingDefaults;
use OCP\IConfig;
use OCP\IRequest;
use Sabre\DAV\Server;
@@ -21,10 +22,11 @@ use Sabre\HTTP\RequestInterface;
*/
class BlockLegacyClientPlugin extends ServerPlugin {
protected ?Server $server = null;
- protected IConfig $config;
- public function __construct(IConfig $config) {
- $this->config = $config;
+ public function __construct(
+ private IConfig $config,
+ private ThemingDefaults $themingDefaults,
+ ) {
}
/**
@@ -47,11 +49,26 @@ class BlockLegacyClientPlugin extends ServerPlugin {
return;
}
- $minimumSupportedDesktopVersion = $this->config->getSystemValue('minimum.supported.desktop.version', '2.3.0');
+ $minimumSupportedDesktopVersion = $this->config->getSystemValueString('minimum.supported.desktop.version', '3.1.0');
+ $maximumSupportedDesktopVersion = $this->config->getSystemValueString('maximum.supported.desktop.version', '99.99.99');
+
+ // Check if the client is a desktop client
preg_match(IRequest::USER_AGENT_CLIENT_DESKTOP, $userAgent, $versionMatches);
- if (isset($versionMatches[1]) &&
- version_compare($versionMatches[1], $minimumSupportedDesktopVersion) === -1) {
- throw new \Sabre\DAV\Exception\Forbidden('Unsupported client version.');
+
+ // If the client is a desktop client and the version is too old, block it
+ if (isset($versionMatches[1]) && version_compare($versionMatches[1], $minimumSupportedDesktopVersion) === -1) {
+ $customClientDesktopLink = htmlspecialchars($this->themingDefaults->getSyncClientUrl());
+ $minimumSupportedDesktopVersion = htmlspecialchars($minimumSupportedDesktopVersion);
+
+ throw new \Sabre\DAV\Exception\Forbidden("This version of the client is unsupported. Upgrade to <a href=\"$customClientDesktopLink\">version $minimumSupportedDesktopVersion or later</a>.");
+ }
+
+ // If the client is a desktop client and the version is too new, block it
+ if (isset($versionMatches[1]) && version_compare($versionMatches[1], $maximumSupportedDesktopVersion) === 1) {
+ $customClientDesktopLink = htmlspecialchars($this->themingDefaults->getSyncClientUrl());
+ $maximumSupportedDesktopVersion = htmlspecialchars($maximumSupportedDesktopVersion);
+
+ throw new \Sabre\DAV\Exception\Forbidden("This version of the client is unsupported. Downgrade to <a href=\"$customClientDesktopLink\">version $maximumSupportedDesktopVersion or earlier</a>.");
}
}
}
diff --git a/apps/dav/lib/Connector/Sabre/CachingTree.php b/apps/dav/lib/Connector/Sabre/CachingTree.php
index a715991fcf6..5d72b530f58 100644
--- a/apps/dav/lib/Connector/Sabre/CachingTree.php
+++ b/apps/dav/lib/Connector/Sabre/CachingTree.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -27,7 +28,7 @@ class CachingTree extends Tree {
// flushing the entire cache
$path = trim($path, '/');
foreach ($this->cache as $nodePath => $node) {
- $nodePath = (string) $nodePath;
+ $nodePath = (string)$nodePath;
if ($path === '' || $nodePath == $path || str_starts_with($nodePath, $path . '/')) {
unset($this->cache[$nodePath]);
}
diff --git a/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php b/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php
index 1bf45b22537..18009080585 100644
--- a/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php
+++ b/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php
@@ -8,6 +8,7 @@ declare(strict_types=1);
namespace OCA\DAV\Connector\Sabre;
+use OCP\AppFramework\Http;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
@@ -26,20 +27,6 @@ class ChecksumUpdatePlugin extends ServerPlugin {
}
/** @return string[] */
- public function getHTTPMethods($path): array {
- $tree = $this->server->tree;
-
- if ($tree->nodeExists($path)) {
- $node = $tree->getNodeForPath($path);
- if ($node instanceof File) {
- return ['PATCH'];
- }
- }
-
- return [];
- }
-
- /** @return string[] */
public function getFeatures(): array {
return ['nextcloud-checksum-update'];
}
@@ -59,7 +46,7 @@ class ChecksumUpdatePlugin extends ServerPlugin {
$node->setChecksum($checksum);
$response->addHeader('OC-Checksum', $checksum);
$response->setHeader('Content-Length', '0');
- $response->setStatus(204);
+ $response->setStatus(Http::STATUS_NO_CONTENT);
return false;
}
diff --git a/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php b/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php
index 71514016bda..e4b6c2636da 100644
--- a/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php
@@ -20,13 +20,12 @@ class CommentPropertiesPlugin extends ServerPlugin {
public const PROPERTY_NAME_UNREAD = '{http://owncloud.org/ns}comments-unread';
protected ?Server $server = null;
- private ICommentsManager $commentsManager;
- private IUserSession $userSession;
private array $cachedUnreadCount = [];
- public function __construct(ICommentsManager $commentsManager, IUserSession $userSession) {
- $this->commentsManager = $commentsManager;
- $this->userSession = $userSession;
+ public function __construct(
+ private ICommentsManager $commentsManager,
+ private IUserSession $userSession,
+ ) {
}
/**
@@ -62,7 +61,7 @@ class CommentPropertiesPlugin extends ServerPlugin {
$ids[] = (string)$id;
}
- $ids[] = (string) $directory->getId();
+ $ids[] = (string)$directory->getId();
$unread = $this->commentsManager->getNumberOfUnreadCommentsForObjects('files', $ids, $this->userSession->getUser());
foreach ($unread as $id => $count) {
@@ -80,7 +79,7 @@ class CommentPropertiesPlugin extends ServerPlugin {
*/
public function handleGetProperties(
PropFind $propFind,
- \Sabre\DAV\INode $node
+ \Sabre\DAV\INode $node,
) {
if (!($node instanceof File) && !($node instanceof Directory)) {
return;
diff --git a/apps/dav/lib/Connector/Sabre/DavAclPlugin.php b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php
index c499f806eba..100d719ef01 100644
--- a/apps/dav/lib/Connector/Sabre/DavAclPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php
@@ -11,6 +11,7 @@ use OCA\DAV\CalDAV\CachedSubscription;
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CardDAV\AddressBook;
use Sabre\CalDAV\Principal\User;
+use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\INode;
use Sabre\DAV\PropFind;
@@ -49,13 +50,19 @@ class DavAclPlugin extends \Sabre\DAVACL\Plugin {
$type = 'Node';
break;
}
- throw new NotFound(
- sprintf(
- "%s with name '%s' could not be found",
- $type,
- $node->getName()
- )
- );
+
+ if ($this->getCurrentUserPrincipal() === $node->getOwner()) {
+ throw new Forbidden('Access denied');
+ } else {
+ throw new NotFound(
+ sprintf(
+ "%s with name '%s' could not be found",
+ $type,
+ $node->getName()
+ )
+ );
+ }
+
}
return $access;
diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php
index 101c0d13935..fe09c3f423f 100644
--- a/apps/dav/lib/Connector/Sabre/Directory.php
+++ b/apps/dav/lib/Connector/Sabre/Directory.php
@@ -13,10 +13,15 @@ use OCA\DAV\AppInfo\Application;
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+use OCA\DAV\Storage\PublicShareWrapper;
+use OCP\App\IAppManager;
+use OCP\Constants;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
use OCP\Files\ForbiddenException;
use OCP\Files\InvalidPathException;
+use OCP\Files\Mount\IMountManager;
+use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\Files\StorageNotAvailableException;
use OCP\IL10N;
@@ -24,6 +29,7 @@ use OCP\IRequest;
use OCP\L10N\IFactory;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
+use OCP\Server;
use OCP\Share\IManager as IShareManager;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\BadRequest;
@@ -33,23 +39,26 @@ use Sabre\DAV\Exception\ServiceUnavailable;
use Sabre\DAV\IFile;
use Sabre\DAV\INode;
-class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota, \Sabre\DAV\IMoveTarget, \Sabre\DAV\ICopyTarget {
+class Directory extends Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota, \Sabre\DAV\IMoveTarget, \Sabre\DAV\ICopyTarget {
/**
* Cached directory content
- * @var \OCP\Files\FileInfo[]
+ * @var FileInfo[]
*/
private ?array $dirContent = null;
/** Cached quota info */
private ?array $quotaInfo = null;
- private ?CachingTree $tree = null;
/**
* Sets up the node, expects a full path name
*/
- public function __construct(View $view, FileInfo $info, ?CachingTree $tree = null, ?IShareManager $shareManager = null) {
+ public function __construct(
+ View $view,
+ FileInfo $info,
+ private ?CachingTree $tree = null,
+ ?IShareManager $shareManager = null,
+ ) {
parent::__construct($view, $info, $shareManager);
- $this->tree = $tree;
}
/**
@@ -101,18 +110,18 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
'type' => FileInfo::TYPE_FILE
], null);
}
- $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info);
+ $node = new File($this->fileView, $info);
// only allow 1 process to upload a file at once but still allow reading the file while writing the part file
$node->acquireLock(ILockingProvider::LOCK_SHARED);
- $this->fileView->lockFile($path . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
+ $this->fileView->lockFile($this->path . '/' . $name . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
$result = $node->put($data);
- $this->fileView->unlockFile($path . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
+ $this->fileView->unlockFile($this->path . '/' . $name . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
$node->releaseLock(ILockingProvider::LOCK_SHARED);
return $result;
- } catch (\OCP\Files\StorageNotAvailableException $e) {
+ } catch (StorageNotAvailableException $e) {
throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
} catch (InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage(), false, $ex);
@@ -143,12 +152,12 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
if (!$this->fileView->mkdir($newPath)) {
throw new \Sabre\DAV\Exception\Forbidden('Could not create directory ' . $newPath);
}
- } catch (\OCP\Files\StorageNotAvailableException $e) {
- throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
+ } catch (StorageNotAvailableException $e) {
+ throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), 0, $e);
} catch (InvalidPathException $ex) {
- throw new InvalidPath($ex->getMessage());
+ throw new InvalidPath($ex->getMessage(), false, $ex);
} catch (ForbiddenException $ex) {
- throw new Forbidden($ex->getMessage(), $ex->getRetry());
+ throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
@@ -158,14 +167,27 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
* Returns a specific child node, referenced by its name
*
* @param string $name
- * @param \OCP\Files\FileInfo $info
+ * @param FileInfo $info
* @return \Sabre\DAV\INode
* @throws InvalidPath
* @throws \Sabre\DAV\Exception\NotFound
* @throws \Sabre\DAV\Exception\ServiceUnavailable
*/
public function getChild($name, $info = null, ?IRequest $request = null, ?IL10N $l10n = null) {
- if (!$this->info->isReadable()) {
+ $storage = $this->info->getStorage();
+ $allowDirectory = false;
+
+ // Checking if we're in a file drop
+ // If we are, then only PUT and MKCOL are allowed (see plugin)
+ // so we are safe to return the directory without a risk of
+ // leaking files and folders structure.
+ if ($storage instanceof PublicShareWrapper) {
+ $share = $storage->getShare();
+ $allowDirectory = ($share->getPermissions() & Constants::PERMISSION_READ) !== Constants::PERMISSION_READ;
+ }
+
+ // For file drop we need to be allowed to read the directory with the nickname
+ if (!$allowDirectory && !$this->info->isReadable()) {
// avoid detecting files through this way
throw new NotFound();
}
@@ -173,14 +195,14 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
$path = $this->path . '/' . $name;
if (is_null($info)) {
try {
- $this->fileView->verifyPath($this->path, $name);
+ $this->fileView->verifyPath($this->path, $name, true);
$info = $this->fileView->getFileInfo($path);
- } catch (\OCP\Files\StorageNotAvailableException $e) {
- throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
+ } catch (StorageNotAvailableException $e) {
+ throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), 0, $e);
} catch (InvalidPathException $ex) {
- throw new InvalidPath($ex->getMessage());
+ throw new InvalidPath($ex->getMessage(), false, $ex);
} catch (ForbiddenException $e) {
- throw new \Sabre\DAV\Exception\Forbidden();
+ throw new \Sabre\DAV\Exception\Forbidden($e->getMessage(), $e->getCode(), $e);
}
}
@@ -191,7 +213,12 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
if ($info->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
$node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager);
} else {
- $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info, $this->shareManager, $request, $l10n);
+ // In case reading a directory was allowed but it turns out the node was a not a directory, reject it now.
+ if (!$this->info->isReadable()) {
+ throw new NotFound();
+ }
+
+ $node = new File($this->fileView, $info, $this->shareManager, $request, $l10n);
}
if ($this->tree) {
$this->tree->cacheNode($node);
@@ -204,7 +231,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
*
* @return \Sabre\DAV\INode[]
* @throws \Sabre\DAV\Exception\Locked
- * @throws \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ * @throws Forbidden
*/
public function getChildren() {
if (!is_null($this->dirContent)) {
@@ -214,7 +241,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
if (!$this->info->isReadable()) {
// return 403 instead of 404 because a 404 would make
// the caller believe that the collection itself does not exist
- if (\OCP\Server::get(\OCP\App\IAppManager::class)->isInstalled('files_accesscontrol')) {
+ if (Server::get(IAppManager::class)->isEnabledForAnyone('files_accesscontrol')) {
throw new Forbidden('No read permissions. This might be caused by files_accesscontrol, check your configured rules');
} else {
throw new Forbidden('No read permissions');
@@ -226,8 +253,8 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
}
$nodes = [];
- $request = \OC::$server->get(IRequest::class);
- $l10nFactory = \OC::$server->get(IFactory::class);
+ $request = Server::get(IRequest::class);
+ $l10nFactory = Server::get(IFactory::class);
$l10n = $l10nFactory->get(Application::APP_ID);
foreach ($folderContent as $info) {
$node = $this->getChild($info->getName(), $info, $request, $l10n);
@@ -280,7 +307,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
}
private function getLogger(): LoggerInterface {
- return \OC::$server->get(LoggerInterface::class);
+ return Server::get(LoggerInterface::class);
}
/**
@@ -294,14 +321,14 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
}
$relativePath = $this->fileView->getRelativePath($this->info->getPath());
if ($relativePath === null) {
- $this->getLogger()->warning("error while getting quota as the relative path cannot be found");
+ $this->getLogger()->warning('error while getting quota as the relative path cannot be found');
return [0, 0];
}
try {
$storageInfo = \OC_Helper::getStorageInfo($relativePath, $this->info, false);
- if ($storageInfo['quota'] === \OCP\Files\FileInfo::SPACE_UNLIMITED) {
- $free = \OCP\Files\FileInfo::SPACE_UNLIMITED;
+ if ($storageInfo['quota'] === FileInfo::SPACE_UNLIMITED) {
+ $free = FileInfo::SPACE_UNLIMITED;
} else {
$free = $storageInfo['free'];
}
@@ -310,14 +337,14 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
$free
];
return $this->quotaInfo;
- } catch (\OCP\Files\NotFoundException $e) {
- $this->getLogger()->warning("error while getting quota into", ['exception' => $e]);
+ } catch (NotFoundException $e) {
+ $this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
return [0, 0];
- } catch (\OCP\Files\StorageNotAvailableException $e) {
- $this->getLogger()->warning("error while getting quota into", ['exception' => $e]);
+ } catch (StorageNotAvailableException $e) {
+ $this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
return [0, 0];
} catch (NotPermittedException $e) {
- $this->getLogger()->warning("error while getting quota into", ['exception' => $e]);
+ $this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
return [0, 0];
}
}
@@ -357,10 +384,6 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
throw new BadRequest('Incompatible node types');
}
- if (!$this->fileView) {
- throw new ServiceUnavailable('filesystem not setup');
- }
-
$destinationPath = $this->getPath() . '/' . $targetName;
@@ -378,7 +401,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
$sourcePath = $sourceNode->getPath();
$isMovableMount = false;
- $sourceMount = \OC::$server->getMountManager()->find($this->fileView->getAbsolutePath($sourcePath));
+ $sourceMount = Server::get(IMountManager::class)->find($this->fileView->getAbsolutePath($sourcePath));
$internalPath = $sourceMount->getInternalPath($this->fileView->getAbsolutePath($sourcePath));
if ($sourceMount instanceof MoveableMount && $internalPath === '') {
$isMovableMount = true;
@@ -418,9 +441,9 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
throw new \Sabre\DAV\Exception\Forbidden('');
}
} catch (StorageNotAvailableException $e) {
- throw new ServiceUnavailable($e->getMessage());
+ throw new ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
} catch (ForbiddenException $ex) {
- throw new Forbidden($ex->getMessage(), $ex->getRetry());
+ throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
@@ -445,11 +468,17 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
throw new InvalidPath($ex->getMessage());
}
- return $this->fileView->copy($sourcePath, $destinationPath);
+ $copyOkay = $this->fileView->copy($sourcePath, $destinationPath);
+
+ if (!$copyOkay) {
+ throw new \Sabre\DAV\Exception\Forbidden('Copy did not proceed');
+ }
+
+ return true;
} catch (StorageNotAvailableException $e) {
- throw new ServiceUnavailable($e->getMessage());
+ throw new ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
} catch (ForbiddenException $ex) {
- throw new Forbidden($ex->getMessage(), $ex->getRetry());
+ throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
diff --git a/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php b/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php
index 26819836001..f6baceb748b 100644
--- a/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php
+++ b/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php
@@ -7,6 +7,7 @@
*/
namespace OCA\DAV\Connector\Sabre;
+use OCP\AppFramework\Http;
use Sabre\DAV\Server;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
@@ -42,13 +43,13 @@ class DummyGetResponsePlugin extends \Sabre\DAV\ServerPlugin {
* @return false
*/
public function httpGet(RequestInterface $request, ResponseInterface $response) {
- $string = 'This is the WebDAV interface. It can only be accessed by ' .
- 'WebDAV clients such as the Nextcloud desktop sync client.';
+ $string = 'This is the WebDAV interface. It can only be accessed by '
+ . 'WebDAV clients such as the Nextcloud desktop sync client.';
$stream = fopen('php://memory', 'r+');
fwrite($stream, $string);
rewind($stream);
- $response->setStatus(200);
+ $response->setStatus(Http::STATUS_OK);
$response->setBody($stream);
return false;
diff --git a/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php b/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php
index 41ace002660..1e1e4aaed04 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-only
diff --git a/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php b/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php
index 5c39e6a437a..38708e945e9 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php
@@ -8,14 +8,15 @@
namespace OCA\DAV\Connector\Sabre\Exception;
use Exception;
+use OCP\Files\LockNotAcquiredException;
class FileLocked extends \Sabre\DAV\Exception {
/**
* @param string $message
* @param int $code
*/
- public function __construct($message = "", $code = 0, ?Exception $previous = null) {
- if ($previous instanceof \OCP\Files\LockNotAcquiredException) {
+ public function __construct($message = '', $code = 0, ?Exception $previous = null) {
+ if ($previous instanceof LockNotAcquiredException) {
$message = sprintf('Target file %s is locked by another process.', $previous->path);
}
parent::__construct($message, $code, $previous);
diff --git a/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php b/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php
index 0dd346a2546..95d4b3ab514 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php
@@ -11,18 +11,16 @@ class Forbidden extends \Sabre\DAV\Exception\Forbidden {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
/**
- * @var bool
- */
- private $retry;
-
- /**
* @param string $message
* @param bool $retry
* @param \Exception $previous
*/
- public function __construct($message, $retry = false, ?\Exception $previous = null) {
+ public function __construct(
+ $message,
+ private $retry = false,
+ ?\Exception $previous = null,
+ ) {
parent::__construct($message, 0, $previous);
- $this->retry = $retry;
}
/**
diff --git a/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php b/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php
index a9ec8ffef9a..dfc08aa8b88 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php
@@ -13,18 +13,16 @@ class InvalidPath extends Exception {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
/**
- * @var bool
- */
- private $retry;
-
- /**
* @param string $message
* @param bool $retry
* @param \Exception|null $previous
*/
- public function __construct($message, $retry = false, ?\Exception $previous = null) {
+ public function __construct(
+ $message,
+ private $retry = false,
+ ?\Exception $previous = null,
+ ) {
parent::__construct($message, 0, $previous);
- $this->retry = $retry;
}
/**
diff --git a/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php b/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php
index 2e8fcefe577..686386dbfef 100644
--- a/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php
@@ -67,15 +67,13 @@ class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin {
ServerMaintenanceMode::class => true,
];
- private string $appName;
- private LoggerInterface $logger;
-
/**
- * @param string $loggerAppName app name to use when logging
+ * @param string $appName app name to use when logging
*/
- public function __construct(string $loggerAppName, LoggerInterface $logger) {
- $this->appName = $loggerAppName;
- $this->logger = $logger;
+ public function __construct(
+ private string $appName,
+ private LoggerInterface $logger,
+ ) {
}
/**
diff --git a/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php b/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php
index 485b6b09a3c..b0c5a079ce1 100644
--- a/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php
@@ -7,9 +7,11 @@
*/
namespace OCA\DAV\Connector\Sabre;
+use OCP\AppFramework\Http;
use Sabre\DAV\INode;
use Sabre\DAV\Locks\LockInfo;
use Sabre\DAV\PropFind;
+use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\DAV\Xml\Property\LockDiscovery;
use Sabre\DAV\Xml\Property\SupportedLock;
@@ -29,11 +31,11 @@ use Sabre\HTTP\ResponseInterface;
* @package OCA\DAV\Connector\Sabre
*/
class FakeLockerPlugin extends ServerPlugin {
- /** @var \Sabre\DAV\Server */
+ /** @var Server */
private $server;
/** {@inheritDoc} */
- public function initialize(\Sabre\DAV\Server $server) {
+ public function initialize(Server $server) {
$this->server = $server;
$this->server->on('method:LOCK', [$this, 'fakeLockProvider'], 1);
$this->server->on('method:UNLOCK', [$this, 'fakeUnlockProvider'], 1);
@@ -111,15 +113,15 @@ class FakeLockerPlugin extends ServerPlugin {
$lockInfo = new LockInfo();
$lockInfo->token = md5($request->getPath());
$lockInfo->uri = $request->getPath();
- $lockInfo->depth = \Sabre\DAV\Server::DEPTH_INFINITY;
+ $lockInfo->depth = Server::DEPTH_INFINITY;
$lockInfo->timeout = 1800;
$body = $this->server->xml->write('{DAV:}prop', [
- '{DAV:}lockdiscovery' =>
- new LockDiscovery([$lockInfo])
+ '{DAV:}lockdiscovery'
+ => new LockDiscovery([$lockInfo])
]);
- $response->setStatus(200);
+ $response->setStatus(Http::STATUS_OK);
$response->setBody($body);
return false;
@@ -134,7 +136,7 @@ class FakeLockerPlugin extends ServerPlugin {
*/
public function fakeUnlockProvider(RequestInterface $request,
ResponseInterface $response) {
- $response->setStatus(204);
+ $response->setStatus(Http::STATUS_NO_CONTENT);
$response->setHeader('Content-Length', '0');
return false;
}
diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php
index 0b9199492fe..d2a71eb3e7b 100644
--- a/apps/dav/lib/Connector/Sabre/File.php
+++ b/apps/dav/lib/Connector/Sabre/File.php
@@ -13,28 +13,32 @@ use OC\Files\Filesystem;
use OC\Files\Stream\HashWrapper;
use OC\Files\View;
use OCA\DAV\AppInfo\Application;
-use OCA\DAV\Connector\Sabre\Exception\BadGateway;
use OCA\DAV\Connector\Sabre\Exception\EntityTooLarge;
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
use OCA\DAV\Connector\Sabre\Exception\Forbidden as DAVForbiddenException;
use OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType;
+use OCP\App\IAppManager;
use OCP\Encryption\Exceptions\GenericEncryptionException;
+use OCP\Files;
use OCP\Files\EntityTooLargeException;
use OCP\Files\FileInfo;
use OCP\Files\ForbiddenException;
use OCP\Files\GenericFileException;
+use OCP\Files\IMimeTypeDetector;
use OCP\Files\InvalidContentException;
use OCP\Files\InvalidPathException;
use OCP\Files\LockNotAcquiredException;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
-use OCP\Files\Storage;
+use OCP\Files\Storage\IWriteStreamStorage;
use OCP\Files\StorageNotAvailableException;
+use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\L10N\IFactory as IL10NFactory;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
+use OCP\Server;
use OCP\Share\IManager;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception;
@@ -51,8 +55,8 @@ class File extends Node implements IFile {
/**
* Sets up the node, expects a full path name
*
- * @param \OC\Files\View $view
- * @param \OCP\Files\FileInfo $info
+ * @param View $view
+ * @param FileInfo $info
* @param ?\OCP\Share\IManager $shareManager
* @param ?IRequest $request
* @param ?IL10N $l10n
@@ -65,14 +69,14 @@ class File extends Node implements IFile {
} else {
// Querying IL10N directly results in a dependency loop
/** @var IL10NFactory $l10nFactory */
- $l10nFactory = \OC::$server->get(IL10NFactory::class);
+ $l10nFactory = Server::get(IL10NFactory::class);
$this->l10n = $l10nFactory->get(Application::APP_ID);
}
if (isset($request)) {
$this->request = $request;
} else {
- $this->request = \OC::$server->get(IRequest::class);
+ $this->request = Server::get(IRequest::class);
}
}
@@ -117,15 +121,18 @@ class File extends Node implements IFile {
// verify path of the target
$this->verifyPath();
- /** @var Storage $partStorage */
[$partStorage] = $this->fileView->resolvePath($this->path);
+ if ($partStorage === null) {
+ throw new ServiceUnavailable($this->l10n->t('Failed to get storage for file'));
+ }
$needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1);
- $view = \OC\Files\Filesystem::getView();
+ $view = Filesystem::getView();
if ($needsPartFile) {
+ $transferId = \rand();
// mark file as partial while uploading (ignored by the scanner)
- $partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . rand() . '.part';
+ $partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . $transferId . '.part';
if (!$view->isCreatable($partFilePath) && $view->isUpdatable($this->path)) {
$needsPartFile = false;
@@ -141,10 +148,11 @@ class File extends Node implements IFile {
}
// the part file and target file might be on a different storage in case of a single file storage (e.g. single file share)
- /** @var \OC\Files\Storage\Storage $partStorage */
[$partStorage, $internalPartPath] = $this->fileView->resolvePath($partFilePath);
- /** @var \OC\Files\Storage\Storage $storage */
[$storage, $internalPath] = $this->fileView->resolvePath($this->path);
+ if ($partStorage === null || $storage === null) {
+ throw new ServiceUnavailable($this->l10n->t('Failed to get storage for file'));
+ }
try {
if (!$needsPartFile) {
try {
@@ -178,100 +186,92 @@ class File extends Node implements IFile {
if ($this->request->getHeader('X-HASH') !== '') {
$hash = $this->request->getHeader('X-HASH');
if ($hash === 'all' || $hash === 'md5') {
- $data = HashWrapper::wrap($data, 'md5', function ($hash) {
+ $data = HashWrapper::wrap($data, 'md5', function ($hash): void {
$this->header('X-Hash-MD5: ' . $hash);
});
}
if ($hash === 'all' || $hash === 'sha1') {
- $data = HashWrapper::wrap($data, 'sha1', function ($hash) {
+ $data = HashWrapper::wrap($data, 'sha1', function ($hash): void {
$this->header('X-Hash-SHA1: ' . $hash);
});
}
if ($hash === 'all' || $hash === 'sha256') {
- $data = HashWrapper::wrap($data, 'sha256', function ($hash) {
+ $data = HashWrapper::wrap($data, 'sha256', function ($hash): void {
$this->header('X-Hash-SHA256: ' . $hash);
});
}
}
- if ($partStorage->instanceOfStorage(Storage\IWriteStreamStorage::class)) {
+ $lengthHeader = $this->request->getHeader('content-length');
+ $expected = $lengthHeader !== '' ? (int)$lengthHeader : null;
+
+ if ($partStorage->instanceOfStorage(IWriteStreamStorage::class)) {
$isEOF = false;
- $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF) {
+ $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF): void {
$isEOF = feof($stream);
});
- $result = true;
- $count = -1;
- try {
- $count = $partStorage->writeStream($internalPartPath, $wrappedData);
- } catch (GenericFileException $e) {
- $result = false;
- } catch (BadGateway $e) {
- throw $e;
- }
-
-
- if ($result === false) {
- $result = $isEOF;
- if (is_resource($wrappedData)) {
- $result = feof($wrappedData);
+ $result = is_resource($wrappedData);
+ if ($result) {
+ $count = -1;
+ try {
+ /** @var IWriteStreamStorage $partStorage */
+ $count = $partStorage->writeStream($internalPartPath, $wrappedData, $expected);
+ } catch (GenericFileException $e) {
+ $logger = Server::get(LoggerInterface::class);
+ $logger->error('Error while writing stream to storage: ' . $e->getMessage(), ['exception' => $e, 'app' => 'webdav']);
+ $result = $isEOF;
+ if (is_resource($wrappedData)) {
+ $result = feof($wrappedData);
+ }
}
}
} else {
$target = $partStorage->fopen($internalPartPath, 'wb');
if ($target === false) {
- \OC::$server->get(LoggerInterface::class)->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']);
+ Server::get(LoggerInterface::class)->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']);
// because we have no clue about the cause we can only throw back a 500/Internal Server Error
throw new Exception($this->l10n->t('Could not write file contents'));
}
- [$count, $result] = \OC_Helper::streamCopy($data, $target);
+ [$count, $result] = Files::streamCopy($data, $target, true);
fclose($target);
}
-
- if ($result === false) {
- $expected = -1;
- $lengthHeader = $this->request->getHeader('content-length');
- if ($lengthHeader) {
- $expected = (int)$lengthHeader;
- }
- if ($expected !== 0) {
- throw new Exception(
- $this->l10n->t(
- 'Error while copying file to target location (copied: %1$s, expected filesize: %2$s)',
- [
- $this->l10n->n('%n byte', '%n bytes', $count),
- $this->l10n->n('%n byte', '%n bytes', $expected),
- ],
- )
- );
- }
+ if ($result === false && $expected !== null) {
+ throw new Exception(
+ $this->l10n->t(
+ 'Error while copying file to target location (copied: %1$s, expected filesize: %2$s)',
+ [
+ $this->l10n->n('%n byte', '%n bytes', $count),
+ $this->l10n->n('%n byte', '%n bytes', $expected),
+ ],
+ )
+ );
}
// if content length is sent by client:
// double check if the file was fully received
// compare expected and actual size
- $lengthHeader = $this->request->getHeader('content-length');
- if ($lengthHeader && $this->request->getMethod() === 'PUT') {
- $expected = (int)$lengthHeader;
- if ($count !== $expected) {
- throw new BadRequest(
- $this->l10n->t(
- 'Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side.',
- [
- $this->l10n->n('%n byte', '%n bytes', $expected),
- $this->l10n->n('%n byte', '%n bytes', $count),
- ],
- )
- );
- }
+ if ($expected !== null
+ && $expected !== $count
+ && $this->request->getMethod() === 'PUT'
+ ) {
+ throw new BadRequest(
+ $this->l10n->t(
+ 'Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side.',
+ [
+ $this->l10n->n('%n byte', '%n bytes', $expected),
+ $this->l10n->n('%n byte', '%n bytes', $count),
+ ],
+ )
+ );
}
} catch (\Exception $e) {
if ($e instanceof LockedException) {
- \OC::$server->get(LoggerInterface::class)->debug($e->getMessage(), ['exception' => $e]);
+ Server::get(LoggerInterface::class)->debug($e->getMessage(), ['exception' => $e]);
} else {
- \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
+ Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
}
if ($needsPartFile) {
@@ -312,7 +312,7 @@ class File extends Node implements IFile {
$renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath);
$fileExists = $storage->file_exists($internalPath);
if ($renameOkay === false || $fileExists === false) {
- \OC::$server->get(LoggerInterface::class)->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']);
+ Server::get(LoggerInterface::class)->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']);
throw new Exception($this->l10n->t('Could not rename part file to final file'));
}
} catch (ForbiddenException $ex) {
@@ -379,11 +379,16 @@ class File extends Node implements IFile {
}
private function getPartFileBasePath($path) {
- $partFileInStorage = \OC::$server->getConfig()->getSystemValue('part_file_in_storage', true);
+ $partFileInStorage = Server::get(IConfig::class)->getSystemValue('part_file_in_storage', true);
if ($partFileInStorage) {
- return $path;
+ $filename = basename($path);
+ // hash does not need to be secure but fast and semi unique
+ $hashedFilename = hash('xxh128', $filename);
+ return substr($path, 0, strlen($path) - strlen($filename)) . $hashedFilename;
} else {
- return md5($path); // will place it in the root of the view with a unique name
+ // will place the .part file in the users root directory
+ // therefor we need to make the name (semi) unique - hash does not need to be secure but fast.
+ return hash('xxh128', $path);
}
}
@@ -399,19 +404,19 @@ class File extends Node implements IFile {
$run = true;
if (!$exists) {
- \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create, [
- \OC\Files\Filesystem::signal_param_path => $hookPath,
- \OC\Files\Filesystem::signal_param_run => &$run,
+ \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_create, [
+ Filesystem::signal_param_path => $hookPath,
+ Filesystem::signal_param_run => &$run,
]);
} else {
- \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_update, [
- \OC\Files\Filesystem::signal_param_path => $hookPath,
- \OC\Files\Filesystem::signal_param_run => &$run,
+ \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_update, [
+ Filesystem::signal_param_path => $hookPath,
+ Filesystem::signal_param_run => &$run,
]);
}
- \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_write, [
- \OC\Files\Filesystem::signal_param_path => $hookPath,
- \OC\Files\Filesystem::signal_param_run => &$run,
+ \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_write, [
+ Filesystem::signal_param_path => $hookPath,
+ Filesystem::signal_param_run => &$run,
]);
return $run;
}
@@ -426,16 +431,16 @@ class File extends Node implements IFile {
return;
}
if (!$exists) {
- \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, [
- \OC\Files\Filesystem::signal_param_path => $hookPath
+ \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_create, [
+ Filesystem::signal_param_path => $hookPath
]);
} else {
- \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_update, [
- \OC\Files\Filesystem::signal_param_path => $hookPath
+ \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_update, [
+ Filesystem::signal_param_path => $hookPath
]);
}
- \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_write, [
- \OC\Files\Filesystem::signal_param_path => $hookPath
+ \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_write, [
+ Filesystem::signal_param_path => $hookPath
]);
}
@@ -453,20 +458,25 @@ class File extends Node implements IFile {
// do a if the file did not exist
throw new NotFound();
}
+ $path = ltrim($this->path, '/');
try {
- $res = $this->fileView->fopen(ltrim($this->path, '/'), 'rb');
+ $res = $this->fileView->fopen($path, 'rb');
} catch (\Exception $e) {
$this->convertToSabreException($e);
}
if ($res === false) {
- throw new ServiceUnavailable($this->l10n->t('Could not open file'));
+ if ($this->fileView->file_exists($path)) {
+ throw new ServiceUnavailable($this->l10n->t('Could not open file: %1$s, file does seem to exist', [$path]));
+ } else {
+ throw new ServiceUnavailable($this->l10n->t('Could not open file: %1$s, file doesn\'t seem to exist', [$path]));
+ }
}
// comparing current file size with the one in DB
// if different, fix DB and refresh cache.
if ($this->getSize() !== $this->fileView->filesize($this->getPath())) {
- $logger = \OC::$server->get(LoggerInterface::class);
+ $logger = Server::get(LoggerInterface::class);
$logger->warning('fixing cached size of file id=' . $this->getId());
$this->getFileInfo()->getStorage()->getUpdater()->update($this->getFileInfo()->getInternalPath());
@@ -525,17 +535,16 @@ class File extends Node implements IFile {
if ($this->request->getMethod() === 'PROPFIND') {
return $mimeType;
}
- return \OC::$server->getMimeTypeDetector()->getSecureMimeType($mimeType);
+ return Server::get(IMimeTypeDetector::class)->getSecureMimeType($mimeType);
}
/**
* @return array|bool
*/
public function getDirectDownload() {
- if (\OCP\Server::get(\OCP\App\IAppManager::class)->isEnabledForUser('encryption')) {
+ if (Server::get(IAppManager::class)->isEnabledForUser('encryption')) {
return [];
}
- /** @var \OCP\Files\Storage $storage */
[$storage, $internalPath] = $this->fileView->resolvePath($this->path);
if (is_null($storage)) {
return [];
diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php
index 3b96f67a82b..843383a0452 100644
--- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php
@@ -8,8 +8,16 @@
namespace OCA\DAV\Connector\Sabre;
use OC\AppFramework\Http\Request;
+use OC\FilesMetadata\Model\FilesMetadata;
+use OC\User\NoUserException;
+use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+use OCA\Files_Sharing\External\Mount as SharingExternalMount;
+use OCP\Accounts\IAccountManager;
use OCP\Constants;
use OCP\Files\ForbiddenException;
+use OCP\Files\IFilenameValidator;
+use OCP\Files\InvalidPathException;
+use OCP\Files\Storage\ISharedStorage;
use OCP\Files\StorageNotAvailableException;
use OCP\FilesMetadata\Exceptions\FilesMetadataException;
use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException;
@@ -19,6 +27,7 @@ use OCP\IConfig;
use OCP\IPreview;
use OCP\IRequest;
use OCP\IUserSession;
+use OCP\L10N\IFactory;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\IFile;
@@ -53,11 +62,12 @@ class FilesPlugin extends ServerPlugin {
public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview';
public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type';
public const MOUNT_ROOT_PROPERTYNAME = '{http://nextcloud.org/ns}is-mount-root';
- public const IS_ENCRYPTED_PROPERTYNAME = '{http://nextcloud.org/ns}is-encrypted';
+ public const IS_FEDERATED_PROPERTYNAME = '{http://nextcloud.org/ns}is-federated';
public const METADATA_ETAG_PROPERTYNAME = '{http://nextcloud.org/ns}metadata_etag';
public const UPLOAD_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}upload_time';
public const CREATION_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}creation_time';
public const SHARE_NOTE = '{http://nextcloud.org/ns}note';
+ public const SHARE_HIDE_DOWNLOAD_PROPERTYNAME = '{http://nextcloud.org/ns}hide-download';
public const SUBFOLDER_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-folder-count';
public const SUBFILE_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-file-count';
public const FILE_METADATA_PREFIX = '{http://nextcloud.org/ns}metadata-';
@@ -65,33 +75,28 @@ class FilesPlugin extends ServerPlugin {
/** Reference to main server object */
private ?Server $server = null;
- private Tree $tree;
- private IUserSession $userSession;
/**
- * Whether this is public webdav.
- * If true, some returned information will be stripped off.
+ * @param Tree $tree
+ * @param IConfig $config
+ * @param IRequest $request
+ * @param IPreview $previewManager
+ * @param IUserSession $userSession
+ * @param bool $isPublic Whether this is public WebDAV. If true, some returned information will be stripped off.
+ * @param bool $downloadAttachment
+ * @return void
*/
- private bool $isPublic;
- private bool $downloadAttachment;
- private IConfig $config;
- private IRequest $request;
- private IPreview $previewManager;
-
- public function __construct(Tree $tree,
- IConfig $config,
- IRequest $request,
- IPreview $previewManager,
- IUserSession $userSession,
- bool $isPublic = false,
- bool $downloadAttachment = true) {
- $this->tree = $tree;
- $this->config = $config;
- $this->request = $request;
- $this->userSession = $userSession;
- $this->isPublic = $isPublic;
- $this->downloadAttachment = $downloadAttachment;
- $this->previewManager = $previewManager;
+ public function __construct(
+ private Tree $tree,
+ private IConfig $config,
+ private IRequest $request,
+ private IPreview $previewManager,
+ private IUserSession $userSession,
+ private IFilenameValidator $validator,
+ private IAccountManager $accountManager,
+ private bool $isPublic = false,
+ private bool $downloadAttachment = true,
+ ) {
}
/**
@@ -121,7 +126,7 @@ class FilesPlugin extends ServerPlugin {
$server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME;
$server->protectedProperties[] = self::HAS_PREVIEW_PROPERTYNAME;
$server->protectedProperties[] = self::MOUNT_TYPE_PROPERTYNAME;
- $server->protectedProperties[] = self::IS_ENCRYPTED_PROPERTYNAME;
+ $server->protectedProperties[] = self::IS_FEDERATED_PROPERTYNAME;
$server->protectedProperties[] = self::SHARE_NOTE;
// normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH
@@ -135,40 +140,79 @@ class FilesPlugin extends ServerPlugin {
$this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']);
$this->server->on('afterMethod:GET', [$this,'httpGet']);
$this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']);
- $this->server->on('afterResponse', function ($request, ResponseInterface $response) {
+ $this->server->on('afterResponse', function ($request, ResponseInterface $response): void {
$body = $response->getBody();
if (is_resource($body)) {
fclose($body);
}
});
$this->server->on('beforeMove', [$this, 'checkMove']);
+ $this->server->on('beforeCopy', [$this, 'checkCopy']);
}
/**
- * Plugin that checks if a move can actually be performed.
+ * Plugin that checks if a copy can actually be performed.
*
* @param string $source source path
- * @param string $destination destination path
- * @throws Forbidden
- * @throws NotFound
+ * @param string $target target path
+ * @throws NotFound If the source does not exist
+ * @throws InvalidPath If the target is invalid
*/
- public function checkMove($source, $destination) {
+ public function checkCopy($source, $target): void {
$sourceNode = $this->tree->getNodeForPath($source);
if (!$sourceNode instanceof Node) {
return;
}
- [$sourceDir,] = \Sabre\Uri\split($source);
- [$destinationDir,] = \Sabre\Uri\split($destination);
- if ($sourceDir !== $destinationDir) {
- $sourceNodeFileInfo = $sourceNode->getFileInfo();
- if ($sourceNodeFileInfo === null) {
- throw new NotFound($source . ' does not exist');
+ // Ensure source exists
+ $sourceNodeFileInfo = $sourceNode->getFileInfo();
+ if ($sourceNodeFileInfo === null) {
+ throw new NotFound($source . ' does not exist');
+ }
+ // Ensure the target name is valid
+ try {
+ [$targetPath, $targetName] = \Sabre\Uri\split($target);
+ $this->validator->validateFilename($targetName);
+ } catch (InvalidPathException $e) {
+ throw new InvalidPath($e->getMessage(), false);
+ }
+ // Ensure the target path is valid
+ $segments = array_slice(explode('/', $targetPath), 2);
+ foreach ($segments as $segment) {
+ if ($this->validator->isFilenameValid($segment) === false) {
+ $l = \OCP\Server::get(IFactory::class)->get('dav');
+ throw new InvalidPath($l->t('Invalid target path'));
}
+ }
+ }
- if (!$sourceNodeFileInfo->isDeletable()) {
- throw new Forbidden($source . " cannot be deleted");
- }
+ /**
+ * Plugin that checks if a move can actually be performed.
+ *
+ * @param string $source source path
+ * @param string $target target path
+ * @throws Forbidden If the source is not deletable
+ * @throws NotFound If the source does not exist
+ * @throws InvalidPath If the target name is invalid
+ */
+ public function checkMove(string $source, string $target): void {
+ $sourceNode = $this->tree->getNodeForPath($source);
+ if (!$sourceNode instanceof Node) {
+ return;
+ }
+
+ // First check copyable (move only needs additional delete permission)
+ $this->checkCopy($source, $target);
+
+ // The source needs to be deletable for moving
+ $sourceNodeFileInfo = $sourceNode->getFileInfo();
+ if (!$sourceNodeFileInfo->isDeletable()) {
+ throw new Forbidden($source . ' cannot be deleted');
+ }
+
+ // The source is not allowed to be the parent of the target
+ if (str_starts_with($source, $target . '/')) {
+ throw new Forbidden($source . ' cannot be moved to it\'s parent');
}
}
@@ -213,8 +257,8 @@ class FilesPlugin extends ServerPlugin {
// adds a 'Content-Disposition: attachment' header in case no disposition
// header has been set before
- if ($this->downloadAttachment &&
- $response->getHeader('Content-Disposition') === null) {
+ if ($this->downloadAttachment
+ && $response->getHeader('Content-Disposition') === null) {
$filename = $node->getName();
if ($this->request->isUserAgent(
[
@@ -229,7 +273,7 @@ class FilesPlugin extends ServerPlugin {
}
}
- if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
+ if ($node instanceof File) {
//Add OC-Checksum header
$checksum = $node->getChecksum();
if ($checksum !== null && $checksum !== '') {
@@ -249,7 +293,7 @@ class FilesPlugin extends ServerPlugin {
public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) {
$httpRequest = $this->server->httpRequest;
- if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
+ if ($node instanceof Node) {
/**
* This was disabled, because it made dir listing throw an exception,
* so users were unable to navigate into folders where one subitem
@@ -320,9 +364,32 @@ class FilesPlugin extends ServerPlugin {
$owner = $node->getOwner();
if (!$owner) {
return null;
- } else {
+ }
+
+ // Get current user to see if we're in a public share or not
+ $user = $this->userSession->getUser();
+
+ // If the user is logged in, we can return the display name
+ if ($user !== null) {
+ return $owner->getDisplayName();
+ }
+
+ // Check if the user published their display name
+ try {
+ $ownerAccount = $this->accountManager->getAccount($owner);
+ } catch (NoUserException) {
+ // do not lock process if owner is not local
+ return null;
+ }
+
+ $ownerNameProperty = $ownerAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME);
+
+ // Since we are not logged in, we need to have at least the published scope
+ if ($ownerNameProperty->getScope() === IAccountManager::SCOPE_PUBLISHED) {
return $owner->getDisplayName();
}
+
+ return null;
});
$propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) {
@@ -345,17 +412,27 @@ class FilesPlugin extends ServerPlugin {
return $node->getNode()->getInternalPath() === '' ? 'true' : 'false';
});
- $propFind->handle(self::SHARE_NOTE, function () use ($node, $httpRequest): ?string {
+ $propFind->handle(self::SHARE_NOTE, function () use ($node): ?string {
$user = $this->userSession->getUser();
- if ($user === null) {
- return null;
- }
return $node->getNoteFromShare(
- $user->getUID()
+ $user?->getUID()
);
});
- $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () use ($node) {
+ $propFind->handle(self::SHARE_HIDE_DOWNLOAD_PROPERTYNAME, function () use ($node) {
+ $storage = $node->getNode()->getStorage();
+ if ($storage->instanceOfStorage(ISharedStorage::class)) {
+ /** @var ISharedStorage $storage */
+ return match($storage->getShare()->getHideDownload()) {
+ true => 'true',
+ false => 'false',
+ };
+ } else {
+ return null;
+ }
+ });
+
+ $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () {
return $this->config->getSystemValue('data-fingerprint', '');
});
$propFind->handle(self::CREATIONDATE_PROPERTYNAME, function () use ($node) {
@@ -386,9 +463,14 @@ class FilesPlugin extends ServerPlugin {
$propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function () use ($node) {
return $node->getName();
});
+
+ $propFind->handle(self::IS_FEDERATED_PROPERTYNAME, function () use ($node) {
+ return $node->getFileInfo()->getMountPoint()
+ instanceof SharingExternalMount;
+ });
}
- if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
+ if ($node instanceof File) {
$propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node) {
try {
$directDownloadUrl = $node->getDirectDownload();
@@ -422,10 +504,6 @@ class FilesPlugin extends ServerPlugin {
return $node->getSize();
});
- $propFind->handle(self::IS_ENCRYPTED_PROPERTYNAME, function () use ($node) {
- return $node->getFileInfo()->isEncrypted() ? '1' : '0';
- });
-
$requestProperties = $propFind->getRequestedProperties();
if (in_array(self::SUBFILE_COUNT_PROPERTYNAME, $requestProperties, true)
@@ -463,8 +541,8 @@ class FilesPlugin extends ServerPlugin {
$ocmPermissions[] = 'read';
}
- if (($ncPermissions & Constants::PERMISSION_CREATE) ||
- ($ncPermissions & Constants::PERMISSION_UPDATE)) {
+ if (($ncPermissions & Constants::PERMISSION_CREATE)
+ || ($ncPermissions & Constants::PERMISSION_UPDATE)) {
$ocmPermissions[] = 'write';
}
@@ -481,7 +559,7 @@ class FilesPlugin extends ServerPlugin {
*/
public function handleUpdateProperties($path, PropPatch $propPatch) {
$node = $this->tree->getNodeForPath($path);
- if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
+ if (!($node instanceof Node)) {
return;
}
@@ -510,7 +588,7 @@ class FilesPlugin extends ServerPlugin {
if (empty($time)) {
return false;
}
- $node->setCreationTime((int) $time);
+ $node->setCreationTime((int)$time);
return true;
});
@@ -552,7 +630,9 @@ class FilesPlugin extends ServerPlugin {
$propPatch->handle(
$mutation,
function (mixed $value) use ($accessRight, $knownMetadata, $node, $mutation, $filesMetadataManager): bool {
+ /** @var FilesMetadata $metadata */
$metadata = $filesMetadataManager->getMetadata((int)$node->getFileId(), true);
+ $metadata->setStorageId($node->getNode()->getStorage()->getCache()->getNumericStorageId());
$metadataKey = substr($mutation, strlen(self::FILE_METADATA_PREFIX));
// confirm metadata key is editable via PROPPATCH
@@ -560,6 +640,12 @@ class FilesPlugin extends ServerPlugin {
throw new FilesMetadataException('you do not have enough rights to update \'' . $metadataKey . '\' on this node');
}
+ if ($value === null) {
+ $metadata->unset($metadataKey);
+ $filesMetadataManager->saveMetadata($metadata);
+ return true;
+ }
+
// If the metadata is unknown, it defaults to string.
try {
$type = $knownMetadata->getType($metadataKey);
@@ -633,8 +719,6 @@ class FilesPlugin extends ServerPlugin {
return IMetadataValueWrapper::EDIT_REQ_READ_PERMISSION;
}
-
-
/**
* @param string $filePath
* @param ?\Sabre\DAV\INode $node
@@ -643,15 +727,15 @@ class FilesPlugin extends ServerPlugin {
*/
public function sendFileIdHeader($filePath, ?\Sabre\DAV\INode $node = null) {
// we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
- if (!$this->server->tree->nodeExists($filePath)) {
- return;
- }
- $node = $this->server->tree->getNodeForPath($filePath);
- if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
- $fileId = $node->getFileId();
- if (!is_null($fileId)) {
- $this->server->httpResponse->setHeader('OC-FileId', $fileId);
+ try {
+ $node = $this->server->tree->getNodeForPath($filePath);
+ if ($node instanceof Node) {
+ $fileId = $node->getFileId();
+ if (!is_null($fileId)) {
+ $this->server->httpResponse->setHeader('OC-FileId', $fileId);
+ }
}
+ } catch (NotFound) {
}
}
}
diff --git a/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php
index 0a978e099dc..b59d1373af5 100644
--- a/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php
@@ -8,7 +8,9 @@
namespace OCA\DAV\Connector\Sabre;
use OC\Files\View;
+use OCA\Circles\Api\v1\Circles;
use OCP\App\IAppManager;
+use OCP\AppFramework\Http;
use OCP\Files\Folder;
use OCP\Files\Node as INode;
use OCP\IGroupManager;
@@ -41,55 +43,8 @@ class FilesReportPlugin extends ServerPlugin {
private $server;
/**
- * @var Tree
- */
- private $tree;
-
- /**
- * @var View
- */
- private $fileView;
-
- /**
- * @var ISystemTagManager
- */
- private $tagManager;
-
- /**
- * @var ISystemTagObjectMapper
- */
- private $tagMapper;
-
- /**
- * Manager for private tags
- *
- * @var ITagManager
- */
- private $fileTagger;
-
- /**
- * @var IUserSession
- */
- private $userSession;
-
- /**
- * @var IGroupManager
- */
- private $groupManager;
-
- /**
- * @var Folder
- */
- private $userFolder;
-
- /**
- * @var IAppManager
- */
- private $appManager;
-
- /**
* @param Tree $tree
- * @param View $view
+ * @param View $fileView
* @param ISystemTagManager $tagManager
* @param ISystemTagObjectMapper $tagMapper
* @param ITagManager $fileTagger manager for private tags
@@ -98,25 +53,20 @@ class FilesReportPlugin extends ServerPlugin {
* @param Folder $userFolder
* @param IAppManager $appManager
*/
- public function __construct(Tree $tree,
- View $view,
- ISystemTagManager $tagManager,
- ISystemTagObjectMapper $tagMapper,
- ITagManager $fileTagger,
- IUserSession $userSession,
- IGroupManager $groupManager,
- Folder $userFolder,
- IAppManager $appManager
+ public function __construct(
+ private Tree $tree,
+ private View $fileView,
+ private ISystemTagManager $tagManager,
+ private ISystemTagObjectMapper $tagMapper,
+ /**
+ * Manager for private tags
+ */
+ private ITagManager $fileTagger,
+ private IUserSession $userSession,
+ private IGroupManager $groupManager,
+ private Folder $userFolder,
+ private IAppManager $appManager,
) {
- $this->tree = $tree;
- $this->fileView = $view;
- $this->tagManager = $tagManager;
- $this->tagMapper = $tagMapper;
- $this->fileTagger = $fileTagger;
- $this->userSession = $userSession;
- $this->groupManager = $groupManager;
- $this->userFolder = $userFolder;
- $this->appManager = $appManager;
}
/**
@@ -206,7 +156,7 @@ class FilesReportPlugin extends ServerPlugin {
// to user backends. I.e. the final result may return more results than requested.
$resultNodes = $this->processFilterRulesForFileNodes($filterRules, $limit ?? null, $offset ?? null);
} catch (TagNotFoundException $e) {
- throw new PreconditionFailed('Cannot filter by non-existing tag', 0, $e);
+ throw new PreconditionFailed('Cannot filter by non-existing tag');
}
$results = [];
@@ -234,7 +184,7 @@ class FilesReportPlugin extends ServerPlugin {
new MultiStatus($responses)
);
- $this->server->httpResponse->setStatus(207);
+ $this->server->httpResponse->setStatus(Http::STATUS_MULTI_STATUS);
$this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
$this->server->httpResponse->setBody($xml);
@@ -355,7 +305,7 @@ class FilesReportPlugin extends ServerPlugin {
if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
return [];
}
- return \OCA\Circles\Api\v1\Circles::getFilesForCircles($circlesIds);
+ return Circles::getFilesForCircles($circlesIds);
}
@@ -363,7 +313,7 @@ class FilesReportPlugin extends ServerPlugin {
* Prepare propfind response for the given nodes
*
* @param string $filesUri $filesUri URI leading to root of the files URI,
- * with a leading slash but no trailing slash
+ * with a leading slash but no trailing slash
* @param string[] $requestedProps requested properties
* @param Node[] nodes nodes for which to fetch and prepare responses
* @return Response[]
@@ -410,7 +360,7 @@ class FilesReportPlugin extends ServerPlugin {
$results = [];
foreach ($fileIds as $fileId) {
- $entry = $folder->getFirstNodeById($fileId);
+ $entry = $folder->getFirstNodeById((int)$fileId);
if ($entry) {
$results[] = $this->wrapNode($entry);
}
@@ -419,7 +369,7 @@ class FilesReportPlugin extends ServerPlugin {
return $results;
}
- protected function wrapNode(\OCP\Files\Node $node): File|Directory {
+ protected function wrapNode(INode $node): File|Directory {
if ($node instanceof \OCP\Files\File) {
return new File($this->fileView, $node);
} else {
diff --git a/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php b/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php
index 8d23a05a3ec..d5ab7f09dfa 100644
--- a/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php
+++ b/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php
@@ -16,10 +16,7 @@ use Sabre\DAV\ServerPlugin;
class MaintenancePlugin extends ServerPlugin {
- /** @var IConfig */
- private $config;
-
- /** @var \OCP\IL10N */
+ /** @var IL10N */
private $l10n;
/**
@@ -32,8 +29,10 @@ class MaintenancePlugin extends ServerPlugin {
/**
* @param IConfig $config
*/
- public function __construct(IConfig $config, IL10N $l10n) {
- $this->config = $config;
+ public function __construct(
+ private IConfig $config,
+ IL10N $l10n,
+ ) {
$this->l10n = \OC::$server->getL10N('dav');
}
diff --git a/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php b/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php
index efed6ce09f8..e18ef58149a 100644
--- a/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php
+++ b/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-only
diff --git a/apps/dav/lib/Connector/Sabre/Node.php b/apps/dav/lib/Connector/Sabre/Node.php
index 63e453c86af..505e6b5eda4 100644
--- a/apps/dav/lib/Connector/Sabre/Node.php
+++ b/apps/dav/lib/Connector/Sabre/Node.php
@@ -12,20 +12,20 @@ use OC\Files\Node\File;
use OC\Files\Node\Folder;
use OC\Files\View;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+use OCP\Constants;
use OCP\Files\DavUtil;
use OCP\Files\FileInfo;
+use OCP\Files\InvalidPathException;
use OCP\Files\IRootFolder;
+use OCP\Files\NotFoundException;
+use OCP\Files\Storage\ISharedStorage;
use OCP\Files\StorageNotAvailableException;
+use OCP\Server;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
abstract class Node implements \Sabre\DAV\INode {
/**
- * @var View
- */
- protected $fileView;
-
- /**
* The path to the current node
*
* @var string
@@ -51,21 +51,24 @@ abstract class Node implements \Sabre\DAV\INode {
/**
* Sets up the node, expects a full path name
*/
- public function __construct(View $view, FileInfo $info, ?IManager $shareManager = null) {
- $this->fileView = $view;
+ public function __construct(
+ protected View $fileView,
+ FileInfo $info,
+ ?IManager $shareManager = null,
+ ) {
$this->path = $this->fileView->getRelativePath($info->getPath());
$this->info = $info;
if ($shareManager) {
$this->shareManager = $shareManager;
} else {
- $this->shareManager = \OC::$server->getShareManager();
+ $this->shareManager = Server::get(\OCP\Share\IManager::class);
}
if ($info instanceof Folder || $info instanceof File) {
$this->node = $info;
} else {
// The Node API assumes that the view passed doesn't have a fake root
- $rootView = \OC::$server->get(View::class);
- $root = \OC::$server->get(IRootFolder::class);
+ $rootView = Server::get(View::class);
+ $root = Server::get(IRootFolder::class);
if ($info->getType() === FileInfo::TYPE_FOLDER) {
$this->node = new Folder($root, $rootView, $this->fileView->getAbsolutePath($this->path), $info);
} else {
@@ -77,11 +80,11 @@ abstract class Node implements \Sabre\DAV\INode {
protected function refreshInfo(): void {
$info = $this->fileView->getFileInfo($this->path);
if ($info === false) {
- throw new \Sabre\DAV\Exception('Failed to get fileinfo for '. $this->path);
+ throw new \Sabre\DAV\Exception('Failed to get fileinfo for ' . $this->path);
}
$this->info = $info;
- $root = \OC::$server->get(IRootFolder::class);
- $rootView = \OC::$server->get(View::class);
+ $root = Server::get(IRootFolder::class);
+ $rootView = Server::get(View::class);
if ($this->info->getType() === FileInfo::TYPE_FOLDER) {
$this->node = new Folder($root, $rootView, $this->path, $this->info);
} else {
@@ -115,21 +118,21 @@ abstract class Node implements \Sabre\DAV\INode {
* @throws \Sabre\DAV\Exception\Forbidden
*/
public function setName($name) {
- // rename is only allowed if the update privilege is granted
- if (!($this->info->isUpdateable() || ($this->info->getMountPoint() instanceof MoveableMount && $this->info->getInternalPath() === ''))) {
+ // rename is only allowed if the delete privilege is granted
+ // (basically rename is a copy with delete of the original node)
+ if (!($this->info->isDeletable() || ($this->info->getMountPoint() instanceof MoveableMount && $this->info->getInternalPath() === ''))) {
throw new \Sabre\DAV\Exception\Forbidden();
}
[$parentPath,] = \Sabre\Uri\split($this->path);
[, $newName] = \Sabre\Uri\split($name);
+ $newPath = $parentPath . '/' . $newName;
// verify path of the target
- $this->verifyPath();
-
- $newPath = $parentPath . '/' . $newName;
+ $this->verifyPath($newPath);
if (!$this->fileView->rename($this->path, $newPath)) {
- throw new \Sabre\DAV\Exception('Failed to rename '. $this->path . ' to ' . $newPath);
+ throw new \Sabre\DAV\Exception('Failed to rename ' . $this->path . ' to ' . $newPath);
}
$this->path = $newPath;
@@ -261,8 +264,8 @@ abstract class Node implements \Sabre\DAV\INode {
$storage = null;
}
- if ($storage && $storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) {
- /** @var \OCA\Files_Sharing\SharedStorage $storage */
+ if ($storage && $storage->instanceOfStorage(ISharedStorage::class)) {
+ /** @var ISharedStorage $storage */
$permissions = (int)$storage->getShare()->getPermissions();
} else {
$permissions = $this->info->getPermissions();
@@ -280,15 +283,15 @@ abstract class Node implements \Sabre\DAV\INode {
}
if (!$mountpoint->getOption('readonly', false) && $mountpointpath === $this->info->getPath()) {
- $permissions |= \OCP\Constants::PERMISSION_DELETE | \OCP\Constants::PERMISSION_UPDATE;
+ $permissions |= Constants::PERMISSION_DELETE | Constants::PERMISSION_UPDATE;
}
}
/*
* Files can't have create or delete permissions
*/
- if ($this->info->getType() === \OCP\Files\FileInfo::TYPE_FILE) {
- $permissions &= ~(\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_DELETE);
+ if ($this->info->getType() === FileInfo::TYPE_FILE) {
+ $permissions &= ~(Constants::PERMISSION_CREATE | Constants::PERMISSION_DELETE);
}
return $permissions;
@@ -298,16 +301,15 @@ abstract class Node implements \Sabre\DAV\INode {
* @return array
*/
public function getShareAttributes(): array {
- $attributes = [];
-
try {
- $storage = $this->info->getStorage();
- } catch (StorageNotAvailableException $e) {
- $storage = null;
+ $storage = $this->node->getStorage();
+ } catch (NotFoundException $e) {
+ return [];
}
- if ($storage && $storage->instanceOfStorage(\OCA\Files_Sharing\SharedStorage::class)) {
- /** @var \OCA\Files_Sharing\SharedStorage $storage */
+ $attributes = [];
+ if ($storage->instanceOfStorage(ISharedStorage::class)) {
+ /** @var ISharedStorage $storage */
$attributes = $storage->getShare()->getAttributes();
if ($attributes === null) {
return [];
@@ -319,29 +321,24 @@ abstract class Node implements \Sabre\DAV\INode {
return $attributes;
}
- /**
- * @param string $user
- * @return string
- */
- public function getNoteFromShare($user) {
- if ($user === null) {
- return '';
+ public function getNoteFromShare(?string $user): ?string {
+ try {
+ $storage = $this->node->getStorage();
+ } catch (NotFoundException) {
+ return null;
}
- // Retrieve note from the share object already loaded into
- // memory, to avoid additional database queries.
- $storage = $this->getNode()->getStorage();
- if (!$storage->instanceOfStorage(\OCA\Files_Sharing\SharedStorage::class)) {
- return '';
+ if ($storage->instanceOfStorage(ISharedStorage::class)) {
+ /** @var ISharedStorage $storage */
+ $share = $storage->getShare();
+ if ($user === $share->getShareOwner()) {
+ // Note is only for recipient not the owner
+ return null;
+ }
+ return $share->getNote();
}
- /** @var \OCA\Files_Sharing\SharedStorage $storage */
- $share = $storage->getShare();
- $note = $share->getNote();
- if ($share->getShareOwner() !== $user) {
- return $note;
- }
- return '';
+ return null;
}
/**
@@ -355,11 +352,14 @@ abstract class Node implements \Sabre\DAV\INode {
return $this->info->getOwner();
}
- protected function verifyPath() {
+ protected function verifyPath(?string $path = null): void {
try {
- $fileName = basename($this->info->getPath());
- $this->fileView->verifyPath($this->path, $fileName);
- } catch (\OCP\Files\InvalidPathException $ex) {
+ $path = $path ?? $this->info->getPath();
+ $this->fileView->verifyPath(
+ dirname($path),
+ basename($path),
+ );
+ } catch (InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage());
}
}
@@ -393,7 +393,7 @@ abstract class Node implements \Sabre\DAV\INode {
return $this->node;
}
- protected function sanitizeMtime($mtimeFromRequest) {
+ protected function sanitizeMtime(string $mtimeFromRequest): int {
return MtimeSanitizer::sanitizeMtime($mtimeFromRequest);
}
}
diff --git a/apps/dav/lib/Connector/Sabre/ObjectTree.php b/apps/dav/lib/Connector/Sabre/ObjectTree.php
index 7ac5b476154..bfbdfb33db0 100644
--- a/apps/dav/lib/Connector/Sabre/ObjectTree.php
+++ b/apps/dav/lib/Connector/Sabre/ObjectTree.php
@@ -9,10 +9,14 @@ namespace OCA\DAV\Connector\Sabre;
use OC\Files\FileInfo;
use OC\Files\Storage\FailedStorage;
+use OC\Files\Storage\Storage;
+use OC\Files\View;
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
use OCP\Files\ForbiddenException;
+use OCP\Files\InvalidPathException;
+use OCP\Files\Mount\IMountManager;
use OCP\Files\StorageInvalidException;
use OCP\Files\StorageNotAvailableException;
use OCP\Lock\LockedException;
@@ -20,12 +24,12 @@ use OCP\Lock\LockedException;
class ObjectTree extends CachingTree {
/**
- * @var \OC\Files\View
+ * @var View
*/
protected $fileView;
/**
- * @var \OCP\Files\Mount\IMountManager
+ * @var IMountManager
*/
protected $mountManager;
@@ -37,10 +41,10 @@ class ObjectTree extends CachingTree {
/**
* @param \Sabre\DAV\INode $rootNode
- * @param \OC\Files\View $view
- * @param \OCP\Files\Mount\IMountManager $mountManager
+ * @param View $view
+ * @param IMountManager $mountManager
*/
- public function init(\Sabre\DAV\INode $rootNode, \OC\Files\View $view, \OCP\Files\Mount\IMountManager $mountManager) {
+ public function init(\Sabre\DAV\INode $rootNode, View $view, IMountManager $mountManager) {
$this->rootNode = $rootNode;
$this->fileView = $view;
$this->mountManager = $mountManager;
@@ -70,7 +74,7 @@ class ObjectTree extends CachingTree {
if ($path) {
try {
$this->fileView->verifyPath($path, basename($path));
- } catch (\OCP\Files\InvalidPathException $ex) {
+ } catch (InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage());
}
}
@@ -88,7 +92,7 @@ class ObjectTree extends CachingTree {
$internalPath = $mount->getInternalPath($absPath);
if ($storage && $storage->file_exists($internalPath)) {
/**
- * @var \OC\Files\Storage\Storage $storage
+ * @var Storage $storage
*/
// get data directly
$data = $storage->getMetaData($internalPath);
@@ -120,9 +124,9 @@ class ObjectTree extends CachingTree {
}
if ($info->getType() === 'dir') {
- $node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this);
+ $node = new Directory($this->fileView, $info, $this);
} else {
- $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info);
+ $node = new File($this->fileView, $info);
}
$this->cache[$path] = $node;
@@ -169,7 +173,7 @@ class ObjectTree extends CachingTree {
[$destinationDir, $destinationName] = \Sabre\Uri\split($destinationPath);
try {
$this->fileView->verifyPath($destinationDir, $destinationName);
- } catch (\OCP\Files\InvalidPathException $ex) {
+ } catch (InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage());
}
diff --git a/apps/dav/lib/Connector/Sabre/Principal.php b/apps/dav/lib/Connector/Sabre/Principal.php
index f09ff4def64..d6ea9fd887d 100644
--- a/apps/dav/lib/Connector/Sabre/Principal.php
+++ b/apps/dav/lib/Connector/Sabre/Principal.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
@@ -7,7 +8,9 @@
namespace OCA\DAV\Connector\Sabre;
use OC\KnownUser\KnownUserService;
+use OCA\Circles\Api\v1\Circles;
use OCA\Circles\Exceptions\CircleNotFoundException;
+use OCA\Circles\Model\Circle;
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCA\DAV\Traits\PrincipalProxyTrait;
use OCP\Accounts\IAccountManager;
@@ -30,24 +33,6 @@ use Sabre\DAVACL\PrincipalBackend\BackendInterface;
class Principal implements BackendInterface {
- /** @var IUserManager */
- private $userManager;
-
- /** @var IGroupManager */
- private $groupManager;
-
- /** @var IAccountManager */
- private $accountManager;
-
- /** @var IShareManager */
- private $shareManager;
-
- /** @var IUserSession */
- private $userSession;
-
- /** @var IAppManager */
- private $appManager;
-
/** @var string */
private $principalPrefix;
@@ -57,40 +42,25 @@ class Principal implements BackendInterface {
/** @var bool */
private $hasCircles;
- /** @var ProxyMapper */
- private $proxyMapper;
-
/** @var KnownUserService */
private $knownUserService;
- /** @var IConfig */
- private $config;
- /** @var IFactory */
- private $languageFactory;
-
- public function __construct(IUserManager $userManager,
- IGroupManager $groupManager,
- IAccountManager $accountManager,
- IShareManager $shareManager,
- IUserSession $userSession,
- IAppManager $appManager,
- ProxyMapper $proxyMapper,
+ public function __construct(
+ private IUserManager $userManager,
+ private IGroupManager $groupManager,
+ private IAccountManager $accountManager,
+ private IShareManager $shareManager,
+ private IUserSession $userSession,
+ private IAppManager $appManager,
+ private ProxyMapper $proxyMapper,
KnownUserService $knownUserService,
- IConfig $config,
- IFactory $languageFactory,
- string $principalPrefix = 'principals/users/') {
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
- $this->accountManager = $accountManager;
- $this->shareManager = $shareManager;
- $this->userSession = $userSession;
- $this->appManager = $appManager;
+ private IConfig $config,
+ private IFactory $languageFactory,
+ string $principalPrefix = 'principals/users/',
+ ) {
$this->principalPrefix = trim($principalPrefix, '/');
$this->hasGroups = $this->hasCircles = ($principalPrefix === 'principals/users/');
- $this->proxyMapper = $proxyMapper;
$this->knownUserService = $knownUserService;
- $this->config = $config;
- $this->languageFactory = $languageFactory;
}
use PrincipalProxyTrait {
@@ -180,7 +150,12 @@ class Principal implements BackendInterface {
} elseif ($prefix === 'principals/system') {
return [
'uri' => 'principals/system/' . $name,
- '{DAV:}displayname' => $this->languageFactory->get('dav')->t("Accounts"),
+ '{DAV:}displayname' => $this->languageFactory->get('dav')->t('Accounts'),
+ ];
+ } elseif ($prefix === 'principals/shares') {
+ return [
+ 'uri' => 'principals/shares/' . $name,
+ '{DAV:}displayname' => $name,
];
}
return null;
@@ -211,6 +186,9 @@ class Principal implements BackendInterface {
if ($this->hasGroups || $needGroups) {
$userGroups = $this->groupManager->getUserGroups($user);
foreach ($userGroups as $userGroup) {
+ if ($userGroup->hideFromCollaboration()) {
+ continue;
+ }
$groups[] = 'principals/groups/' . urlencode($userGroup->getGID());
}
}
@@ -530,7 +508,7 @@ class Principal implements BackendInterface {
}
try {
- $circle = \OCA\Circles\Api\v1\Circles::detailsCircle($circleUniqueId, true);
+ $circle = Circles::detailsCircle($circleUniqueId, true);
} catch (QueryException $ex) {
return null;
} catch (CircleNotFoundException $ex) {
@@ -555,7 +533,7 @@ class Principal implements BackendInterface {
* @param string $principal
* @return array
* @throws Exception
- * @throws \OCP\AppFramework\QueryException
+ * @throws QueryException
* @suppress PhanUndeclaredClassMethod
*/
public function getCircleMembership($principal):array {
@@ -570,10 +548,10 @@ class Principal implements BackendInterface {
throw new Exception('Principal not found');
}
- $circles = \OCA\Circles\Api\v1\Circles::joinedCircles($name, true);
+ $circles = Circles::joinedCircles($name, true);
$circles = array_map(function ($circle) {
- /** @var \OCA\Circles\Model\Circle $circle */
+ /** @var Circle $circle */
return 'principals/circles/' . urlencode($circle->getSingleId());
}, $circles);
diff --git a/apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php b/apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php
new file mode 100644
index 00000000000..130d4562146
--- /dev/null
+++ b/apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php
@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types = 1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Connector\Sabre;
+
+use Sabre\DAV\Server as SabreServer;
+use Sabre\DAV\ServerPlugin;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+
+/**
+ * This plugin runs after requests and logs an error if a plugin is detected
+ * to be doing too many SQL requests.
+ */
+class PropFindMonitorPlugin extends ServerPlugin {
+
+ /**
+ * A Plugin can scan up to this amount of nodes without an error being
+ * reported.
+ */
+ public const THRESHOLD_NODES = 50;
+
+ /**
+ * A plugin can use up to this amount of queries per node.
+ */
+ public const THRESHOLD_QUERY_FACTOR = 1;
+
+ private SabreServer $server;
+
+ public function initialize(SabreServer $server): void {
+ $this->server = $server;
+ $this->server->on('afterResponse', [$this, 'afterResponse']);
+ }
+
+ public function afterResponse(
+ RequestInterface $request,
+ ResponseInterface $response): void {
+ if (!$this->server instanceof Server) {
+ return;
+ }
+
+ $pluginQueries = $this->server->getPluginQueries();
+ if (empty($pluginQueries)) {
+ return;
+ }
+ $maxDepth = max(0, ...array_keys($pluginQueries));
+ // entries at the top are usually not interesting
+ unset($pluginQueries[$maxDepth]);
+
+ $logger = $this->server->getLogger();
+ foreach ($pluginQueries as $depth => $propFinds) {
+ foreach ($propFinds as $pluginName => $propFind) {
+ [
+ 'queries' => $queries,
+ 'nodes' => $nodes
+ ] = $propFind;
+ if ($queries === 0 || $nodes > $queries || $nodes < self::THRESHOLD_NODES
+ || $queries < $nodes * self::THRESHOLD_QUERY_FACTOR) {
+ continue;
+ }
+ $logger->error(
+ '{name} scanned {scans} nodes with {count} queries in depth {depth}/{maxDepth}. This is bad for performance, please report to the plugin developer!', [
+ 'name' => $pluginName,
+ 'scans' => $nodes,
+ 'count' => $queries,
+ 'depth' => $depth,
+ 'maxDepth' => $maxDepth,
+ ]
+ );
+ }
+ }
+ }
+}
diff --git a/apps/dav/lib/Connector/Sabre/PublicAuth.php b/apps/dav/lib/Connector/Sabre/PublicAuth.php
index 3e2cd81a800..2ca1c25e2f6 100644
--- a/apps/dav/lib/Connector/Sabre/PublicAuth.php
+++ b/apps/dav/lib/Connector/Sabre/PublicAuth.php
@@ -11,9 +11,12 @@ declare(strict_types=1);
namespace OCA\DAV\Connector\Sabre;
+use OCP\Defaults;
use OCP\IRequest;
use OCP\ISession;
+use OCP\IURLGenerator;
use OCP\Security\Bruteforce\IThrottler;
+use OCP\Security\Bruteforce\MaxDelayReached;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IShare;
@@ -21,6 +24,7 @@ use Psr\Log\LoggerInterface;
use Sabre\DAV\Auth\Backend\AbstractBasic;
use Sabre\DAV\Exception\NotAuthenticated;
use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\Exception\PreconditionFailed;
use Sabre\DAV\Exception\ServiceUnavailable;
use Sabre\HTTP;
use Sabre\HTTP\RequestInterface;
@@ -36,40 +40,33 @@ class PublicAuth extends AbstractBasic {
public const DAV_AUTHENTICATED = 'public_link_authenticated';
private ?IShare $share = null;
- private IManager $shareManager;
- private ISession $session;
- private IRequest $request;
- private IThrottler $throttler;
- private LoggerInterface $logger;
-
- public function __construct(IRequest $request,
- IManager $shareManager,
- ISession $session,
- IThrottler $throttler,
- LoggerInterface $logger) {
- $this->request = $request;
- $this->shareManager = $shareManager;
- $this->session = $session;
- $this->throttler = $throttler;
- $this->logger = $logger;
+ public function __construct(
+ private IRequest $request,
+ private IManager $shareManager,
+ private ISession $session,
+ private IThrottler $throttler,
+ private LoggerInterface $logger,
+ private IURLGenerator $urlGenerator,
+ ) {
// setup realm
- $defaults = new \OCP\Defaults();
+ $defaults = new Defaults();
$this->realm = $defaults->getName();
}
/**
- * @param RequestInterface $request
- * @param ResponseInterface $response
- *
- * @return array
* @throws NotAuthenticated
+ * @throws MaxDelayReached
* @throws ServiceUnavailable
*/
public function check(RequestInterface $request, ResponseInterface $response): array {
try {
$this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION);
+ if (count($_COOKIE) > 0 && !$this->request->passesStrictCookieCheck() && $this->getShare()->getPassword() !== null) {
+ throw new PreconditionFailed('Strict cookie check failed');
+ }
+
$auth = new HTTP\Auth\Basic(
$this->realm,
$request,
@@ -83,7 +80,17 @@ class PublicAuth extends AbstractBasic {
}
return $this->checkToken();
- } catch (NotAuthenticated $e) {
+ } catch (NotAuthenticated|MaxDelayReached $e) {
+ $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
+ throw $e;
+ } catch (PreconditionFailed $e) {
+ $response->setHeader(
+ 'Location',
+ $this->urlGenerator->linkToRoute(
+ 'files_sharing.share.showShare',
+ [ 'token' => $this->getToken() ],
+ ),
+ );
throw $e;
} catch (\Exception $e) {
$class = get_class($e);
@@ -95,14 +102,13 @@ class PublicAuth extends AbstractBasic {
/**
* Extract token from request url
- * @return string
* @throws NotFound
*/
private function getToken(): string {
$path = $this->request->getPathInfo() ?: '';
// ['', 'dav', 'files', 'token']
$splittedPath = explode('/', $path);
-
+
if (count($splittedPath) < 4 || $splittedPath[3] === '') {
throw new NotFound();
}
@@ -112,7 +118,7 @@ class PublicAuth extends AbstractBasic {
/**
* Check token validity
- * @return array
+ *
* @throws NotFound
* @throws NotAuthenticated
*/
@@ -160,15 +166,13 @@ class PublicAuth extends AbstractBasic {
protected function validateUserPass($username, $password) {
$this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION);
- $token = $this->getToken();
try {
- $share = $this->shareManager->getShareByToken($token);
+ $share = $this->getShare();
} catch (ShareNotFound $e) {
$this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
return false;
}
- $this->share = $share;
\OC_User::setIncognitoMode(true);
// check if the share is password protected
@@ -184,7 +188,7 @@ class PublicAuth extends AbstractBasic {
}
return true;
}
-
+
if ($this->session->exists(PublicAuth::DAV_AUTHENTICATED)
&& $this->session->get(PublicAuth::DAV_AUTHENTICATED) === $share->getId()) {
return true;
@@ -211,7 +215,13 @@ class PublicAuth extends AbstractBasic {
}
public function getShare(): IShare {
- assert($this->share !== null);
+ $token = $this->getToken();
+
+ if ($this->share === null) {
+ $share = $this->shareManager->getShareByToken($token);
+ $this->share = $share;
+ }
+
return $this->share;
}
}
diff --git a/apps/dav/lib/Connector/Sabre/QuotaPlugin.php b/apps/dav/lib/Connector/Sabre/QuotaPlugin.php
index deaa4cf672b..bbb378edc9b 100644
--- a/apps/dav/lib/Connector/Sabre/QuotaPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/QuotaPlugin.php
@@ -8,12 +8,15 @@
*/
namespace OCA\DAV\Connector\Sabre;
+use OC\Files\View;
use OCA\DAV\Upload\FutureFile;
use OCA\DAV\Upload\UploadFolder;
use OCP\Files\StorageNotAvailableException;
use Sabre\DAV\Exception\InsufficientStorage;
use Sabre\DAV\Exception\ServiceUnavailable;
use Sabre\DAV\INode;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
/**
* This plugin check user quota and deny creating files when they exceeds the quota.
@@ -23,9 +26,6 @@ use Sabre\DAV\INode;
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
- /** @var \OC\Files\View */
- private $view;
-
/**
* Reference to main server object
*
@@ -34,10 +34,11 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
private $server;
/**
- * @param \OC\Files\View $view
+ * @param View $view
*/
- public function __construct($view) {
- $this->view = $view;
+ public function __construct(
+ private $view,
+ ) {
}
/**
@@ -56,6 +57,7 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
$server->on('beforeWriteContent', [$this, 'beforeWriteContent'], 10);
$server->on('beforeCreateFile', [$this, 'beforeCreateFile'], 10);
+ $server->on('method:MKCOL', [$this, 'onCreateCollection'], 30);
$server->on('beforeMove', [$this, 'beforeMove'], 10);
$server->on('beforeCopy', [$this, 'beforeCopy'], 10);
}
@@ -90,6 +92,31 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
}
/**
+ * Check quota before creating directory
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return bool
+ * @throws InsufficientStorage
+ * @throws \Sabre\DAV\Exception\Forbidden
+ */
+ public function onCreateCollection(RequestInterface $request, ResponseInterface $response): bool {
+ try {
+ $destinationPath = $this->server->calculateUri($request->getUrl());
+ $quotaPath = $this->getPathForDestination($destinationPath);
+ } catch (\Exception $e) {
+ return true;
+ }
+ if ($quotaPath) {
+ // MKCOL does not have a Content-Length header, so we can use
+ // a fixed value for the quota check.
+ return $this->checkQuota($quotaPath, 4096, true);
+ }
+
+ return true;
+ }
+
+ /**
* Check quota before writing content
*
* @param string $uri target file URI
@@ -175,7 +202,7 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
* @throws InsufficientStorage
* @return bool
*/
- public function checkQuota(string $path, $length = null) {
+ public function checkQuota(string $path, $length = null, $isDir = false) {
if ($length === null) {
$length = $this->getLength();
}
@@ -192,6 +219,10 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
$freeSpace = $this->getFreeSpace($path);
if ($freeSpace >= 0 && $length > $freeSpace) {
+ if ($isDir) {
+ throw new InsufficientStorage("Insufficient space in $path. $freeSpace available. Cannot create directory");
+ }
+
throw new InsufficientStorage("Insufficient space in $path, $length required, $freeSpace available");
}
}
diff --git a/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php b/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php
index f856fca51e1..5484bab9237 100644
--- a/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php
@@ -13,11 +13,9 @@ use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class RequestIdHeaderPlugin extends \Sabre\DAV\ServerPlugin {
- /** @var IRequest */
- private $request;
-
- public function __construct(IRequest $request) {
- $this->request = $request;
+ public function __construct(
+ private IRequest $request,
+ ) {
}
public function initialize(\Sabre\DAV\Server $server) {
diff --git a/apps/dav/lib/Connector/Sabre/Server.php b/apps/dav/lib/Connector/Sabre/Server.php
index b7ca8a0a1c0..dda9c29b763 100644
--- a/apps/dav/lib/Connector/Sabre/Server.php
+++ b/apps/dav/lib/Connector/Sabre/Server.php
@@ -7,6 +7,14 @@
*/
namespace OCA\DAV\Connector\Sabre;
+use OC\DB\Connection;
+use Override;
+use Sabre\DAV\Exception;
+use Sabre\DAV\INode;
+use Sabre\DAV\PropFind;
+use Sabre\DAV\Version;
+use TypeError;
+
/**
* Class \OCA\DAV\Connector\Sabre\Server
*
@@ -18,6 +26,14 @@ class Server extends \Sabre\DAV\Server {
/** @var CachingTree $tree */
/**
+ * Tracks queries done by plugins.
+ * @var array<int, array<string, array{nodes:int, queries:int}>>
+ */
+ private array $pluginQueries = [];
+
+ public bool $debugEnabled = false;
+
+ /**
* @see \Sabre\DAV\Server
*/
public function __construct($treeOrNode = null) {
@@ -25,4 +41,190 @@ class Server extends \Sabre\DAV\Server {
self::$exposeVersion = false;
$this->enablePropfindDepthInfinity = true;
}
+
+ #[Override]
+ public function once(
+ string $eventName,
+ callable $callBack,
+ int $priority = 100,
+ ): void {
+ $this->debugEnabled ? $this->monitorPropfindQueries(
+ parent::once(...),
+ ...func_get_args(),
+ ) : parent::once(...func_get_args());
+ }
+
+ #[Override]
+ public function on(
+ string $eventName,
+ callable $callBack,
+ int $priority = 100,
+ ): void {
+ $this->debugEnabled ? $this->monitorPropfindQueries(
+ parent::on(...),
+ ...func_get_args(),
+ ) : parent::on(...func_get_args());
+ }
+
+ /**
+ * Wraps the handler $callBack into a query-monitoring function and calls
+ * $parentFn to register it.
+ */
+ private function monitorPropfindQueries(
+ callable $parentFn,
+ string $eventName,
+ callable $callBack,
+ int $priority = 100,
+ ): void {
+ if ($eventName !== 'propFind') {
+ $parentFn($eventName, $callBack, $priority);
+ return;
+ }
+
+ $pluginName = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['class'] ?? 'unknown';
+ $callback = $this->getMonitoredCallback($callBack, $pluginName);
+
+ $parentFn($eventName, $callback, $priority);
+ }
+
+ /**
+ * Returns a callable that wraps $callBack with code that monitors and
+ * records queries per plugin.
+ */
+ private function getMonitoredCallback(
+ callable $callBack,
+ string $pluginName,
+ ): callable {
+ return function (PropFind $propFind, INode $node) use (
+ $callBack,
+ $pluginName,
+ ) {
+ $connection = \OCP\Server::get(Connection::class);
+ $queriesBefore = $connection->getStats()['executed'];
+ $result = $callBack($propFind, $node);
+ $queriesAfter = $connection->getStats()['executed'];
+ $this->trackPluginQueries(
+ $pluginName,
+ $queriesAfter - $queriesBefore,
+ $propFind->getDepth()
+ );
+
+ return $result;
+ };
+ }
+
+ /**
+ * Tracks the queries executed by a specific plugin.
+ */
+ private function trackPluginQueries(
+ string $pluginName,
+ int $queriesExecuted,
+ int $depth,
+ ): void {
+ // report only nodes which cause queries to the DB
+ if ($queriesExecuted === 0) {
+ return;
+ }
+
+ $this->pluginQueries[$depth][$pluginName]['nodes']
+ = ($this->pluginQueries[$depth][$pluginName]['nodes'] ?? 0) + 1;
+
+ $this->pluginQueries[$depth][$pluginName]['queries']
+ = ($this->pluginQueries[$depth][$pluginName]['queries'] ?? 0) + $queriesExecuted;
+ }
+
+ /**
+ *
+ * @return void
+ */
+ public function start() {
+ try {
+ // If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an
+ // origin, we must make sure we send back HTTP/1.0 if this was
+ // requested.
+ // This is mainly because nginx doesn't support Chunked Transfer
+ // Encoding, and this forces the webserver SabreDAV is running on,
+ // to buffer entire responses to calculate Content-Length.
+ $this->httpResponse->setHTTPVersion($this->httpRequest->getHTTPVersion());
+
+ // Setting the base url
+ $this->httpRequest->setBaseUrl($this->getBaseUri());
+ $this->invokeMethod($this->httpRequest, $this->httpResponse);
+ } catch (\Throwable $e) {
+ try {
+ $this->emit('exception', [$e]);
+ } catch (\Exception) {
+ }
+
+ if ($e instanceof TypeError) {
+ /*
+ * The TypeError includes the file path where the error occurred,
+ * potentially revealing the installation directory.
+ */
+ $e = new TypeError('A type error occurred. For more details, please refer to the logs, which provide additional context about the type error.');
+ }
+
+ $DOM = new \DOMDocument('1.0', 'utf-8');
+ $DOM->formatOutput = true;
+
+ $error = $DOM->createElementNS('DAV:', 'd:error');
+ $error->setAttribute('xmlns:s', self::NS_SABREDAV);
+ $DOM->appendChild($error);
+
+ $h = function ($v) {
+ return htmlspecialchars((string)$v, ENT_NOQUOTES, 'UTF-8');
+ };
+
+ if (self::$exposeVersion) {
+ $error->appendChild($DOM->createElement('s:sabredav-version', $h(Version::VERSION)));
+ }
+
+ $error->appendChild($DOM->createElement('s:exception', $h(get_class($e))));
+ $error->appendChild($DOM->createElement('s:message', $h($e->getMessage())));
+ if ($this->debugExceptions) {
+ $error->appendChild($DOM->createElement('s:file', $h($e->getFile())));
+ $error->appendChild($DOM->createElement('s:line', $h($e->getLine())));
+ $error->appendChild($DOM->createElement('s:code', $h($e->getCode())));
+ $error->appendChild($DOM->createElement('s:stacktrace', $h($e->getTraceAsString())));
+ }
+
+ if ($this->debugExceptions) {
+ $previous = $e;
+ while ($previous = $previous->getPrevious()) {
+ $xPrevious = $DOM->createElement('s:previous-exception');
+ $xPrevious->appendChild($DOM->createElement('s:exception', $h(get_class($previous))));
+ $xPrevious->appendChild($DOM->createElement('s:message', $h($previous->getMessage())));
+ $xPrevious->appendChild($DOM->createElement('s:file', $h($previous->getFile())));
+ $xPrevious->appendChild($DOM->createElement('s:line', $h($previous->getLine())));
+ $xPrevious->appendChild($DOM->createElement('s:code', $h($previous->getCode())));
+ $xPrevious->appendChild($DOM->createElement('s:stacktrace', $h($previous->getTraceAsString())));
+ $error->appendChild($xPrevious);
+ }
+ }
+
+ if ($e instanceof Exception) {
+ $httpCode = $e->getHTTPCode();
+ $e->serialize($this, $error);
+ $headers = $e->getHTTPHeaders($this);
+ } else {
+ $httpCode = 500;
+ $headers = [];
+ }
+ $headers['Content-Type'] = 'application/xml; charset=utf-8';
+
+ $this->httpResponse->setStatus($httpCode);
+ $this->httpResponse->setHeaders($headers);
+ $this->httpResponse->setBody($DOM->saveXML());
+ $this->sapi->sendResponse($this->httpResponse);
+ }
+ }
+
+ /**
+ * Returns queries executed by registered plugins.
+ *
+ * @return array<int, array<string, array{nodes:int, queries:int}>>
+ */
+ public function getPluginQueries(): array {
+ return $this->pluginQueries;
+ }
}
diff --git a/apps/dav/lib/Connector/Sabre/ServerFactory.php b/apps/dav/lib/Connector/Sabre/ServerFactory.php
index b07cbc904da..a6a27057177 100644
--- a/apps/dav/lib/Connector/Sabre/ServerFactory.php
+++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php
@@ -7,84 +7,105 @@
*/
namespace OCA\DAV\Connector\Sabre;
+use OC\Files\View;
+use OC\KnownUser\KnownUserService;
use OCA\DAV\AppInfo\PluginManager;
+use OCA\DAV\CalDAV\DefaultCalendarValidator;
+use OCA\DAV\CalDAV\Proxy\ProxyMapper;
+use OCA\DAV\DAV\CustomPropertiesBackend;
use OCA\DAV\DAV\ViewOnlyPlugin;
use OCA\DAV\Files\BrowserErrorPagePlugin;
+use OCA\DAV\Files\Sharing\RootCollection;
+use OCA\DAV\Upload\CleanupService;
+use OCA\Theming\ThemingDefaults;
+use OCP\Accounts\IAccountManager;
+use OCP\App\IAppManager;
+use OCP\Comments\ICommentsManager;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Folder;
+use OCP\Files\IFilenameValidator;
+use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountManager;
use OCP\IConfig;
use OCP\IDBConnection;
+use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IPreview;
use OCP\IRequest;
use OCP\ITagManager;
+use OCP\IUserManager;
use OCP\IUserSession;
use OCP\SabrePluginEvent;
+use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Auth\Plugin;
+use Sabre\DAV\SimpleCollection;
class ServerFactory {
- private IConfig $config;
- private LoggerInterface $logger;
- private IDBConnection $databaseConnection;
- private IUserSession $userSession;
- private IMountManager $mountManager;
- private ITagManager $tagManager;
- private IRequest $request;
- private IPreview $previewManager;
- private IEventDispatcher $eventDispatcher;
- private IL10N $l10n;
public function __construct(
- IConfig $config,
- LoggerInterface $logger,
- IDBConnection $databaseConnection,
- IUserSession $userSession,
- IMountManager $mountManager,
- ITagManager $tagManager,
- IRequest $request,
- IPreview $previewManager,
- IEventDispatcher $eventDispatcher,
- IL10N $l10n
+ private IConfig $config,
+ private LoggerInterface $logger,
+ private IDBConnection $databaseConnection,
+ private IUserSession $userSession,
+ private IMountManager $mountManager,
+ private ITagManager $tagManager,
+ private IRequest $request,
+ private IPreview $previewManager,
+ private IEventDispatcher $eventDispatcher,
+ private IL10N $l10n,
) {
- $this->config = $config;
- $this->logger = $logger;
- $this->databaseConnection = $databaseConnection;
- $this->userSession = $userSession;
- $this->mountManager = $mountManager;
- $this->tagManager = $tagManager;
- $this->request = $request;
- $this->previewManager = $previewManager;
- $this->eventDispatcher = $eventDispatcher;
- $this->l10n = $l10n;
}
/**
* @param callable $viewCallBack callback that should return the view for the dav endpoint
*/
- public function createServer(string $baseUri,
+ public function createServer(
+ bool $isPublicShare,
+ string $baseUri,
string $requestUri,
Plugin $authPlugin,
- callable $viewCallBack): Server {
+ callable $viewCallBack,
+ ): Server {
+ $debugEnabled = $this->config->getSystemValue('debug', false);
// Fire up server
- $objectTree = new \OCA\DAV\Connector\Sabre\ObjectTree();
- $server = new \OCA\DAV\Connector\Sabre\Server($objectTree);
+ if ($isPublicShare) {
+ $rootCollection = new SimpleCollection('root');
+ $tree = new CachingTree($rootCollection);
+ } else {
+ $rootCollection = null;
+ $tree = new ObjectTree();
+ }
+ $server = new Server($tree);
// Set URL explicitly due to reverse-proxy situations
$server->httpRequest->setUrl($requestUri);
$server->setBaseUri($baseUri);
// Load plugins
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\MaintenancePlugin($this->config, $this->l10n));
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin($this->config));
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin());
+ $server->addPlugin(new MaintenancePlugin($this->config, $this->l10n));
+ $server->addPlugin(new BlockLegacyClientPlugin(
+ $this->config,
+ \OCP\Server::get(ThemingDefaults::class),
+ ));
+ $server->addPlugin(new AnonymousOptionsPlugin());
$server->addPlugin($authPlugin);
+ if ($debugEnabled) {
+ $server->debugEnabled = $debugEnabled;
+ $server->addPlugin(new PropFindMonitorPlugin());
+ }
// FIXME: The following line is a workaround for legacy components relying on being able to send a GET to /
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\DummyGetResponsePlugin());
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $this->logger));
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin());
+ $server->addPlugin(new DummyGetResponsePlugin());
+ $server->addPlugin(new ExceptionLoggerPlugin('webdav', $this->logger));
+ $server->addPlugin(new LockPlugin());
+
+ $server->addPlugin(new RequestIdHeaderPlugin($this->request));
- $server->addPlugin(new RequestIdHeaderPlugin(\OC::$server->get(IRequest::class)));
+ $server->addPlugin(new ZipFolderPlugin(
+ $tree,
+ $this->logger,
+ $this->eventDispatcher,
+ ));
// Some WebDAV clients do require Class 2 WebDAV support (locking), since
// we do not provide locking we emulate it using a fake locking plugin.
@@ -93,7 +114,7 @@ class ServerFactory {
'/OneNote/',
'/Microsoft-WebDAV-MiniRedir/',
])) {
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\FakeLockerPlugin());
+ $server->addPlugin(new FakeLockerPlugin());
}
if (BrowserErrorPagePlugin::isBrowserRequest($this->request)) {
@@ -101,11 +122,12 @@ class ServerFactory {
}
// wait with registering these until auth is handled and the filesystem is setup
- $server->on('beforeMethod:*', function () use ($server, $objectTree, $viewCallBack) {
+ $server->on('beforeMethod:*', function () use ($server, $tree,
+ $viewCallBack, $isPublicShare, $rootCollection, $debugEnabled): void {
// ensure the skeleton is copied
$userFolder = \OC::$server->getUserFolder();
- /** @var \OC\Files\View $view */
+ /** @var View $view */
$view = $viewCallBack($server);
if ($userFolder instanceof Folder && $userFolder->getPath() === $view->getRoot()) {
$rootInfo = $userFolder;
@@ -115,25 +137,61 @@ class ServerFactory {
// Create Nextcloud Dir
if ($rootInfo->getType() === 'dir') {
- $root = new \OCA\DAV\Connector\Sabre\Directory($view, $rootInfo, $objectTree);
+ $root = new Directory($view, $rootInfo, $tree);
+ } else {
+ $root = new File($view, $rootInfo);
+ }
+
+ if ($isPublicShare) {
+ $userPrincipalBackend = new Principal(
+ \OCP\Server::get(IUserManager::class),
+ \OCP\Server::get(IGroupManager::class),
+ \OCP\Server::get(IAccountManager::class),
+ \OCP\Server::get(\OCP\Share\IManager::class),
+ \OCP\Server::get(IUserSession::class),
+ \OCP\Server::get(IAppManager::class),
+ \OCP\Server::get(ProxyMapper::class),
+ \OCP\Server::get(KnownUserService::class),
+ \OCP\Server::get(IConfig::class),
+ \OC::$server->getL10NFactory(),
+ );
+
+ // Mount the share collection at /public.php/dav/shares/<share token>
+ $rootCollection->addChild(new RootCollection(
+ $root,
+ $userPrincipalBackend,
+ 'principals/shares',
+ ));
+
+ // Mount the upload collection at /public.php/dav/uploads/<share token>
+ $rootCollection->addChild(new \OCA\DAV\Upload\RootCollection(
+ $userPrincipalBackend,
+ 'principals/shares',
+ \OCP\Server::get(CleanupService::class),
+ \OCP\Server::get(IRootFolder::class),
+ \OCP\Server::get(IUserSession::class),
+ \OCP\Server::get(\OCP\Share\IManager::class),
+ ));
} else {
- $root = new \OCA\DAV\Connector\Sabre\File($view, $rootInfo);
+ /** @var ObjectTree $tree */
+ $tree->init($root, $view, $this->mountManager);
}
- $objectTree->init($root, $view, $this->mountManager);
$server->addPlugin(
- new \OCA\DAV\Connector\Sabre\FilesPlugin(
- $objectTree,
+ new FilesPlugin(
+ $tree,
$this->config,
$this->request,
$this->previewManager,
$this->userSession,
+ \OCP\Server::get(IFilenameValidator::class),
+ \OCP\Server::get(IAccountManager::class),
false,
- !$this->config->getSystemValue('debug', false)
+ !$debugEnabled
)
);
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\QuotaPlugin($view, true));
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\ChecksumUpdatePlugin());
+ $server->addPlugin(new QuotaPlugin($view));
+ $server->addPlugin(new ChecksumUpdatePlugin());
// Allow view-only plugin for webdav requests
$server->addPlugin(new ViewOnlyPlugin(
@@ -141,45 +199,46 @@ class ServerFactory {
));
if ($this->userSession->isLoggedIn()) {
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\TagsPlugin($objectTree, $this->tagManager));
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\SharesPlugin(
- $objectTree,
+ $server->addPlugin(new TagsPlugin($tree, $this->tagManager, $this->eventDispatcher, $this->userSession));
+ $server->addPlugin(new SharesPlugin(
+ $tree,
$this->userSession,
$userFolder,
- \OC::$server->getShareManager()
+ \OCP\Server::get(\OCP\Share\IManager::class)
));
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\CommentPropertiesPlugin(\OC::$server->getCommentsManager(), $this->userSession));
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\FilesReportPlugin(
- $objectTree,
+ $server->addPlugin(new CommentPropertiesPlugin(\OCP\Server::get(ICommentsManager::class), $this->userSession));
+ $server->addPlugin(new FilesReportPlugin(
+ $tree,
$view,
- \OC::$server->getSystemTagManager(),
- \OC::$server->getSystemTagObjectMapper(),
- \OC::$server->getTagManager(),
+ \OCP\Server::get(ISystemTagManager::class),
+ \OCP\Server::get(ISystemTagObjectMapper::class),
+ \OCP\Server::get(ITagManager::class),
$this->userSession,
- \OC::$server->getGroupManager(),
+ \OCP\Server::get(IGroupManager::class),
$userFolder,
- \OC::$server->getAppManager()
+ \OCP\Server::get(IAppManager::class)
));
// custom properties plugin must be the last one
$server->addPlugin(
new \Sabre\DAV\PropertyStorage\Plugin(
- new \OCA\DAV\DAV\CustomPropertiesBackend(
+ new CustomPropertiesBackend(
$server,
- $objectTree,
+ $tree,
$this->databaseConnection,
- $this->userSession->getUser()
+ $this->userSession->getUser(),
+ \OCP\Server::get(DefaultCalendarValidator::class),
)
)
);
}
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin());
+ $server->addPlugin(new CopyEtagHeaderPlugin());
// Load dav plugins from apps
$event = new SabrePluginEvent($server);
$this->eventDispatcher->dispatchTyped($event);
$pluginManager = new PluginManager(
\OC::$server,
- \OC::$server->getAppManager()
+ \OCP\Server::get(IAppManager::class)
);
foreach ($pluginManager->getAppPlugins() as $appPlugin) {
$server->addPlugin($appPlugin);
diff --git a/apps/dav/lib/Connector/Sabre/ShareTypeList.php b/apps/dav/lib/Connector/Sabre/ShareTypeList.php
index abe56cd0301..0b66ed27576 100644
--- a/apps/dav/lib/Connector/Sabre/ShareTypeList.php
+++ b/apps/dav/lib/Connector/Sabre/ShareTypeList.php
@@ -20,17 +20,14 @@ class ShareTypeList implements Element {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
/**
- * Share types
- *
- * @var int[]
- */
- private $shareTypes;
-
- /**
* @param int[] $shareTypes
*/
- public function __construct($shareTypes) {
- $this->shareTypes = $shareTypes;
+ public function __construct(
+ /**
+ * Share types
+ */
+ private $shareTypes,
+ ) {
}
/**
diff --git a/apps/dav/lib/Connector/Sabre/ShareeList.php b/apps/dav/lib/Connector/Sabre/ShareeList.php
index b8f9f98d1c7..909c29fc24b 100644
--- a/apps/dav/lib/Connector/Sabre/ShareeList.php
+++ b/apps/dav/lib/Connector/Sabre/ShareeList.php
@@ -18,11 +18,10 @@ use Sabre\Xml\XmlSerializable;
class ShareeList implements XmlSerializable {
public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
- /** @var IShare[] */
- private $shares;
-
- public function __construct(array $shares) {
- $this->shares = $shares;
+ public function __construct(
+ /** @var IShare[] */
+ private array $shares,
+ ) {
}
/**
diff --git a/apps/dav/lib/Connector/Sabre/SharesPlugin.php b/apps/dav/lib/Connector/Sabre/SharesPlugin.php
index 20e94e9aede..f49e85333f3 100644
--- a/apps/dav/lib/Connector/Sabre/SharesPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/SharesPlugin.php
@@ -164,7 +164,7 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
*/
public function handleGetProperties(
PropFind $propFind,
- \Sabre\DAV\INode $sabreNode
+ \Sabre\DAV\INode $sabreNode,
) {
if (!($sabreNode instanceof DavNode)) {
return;
@@ -176,8 +176,8 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
if ($sabreNode instanceof Directory
&& $propFind->getDepth() !== 0
&& (
- !is_null($propFind->getStatus(self::SHARETYPES_PROPERTYNAME)) ||
- !is_null($propFind->getStatus(self::SHAREES_PROPERTYNAME))
+ !is_null($propFind->getStatus(self::SHARETYPES_PROPERTYNAME))
+ || !is_null($propFind->getStatus(self::SHAREES_PROPERTYNAME))
)
) {
$folderNode = $sabreNode->getNode();
diff --git a/apps/dav/lib/Connector/Sabre/TagList.php b/apps/dav/lib/Connector/Sabre/TagList.php
index 5537acc452c..9a5cd0d51cf 100644
--- a/apps/dav/lib/Connector/Sabre/TagList.php
+++ b/apps/dav/lib/Connector/Sabre/TagList.php
@@ -20,17 +20,14 @@ class TagList implements Element {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
/**
- * tags
- *
- * @var array
- */
- private $tags;
-
- /**
* @param array $tags
*/
- public function __construct(array $tags) {
- $this->tags = $tags;
+ public function __construct(
+ /**
+ * tags
+ */
+ private array $tags,
+ ) {
}
/**
diff --git a/apps/dav/lib/Connector/Sabre/TagsPlugin.php b/apps/dav/lib/Connector/Sabre/TagsPlugin.php
index d7572c46ed3..25c1633df36 100644
--- a/apps/dav/lib/Connector/Sabre/TagsPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/TagsPlugin.php
@@ -27,7 +27,10 @@ namespace OCA\DAV\Connector\Sabre;
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
-
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\ITagManager;
+use OCP\ITags;
+use OCP\IUserSession;
use Sabre\DAV\PropFind;
use Sabre\DAV\PropPatch;
@@ -47,12 +50,7 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
private $server;
/**
- * @var \OCP\ITagManager
- */
- private $tagManager;
-
- /**
- * @var \OCP\ITags
+ * @var ITags
*/
private $tagger;
@@ -65,17 +63,15 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
private $cachedTags;
/**
- * @var \Sabre\DAV\Tree
- */
- private $tree;
-
- /**
* @param \Sabre\DAV\Tree $tree tree
- * @param \OCP\ITagManager $tagManager tag manager
+ * @param ITagManager $tagManager tag manager
*/
- public function __construct(\Sabre\DAV\Tree $tree, \OCP\ITagManager $tagManager) {
- $this->tree = $tree;
- $this->tagManager = $tagManager;
+ public function __construct(
+ private \Sabre\DAV\Tree $tree,
+ private ITagManager $tagManager,
+ private IEventDispatcher $eventDispatcher,
+ private IUserSession $userSession,
+ ) {
$this->tagger = null;
$this->cachedTags = [];
}
@@ -98,12 +94,13 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
$this->server = $server;
$this->server->on('propFind', [$this, 'handleGetProperties']);
$this->server->on('propPatch', [$this, 'handleUpdateProperties']);
+ $this->server->on('preloadProperties', [$this, 'handlePreloadProperties']);
}
/**
* Returns the tagger
*
- * @return \OCP\ITags tagger
+ * @return ITags tagger
*/
private function getTagger() {
if (!$this->tagger) {
@@ -117,7 +114,7 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
*
* @param integer $fileId file id
* @return array list($tags, $favorite) with $tags as tag array
- * and $favorite is a boolean whether the file was favorited
+ * and $favorite is a boolean whether the file was favorited
*/
private function getTagsAndFav($fileId) {
$isFav = false;
@@ -154,6 +151,24 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
}
/**
+ * Prefetches tags for a list of file IDs and caches the results
+ *
+ * @param array $fileIds List of file IDs to prefetch tags for
+ * @return void
+ */
+ private function prefetchTagsForFileIds(array $fileIds) {
+ $tags = $this->getTagger()->getTagsForObjects($fileIds);
+ if ($tags === false) {
+ // the tags API returns false on error...
+ $tags = [];
+ }
+
+ foreach ($fileIds as $fileId) {
+ $this->cachedTags[$fileId] = $tags[$fileId] ?? [];
+ }
+ }
+
+ /**
* Updates the tags of the given file id
*
* @param int $fileId
@@ -189,36 +204,25 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
*/
public function handleGetProperties(
PropFind $propFind,
- \Sabre\DAV\INode $node
+ \Sabre\DAV\INode $node,
) {
- if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
+ if (!($node instanceof Node)) {
return;
}
// need prefetch ?
- if ($node instanceof \OCA\DAV\Connector\Sabre\Directory
+ if ($node instanceof Directory
&& $propFind->getDepth() !== 0
&& (!is_null($propFind->getStatus(self::TAGS_PROPERTYNAME))
|| !is_null($propFind->getStatus(self::FAVORITE_PROPERTYNAME))
)) {
// note: pre-fetching only supported for depth <= 1
$folderContent = $node->getChildren();
- $fileIds[] = (int)$node->getId();
+ $fileIds = [(int)$node->getId()];
foreach ($folderContent as $info) {
$fileIds[] = (int)$info->getId();
}
- $tags = $this->getTagger()->getTagsForObjects($fileIds);
- if ($tags === false) {
- // the tags API returns false on error...
- $tags = [];
- }
-
- $this->cachedTags = $this->cachedTags + $tags;
- $emptyFileIds = array_diff($fileIds, array_keys($tags));
- // also cache the ones that were not found
- foreach ($emptyFileIds as $fileId) {
- $this->cachedTags[$fileId] = [];
- }
+ $this->prefetchTagsForFileIds($fileIds);
}
$isFav = null;
@@ -250,7 +254,7 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
*/
public function handleUpdateProperties($path, PropPatch $propPatch) {
$node = $this->tree->getNodeForPath($path);
- if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
+ if (!($node instanceof Node)) {
return;
}
@@ -259,7 +263,7 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
return true;
});
- $propPatch->handle(self::FAVORITE_PROPERTYNAME, function ($favState) use ($node) {
+ $propPatch->handle(self::FAVORITE_PROPERTYNAME, function ($favState) use ($node, $path) {
if ((int)$favState === 1 || $favState === 'true') {
$this->getTagger()->tagAs($node->getId(), self::TAG_FAVORITE);
} else {
@@ -274,4 +278,14 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
return 200;
});
}
+
+ public function handlePreloadProperties(array $nodes, array $requestProperties): void {
+ if (
+ !in_array(self::FAVORITE_PROPERTYNAME, $requestProperties, true)
+ && !in_array(self::TAGS_PROPERTYNAME, $requestProperties, true)
+ ) {
+ return;
+ }
+ $this->prefetchTagsForFileIds(array_map(fn ($node) => $node->getId(), $nodes));
+ }
}
diff --git a/apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php b/apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php
new file mode 100644
index 00000000000..f198519b454
--- /dev/null
+++ b/apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php
@@ -0,0 +1,193 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Connector\Sabre;
+
+use OC\Streamer;
+use OCA\DAV\Connector\Sabre\Exception\Forbidden;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\Events\BeforeZipCreatedEvent;
+use OCP\Files\File as NcFile;
+use OCP\Files\Folder as NcFolder;
+use OCP\Files\Node as NcNode;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV\Server;
+use Sabre\DAV\ServerPlugin;
+use Sabre\DAV\Tree;
+use Sabre\HTTP\Request;
+use Sabre\HTTP\Response;
+
+/**
+ * This plugin allows to download folders accessed by GET HTTP requests on DAV.
+ * The WebDAV standard explicitly say that GET is not covered and should return what ever the application thinks would be a good representation.
+ *
+ * When a collection is accessed using GET, this will provide the content as a archive.
+ * The type can be set by the `Accept` header (MIME type of zip or tar), or as browser fallback using a `accept` GET parameter.
+ * It is also possible to only include some child nodes (from the collection it self) by providing a `filter` GET parameter or `X-NC-Files` custom header.
+ */
+class ZipFolderPlugin extends ServerPlugin {
+
+ /**
+ * Reference to main server object
+ */
+ private ?Server $server = null;
+
+ public function __construct(
+ private Tree $tree,
+ private LoggerInterface $logger,
+ private IEventDispatcher $eventDispatcher,
+ ) {
+ }
+
+ /**
+ * This initializes the plugin.
+ *
+ * This function is called by \Sabre\DAV\Server, after
+ * addPlugin is called.
+ *
+ * This method should set up the required event subscriptions.
+ */
+ public function initialize(Server $server): void {
+ $this->server = $server;
+ $this->server->on('method:GET', $this->handleDownload(...), 100);
+ // low priority to give any other afterMethod:* a chance to fire before we cancel everything
+ $this->server->on('afterMethod:GET', $this->afterDownload(...), 999);
+ }
+
+ /**
+ * Adding a node to the archive streamer.
+ * This will recursively add new nodes to the stream if the node is a directory.
+ */
+ protected function streamNode(Streamer $streamer, NcNode $node, string $rootPath): void {
+ // Remove the root path from the filename to make it relative to the requested folder
+ $filename = str_replace($rootPath, '', $node->getPath());
+
+ $mtime = $node->getMTime();
+ if ($node instanceof NcFile) {
+ $resource = $node->fopen('rb');
+ if ($resource === false) {
+ $this->logger->info('Cannot read file for zip stream', ['filePath' => $node->getPath()]);
+ throw new \Sabre\DAV\Exception\ServiceUnavailable('Requested file can currently not be accessed.');
+ }
+ $streamer->addFileFromStream($resource, $filename, $node->getSize(), $mtime);
+ } elseif ($node instanceof NcFolder) {
+ $streamer->addEmptyDir($filename, $mtime);
+ $content = $node->getDirectoryListing();
+ foreach ($content as $subNode) {
+ $this->streamNode($streamer, $subNode, $rootPath);
+ }
+ }
+ }
+
+ /**
+ * Download a folder as an archive.
+ * It is possible to filter / limit the files that should be downloaded,
+ * either by passing (multiple) `X-NC-Files: the-file` headers
+ * or by setting a `files=JSON_ARRAY_OF_FILES` URL query.
+ *
+ * @return false|null
+ */
+ public function handleDownload(Request $request, Response $response): ?bool {
+ $node = $this->tree->getNodeForPath($request->getPath());
+ if (!($node instanceof Directory)) {
+ // only handle directories
+ return null;
+ }
+
+ $query = $request->getQueryParameters();
+
+ // Get accept header - or if set overwrite with accept GET-param
+ $accept = $request->getHeaderAsArray('Accept');
+ $acceptParam = $query['accept'] ?? '';
+ if ($acceptParam !== '') {
+ $accept = array_map(fn (string $name) => strtolower(trim($name)), explode(',', $acceptParam));
+ }
+ $zipRequest = !empty(array_intersect(['application/zip', 'zip'], $accept));
+ $tarRequest = !empty(array_intersect(['application/x-tar', 'tar'], $accept));
+ if (!$zipRequest && !$tarRequest) {
+ // does not accept zip or tar stream
+ return null;
+ }
+
+ $files = $request->getHeaderAsArray('X-NC-Files');
+ $filesParam = $query['files'] ?? '';
+ // The preferred way would be headers, but this is not possible for simple browser requests ("links")
+ // so we also need to support GET parameters
+ if ($filesParam !== '') {
+ $files = json_decode($filesParam);
+ if (!is_array($files)) {
+ $files = [$files];
+ }
+
+ foreach ($files as $file) {
+ if (!is_string($file)) {
+ // we log this as this means either we - or an app - have a bug somewhere or a user is trying invalid things
+ $this->logger->notice('Invalid files filter parameter for ZipFolderPlugin', ['filter' => $filesParam]);
+ // no valid parameter so continue with Sabre behavior
+ return null;
+ }
+ }
+ }
+
+ $folder = $node->getNode();
+ $event = new BeforeZipCreatedEvent($folder, $files);
+ $this->eventDispatcher->dispatchTyped($event);
+ if ((!$event->isSuccessful()) || $event->getErrorMessage() !== null) {
+ $errorMessage = $event->getErrorMessage();
+ if ($errorMessage === null) {
+ // Not allowed to download but also no explaining error
+ // so we abort the ZIP creation and fall back to Sabre default behavior.
+ return null;
+ }
+ // Downloading was denied by an app
+ throw new Forbidden($errorMessage);
+ }
+
+ $content = empty($files) ? $folder->getDirectoryListing() : [];
+ foreach ($files as $path) {
+ $child = $node->getChild($path);
+ assert($child instanceof Node);
+ $content[] = $child->getNode();
+ }
+
+ $archiveName = 'download';
+ $rootPath = $folder->getPath();
+ if (empty($files)) {
+ // We download the full folder so keep it in the tree
+ $rootPath = dirname($folder->getPath());
+ // Full folder is loaded to rename the archive to the folder name
+ $archiveName = $folder->getName();
+ }
+ $streamer = new Streamer($tarRequest, -1, count($content));
+ $streamer->sendHeaders($archiveName);
+ // For full folder downloads we also add the folder itself to the archive
+ if (empty($files)) {
+ $streamer->addEmptyDir($archiveName);
+ }
+ foreach ($content as $node) {
+ $this->streamNode($streamer, $node, $rootPath);
+ }
+ $streamer->finalize();
+ return false;
+ }
+
+ /**
+ * Tell sabre/dav not to trigger it's own response sending logic as the handleDownload will have already send the response
+ *
+ * @return false|null
+ */
+ public function afterDownload(Request $request, Response $response): ?bool {
+ $node = $this->tree->getNodeForPath($request->getPath());
+ if (!($node instanceof Directory)) {
+ // only handle directories
+ return null;
+ } else {
+ return false;
+ }
+ }
+}