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.php28
-rw-r--r--apps/dav/lib/Connector/Sabre/AppEnabledPlugin.php88
-rw-r--r--apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php112
-rw-r--r--apps/dav/lib/Connector/Sabre/Auth.php157
-rw-r--r--apps/dav/lib/Connector/Sabre/BearerAuth.php73
-rw-r--r--apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php72
-rw-r--r--apps/dav/lib/Connector/Sabre/CachingTree.php36
-rw-r--r--apps/dav/lib/Connector/Sabre/ChecksumList.php34
-rw-r--r--apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php44
-rw-r--r--apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php85
-rw-r--r--apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php29
-rw-r--r--apps/dav/lib/Connector/Sabre/DavAclPlugin.php78
-rw-r--r--apps/dav/lib/Connector/Sabre/Directory.php240
-rw-r--r--apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php40
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/BadGateway.php21
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/EntityTooLarge.php24
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/FileLocked.php36
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/Forbidden.php41
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php42
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/PasswordLoginForbidden.php24
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/TooManyRequests.php38
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/UnsupportedMediaType.php24
-rw-r--r--apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php71
-rw-r--r--apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php50
-rw-r--r--apps/dav/lib/Connector/Sabre/File.php485
-rw-r--r--apps/dav/lib/Connector/Sabre/FilesPlugin.php540
-rw-r--r--apps/dav/lib/Connector/Sabre/FilesReportPlugin.php259
-rw-r--r--apps/dav/lib/Connector/Sabre/LockPlugin.php31
-rw-r--r--apps/dav/lib/Connector/Sabre/MaintenancePlugin.php44
-rw-r--r--apps/dav/lib/Connector/Sabre/MtimeSanitizer.php21
-rw-r--r--apps/dav/lib/Connector/Sabre/Node.php229
-rw-r--r--apps/dav/lib/Connector/Sabre/ObjectTree.php85
-rw-r--r--apps/dav/lib/Connector/Sabre/Principal.php180
-rw-r--r--apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php78
-rw-r--r--apps/dav/lib/Connector/Sabre/PropfindCompressionPlugin.php23
-rw-r--r--apps/dav/lib/Connector/Sabre/PublicAuth.php227
-rw-r--r--apps/dav/lib/Connector/Sabre/QuotaPlugin.php189
-rw-r--r--apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php31
-rw-r--r--apps/dav/lib/Connector/Sabre/Server.php228
-rw-r--r--apps/dav/lib/Connector/Sabre/ServerFactory.php272
-rw-r--r--apps/dav/lib/Connector/Sabre/ShareTypeList.php40
-rw-r--r--apps/dav/lib/Connector/Sabre/ShareeList.php34
-rw-r--r--apps/dav/lib/Connector/Sabre/SharesPlugin.php147
-rw-r--r--apps/dav/lib/Connector/Sabre/TagList.php41
-rw-r--r--apps/dav/lib/Connector/Sabre/TagsPlugin.php118
-rw-r--r--apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php193
46 files changed, 2550 insertions, 2432 deletions
diff --git a/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php b/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php
index 6c3600fa5eb..0e2b1c58748 100644
--- a/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php
@@ -1,28 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
- *
- * @author Bastien Durel <bastien@durel.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Connector\Sabre;
@@ -54,7 +34,7 @@ class AnonymousOptionsPlugin extends ServerPlugin {
* @return bool
*/
public function isRequestInRoot($path) {
- return $path === '' || (is_string($path) && strpos($path, '/') === false);
+ return $path === '' || (is_string($path) && !str_contains($path, '/'));
}
/**
diff --git a/apps/dav/lib/Connector/Sabre/AppEnabledPlugin.php b/apps/dav/lib/Connector/Sabre/AppEnabledPlugin.php
deleted file mode 100644
index 244e5de0683..00000000000
--- a/apps/dav/lib/Connector/Sabre/AppEnabledPlugin.php
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php
-/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace OCA\DAV\Connector\Sabre;
-
-use OCP\App\IAppManager;
-use Sabre\DAV\Exception\Forbidden;
-use Sabre\DAV\ServerPlugin;
-
-/**
- * Plugin to check if an app is enabled for the current user
- */
-class AppEnabledPlugin extends ServerPlugin {
-
- /**
- * Reference to main server object
- *
- * @var \Sabre\DAV\Server
- */
- private $server;
-
- /**
- * @var string
- */
- private $app;
-
- /**
- * @var \OCP\App\IAppManager
- */
- private $appManager;
-
- /**
- * @param string $app
- * @param \OCP\App\IAppManager $appManager
- */
- public function __construct($app, IAppManager $appManager) {
- $this->app = $app;
- $this->appManager = $appManager;
- }
-
- /**
- * 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.
- *
- * @param \Sabre\DAV\Server $server
- * @return void
- */
- public function initialize(\Sabre\DAV\Server $server) {
- $this->server = $server;
- $this->server->on('beforeMethod:*', [$this, 'checkAppEnabled'], 30);
- }
-
- /**
- * This method is called before any HTTP after auth and checks if the user has access to the app
- *
- * @throws \Sabre\DAV\Exception\Forbidden
- * @return bool
- */
- public function checkAppEnabled() {
- if (!$this->appManager->isEnabledForUser($this->app)) {
- throw new Forbidden();
- }
- }
-}
diff --git a/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php b/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php
new file mode 100644
index 00000000000..9cff113140a
--- /dev/null
+++ b/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php
@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Connector\Sabre;
+
+use Sabre\DAV\Server;
+use Sabre\DAV\ServerPlugin;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+
+/**
+ * A plugin which tries to work-around peculiarities of the MacOS DAV client
+ * apps. The following problems are addressed:
+ *
+ * - OSX calendar client sends REPORT requests to a random principal
+ * collection but expects to find all principals (forgot to set
+ * {DAV:}principal-property-search flag?)
+ */
+class AppleQuirksPlugin extends ServerPlugin {
+
+ /*
+ private const OSX_CALENDAR_AGENT = 'CalendarAgent';
+ private const OSX_DATAACCESSD_AGENT = 'dataaccessd';
+ private const OSX_ACCOUNTSD_AGENT = 'accountsd';
+ private const OSX_CONTACTS_AGENT = 'AddressBookCore';
+ */
+
+ private const OSX_AGENT_PREFIX = 'macOS';
+
+ /** @var bool */
+ private $isMacOSDavAgent = false;
+
+ /**
+ * Sets up the plugin.
+ *
+ * This method is automatically called by the server class.
+ *
+ * @return void
+ */
+ public function initialize(Server $server) {
+ $server->on('beforeMethod:REPORT', [$this, 'beforeReport'], 0);
+ $server->on('report', [$this, 'report'], 0);
+ }
+
+ /**
+ * Triggered before any method is handled.
+ *
+ * @return void
+ */
+ public function beforeReport(RequestInterface $request, ResponseInterface $response) {
+ $userAgent = $request->getRawServerValue('HTTP_USER_AGENT') ?? 'unknown';
+ $this->isMacOSDavAgent = $this->isMacOSUserAgent($userAgent);
+ }
+
+ /**
+ * This method handles HTTP REPORT requests.
+ *
+ * @param string $reportName
+ * @param mixed $report
+ * @param mixed $path
+ *
+ * @return bool
+ */
+ public function report($reportName, $report, $path) {
+ if ($reportName == '{DAV:}principal-property-search' && $this->isMacOSDavAgent) {
+ /** @var \Sabre\DAVACL\Xml\Request\PrincipalPropertySearchReport $report */
+ $report->applyToPrincipalCollectionSet = true;
+ }
+ return true;
+ }
+
+ /**
+ * Check whether the given $userAgent string pretends to originate from OSX.
+ *
+ * @param string $userAgent
+ *
+ * @return bool
+ */
+ protected function isMacOSUserAgent(string $userAgent):bool {
+ return str_starts_with(self::OSX_AGENT_PREFIX, $userAgent);
+ }
+
+ /**
+ * Decode the given OSX DAV agent string.
+ *
+ * @param string $agent
+ *
+ * @return null|array
+ */
+ protected function decodeMacOSAgentString(string $userAgent):?array {
+ // OSX agent string is like: macOS/13.2.1 (22D68) dataaccessd/1.0
+ if (preg_match('|^' . self::OSX_AGENT_PREFIX . '/([0-9]+)\\.([0-9]+)\\.([0-9]+)\s+\((\w+)\)\s+([^/]+)/([0-9]+)(?:\\.([0-9]+))?(?:\\.([0-9]+))?$|i', $userAgent, $matches)) {
+ return [
+ 'macOSVersion' => [
+ 'major' => $matches[1],
+ 'minor' => $matches[2],
+ 'patch' => $matches[3],
+ ],
+ 'macOSAgent' => $matches[5],
+ 'macOSAgentVersion' => [
+ 'major' => $matches[6],
+ 'minor' => $matches[7] ?? null,
+ 'patch' => $matches[8] ?? null,
+ ],
+ ];
+ }
+ return null;
+ }
+}
diff --git a/apps/dav/lib/Connector/Sabre/Auth.php b/apps/dav/lib/Connector/Sabre/Auth.php
index df4e3c65ce0..a174920946a 100644
--- a/apps/dav/lib/Connector/Sabre/Auth.php
+++ b/apps/dav/lib/Connector/Sabre/Auth.php
@@ -1,46 +1,26 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Jakob Sack <mail@jakobsack.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Markus Goetz <markus@woboq.com>
- * @author Michael Gapczynski <GapczynskiM@gmail.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
use Exception;
use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
use OC\Authentication\TwoFactorAuth\Manager;
-use OC\Security\Bruteforce\Throttler;
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;
use Sabre\DAV\Exception\ServiceUnavailable;
@@ -49,44 +29,21 @@ use Sabre\HTTP\ResponseInterface;
class Auth extends AbstractBasic {
public const DAV_AUTHENTICATED = 'AUTHENTICATED_TO_DAV_BACKEND';
-
- /** @var ISession */
- private $session;
- /** @var Session */
- private $userSession;
- /** @var IRequest */
- private $request;
- /** @var string */
- private $currentUser;
- /** @var Manager */
- private $twoFactorManager;
- /** @var Throttler */
- private $throttler;
-
- /**
- * @param ISession $session
- * @param Session $userSession
- * @param IRequest $request
- * @param Manager $twoFactorManager
- * @param Throttler $throttler
- * @param string $principalPrefix
- */
- public function __construct(ISession $session,
- Session $userSession,
- IRequest $request,
- Manager $twoFactorManager,
- Throttler $throttler,
- $principalPrefix = 'principals/users/') {
- $this->session = $session;
- $this->userSession = $userSession;
- $this->twoFactorManager = $twoFactorManager;
- $this->request = $request;
- $this->throttler = $throttler;
+ private ?string $currentUser = null;
+
+ 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();
- $this->realm = $defaults->getName();
+ $defaults = new Defaults();
+ $this->realm = $defaults->getName() ?: 'Nextcloud';
}
/**
@@ -96,13 +53,10 @@ class Auth extends AbstractBasic {
* account was changed.
*
* @see https://github.com/owncloud/core/issues/13245
- *
- * @param string $username
- * @return bool
*/
- public function isDavAuthenticated($username) {
- return !is_null($this->session->get(self::DAV_AUTHENTICATED)) &&
- $this->session->get(self::DAV_AUTHENTICATED) === $username;
+ public function isDavAuthenticated(string $username): bool {
+ return !is_null($this->session->get(self::DAV_AUTHENTICATED))
+ && $this->session->get(self::DAV_AUTHENTICATED) === $username;
}
/**
@@ -117,17 +71,14 @@ 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())
) {
- \OC_Util::setupFS($this->userSession->getUser()->getUID());
$this->session->close();
return true;
} else {
- \OC_Util::setupFS(); //login hooks may need early access to the filesystem
try {
if ($this->userSession->logClientIn($username, $password, $this->request, $this->throttler)) {
- \OC_Util::setupFS($this->userSession->getUser()->getUID());
$this->session->set(self::DAV_AUTHENTICATED, $this->userSession->getUser()->getUID());
$this->session->close();
return true;
@@ -138,14 +89,15 @@ class Auth extends AbstractBasic {
} catch (PasswordLoginForbiddenException $ex) {
$this->session->close();
throw new PasswordLoginForbidden();
+ } catch (MaxDelayReached $ex) {
+ $this->session->close();
+ throw new TooManyRequests();
}
}
}
/**
- * @param RequestInterface $request
- * @param ResponseInterface $response
- * @return array
+ * @return array{bool, string}
* @throws NotAuthenticated
* @throws ServiceUnavailable
*/
@@ -157,19 +109,18 @@ class Auth extends AbstractBasic {
} catch (Exception $e) {
$class = get_class($e);
$msg = $e->getMessage();
- \OC::$server->getLogger()->logException($e);
+ Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
throw new ServiceUnavailable("$class: $msg");
}
}
/**
* Checks whether a CSRF check is required on the request
- *
- * @return bool
*/
- private function requiresCSRFCheck() {
- // GET requires no check at all
- if ($this->request->getMethod() === 'GET') {
+ private function requiresCSRFCheck(): bool {
+
+ $methodsWithoutCsrf = ['GET', 'HEAD', 'OPTIONS'];
+ if (in_array($this->request->getMethod(), $methodsWithoutCsrf)) {
return false;
}
@@ -193,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;
}
@@ -202,21 +153,19 @@ class Auth extends AbstractBasic {
}
/**
- * @param RequestInterface $request
- * @param ResponseInterface $response
- * @return array
+ * @return array{bool, string}
* @throws NotAuthenticated
*/
- private function auth(RequestInterface $request, ResponseInterface $response) {
+ 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.');
}
}
@@ -229,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() && $request->getHeader('Authorization') === null) ||
- \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;
@@ -241,18 +190,16 @@ class Auth extends AbstractBasic {
}
}
- if (!$this->userSession->isLoggedIn() && in_array('XMLHttpRequest', explode(',', $request->getHeader('X-Requested-With') ?? ''))) {
- // do not re-authenticate over ajax, use dummy auth name to prevent browser popup
- $response->addHeader('WWW-Authenticate','DummyBasic realm="' . $this->realm . '"');
- $response->setStatus(401);
- throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls');
- }
-
$data = parent::check($request, $response);
if ($data[0] === true) {
$startPos = strrpos($data[1], '/') + 1;
$user = $this->userSession->getUser()->getUID();
$data[1] = substr_replace($data[1], $user, $startPos);
+ } 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(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 d28a9cfdb84..23453ae8efb 100644
--- a/apps/dav/lib/Connector/Sabre/BearerAuth.php
+++ b/apps/dav/lib/Connector/Sabre/BearerAuth.php
@@ -1,28 +1,14 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * 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;
@@ -31,33 +17,16 @@ use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class BearerAuth extends AbstractBearer {
- /** @var IUserSession */
- private $userSession;
- /** @var ISession */
- private $session;
- /** @var IRequest */
- private $request;
- /** @var string */
- private $principalPrefix;
-
- /**
- * @param IUserSession $userSession
- * @param ISession $session
- * @param string $principalPrefix
- * @param IRequest $request
- */
- 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();
- $this->realm = $defaults->getName();
+ $defaults = new Defaults();
+ $this->realm = $defaults->getName() ?: 'Nextcloud';
}
private function setupUserFs($userId) {
@@ -90,7 +59,15 @@ class BearerAuth extends AbstractBearer {
* @param RequestInterface $request
* @param ResponseInterface $response
*/
- public function challenge(RequestInterface $request, ResponseInterface $response) {
- $response->setStatus(401);
+ public function challenge(RequestInterface $request, ResponseInterface $response): void {
+ // 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 8e2ea4d4e16..21358406a4a 100644
--- a/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php
@@ -1,31 +1,16 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
+use OCA\Theming\ThemingDefaults;
use OCP\IConfig;
+use OCP\IRequest;
+use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
@@ -36,23 +21,18 @@ use Sabre\HTTP\RequestInterface;
* @package OCA\DAV\Connector\Sabre
*/
class BlockLegacyClientPlugin extends ServerPlugin {
- /** @var \Sabre\DAV\Server */
- protected $server;
- /** @var IConfig */
- protected $config;
+ protected ?Server $server = null;
- /**
- * @param IConfig $config
- */
- public function __construct(IConfig $config) {
- $this->config = $config;
+ public function __construct(
+ private IConfig $config,
+ private ThemingDefaults $themingDefaults,
+ ) {
}
/**
- * @param \Sabre\DAV\Server $server
* @return void
*/
- public function initialize(\Sabre\DAV\Server $server) {
+ public function initialize(Server $server) {
$this->server = $server;
$this->server->on('beforeMethod:*', [$this, 'beforeHandler'], 200);
}
@@ -69,14 +49,26 @@ class BlockLegacyClientPlugin extends ServerPlugin {
return;
}
- $minimumSupportedDesktopVersion = $this->config->getSystemValue('minimum.supported.desktop.version', '2.0.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 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);
- // Match on the mirall version which is in scheme "Mozilla/5.0 (%1) mirall/%2" or
- // "mirall/%1" for older releases
- preg_match("/(?:mirall\\/)([\d.]+)/i", $userAgent, $versionMatches);
- if (isset($versionMatches[1]) &&
- version_compare($versionMatches[1], $minimumSupportedDesktopVersion) === -1) {
- throw new \Sabre\DAV\Exception\Forbidden('Unsupported client version.');
+ 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 eb1233d3540..5d72b530f58 100644
--- a/apps/dav/lib/Connector/Sabre/CachingTree.php
+++ b/apps/dav/lib/Connector/Sabre/CachingTree.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Connector\Sabre;
@@ -28,24 +11,25 @@ use Sabre\DAV\Tree;
class CachingTree extends Tree {
/**
* Store a node in the cache
- *
- * @param Node $node
- * @param null|string $path
*/
- public function cacheNode(Node $node, $path = null) {
+ public function cacheNode(Node $node, ?string $path = null): void {
if (is_null($path)) {
$path = $node->getPath();
}
$this->cache[trim($path, '/')] = $node;
}
+ /**
+ * @param string $path
+ * @return void
+ */
public function markDirty($path) {
// We don't care enough about sub-paths
// flushing the entire cache
$path = trim($path, '/');
foreach ($this->cache as $nodePath => $node) {
- $nodePath = (string) $nodePath;
- if ('' === $path || $nodePath == $path || 0 === strpos($nodePath, $path.'/')) {
+ $nodePath = (string)$nodePath;
+ if ($path === '' || $nodePath == $path || str_starts_with($nodePath, $path . '/')) {
unset($this->cache[$nodePath]);
}
}
diff --git a/apps/dav/lib/Connector/Sabre/ChecksumList.php b/apps/dav/lib/Connector/Sabre/ChecksumList.php
index 74cdc98ef4f..75d1d718de1 100644
--- a/apps/dav/lib/Connector/Sabre/ChecksumList.php
+++ b/apps/dav/lib/Connector/Sabre/ChecksumList.php
@@ -1,24 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
@@ -35,17 +20,14 @@ class ChecksumList implements XmlSerializable {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
/** @var string[] of TYPE:CHECKSUM */
- private $checksums;
+ private array $checksums;
- /**
- * @param string $checksum
- */
- public function __construct($checksum) {
- $this->checksums = explode(',', $checksum);
+ public function __construct(string $checksum) {
+ $this->checksums = explode(' ', $checksum);
}
/**
- * The xmlSerialize metod is called during xml writing.
+ * The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
diff --git a/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php b/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php
index 3247259357f..18009080585 100644
--- a/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php
+++ b/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php
@@ -2,38 +2,22 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Connector\Sabre;
+use OCP\AppFramework\Http;
+use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class ChecksumUpdatePlugin extends ServerPlugin {
- /**
- * @var \Sabre\DAV\Server
- */
- protected $server;
+ protected ?Server $server = null;
- public function initialize(\Sabre\DAV\Server $server) {
+ public function initialize(Server $server) {
$this->server = $server;
$server->on('method:PATCH', [$this, 'httpPatch']);
}
@@ -42,19 +26,7 @@ class ChecksumUpdatePlugin extends ServerPlugin {
return 'checksumupdate';
}
- 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'];
}
@@ -74,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 82980553fa8..e4b6c2636da 100644
--- a/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php
@@ -1,33 +1,17 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
use OCP\Comments\ICommentsManager;
use OCP\IUserSession;
use Sabre\DAV\PropFind;
+use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
class CommentPropertiesPlugin extends ServerPlugin {
@@ -35,20 +19,13 @@ class CommentPropertiesPlugin extends ServerPlugin {
public const PROPERTY_NAME_COUNT = '{http://owncloud.org/ns}comments-count';
public const PROPERTY_NAME_UNREAD = '{http://owncloud.org/ns}comments-unread';
- /** @var \Sabre\DAV\Server */
- protected $server;
-
- /** @var ICommentsManager */
- private $commentsManager;
-
- /** @var IUserSession */
- private $userSession;
-
- private $cachedUnreadCount = [];
+ protected ?Server $server = null;
+ 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,
+ ) {
}
/**
@@ -67,7 +44,7 @@ class CommentPropertiesPlugin extends ServerPlugin {
$this->server->on('propFind', [$this, 'handleGetProperties']);
}
- private function cacheDirectory(Directory $directory) {
+ private function cacheDirectory(Directory $directory): void {
$children = $directory->getChildren();
$ids = [];
@@ -84,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) {
@@ -102,62 +79,52 @@ 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;
}
// need prefetch ?
- if ($node instanceof \OCA\DAV\Connector\Sabre\Directory
+ if ($node instanceof Directory
&& $propFind->getDepth() !== 0
&& !is_null($propFind->getStatus(self::PROPERTY_NAME_UNREAD))
) {
$this->cacheDirectory($node);
}
- $propFind->handle(self::PROPERTY_NAME_COUNT, function () use ($node) {
+ $propFind->handle(self::PROPERTY_NAME_COUNT, function () use ($node): int {
return $this->commentsManager->getNumberOfCommentsForObject('files', (string)$node->getId());
});
- $propFind->handle(self::PROPERTY_NAME_HREF, function () use ($node) {
+ $propFind->handle(self::PROPERTY_NAME_HREF, function () use ($node): ?string {
return $this->getCommentsLink($node);
});
- $propFind->handle(self::PROPERTY_NAME_UNREAD, function () use ($node) {
- if (isset($this->cachedUnreadCount[$node->getId()])) {
- return $this->cachedUnreadCount[$node->getId()];
- }
- return $this->getUnreadCount($node);
+ $propFind->handle(self::PROPERTY_NAME_UNREAD, function () use ($node): ?int {
+ return $this->cachedUnreadCount[$node->getId()] ?? $this->getUnreadCount($node);
});
}
/**
- * returns a reference to the comments node
- *
- * @param Node $node
- * @return mixed|string
+ * Returns a reference to the comments node
*/
- public function getCommentsLink(Node $node) {
+ public function getCommentsLink(Node $node): ?string {
$href = $this->server->getBaseUri();
$entryPoint = strpos($href, '/remote.php/');
if ($entryPoint === false) {
// in case we end up somewhere else, unexpectedly.
return null;
}
- $commentsPart = 'dav/comments/files/' . rawurldecode($node->getId());
- $href = substr_replace($href, $commentsPart, $entryPoint + strlen('/remote.php/'));
- return $href;
+ $commentsPart = 'dav/comments/files/' . rawurldecode((string)$node->getId());
+ return substr_replace($href, $commentsPart, $entryPoint + strlen('/remote.php/'));
}
/**
- * returns the number of unread comments for the currently logged in user
+ * Returns the number of unread comments for the currently logged in user
* on the given file or directory node
- *
- * @param Node $node
- * @return Int|null
*/
- public function getUnreadCount(Node $node) {
+ public function getUnreadCount(Node $node): ?int {
$user = $this->userSession->getUser();
if (is_null($user)) {
return null;
diff --git a/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php b/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php
index 029e631f4d9..609ac295b4c 100644
--- a/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php
@@ -1,30 +1,14 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\Server;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
@@ -34,9 +18,8 @@ use Sabre\HTTP\ResponseInterface;
* or mangle Etag headers.
*/
class CopyEtagHeaderPlugin extends \Sabre\DAV\ServerPlugin {
+ private ?Server $server = null;
- /** @var \Sabre\DAV\Server */
- private $server;
/**
* This initializes the plugin.
*
diff --git a/apps/dav/lib/Connector/Sabre/DavAclPlugin.php b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php
index 6842975835d..100d719ef01 100644
--- a/apps/dav/lib/Connector/Sabre/DavAclPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php
@@ -1,33 +1,17 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
+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;
@@ -58,23 +42,38 @@ class DavAclPlugin extends \Sabre\DAVACL\Plugin {
case AddressBook::class:
$type = 'Addressbook';
break;
+ case Calendar::class:
+ case CachedSubscription::class:
+ $type = 'Calendar';
+ break;
default:
$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;
}
public function propFind(PropFind $propFind, INode $node) {
+ if ($node instanceof Node) {
+ // files don't use dav acls
+ return;
+ }
+
// If the node is neither readable nor writable then fail unless its of
// the standard user-principal
if (!($node instanceof User)) {
@@ -94,8 +93,23 @@ class DavAclPlugin extends \Sabre\DAVACL\Plugin {
$path = $request->getPath();
// prevent the plugin from causing an unneeded overhead for file requests
- if (strpos($path, 'files/') !== 0) {
- parent::beforeMethod($request, $response);
+ if (str_starts_with($path, 'files/')) {
+ return;
+ }
+
+ parent::beforeMethod($request, $response);
+
+ if (!str_starts_with($path, 'addressbooks/') && !str_starts_with($path, 'calendars/')) {
+ return;
+ }
+
+ [$parentName] = \Sabre\Uri\split($path);
+ if ($request->getMethod() === 'REPORT') {
+ // is calendars/users/bob or addressbooks/users/bob readable?
+ $this->checkPrivileges($parentName, '{DAV:}read');
+ } elseif ($request->getMethod() === 'MKCALENDAR' || $request->getMethod() === 'MKCOL') {
+ // is calendars/users/bob or addressbooks/users/bob writeable?
+ $this->checkPrivileges($parentName, '{DAV:}write');
}
}
}
diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php
index ed98b5050f8..fe09c3f423f 100644
--- a/apps/dav/lib/Connector/Sabre/Directory.php
+++ b/apps/dav/lib/Connector/Sabre/Directory.php
@@ -1,52 +1,36 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Jakob Sack <mail@jakobsack.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
use OC\Files\Mount\MoveableMount;
use OC\Files\View;
-use OC\Metadata\FileMetadata;
-use OC\Metadata\MetadataGroup;
+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;
+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;
use Sabre\DAV\Exception\Locked;
@@ -55,41 +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[]
- */
- private $dirContent;
-
- /**
- * Cached quota info
- *
- * @var array
+ * @var FileInfo[]
*/
- private $quotaInfo;
+ private ?array $dirContent = null;
- /**
- * @var ObjectTree|null
- */
- private $tree;
-
- /** @var array<string, array<int, FileMetadata>> */
- private array $metadata = [];
+ /** Cached quota info */
+ private ?array $quotaInfo = null;
/**
* Sets up the node, expects a full path name
- *
- * @param \OC\Files\View $view
- * @param \OCP\Files\FileInfo $info
- * @param ObjectTree|null $tree
- * @param \OCP\Share\IManager $shareManager
*/
- public function __construct(View $view, FileInfo $info, $tree = null, $shareManager = null) {
+ public function __construct(
+ View $view,
+ FileInfo $info,
+ private ?CachingTree $tree = null,
+ ?IShareManager $shareManager = null,
+ ) {
parent::__construct($view, $info, $shareManager);
- $this->tree = $tree;
}
/**
@@ -126,22 +95,8 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
*/
public function createFile($name, $data = null) {
try {
- // for chunked upload also updating a existing file is a "createFile"
- // because we create all the chunks before re-assemble them to the existing file.
- if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
-
- // exit if we can't create a new file and we don't updatable existing file
- $chunkInfo = \OC_FileChunking::decodeName($name);
- if (!$this->fileView->isCreatable($this->path) &&
- !$this->fileView->isUpdatable($this->path . '/' . $chunkInfo['name'])
- ) {
- throw new \Sabre\DAV\Exception\Forbidden();
- }
- } else {
- // For non-chunked upload it is enough to check if we can create a new file
- if (!$this->fileView->isCreatable($this->path)) {
- throw new \Sabre\DAV\Exception\Forbidden();
- }
+ if (!$this->fileView->isCreatable($this->path)) {
+ throw new \Sabre\DAV\Exception\Forbidden();
}
$this->fileView->verifyPath($this->path, $name);
@@ -155,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);
@@ -197,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);
}
@@ -212,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) {
- if (!$this->info->isReadable()) {
+ public function getChild($name, $info = null, ?IRequest $request = null, ?IL10N $l10n = null) {
+ $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();
}
@@ -227,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);
}
}
@@ -245,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);
+ // 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);
@@ -258,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)) {
@@ -268,7 +241,11 @@ 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
- throw new Forbidden('No read permissions');
+ 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');
+ }
}
$folderContent = $this->getNode()->getDirectoryListing();
} catch (LockedException $e) {
@@ -276,8 +253,11 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
}
$nodes = [];
+ $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);
+ $node = $this->getChild($info->getName(), $info, $request, $l10n);
$nodes[] = $node;
}
$this->dirContent = $nodes;
@@ -326,21 +306,29 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
}
}
+ private function getLogger(): LoggerInterface {
+ return Server::get(LoggerInterface::class);
+ }
+
/**
* Returns available diskspace information
*
* @return array
*/
public function getQuotaInfo() {
- /** @var LoggerInterface $logger */
- $logger = \OC::$server->get(LoggerInterface::class);
if ($this->quotaInfo) {
return $this->quotaInfo;
}
+ $relativePath = $this->fileView->getRelativePath($this->info->getPath());
+ if ($relativePath === null) {
+ $this->getLogger()->warning('error while getting quota as the relative path cannot be found');
+ return [0, 0];
+ }
+
try {
- $storageInfo = \OC_Helper::getStorageInfo($this->info->getPath(), $this->info, false);
- if ($storageInfo['quota'] === \OCP\Files\FileInfo::SPACE_UNLIMITED) {
- $free = \OCP\Files\FileInfo::SPACE_UNLIMITED;
+ $storageInfo = \OC_Helper::getStorageInfo($relativePath, $this->info, false);
+ if ($storageInfo['quota'] === FileInfo::SPACE_UNLIMITED) {
+ $free = FileInfo::SPACE_UNLIMITED;
} else {
$free = $storageInfo['free'];
}
@@ -349,14 +337,14 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
$free
];
return $this->quotaInfo;
- } catch (\OCP\Files\NotFoundException $e) {
- $logger->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) {
- $logger->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) {
- $logger->warning("error while getting quota into", ['exception' => $e]);
+ $this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
return [0, 0];
}
}
@@ -396,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;
@@ -417,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;
@@ -457,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);
}
@@ -470,20 +454,34 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
public function copyInto($targetName, $sourcePath, INode $sourceNode) {
if ($sourceNode instanceof File || $sourceNode instanceof Directory) {
- $destinationPath = $this->getPath() . '/' . $targetName;
- $sourcePath = $sourceNode->getPath();
+ try {
+ $destinationPath = $this->getPath() . '/' . $targetName;
+ $sourcePath = $sourceNode->getPath();
- if (!$this->fileView->isCreatable($this->getPath())) {
- throw new \Sabre\DAV\Exception\Forbidden();
- }
+ if (!$this->fileView->isCreatable($this->getPath())) {
+ throw new \Sabre\DAV\Exception\Forbidden();
+ }
- try {
- $this->fileView->verifyPath($this->getPath(), $targetName);
- } catch (InvalidPathException $ex) {
- throw new InvalidPath($ex->getMessage());
- }
+ try {
+ $this->fileView->verifyPath($this->getPath(), $targetName);
+ } catch (InvalidPathException $ex) {
+ throw new InvalidPath($ex->getMessage());
+ }
+
+ $copyOkay = $this->fileView->copy($sourcePath, $destinationPath);
- return $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(), $e->getCode(), $e);
+ } catch (ForbiddenException $ex) {
+ throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
+ } catch (LockedException $e) {
+ throw new FileLocked($e->getMessage(), $e->getCode(), $e);
+ }
}
return false;
diff --git a/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php b/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php
index 3a8469e99b4..f6baceb748b 100644
--- a/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php
+++ b/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php
@@ -1,31 +1,14 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wickert <cwickert@suse.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
+use OCP\AppFramework\Http;
+use Sabre\DAV\Server;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
@@ -43,8 +26,7 @@ use Sabre\HTTP\ResponseInterface;
* @package OCA\DAV\Connector\Sabre
*/
class DummyGetResponsePlugin extends \Sabre\DAV\ServerPlugin {
- /** @var \Sabre\DAV\Server */
- protected $server;
+ protected ?Server $server = null;
/**
* @param \Sabre\DAV\Server $server
@@ -61,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.';
- $stream = fopen('php://memory','r+');
+ $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 c4cd6db190a..1e1e4aaed04 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php
@@ -1,23 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me>
- *
- * @author Louis Chemineau <louis@chmn.me>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre\Exception;
diff --git a/apps/dav/lib/Connector/Sabre/Exception/EntityTooLarge.php b/apps/dav/lib/Connector/Sabre/Exception/EntityTooLarge.php
index 4fc3399ca81..60b3b06ea01 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/EntityTooLarge.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/EntityTooLarge.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre\Exception;
diff --git a/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php b/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php
index 36063da8d65..38708e945e9 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php
@@ -1,36 +1,22 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Owen Winkler <a_github@midnightcircus.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre\Exception;
use Exception;
+use OCP\Files\LockNotAcquiredException;
class FileLocked extends \Sabre\DAV\Exception {
- public function __construct($message = "", $code = 0, Exception $previous = null) {
- if ($previous instanceof \OCP\Files\LockNotAcquiredException) {
+ /**
+ * @param string $message
+ * @param int $code
+ */
+ 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 aabd5fda2fb..95d4b3ab514 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php
@@ -1,24 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre\Exception;
@@ -26,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;
}
/**
@@ -48,17 +31,17 @@ class Forbidden extends \Sabre\DAV\Exception\Forbidden {
* @param \DOMElement $errorNode
* @return void
*/
- public function serialize(\Sabre\DAV\Server $server,\DOMElement $errorNode) {
+ public function serialize(\Sabre\DAV\Server $server, \DOMElement $errorNode) {
// set ownCloud namespace
$errorNode->setAttribute('xmlns:o', self::NS_OWNCLOUD);
// adding the retry node
- $error = $errorNode->ownerDocument->createElementNS('o:','o:retry', var_export($this->retry, true));
+ $error = $errorNode->ownerDocument->createElementNS('o:', 'o:retry', var_export($this->retry, true));
$errorNode->appendChild($error);
// adding the message node
- $error = $errorNode->ownerDocument->createElementNS('o:','o:reason', $this->getMessage());
+ $error = $errorNode->ownerDocument->createElementNS('o:', 'o:reason', $this->getMessage());
$errorNode->appendChild($error);
}
}
diff --git a/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php b/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php
index c504483d45a..dfc08aa8b88 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre\Exception;
@@ -29,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;
}
/**
@@ -60,17 +42,17 @@ class InvalidPath extends Exception {
* @param \DOMElement $errorNode
* @return void
*/
- public function serialize(\Sabre\DAV\Server $server,\DOMElement $errorNode) {
+ public function serialize(\Sabre\DAV\Server $server, \DOMElement $errorNode) {
// set ownCloud namespace
$errorNode->setAttribute('xmlns:o', self::NS_OWNCLOUD);
// adding the retry node
- $error = $errorNode->ownerDocument->createElementNS('o:','o:retry', var_export($this->retry, true));
+ $error = $errorNode->ownerDocument->createElementNS('o:', 'o:retry', var_export($this->retry, true));
$errorNode->appendChild($error);
// adding the message node
- $error = $errorNode->ownerDocument->createElementNS('o:','o:reason', $this->getMessage());
+ $error = $errorNode->ownerDocument->createElementNS('o:', 'o:reason', $this->getMessage());
$errorNode->appendChild($error);
}
}
diff --git a/apps/dav/lib/Connector/Sabre/Exception/PasswordLoginForbidden.php b/apps/dav/lib/Connector/Sabre/Exception/PasswordLoginForbidden.php
index 7c00d15f627..f5cc117fafc 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/PasswordLoginForbidden.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/PasswordLoginForbidden.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre\Exception;
diff --git a/apps/dav/lib/Connector/Sabre/Exception/TooManyRequests.php b/apps/dav/lib/Connector/Sabre/Exception/TooManyRequests.php
new file mode 100644
index 00000000000..67455fc9474
--- /dev/null
+++ b/apps/dav/lib/Connector/Sabre/Exception/TooManyRequests.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Connector\Sabre\Exception;
+
+use DOMElement;
+use Sabre\DAV\Exception\NotAuthenticated;
+use Sabre\DAV\Server;
+
+class TooManyRequests extends NotAuthenticated {
+ public const NS_OWNCLOUD = 'http://owncloud.org/ns';
+
+ public function getHTTPCode() {
+ return 429;
+ }
+
+ /**
+ * This method allows the exception to include additional information
+ * into the WebDAV error response
+ *
+ * @param Server $server
+ * @param DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(Server $server, DOMElement $errorNode) {
+
+ // set ownCloud namespace
+ $errorNode->setAttribute('xmlns:o', self::NS_OWNCLOUD);
+
+ $error = $errorNode->ownerDocument->createElementNS('o:', 'o:hint', 'too many requests');
+ $errorNode->appendChild($error);
+ }
+}
diff --git a/apps/dav/lib/Connector/Sabre/Exception/UnsupportedMediaType.php b/apps/dav/lib/Connector/Sabre/Exception/UnsupportedMediaType.php
index a7e935d2497..c5fbfa3a16c 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/UnsupportedMediaType.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/UnsupportedMediaType.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre\Exception;
diff --git a/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php b/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php
index b4df1f582db..686386dbfef 100644
--- a/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php
@@ -1,36 +1,17 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
use OCA\DAV\Connector\Sabre\Exception\PasswordLoginForbidden;
+use OCA\DAV\Exception\ServerMaintenanceMode;
use OCP\Files\StorageNotAvailableException;
-use OCP\ILogger;
+use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Conflict;
use Sabre\DAV\Exception\Forbidden;
@@ -41,7 +22,6 @@ use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\Exception\NotImplemented;
use Sabre\DAV\Exception\PreconditionFailed;
use Sabre\DAV\Exception\RequestedRangeNotSatisfiable;
-use Sabre\DAV\Exception\ServiceUnavailable;
class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin {
protected $nonFatalExceptions = [
@@ -65,6 +45,9 @@ class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin {
// forbidden can be expected when trying to upload to
// read-only folders for example
Forbidden::class => true,
+ // our forbidden is expected when access control is blocking
+ // an item in a folder
+ \OCA\DAV\Connector\Sabre\Exception\Forbidden::class => true,
// Happens when an external storage or federated share is temporarily
// not available
StorageNotAvailableException::class => true,
@@ -81,21 +64,16 @@ class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin {
FileLocked::class => true,
// An invalid range is requested
RequestedRangeNotSatisfiable::class => true,
+ ServerMaintenanceMode::class => true,
];
- /** @var string */
- private $appName;
-
- /** @var ILogger */
- private $logger;
-
/**
- * @param string $loggerAppName app name to use when logging
- * @param ILogger $logger
+ * @param string $appName app name to use when logging
*/
- public function __construct($loggerAppName, $logger) {
- $this->appName = $loggerAppName;
- $this->logger = $logger;
+ public function __construct(
+ private string $appName,
+ private LoggerInterface $logger,
+ ) {
}
/**
@@ -115,23 +93,20 @@ class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin {
/**
* Log exception
- *
*/
public function logException(\Throwable $ex) {
$exceptionClass = get_class($ex);
- $level = ILogger::FATAL;
- if (isset($this->nonFatalExceptions[$exceptionClass]) ||
- (
- $exceptionClass === ServiceUnavailable::class &&
- $ex->getMessage() === 'System in maintenance mode.'
- )
- ) {
- $level = ILogger::DEBUG;
+ if (isset($this->nonFatalExceptions[$exceptionClass])) {
+ $this->logger->debug($ex->getMessage(), [
+ 'app' => $this->appName,
+ 'exception' => $ex,
+ ]);
+ return;
}
- $this->logger->logException($ex, [
+ $this->logger->critical($ex->getMessage(), [
'app' => $this->appName,
- 'level' => $level,
+ 'exception' => $ex,
]);
}
}
diff --git a/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php b/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php
index d05cd6d2e6d..b0c5a079ce1 100644
--- a/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php
@@ -1,33 +1,17 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
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;
@@ -47,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);
@@ -90,7 +74,7 @@ class FakeLockerPlugin extends ServerPlugin {
*/
public function propFind(PropFind $propFind, INode $node) {
$propFind->handle('{DAV:}supportedlock', function () {
- return new SupportedLock(true);
+ return new SupportedLock();
});
$propFind->handle('{DAV:}lockdiscovery', function () use ($propFind) {
return new LockDiscovery([]);
@@ -108,7 +92,7 @@ class FakeLockerPlugin extends ServerPlugin {
if (isset($fileCondition['tokens'])) {
foreach ($fileCondition['tokens'] as &$token) {
if (isset($token['token'])) {
- if (substr($token['token'], 0, 16) === 'opaquelocktoken:') {
+ if (str_starts_with($token['token'], 'opaquelocktoken:')) {
$token['validToken'] = true;
}
}
@@ -125,19 +109,19 @@ class FakeLockerPlugin extends ServerPlugin {
* @return bool
*/
public function fakeLockProvider(RequestInterface $request,
- ResponseInterface $response) {
+ ResponseInterface $response) {
$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;
@@ -151,8 +135,8 @@ class FakeLockerPlugin extends ServerPlugin {
* @return bool
*/
public function fakeUnlockProvider(RequestInterface $request,
- ResponseInterface $response) {
- $response->setStatus(204);
+ ResponseInterface $response) {
+ $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 6c379984995..d2a71eb3e7b 100644
--- a/apps/dav/lib/Connector/Sabre/File.php
+++ b/apps/dav/lib/Connector/Sabre/File.php
@@ -1,40 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
- * @author Jakob Sack <mail@jakobsack.de>
- * @author Jan-Philipp Litza <jplitza@users.noreply.github.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Owen Winkler <a_github@midnightcircus.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Semih Serhat Karakaya <karakayasemi@itu.edu.tr>
- * @author Stefan Schneider <stefan.schneider@squareweave.com.au>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
@@ -43,67 +12,71 @@ use OC\AppFramework\Http\Request;
use OC\Files\Filesystem;
use OC\Files\Stream\HashWrapper;
use OC\Files\View;
-use OC\Metadata\FileMetadata;
use OCA\DAV\AppInfo\Application;
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 OCA\DAV\Connector\Sabre\Exception\BadGateway;
+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\ILogger;
+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;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
-use Sabre\DAV\Exception\NotImplemented;
use Sabre\DAV\Exception\ServiceUnavailable;
use Sabre\DAV\IFile;
class File extends Node implements IFile {
- protected $request;
-
+ protected IRequest $request;
protected IL10N $l10n;
- /** @var array<string, FileMetadata> */
- private array $metadata = [];
-
/**
* Sets up the node, expects a full path name
*
- * @param \OC\Files\View $view
- * @param \OCP\Files\FileInfo $info
- * @param \OCP\Share\IManager $shareManager
- * @param \OC\AppFramework\Http\Request $request
+ * @param View $view
+ * @param FileInfo $info
+ * @param ?\OCP\Share\IManager $shareManager
+ * @param ?IRequest $request
+ * @param ?IL10N $l10n
*/
- public function __construct(View $view, FileInfo $info, IManager $shareManager = null, Request $request = null) {
+ public function __construct(View $view, FileInfo $info, ?IManager $shareManager = null, ?IRequest $request = null, ?IL10N $l10n = null) {
parent::__construct($view, $info, $shareManager);
- // Querying IL10N directly results in a dependency loop
- /** @var IL10NFactory $l10nFactory */
- $l10nFactory = \OC::$server->get(IL10NFactory::class);
- $this->l10n = $l10nFactory->get(Application::APP_ID);
+ if ($l10n) {
+ $this->l10n = $l10n;
+ } else {
+ // Querying IL10N directly results in a dependency loop
+ /** @var IL10NFactory $l10nFactory */
+ $l10nFactory = Server::get(IL10NFactory::class);
+ $this->l10n = $l10nFactory->get(Application::APP_ID);
+ }
if (isset($request)) {
$this->request = $request;
} else {
- $this->request = \OC::$server->getRequest();
+ $this->request = Server::get(IRequest::class);
}
}
@@ -124,7 +97,7 @@ class File extends Node implements IFile {
* different object on a subsequent GET you are strongly recommended to not
* return an ETag, and just return null.
*
- * @param resource $data
+ * @param resource|string $data
*
* @throws Forbidden
* @throws UnsupportedMediaType
@@ -138,7 +111,7 @@ class File extends Node implements IFile {
public function put($data) {
try {
$exists = $this->fileView->file_exists($this->path);
- if ($this->info && $exists && !$this->info->isUpdateable()) {
+ if ($exists && !$this->info->isUpdateable()) {
throw new Forbidden();
}
} catch (StorageNotAvailableException $e) {
@@ -148,24 +121,18 @@ class File extends Node implements IFile {
// verify path of the target
$this->verifyPath();
- // chunked handling
- if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
- try {
- return $this->createFileChunked($data);
- } catch (\Exception $e) {
- $this->convertToSabreException($e);
- }
- }
-
- /** @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;
@@ -181,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 {
@@ -215,93 +183,97 @@ class File extends Node implements IFile {
$data = $tmpData;
}
- $data = HashWrapper::wrap($data, 'md5', function ($hash) {
- $this->header('X-Hash-MD5: ' . $hash);
- });
- $data = HashWrapper::wrap($data, 'sha1', function ($hash) {
- $this->header('X-Hash-SHA1: ' . $hash);
- });
- $data = HashWrapper::wrap($data, 'sha256', function ($hash) {
- $this->header('X-Hash-SHA256: ' . $hash);
- });
-
- if ($partStorage->instanceOfStorage(Storage\IWriteStreamStorage::class)) {
- $isEOF = false;
- $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF) {
- $isEOF = feof($stream);
- });
+ if ($this->request->getHeader('X-HASH') !== '') {
+ $hash = $this->request->getHeader('X-HASH');
+ if ($hash === 'all' || $hash === 'md5') {
+ $data = HashWrapper::wrap($data, 'md5', function ($hash): void {
+ $this->header('X-Hash-MD5: ' . $hash);
+ });
+ }
- $result = true;
- $count = -1;
- try {
- $count = $partStorage->writeStream($internalPartPath, $wrappedData);
- } catch (GenericFileException $e) {
- $result = false;
- } catch (BadGateway $e) {
- throw $e;
+ if ($hash === 'all' || $hash === 'sha1') {
+ $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): void {
+ $this->header('X-Hash-SHA256: ' . $hash);
+ });
+ }
+ }
+
+ $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): void {
+ $isEOF = feof($stream);
+ });
- 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->getLogger()->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;
- if (isset($_SERVER['CONTENT_LENGTH'])) {
- $expected = $_SERVER['CONTENT_LENGTH'];
- }
- 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
- if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
- $expected = (int)$_SERVER['CONTENT_LENGTH'];
- 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) {
- $context = [];
-
if ($e instanceof LockedException) {
- $context['level'] = ILogger::DEBUG;
+ Server::get(LoggerInterface::class)->debug($e->getMessage(), ['exception' => $e]);
+ } else {
+ Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
}
- \OC::$server->getLogger()->logException($e, $context);
if ($needsPartFile) {
$partStorage->unlink($internalPartPath);
}
@@ -340,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->getLogger()->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) {
@@ -364,8 +336,9 @@ class File extends Node implements IFile {
}
// allow sync clients to send the mtime along in a header
- if (isset($this->request->server['HTTP_X_OC_MTIME'])) {
- $mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']);
+ $mtimeHeader = $this->request->getHeader('x-oc-mtime');
+ if ($mtimeHeader !== '') {
+ $mtime = $this->sanitizeMtime($mtimeHeader);
if ($this->fileView->touch($this->path, $mtime)) {
$this->header('X-OC-MTime: accepted');
}
@@ -376,8 +349,9 @@ class File extends Node implements IFile {
];
// allow sync clients to send the creation time along in a header
- if (isset($this->request->server['HTTP_X_OC_CTIME'])) {
- $ctime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_CTIME']);
+ $ctimeHeader = $this->request->getHeader('x-oc-ctime');
+ if ($ctimeHeader) {
+ $ctime = $this->sanitizeMtime($ctimeHeader);
$fileInfoUpdate['creation_time'] = $ctime;
$this->header('X-OC-CTime: accepted');
}
@@ -390,8 +364,9 @@ class File extends Node implements IFile {
$this->refreshInfo();
- if (isset($this->request->server['HTTP_OC_CHECKSUM'])) {
- $checksum = trim($this->request->server['HTTP_OC_CHECKSUM']);
+ $checksumHeader = $this->request->getHeader('oc-checksum');
+ if ($checksumHeader) {
+ $checksum = trim($checksumHeader);
$this->setChecksum($checksum);
} elseif ($this->getChecksum() !== null && $this->getChecksum() !== '') {
$this->setChecksum('');
@@ -404,61 +379,68 @@ 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);
}
}
- /**
- * @param string $path
- */
- private function emitPreHooks($exists, $path = null) {
+ private function emitPreHooks(bool $exists, ?string $path = null): bool {
if (is_null($path)) {
$path = $this->path;
}
$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
+ if ($hookPath === null) {
+ // We only trigger hooks from inside default view
+ return true;
+ }
$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;
}
- /**
- * @param string $path
- */
- private function emitPostHooks($exists, $path = null) {
+ private function emitPostHooks(bool $exists, ?string $path = null): void {
if (is_null($path)) {
$path = $this->path;
}
$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
+ if ($hookPath === null) {
+ // We only trigger hooks from inside default view
+ 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
]);
}
@@ -476,14 +458,31 @@ 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 = Server::get(LoggerInterface::class);
+ $logger->warning('fixing cached size of file id=' . $this->getId());
+
+ $this->getFileInfo()->getStorage()->getUpdater()->update($this->getFileInfo()->getInternalPath());
+ $this->refreshInfo();
+ }
+
return $res;
} catch (GenericEncryptionException $e) {
// returning 503 will allow retry of the operation at a later point in time
@@ -533,20 +532,19 @@ class File extends Node implements IFile {
$mimeType = $this->info->getMimetype();
// PROPFIND needs to return the correct mime type, for consistency with the web UI
- if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND') {
+ 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\App::isEnabled('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 [];
@@ -556,132 +554,6 @@ class File extends Node implements IFile {
}
/**
- * @param resource $data
- * @return null|string
- * @throws Exception
- * @throws BadRequest
- * @throws NotImplemented
- * @throws ServiceUnavailable
- */
- private function createFileChunked($data) {
- [$path, $name] = \Sabre\Uri\split($this->path);
-
- $info = \OC_FileChunking::decodeName($name);
- if (empty($info)) {
- throw new NotImplemented($this->l10n->t('Invalid chunk name'));
- }
-
- $chunk_handler = new \OC_FileChunking($info);
- $bytesWritten = $chunk_handler->store($info['index'], $data);
-
- //detect aborted upload
- if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
- if (isset($_SERVER['CONTENT_LENGTH'])) {
- $expected = (int)$_SERVER['CONTENT_LENGTH'];
- if ($bytesWritten !== $expected) {
- $chunk_handler->remove($info['index']);
- 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', $bytesWritten),
- ],
- )
- );
- }
- }
- }
-
- if ($chunk_handler->isComplete()) {
- /** @var Storage $storage */
- [$storage,] = $this->fileView->resolvePath($path);
- $needsPartFile = $storage->needsPartFile();
- $partFile = null;
-
- $targetPath = $path . '/' . $info['name'];
- /** @var \OC\Files\Storage\Storage $targetStorage */
- [$targetStorage, $targetInternalPath] = $this->fileView->resolvePath($targetPath);
-
- $exists = $this->fileView->file_exists($targetPath);
-
- try {
- $this->fileView->lockFile($targetPath, ILockingProvider::LOCK_SHARED);
-
- $this->emitPreHooks($exists, $targetPath);
- $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
- /** @var \OC\Files\Storage\Storage $targetStorage */
- [$targetStorage, $targetInternalPath] = $this->fileView->resolvePath($targetPath);
-
- if ($needsPartFile) {
- // we first assembly the target file as a part file
- $partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part';
- /** @var \OC\Files\Storage\Storage $targetStorage */
- [$partStorage, $partInternalPath] = $this->fileView->resolvePath($partFile);
-
-
- $chunk_handler->file_assemble($partStorage, $partInternalPath);
-
- // here is the final atomic rename
- $renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath);
- $fileExists = $targetStorage->file_exists($targetInternalPath);
- if ($renameOkay === false || $fileExists === false) {
- \OC::$server->getLogger()->error('\OC\Files\Filesystem::rename() failed', ['app' => 'webdav']);
- // only delete if an error occurred and the target file was already created
- if ($fileExists) {
- // set to null to avoid double-deletion when handling exception
- // stray part file
- $partFile = null;
- $targetStorage->unlink($targetInternalPath);
- }
- $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
- throw new Exception($this->l10n->t('Could not rename part file assembled from chunks'));
- }
- } else {
- // assemble directly into the final file
- $chunk_handler->file_assemble($targetStorage, $targetInternalPath);
- }
-
- // allow sync clients to send the mtime along in a header
- if (isset($this->request->server['HTTP_X_OC_MTIME'])) {
- $mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']);
- if ($targetStorage->touch($targetInternalPath, $mtime)) {
- $this->header('X-OC-MTime: accepted');
- }
- }
-
- // since we skipped the view we need to scan and emit the hooks ourselves
- $targetStorage->getUpdater()->update($targetInternalPath);
-
- $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
-
- $this->emitPostHooks($exists, $targetPath);
-
- // FIXME: should call refreshInfo but can't because $this->path is not the of the final file
- $info = $this->fileView->getFileInfo($targetPath);
-
- if (isset($this->request->server['HTTP_OC_CHECKSUM'])) {
- $checksum = trim($this->request->server['HTTP_OC_CHECKSUM']);
- $this->fileView->putFileInfo($targetPath, ['checksum' => $checksum]);
- } elseif ($info->getChecksum() !== null && $info->getChecksum() !== '') {
- $this->fileView->putFileInfo($this->path, ['checksum' => '']);
- }
-
- $this->fileView->unlockFile($targetPath, ILockingProvider::LOCK_SHARED);
-
- return $info->getEtag();
- } catch (\Exception $e) {
- if ($partFile !== null) {
- $targetStorage->unlink($targetInternalPath);
- }
- $this->convertToSabreException($e);
- }
- }
-
- return null;
- }
-
- /**
* Convert the given exception to a SabreException instance
*
* @param \Exception $e
@@ -737,9 +609,6 @@ class File extends Node implements IFile {
* @return string|null
*/
public function getChecksum() {
- if (!$this->info) {
- return null;
- }
return $this->info->getChecksum();
}
@@ -761,16 +630,4 @@ class File extends Node implements IFile {
public function getNode(): \OCP\Files\File {
return $this->node;
}
-
- public function getMetadata(string $group): FileMetadata {
- return $this->metadata[$group];
- }
-
- public function setMetadata(string $group, FileMetadata $metadata): void {
- $this->metadata[$group] = $metadata;
- }
-
- public function hasMetadata(string $group) {
- return array_key_exists($group, $this->metadata);
- }
}
diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php
index 9c4f912610b..843383a0452 100644
--- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php
@@ -1,61 +1,45 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Michael Jobst <mjobst+github@tecratech.de>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Tobias Kaminsky <tobias@kaminsky.me>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
use OC\AppFramework\Http\Request;
-use OC\Metadata\IMetadataManager;
+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;
+use OCP\FilesMetadata\IFilesMetadataManager;
+use OCP\FilesMetadata\Model\IMetadataValueWrapper;
use OCP\IConfig;
use OCP\IPreview;
use OCP\IRequest;
use OCP\IUserSession;
-use Psr\Log\LoggerInterface;
+use OCP\L10N\IFactory;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\IFile;
use Sabre\DAV\PropFind;
use Sabre\DAV\PropPatch;
+use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\DAV\Tree;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
-use Sabre\Uri;
class FilesPlugin extends ServerPlugin {
-
// namespace
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
@@ -64,93 +48,55 @@ class FilesPlugin extends ServerPlugin {
public const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions';
public const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions';
public const OCM_SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-cloud-mesh.org/ns}share-permissions';
+ public const SHARE_ATTRIBUTES_PROPERTYNAME = '{http://nextcloud.org/ns}share-attributes';
public const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL';
public const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size';
public const GETETAG_PROPERTYNAME = '{DAV:}getetag';
public const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified';
public const CREATIONDATE_PROPERTYNAME = '{DAV:}creationdate';
+ public const DISPLAYNAME_PROPERTYNAME = '{DAV:}displayname';
public const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id';
public const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name';
public const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums';
public const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint';
public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview';
public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type';
- public const IS_ENCRYPTED_PROPERTYNAME = '{http://nextcloud.org/ns}is-encrypted';
+ public const MOUNT_ROOT_PROPERTYNAME = '{http://nextcloud.org/ns}is-mount-root';
+ 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_SIZE = '{http://nextcloud.org/ns}file-metadata-size';
-
- /**
- * Reference to main server object
- *
- * @var \Sabre\DAV\Server
- */
- private $server;
-
- /**
- * @var Tree
- */
- private $tree;
-
- /**
- * @var IUserSession
- */
- private $userSession;
-
- /**
- * Whether this is public webdav.
- * If true, some returned information will be stripped off.
- *
- * @var bool
- */
- private $isPublic;
+ public const FILE_METADATA_PREFIX = '{http://nextcloud.org/ns}metadata-';
+ public const HIDDEN_PROPERTYNAME = '{http://nextcloud.org/ns}hidden';
- /**
- * @var bool
- */
- private $downloadAttachment;
-
- /**
- * @var IConfig
- */
- private $config;
-
- /**
- * @var IRequest
- */
- private $request;
-
- /**
- * @var IPreview
- */
- private $previewManager;
+ /** Reference to main server object */
+ private ?Server $server = null;
/**
* @param Tree $tree
* @param IConfig $config
* @param IRequest $request
* @param IPreview $previewManager
- * @param bool $isPublic
+ * @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
*/
- public function __construct(Tree $tree,
- IConfig $config,
- IRequest $request,
- IPreview $previewManager,
- IUserSession $userSession,
- $isPublic = false,
- $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,
+ ) {
}
/**
@@ -161,10 +107,9 @@ class FilesPlugin extends ServerPlugin {
*
* This method should set up the required event subscriptions.
*
- * @param \Sabre\DAV\Server $server
* @return void
*/
- public function initialize(\Sabre\DAV\Server $server) {
+ public function initialize(Server $server) {
$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
$server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc';
$server->protectedProperties[] = self::FILEID_PROPERTYNAME;
@@ -172,6 +117,7 @@ class FilesPlugin extends ServerPlugin {
$server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME;
$server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME;
$server->protectedProperties[] = self::OCM_SHARE_PERMISSIONS_PROPERTYNAME;
+ $server->protectedProperties[] = self::SHARE_ATTRIBUTES_PROPERTYNAME;
$server->protectedProperties[] = self::SIZE_PROPERTYNAME;
$server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME;
$server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME;
@@ -180,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
@@ -194,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');
}
}
@@ -272,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(
[
@@ -288,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 !== '') {
@@ -308,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
@@ -347,7 +332,7 @@ class FilesPlugin extends ServerPlugin {
);
});
- $propFind->handle(self::OCM_SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
+ $propFind->handle(self::OCM_SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest): ?string {
$user = $this->userSession->getUser();
if ($user === null) {
return null;
@@ -356,14 +341,18 @@ class FilesPlugin extends ServerPlugin {
$user->getUID()
);
$ocmPermissions = $this->ncPermissions2ocmPermissions($ncPermissions);
- return json_encode($ocmPermissions);
+ return json_encode($ocmPermissions, JSON_THROW_ON_ERROR);
+ });
+
+ $propFind->handle(self::SHARE_ATTRIBUTES_PROPERTYNAME, function () use ($node, $httpRequest) {
+ return json_encode($node->getShareAttributes(), JSON_THROW_ON_ERROR);
});
- $propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node) {
+ $propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node): string {
return $node->getETag();
});
- $propFind->handle(self::OWNER_ID_PROPERTYNAME, function () use ($node) {
+ $propFind->handle(self::OWNER_ID_PROPERTYNAME, function () use ($node): ?string {
$owner = $node->getOwner();
if (!$owner) {
return null;
@@ -371,36 +360,79 @@ class FilesPlugin extends ServerPlugin {
return $owner->getUID();
}
});
- $propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function () use ($node) {
+ $propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function () use ($node): ?string {
$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) {
- return json_encode($this->previewManager->isAvailable($node->getFileInfo()));
+ return json_encode($this->previewManager->isAvailable($node->getFileInfo()), JSON_THROW_ON_ERROR);
});
- $propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) {
+ $propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node): int|float {
return $node->getSize();
});
$propFind->handle(self::MOUNT_TYPE_PROPERTYNAME, function () use ($node) {
return $node->getFileInfo()->getMountPoint()->getMountType();
});
- $propFind->handle(self::SHARE_NOTE, function () use ($node, $httpRequest) {
+ /**
+ * This is a special property which is used to determine if a node
+ * is a mount root or not, e.g. a shared folder.
+ * If so, then the node can only be unshared and not deleted.
+ * @see https://github.com/nextcloud/server/blob/cc75294eb6b16b916a342e69998935f89222619d/lib/private/Files/View.php#L696-L698
+ */
+ $propFind->handle(self::MOUNT_ROOT_PROPERTYNAME, function () use ($node) {
+ return $node->getNode()->getInternalPath() === '' ? 'true' : 'false';
+ });
+
+ $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) {
@@ -411,9 +443,34 @@ class FilesPlugin extends ServerPlugin {
$propFind->handle(self::CREATION_TIME_PROPERTYNAME, function () use ($node) {
return $node->getFileInfo()->getCreationTime();
});
+
+ foreach ($node->getFileInfo()->getMetadata() as $metadataKey => $metadataValue) {
+ $propFind->handle(self::FILE_METADATA_PREFIX . $metadataKey, $metadataValue);
+ }
+
+ $propFind->handle(self::HIDDEN_PROPERTYNAME, function () use ($node) {
+ $isLivePhoto = isset($node->getFileInfo()->getMetadata()['files-live-photo']);
+ $isMovFile = $node->getFileInfo()->getMimetype() === 'video/quicktime';
+ return ($isLivePhoto && $isMovFile) ? 'true' : 'false';
+ });
+
+ /**
+ * Return file/folder name as displayname. The primary reason to
+ * implement it this way is to avoid costly fallback to
+ * CustomPropertiesBackend (esp. visible when querying all files
+ * in a folder).
+ */
+ $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();
@@ -440,29 +497,6 @@ class FilesPlugin extends ServerPlugin {
$propFind->handle(self::UPLOAD_TIME_PROPERTYNAME, function () use ($node) {
return $node->getFileInfo()->getUploadTime();
});
-
- if ($this->config->getSystemValueBool('enable_file_metadata', true)) {
- $propFind->handle(self::FILE_METADATA_SIZE, function () use ($node) {
- if (!str_starts_with($node->getFileInfo()->getMimetype(), 'image')) {
- return json_encode((object)[]);
- }
-
- if ($node->hasMetadata('size')) {
- $sizeMetadata = $node->getMetadata('size');
- } else {
- // This code path should not be called since we try to preload
- // the metadata when loading the folder or the search results
- // in one go
- $metadataManager = \OC::$server->get(IMetadataManager::class);
- $sizeMetadata = $metadataManager->fetchMetadataFor('size', [$node->getId()])[$node->getId()];
-
- // TODO would be nice to display this in the profiler...
- \OC::$server->get(LoggerInterface::class)->debug('Inefficient fetching of metadata');
- }
-
- return json_encode((object)$sizeMetadata->getMetadata());
- });
- }
}
if ($node instanceof Directory) {
@@ -470,37 +504,8 @@ 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();
- // TODO detect dynamically which metadata groups are requested and
- // preload all of them and not just size
- if ($this->config->getSystemValueBool('enable_file_metadata', true)
- && in_array(self::FILE_METADATA_SIZE, $requestProperties, true)) {
- // Preloading of the metadata
- $fileIds = [];
- foreach ($node->getChildren() as $child) {
- /** @var \OCP\Files\Node|Node $child */
- if (str_starts_with($child->getFileInfo()->getMimeType(), 'image/')) {
- /** @var File $child */
- $fileIds[] = $child->getFileInfo()->getId();
- }
- }
- /** @var IMetaDataManager $metadataManager */
- $metadataManager = \OC::$server->get(IMetadataManager::class);
- $preloadedMetadata = $metadataManager->fetchMetadataFor('size', $fileIds);
- foreach ($node->getChildren() as $child) {
- /** @var \OCP\Files\Node|Node $child */
- if (str_starts_with($child->getFileInfo()->getMimeType(), 'image')) {
- /** @var File $child */
- $child->setMetadata('size', $preloadedMetadata[$child->getFileInfo()->getId()]);
- }
- }
- }
-
if (in_array(self::SUBFILE_COUNT_PROPERTYNAME, $requestProperties, true)
|| in_array(self::SUBFOLDER_COUNT_PROPERTYNAME, $requestProperties, true)) {
$nbFiles = 0;
@@ -536,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';
}
@@ -554,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;
}
@@ -569,10 +574,7 @@ class FilesPlugin extends ServerPlugin {
if (empty($etag)) {
return false;
}
- if ($node->setEtag($etag) !== -1) {
- return true;
- }
- return false;
+ return $node->setEtag($etag) !== -1;
});
$propPatch->handle(self::CREATIONDATE_PROPERTYNAME, function ($time) use ($node) {
if (empty($time)) {
@@ -586,36 +588,154 @@ class FilesPlugin extends ServerPlugin {
if (empty($time)) {
return false;
}
- $node->setCreationTime((int) $time);
+ $node->setCreationTime((int)$time);
return true;
});
+
+ $this->handleUpdatePropertiesMetadata($propPatch, $node);
+
+ /**
+ * Disable modification of the displayname property for files and
+ * folders via PROPPATCH. See PROPFIND for more information.
+ */
+ $propPatch->handle(self::DISPLAYNAME_PROPERTYNAME, function ($displayName) {
+ return 403;
+ });
}
+
/**
- * @param string $filePath
- * @param \Sabre\DAV\INode $node
- * @throws \Sabre\DAV\Exception\BadRequest
+ * handle the update of metadata from PROPPATCH requests
+ *
+ * @param PropPatch $propPatch
+ * @param Node $node
+ *
+ * @throws FilesMetadataException
*/
- public function sendFileIdHeader($filePath, \Sabre\DAV\INode $node = null) {
- // chunked upload handling
- if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
- [$path, $name] = \Sabre\Uri\split($filePath);
- $info = \OC_FileChunking::decodeName($name);
- if (!empty($info)) {
- $filePath = $path . '/' . $info['name'];
+ private function handleUpdatePropertiesMetadata(PropPatch $propPatch, Node $node): void {
+ $userId = $this->userSession->getUser()?->getUID();
+ if ($userId === null) {
+ return;
+ }
+
+ $accessRight = $this->getMetadataFileAccessRight($node, $userId);
+ $filesMetadataManager = $this->initFilesMetadataManager();
+ $knownMetadata = $filesMetadataManager->getKnownMetadata();
+
+ foreach ($propPatch->getRemainingMutations() as $mutation) {
+ if (!str_starts_with($mutation, self::FILE_METADATA_PREFIX)) {
+ continue;
}
+
+ $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
+ if ($knownMetadata->getEditPermission($metadataKey) < $accessRight) {
+ 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);
+ } catch (FilesMetadataNotFoundException) {
+ $type = IMetadataValueWrapper::TYPE_STRING;
+ }
+
+ switch ($type) {
+ case IMetadataValueWrapper::TYPE_STRING:
+ $metadata->setString($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
+ break;
+ case IMetadataValueWrapper::TYPE_INT:
+ $metadata->setInt($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
+ break;
+ case IMetadataValueWrapper::TYPE_FLOAT:
+ $metadata->setFloat($metadataKey, $value);
+ break;
+ case IMetadataValueWrapper::TYPE_BOOL:
+ $metadata->setBool($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
+ break;
+ case IMetadataValueWrapper::TYPE_ARRAY:
+ $metadata->setArray($metadataKey, $value);
+ break;
+ case IMetadataValueWrapper::TYPE_STRING_LIST:
+ $metadata->setStringList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
+ break;
+ case IMetadataValueWrapper::TYPE_INT_LIST:
+ $metadata->setIntList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
+ break;
+ }
+
+ $filesMetadataManager->saveMetadata($metadata);
+
+ return true;
+ }
+ );
}
+ }
- // 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;
+ /**
+ * init default internal metadata
+ *
+ * @return IFilesMetadataManager
+ */
+ private function initFilesMetadataManager(): IFilesMetadataManager {
+ /** @var IFilesMetadataManager $manager */
+ $manager = \OCP\Server::get(IFilesMetadataManager::class);
+ $manager->initMetadata('files-live-photo', IMetadataValueWrapper::TYPE_STRING, false, IMetadataValueWrapper::EDIT_REQ_OWNERSHIP);
+
+ return $manager;
+ }
+
+ /**
+ * based on owner and shares, returns the bottom limit to update related metadata
+ *
+ * @param Node $node
+ * @param string $userId
+ *
+ * @return int
+ */
+ private function getMetadataFileAccessRight(Node $node, string $userId): int {
+ if ($node->getOwner()?->getUID() === $userId) {
+ return IMetadataValueWrapper::EDIT_REQ_OWNERSHIP;
+ } else {
+ $filePermissions = $node->getSharePermissions($userId);
+ if ($filePermissions & Constants::PERMISSION_UPDATE) {
+ return IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION;
+ }
}
- $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);
+
+ return IMetadataValueWrapper::EDIT_REQ_READ_PERMISSION;
+ }
+
+ /**
+ * @param string $filePath
+ * @param ?\Sabre\DAV\INode $node
+ * @return void
+ * @throws \Sabre\DAV\Exception\BadRequest
+ */
+ 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
+ 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 4876e9ad8f3..b59d1373af5 100644
--- a/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php
@@ -1,35 +1,18 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- * @author Vinicius Cubas Brand <vinicius@eita.org.br>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
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;
use OCP\ITagManager;
use OCP\IUserSession;
@@ -45,9 +28,9 @@ use Sabre\DAV\Xml\Element\Response;
use Sabre\DAV\Xml\Response\MultiStatus;
class FilesReportPlugin extends ServerPlugin {
-
// namespace
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
+ public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
public const REPORT_NAME = '{http://owncloud.org/ns}filter-files';
public const SYSTEMTAG_PROPERTYNAME = '{http://owncloud.org/ns}systemtag';
public const CIRCLE_PROPERTYNAME = '{http://owncloud.org/ns}circle';
@@ -60,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
@@ -117,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;
}
/**
@@ -186,6 +117,7 @@ class FilesReportPlugin extends ServerPlugin {
}
$ns = '{' . $this::NS_OWNCLOUD . '}';
+ $ncns = '{' . $this::NS_NEXTCLOUD . '}';
$requestedProps = [];
$filterRules = [];
@@ -199,6 +131,14 @@ class FilesReportPlugin extends ServerPlugin {
foreach ($reportProps['value'] as $propVal) {
$requestedProps[] = $propVal['name'];
}
+ } elseif ($name === '{DAV:}limit') {
+ foreach ($reportProps['value'] as $propVal) {
+ if ($propVal['name'] === '{DAV:}nresults') {
+ $limit = (int)$propVal['value'];
+ } elseif ($propVal['name'] === $ncns . 'firstresult') {
+ $offset = (int)$propVal['value'];
+ }
+ }
}
}
@@ -209,13 +149,32 @@ class FilesReportPlugin extends ServerPlugin {
// gather all file ids matching filter
try {
- $resultFileIds = $this->processFilterRules($filterRules);
+ $resultFileIds = $this->processFilterRulesForFileIDs($filterRules);
+ // no logic in circles and favorites for paging, we always have all results, and slice later on
+ $resultFileIds = array_slice($resultFileIds, $offset ?? 0, $limit ?? null);
+ // fetching nodes has paging on DB level – therefore we cannot mix and slice the results, similar
+ // 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 = [];
+ foreach ($resultNodes as $entry) {
+ if ($entry) {
+ $results[] = $this->wrapNode($entry);
+ }
}
// find sabre nodes by file id, restricted to the root node path
- $results = $this->findNodesByFileIds($reportTargetNode, $resultFileIds);
+ $additionalNodes = $this->findNodesByFileIds($reportTargetNode, $resultFileIds);
+ if ($additionalNodes && $results) {
+ $results = array_uintersect($results, $additionalNodes, function (Node $a, Node $b): int {
+ return $a->getId() - $b->getId();
+ });
+ } elseif (!$results && $additionalNodes) {
+ $results = $additionalNodes;
+ }
$filesUri = $this->getFilesBaseUri($uri, $reportTargetNode->getPath());
$responses = $this->prepareResponses($filesUri, $requestedProps, $results);
@@ -225,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);
@@ -261,19 +220,13 @@ class FilesReportPlugin extends ServerPlugin {
*
* @param array $filterRules
* @return array array of unique file id results
- *
- * @throws TagNotFoundException whenever a tag was not found
*/
- protected function processFilterRules($filterRules) {
+ protected function processFilterRulesForFileIDs(array $filterRules): array {
$ns = '{' . $this::NS_OWNCLOUD . '}';
- $resultFileIds = null;
- $systemTagIds = [];
+ $resultFileIds = [];
$circlesIds = [];
$favoriteFilter = null;
foreach ($filterRules as $filterRule) {
- if ($filterRule['name'] === $ns . 'systemtag') {
- $systemTagIds[] = $filterRule['value'];
- }
if ($filterRule['name'] === self::CIRCLE_PROPERTYNAME) {
$circlesIds[] = $filterRule['value'];
}
@@ -289,15 +242,6 @@ class FilesReportPlugin extends ServerPlugin {
}
}
- if (!empty($systemTagIds)) {
- $fileIds = $this->getSystemTagFileIds($systemTagIds);
- if (empty($resultFileIds)) {
- $resultFileIds = $fileIds;
- } else {
- $resultFileIds = array_intersect($fileIds, $resultFileIds);
- }
- }
-
if (!empty($circlesIds)) {
$fileIds = $this->getCirclesFileIds($circlesIds);
if (empty($resultFileIds)) {
@@ -310,47 +254,46 @@ class FilesReportPlugin extends ServerPlugin {
return $resultFileIds;
}
- private function getSystemTagFileIds($systemTagIds) {
- $resultFileIds = null;
-
- // check user permissions, if applicable
- if (!$this->isAdmin()) {
- // check visibility/permission
- $tags = $this->tagManager->getTagsByIds($systemTagIds);
- $unknownTagIds = [];
- foreach ($tags as $tag) {
- if (!$tag->isUserVisible()) {
- $unknownTagIds[] = $tag->getId();
- }
- }
-
- if (!empty($unknownTagIds)) {
- throw new TagNotFoundException('Tag with ids ' . implode(', ', $unknownTagIds) . ' not found');
+ protected function processFilterRulesForFileNodes(array $filterRules, ?int $limit, ?int $offset): array {
+ $systemTagIds = [];
+ foreach ($filterRules as $filterRule) {
+ if ($filterRule['name'] === self::SYSTEMTAG_PROPERTYNAME) {
+ $systemTagIds[] = $filterRule['value'];
}
}
- // fetch all file ids and intersect them
- foreach ($systemTagIds as $systemTagId) {
- $fileIds = $this->tagMapper->getObjectIdsForTags($systemTagId, 'files');
+ $nodes = [];
- if (empty($fileIds)) {
- // This tag has no files, nothing can ever show up
- return [];
- }
+ if (!empty($systemTagIds)) {
+ $tags = $this->tagManager->getTagsByIds($systemTagIds, $this->userSession->getUser());
- // first run ?
- if ($resultFileIds === null) {
- $resultFileIds = $fileIds;
- } else {
- $resultFileIds = array_intersect($resultFileIds, $fileIds);
+ // For we run DB queries per tag and require intersection, we cannot apply limit and offset for DB queries on multi tag search.
+ $oneTagSearch = count($tags) === 1;
+ $dbLimit = $oneTagSearch ? $limit ?? 0 : 0;
+ $dbOffset = $oneTagSearch ? $offset ?? 0 : 0;
+
+ foreach ($tags as $tag) {
+ $tagName = $tag->getName();
+ $tmpNodes = $this->userFolder->searchBySystemTag($tagName, $this->userSession->getUser()->getUID(), $dbLimit, $dbOffset);
+ if (count($nodes) === 0) {
+ $nodes = $tmpNodes;
+ } else {
+ $nodes = array_uintersect($nodes, $tmpNodes, function (INode $a, INode $b): int {
+ return $a->getId() - $b->getId();
+ });
+ }
+ if ($nodes === []) {
+ // there cannot be a common match when nodes are empty early.
+ return $nodes;
+ }
}
- if (empty($resultFileIds)) {
- // Empty intersection, nothing can show up anymore
- return [];
+ if (!$oneTagSearch && ($limit !== null || $offset !== null)) {
+ $nodes = array_slice($nodes, $offset, $limit);
}
}
- return $resultFileIds;
+
+ return $nodes;
}
/**
@@ -362,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);
}
@@ -370,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[]
@@ -393,7 +336,6 @@ class FilesReportPlugin extends ServerPlugin {
$responses[] = new Response(
rtrim($this->server->getBaseUri(), '/') . $filesUri . $node->getPath(),
$result,
- 200
);
}
return $responses;
@@ -406,28 +348,35 @@ class FilesReportPlugin extends ServerPlugin {
* @param array $fileIds file ids
* @return Node[] array of Sabre nodes
*/
- public function findNodesByFileIds($rootNode, $fileIds) {
+ public function findNodesByFileIds(Node $rootNode, array $fileIds): array {
+ if (empty($fileIds)) {
+ return [];
+ }
$folder = $this->userFolder;
if (trim($rootNode->getPath(), '/') !== '') {
+ /** @var Folder $folder */
$folder = $folder->get($rootNode->getPath());
}
$results = [];
foreach ($fileIds as $fileId) {
- $entry = $folder->getById($fileId);
+ $entry = $folder->getFirstNodeById((int)$fileId);
if ($entry) {
- $entry = current($entry);
- if ($entry instanceof \OCP\Files\File) {
- $results[] = new File($this->fileView, $entry);
- } elseif ($entry instanceof \OCP\Files\Folder) {
- $results[] = new Directory($this->fileView, $entry);
- }
+ $results[] = $this->wrapNode($entry);
}
}
return $results;
}
+ protected function wrapNode(INode $node): File|Directory {
+ if ($node instanceof \OCP\Files\File) {
+ return new File($this->fileView, $node);
+ } else {
+ return new Directory($this->fileView, $node);
+ }
+ }
+
/**
* Returns whether the currently logged in user is an administrator
*/
diff --git a/apps/dav/lib/Connector/Sabre/LockPlugin.php b/apps/dav/lib/Connector/Sabre/LockPlugin.php
index 6305b0ec138..6640771dc31 100644
--- a/apps/dav/lib/Connector/Sabre/LockPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/LockPlugin.php
@@ -1,28 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Jaakko Salo <jaakkos@gmail.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Stefan Weil <sw@weilnetz.de>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
@@ -61,7 +42,7 @@ class LockPlugin extends ServerPlugin {
public function getLock(RequestInterface $request) {
// we can't listen on 'beforeMethod:PUT' due to order of operations with setting up the tree
// so instead we limit ourselves to the PUT method manually
- if ($request->getMethod() !== 'PUT' || isset($_SERVER['HTTP_OC_CHUNKED'])) {
+ if ($request->getMethod() !== 'PUT') {
return;
}
try {
@@ -84,7 +65,7 @@ class LockPlugin extends ServerPlugin {
if ($this->isLocked === false) {
return;
}
- if ($request->getMethod() !== 'PUT' || isset($_SERVER['HTTP_OC_CHUNKED'])) {
+ if ($request->getMethod() !== 'PUT') {
return;
}
try {
diff --git a/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php b/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php
index e7e3b273b98..d5ab7f09dfa 100644
--- a/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php
+++ b/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php
@@ -1,32 +1,13 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Valdnet <47037905+Valdnet@users.noreply.github.com>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
+use OCA\DAV\Exception\ServerMaintenanceMode;
use OCP\IConfig;
use OCP\IL10N;
use OCP\Util;
@@ -35,10 +16,7 @@ use Sabre\DAV\ServerPlugin;
class MaintenancePlugin extends ServerPlugin {
- /** @var IConfig */
- private $config;
-
- /** @var \OCP\IL10N */
+ /** @var IL10N */
private $l10n;
/**
@@ -51,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');
}
@@ -82,10 +62,10 @@ class MaintenancePlugin extends ServerPlugin {
*/
public function checkMaintenanceMode() {
if ($this->config->getSystemValueBool('maintenance')) {
- throw new ServiceUnavailable($this->l10n->t('System is in maintenance mode.'));
+ throw new ServerMaintenanceMode($this->l10n->t('System is in maintenance mode.'));
}
if (Util::needUpgrade()) {
- throw new ServiceUnavailable($this->l10n->t('Upgrade needed'));
+ throw new ServerMaintenanceMode($this->l10n->t('Upgrade needed'));
}
return true;
diff --git a/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php b/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php
index 6700b1eb81b..e18ef58149a 100644
--- a/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php
+++ b/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php
@@ -1,23 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me>
- *
- * @author Louis Chemineau <louis@chmn.me>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
diff --git a/apps/dav/lib/Connector/Sabre/Node.php b/apps/dav/lib/Connector/Sabre/Node.php
index e4517068f42..505e6b5eda4 100644
--- a/apps/dav/lib/Connector/Sabre/Node.php
+++ b/apps/dav/lib/Connector/Sabre/Node.php
@@ -1,37 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
- * @author Jakob Sack <mail@jakobsack.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Klaas Freitag <freitag@owncloud.com>
- * @author Markus Goetz <markus@woboq.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Tobias Kaminsky <tobias@kaminsky.me>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
@@ -40,20 +12,19 @@ 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\Share\IShare;
+use OCP\Server;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
abstract class Node implements \Sabre\DAV\INode {
-
- /**
- * @var \OC\Files\View
- */
- protected $fileView;
-
/**
* The path to the current node
*
@@ -68,10 +39,7 @@ abstract class Node implements \Sabre\DAV\INode {
*/
protected $property_cache = null;
- /**
- * @var \OCP\Files\FileInfo
- */
- protected $info;
+ protected FileInfo $info;
/**
* @var IManager
@@ -82,39 +50,45 @@ abstract class Node implements \Sabre\DAV\INode {
/**
* Sets up the node, expects a full path name
- *
- * @param \OC\Files\View $view
- * @param \OCP\Files\FileInfo $info
- * @param IManager $shareManager
*/
- 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 {
- $root = \OC::$server->get(IRootFolder::class);
+ // The Node API assumes that the view passed doesn't have a fake root
+ $rootView = Server::get(View::class);
+ $root = Server::get(IRootFolder::class);
if ($info->getType() === FileInfo::TYPE_FOLDER) {
- $this->node = new Folder($root, $view, $this->path, $info);
+ $this->node = new Folder($root, $rootView, $this->fileView->getAbsolutePath($this->path), $info);
} else {
- $this->node = new File($root, $view, $this->path, $info);
+ $this->node = new File($root, $rootView, $this->fileView->getAbsolutePath($this->path), $info);
}
}
}
- protected function refreshInfo() {
- $this->info = $this->fileView->getFileInfo($this->path);
- $root = \OC::$server->get(IRootFolder::class);
+ 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);
+ }
+ $this->info = $info;
+ $root = Server::get(IRootFolder::class);
+ $rootView = Server::get(View::class);
if ($this->info->getType() === FileInfo::TYPE_FOLDER) {
- $this->node = new Folder($root, $this->fileView, $this->path, $this->info);
+ $this->node = new Folder($root, $rootView, $this->path, $this->info);
} else {
- $this->node = new File($root, $this->fileView, $this->path, $this->info);
+ $this->node = new File($root, $rootView, $this->path, $this->info);
}
}
@@ -144,22 +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;
@@ -232,9 +205,10 @@ abstract class Node implements \Sabre\DAV\INode {
/**
* Returns the size of the node, in bytes
*
- * @return integer
+ * @psalm-suppress ImplementedReturnTypeMismatch \Sabre\DAV\IFile::getSize signature does not support 32bit
+ * @return int|float
*/
- public function getSize() {
+ public function getSize(): int|float {
return $this->info->getSize();
}
@@ -251,10 +225,8 @@ abstract class Node implements \Sabre\DAV\INode {
* @return string|null
*/
public function getFileId() {
- if ($this->info->getId()) {
- $instanceId = \OC_Util::getInstanceId();
- $id = sprintf('%08d', $this->info->getId());
- return $id . $instanceId;
+ if ($id = $this->info->getId()) {
+ return DavUtil::getDavFileId($id);
}
return null;
@@ -267,12 +239,15 @@ abstract class Node implements \Sabre\DAV\INode {
return $this->info->getId();
}
+ public function getInternalPath(): string {
+ return $this->info->getInternalPath();
+ }
+
/**
* @param string $user
* @return int
*/
public function getSharePermissions($user) {
-
// check of we access a federated share
if ($user !== null) {
try {
@@ -289,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();
@@ -303,98 +278,88 @@ abstract class Node implements \Sabre\DAV\INode {
$mountpoint = $this->info->getMountPoint();
if (!($mountpoint instanceof MoveableMount)) {
$mountpointpath = $mountpoint->getMountPoint();
- if (substr($mountpointpath, -1) === '/') {
+ if (str_ends_with($mountpointpath, '/')) {
$mountpointpath = substr($mountpointpath, 0, -1);
}
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;
}
/**
- * @param string $user
- * @return string
+ * @return array
*/
- public function getNoteFromShare($user) {
- if ($user === null) {
- return '';
+ public function getShareAttributes(): array {
+ try {
+ $storage = $this->node->getStorage();
+ } catch (NotFoundException $e) {
+ return [];
}
- $types = [
- IShare::TYPE_USER,
- IShare::TYPE_GROUP,
- IShare::TYPE_CIRCLE,
- IShare::TYPE_ROOM
- ];
-
- foreach ($types as $shareType) {
- $shares = $this->shareManager->getSharedWith($user, $shareType, $this, -1);
- foreach ($shares as $share) {
- $note = $share->getNote();
- if ($share->getShareOwner() !== $user && !empty($note)) {
- return $note;
- }
+ $attributes = [];
+ if ($storage->instanceOfStorage(ISharedStorage::class)) {
+ /** @var ISharedStorage $storage */
+ $attributes = $storage->getShare()->getAttributes();
+ if ($attributes === null) {
+ return [];
+ } else {
+ return $attributes->toArray();
+ }
+ }
+
+ return $attributes;
+ }
+
+ public function getNoteFromShare(?string $user): ?string {
+ try {
+ $storage = $this->node->getStorage();
+ } catch (NotFoundException) {
+ return null;
+ }
+
+ 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();
}
- return '';
+ return null;
}
/**
* @return string
*/
public function getDavPermissions() {
- $p = '';
- if ($this->info->isShared()) {
- $p .= 'S';
- }
- if ($this->info->isShareable()) {
- $p .= 'R';
- }
- if ($this->info->isMounted()) {
- $p .= 'M';
- }
- if ($this->info->isReadable()) {
- $p .= 'G';
- }
- if ($this->info->isDeletable()) {
- $p .= 'D';
- }
- if ($this->info->isUpdateable()) {
- $p .= 'NV'; // Renameable, Moveable
- }
- if ($this->info->getType() === \OCP\Files\FileInfo::TYPE_FILE) {
- if ($this->info->isUpdateable()) {
- $p .= 'W';
- }
- } else {
- if ($this->info->isCreatable()) {
- $p .= 'CK';
- }
- }
- return $p;
+ return DavUtil::getDavPermissions($this->info);
}
public function getOwner() {
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());
}
}
@@ -428,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 c129371e376..bfbdfb33db0 100644
--- a/apps/dav/lib/Connector/Sabre/ObjectTree.php
+++ b/apps/dav/lib/Connector/Sabre/ObjectTree.php
@@ -1,39 +1,22 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
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;
@@ -41,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;
@@ -58,45 +41,16 @@ 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;
}
/**
- * If the given path is a chunked file name, converts it
- * to the real file name. Only applies if the OC-CHUNKED header
- * is present.
- *
- * @param string $path chunk file path to convert
- *
- * @return string path to real file
- */
- private function resolveChunkFile($path) {
- if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
- // resolve to real file name to find the proper node
- [$dir, $name] = \Sabre\Uri\split($path);
- if ($dir === '/' || $dir === '.') {
- $dir = '';
- }
-
- $info = \OC_FileChunking::decodeName($name);
- // only replace path if it was really the chunked file
- if (isset($info['transferid'])) {
- // getNodePath is called for multiple nodes within a chunk
- // upload call
- $path = $dir . '/' . $info['name'];
- $path = ltrim($path, '/');
- }
- }
- return $path;
- }
-
- /**
* Returns the INode object for the requested path
*
* @param string $path
@@ -120,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());
}
}
@@ -138,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);
@@ -147,9 +101,6 @@ class ObjectTree extends CachingTree {
$info = null;
}
} else {
- // resolve chunk file name to real name, if applicable
- $path = $this->resolveChunkFile($path);
-
// read from cache
try {
$info = $this->fileView->getFileInfo($path);
@@ -173,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;
@@ -222,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 c3f06f95783..d6ea9fd887d 100644
--- a/apps/dav/lib/Connector/Sabre/Principal.php
+++ b/apps/dav/lib/Connector/Sabre/Principal.php
@@ -1,46 +1,21 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (c) 2018, Georg Ehrke
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Christoph Seitz <christoph.seitz@posteo.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Jakob Sack <mail@jakobsack.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Maxence Lange <maxence@artificial-owl.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- * @author Vinicius Cubas Brand <vinicius@eita.org.br>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
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;
+use OCP\Accounts\IAccountProperty;
+use OCP\Accounts\PropertyDoesNotExistException;
use OCP\App\IAppManager;
use OCP\AppFramework\QueryException;
use OCP\Constants;
@@ -58,21 +33,6 @@ use Sabre\DAVACL\PrincipalBackend\BackendInterface;
class Principal implements BackendInterface {
- /** @var IUserManager */
- private $userManager;
-
- /** @var IGroupManager */
- private $groupManager;
-
- /** @var IShareManager */
- private $shareManager;
-
- /** @var IUserSession */
- private $userSession;
-
- /** @var IAppManager */
- private $appManager;
-
/** @var string */
private $principalPrefix;
@@ -82,38 +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,
- IShareManager $shareManager,
- IUserSession $userSession,
- IAppManager $appManager,
- ProxyMapper $proxyMapper,
- KnownUserService $knownUserService,
- IConfig $config,
- IFactory $languageFactory,
- string $principalPrefix = 'principals/users/') {
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
- $this->shareManager = $shareManager;
- $this->userSession = $userSession;
- $this->appManager = $appManager;
+ 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,
+ 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 {
@@ -200,6 +147,16 @@ class Principal implements BackendInterface {
'{DAV:}displayname' => $group->getDisplayName(),
];
}
+ } elseif ($prefix === 'principals/system') {
+ return [
+ 'uri' => 'principals/system/' . $name,
+ '{DAV:}displayname' => $this->languageFactory->get('dav')->t('Accounts'),
+ ];
+ } elseif ($prefix === 'principals/shares') {
+ return [
+ 'uri' => 'principals/shares/' . $name,
+ '{DAV:}displayname' => $name,
+ ];
}
return null;
}
@@ -229,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());
}
}
@@ -247,6 +207,7 @@ class Principal implements BackendInterface {
* @return int
*/
public function updatePrincipal($path, PropPatch $propPatch) {
+ // Updating schedule-default-calendar-URL is handled in CustomPropertiesBackend
return 0;
}
@@ -270,6 +231,8 @@ class Principal implements BackendInterface {
$limitEnumerationGroup = $this->shareManager->limitEnumerationToGroups();
$limitEnumerationPhone = $this->shareManager->limitEnumerationToPhone();
$allowEnumerationFullMatch = $this->shareManager->allowEnumerationFullMatch();
+ $ignoreSecondDisplayName = $this->shareManager->ignoreSecondDisplayName();
+ $matchEmail = $this->shareManager->matchEmail();
// If sharing is restricted to group members only,
// return only members that have groups in common
@@ -298,7 +261,7 @@ class Principal implements BackendInterface {
switch ($prop) {
case '{http://sabredav.org/ns}email-address':
if (!$allowEnumeration) {
- if ($allowEnumerationFullMatch) {
+ if ($allowEnumerationFullMatch && $matchEmail) {
$users = $this->userManager->getByEmail($value);
} else {
$users = [];
@@ -349,8 +312,9 @@ class Principal implements BackendInterface {
if ($allowEnumerationFullMatch) {
$lowerSearch = strtolower($value);
$users = $this->userManager->searchDisplayName($value, $searchLimit);
- $users = \array_filter($users, static function (IUser $user) use ($lowerSearch) {
- return strtolower($user->getDisplayName()) === $lowerSearch;
+ $users = \array_filter($users, static function (IUser $user) use ($lowerSearch, $ignoreSecondDisplayName) {
+ $lowerDisplayName = strtolower($user->getDisplayName());
+ return $lowerDisplayName === $lowerSearch || ($ignoreSecondDisplayName && trim(preg_replace('/ \(.*\)$/', '', $lowerDisplayName)) === $lowerSearch);
});
} else {
$users = [];
@@ -471,7 +435,7 @@ class Principal implements BackendInterface {
$restrictGroups = $this->groupManager->getUserGroupIds($user);
}
- if (strpos($uri, 'mailto:') === 0) {
+ if (str_starts_with($uri, 'mailto:')) {
if ($principalPrefix === 'principals/users') {
$users = $this->userManager->getByEmail(substr($uri, 7));
if (count($users) !== 1) {
@@ -489,7 +453,7 @@ class Principal implements BackendInterface {
return $this->principalPrefix . '/' . $user->getUID();
}
}
- if (substr($uri, 0, 10) === 'principal:') {
+ if (str_starts_with($uri, 'principal:')) {
$principal = substr($uri, 10);
$principal = $this->getPrincipalByPath($principal);
if ($principal !== null) {
@@ -503,6 +467,7 @@ class Principal implements BackendInterface {
/**
* @param IUser $user
* @return array
+ * @throws PropertyDoesNotExistException
*/
protected function userToPrincipal($user) {
$userId = $user->getUID();
@@ -514,11 +479,18 @@ class Principal implements BackendInterface {
'{http://nextcloud.com/ns}language' => $this->languageFactory->getUserLanguage($user),
];
+ $account = $this->accountManager->getAccount($user);
+ $alternativeEmails = array_map(fn (IAccountProperty $property) => 'mailto:' . $property->getValue(), $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties());
+
$email = $user->getSystemEMailAddress();
if (!empty($email)) {
$principal['{http://sabredav.org/ns}email-address'] = $email;
}
+ if (!empty($alternativeEmails)) {
+ $principal['{DAV:}alternate-URI-set'] = $alternativeEmails;
+ }
+
return $principal;
}
@@ -536,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) {
@@ -561,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 {
@@ -576,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);
@@ -588,4 +560,44 @@ class Principal implements BackendInterface {
return [];
}
+
+ /**
+ * Get all email addresses associated to a principal.
+ *
+ * @param array $principal Data from getPrincipal*()
+ * @return string[] All email addresses without the mailto: prefix
+ */
+ public function getEmailAddressesOfPrincipal(array $principal): array {
+ $emailAddresses = [];
+
+ if (isset($principal['{http://sabredav.org/ns}email-address'])) {
+ $emailAddresses[] = $principal['{http://sabredav.org/ns}email-address'];
+ }
+
+ if (isset($principal['{DAV:}alternate-URI-set'])) {
+ foreach ($principal['{DAV:}alternate-URI-set'] as $address) {
+ if (str_starts_with($address, 'mailto:')) {
+ $emailAddresses[] = substr($address, 7);
+ }
+ }
+ }
+
+ if (isset($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'])) {
+ foreach ($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'] as $address) {
+ if (str_starts_with($address, 'mailto:')) {
+ $emailAddresses[] = substr($address, 7);
+ }
+ }
+ }
+
+ if (isset($principal['{http://calendarserver.org/ns/}email-address-set'])) {
+ foreach ($principal['{http://calendarserver.org/ns/}email-address-set'] as $address) {
+ if (str_starts_with($address, 'mailto:')) {
+ $emailAddresses[] = substr($address, 7);
+ }
+ }
+ }
+
+ return array_values(array_unique($emailAddresses));
+ }
}
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/PropfindCompressionPlugin.php b/apps/dav/lib/Connector/Sabre/PropfindCompressionPlugin.php
index d0251b2755f..15daf1f34b6 100644
--- a/apps/dav/lib/Connector/Sabre/PropfindCompressionPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/PropfindCompressionPlugin.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Connector\Sabre;
@@ -61,7 +44,7 @@ class PropfindCompressionPlugin extends ServerPlugin {
return $response;
}
- if (strpos($header, 'gzip') !== false) {
+ if (str_contains($header, 'gzip')) {
$body = $response->getBody();
if (is_string($body)) {
$response->setHeader('Content-Encoding', 'gzip');
diff --git a/apps/dav/lib/Connector/Sabre/PublicAuth.php b/apps/dav/lib/Connector/Sabre/PublicAuth.php
new file mode 100644
index 00000000000..2ca1c25e2f6
--- /dev/null
+++ b/apps/dav/lib/Connector/Sabre/PublicAuth.php
@@ -0,0 +1,227 @@
+<?php
+
+declare(strict_types=1);
+
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+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;
+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;
+use Sabre\HTTP\ResponseInterface;
+
+/**
+ * Class PublicAuth
+ *
+ * @package OCA\DAV\Connector
+ */
+class PublicAuth extends AbstractBasic {
+ private const BRUTEFORCE_ACTION = 'public_dav_auth';
+ public const DAV_AUTHENTICATED = 'public_link_authenticated';
+
+ private ?IShare $share = null;
+
+ 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 Defaults();
+ $this->realm = $defaults->getName();
+ }
+
+ /**
+ * @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,
+ $response
+ );
+
+ $userpass = $auth->getCredentials();
+ // If authentication provided, checking its validity
+ if ($userpass && !$this->validateUserPass($userpass[0], $userpass[1])) {
+ return [false, 'Username or password was incorrect'];
+ }
+
+ return $this->checkToken();
+ } 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);
+ $msg = $e->getMessage();
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
+ throw new ServiceUnavailable("$class: $msg");
+ }
+ }
+
+ /**
+ * Extract token from request url
+ * @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();
+ }
+
+ return $splittedPath[3];
+ }
+
+ /**
+ * Check token validity
+ *
+ * @throws NotFound
+ * @throws NotAuthenticated
+ */
+ private function checkToken(): array {
+ $token = $this->getToken();
+
+ try {
+ /** @var IShare $share */
+ $share = $this->shareManager->getShareByToken($token);
+ } catch (ShareNotFound $e) {
+ $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
+ throw new NotFound();
+ }
+
+ $this->share = $share;
+ \OC_User::setIncognitoMode(true);
+
+ // If already authenticated
+ if ($this->session->exists(self::DAV_AUTHENTICATED)
+ && $this->session->get(self::DAV_AUTHENTICATED) === $share->getId()) {
+ return [true, $this->principalPrefix . $token];
+ }
+
+ // If the share is protected but user is not authenticated
+ if ($share->getPassword() !== null) {
+ $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
+ throw new NotAuthenticated();
+ }
+
+ return [true, $this->principalPrefix . $token];
+ }
+
+ /**
+ * Validates a username and password
+ *
+ * This method should return true or false depending on if login
+ * succeeded.
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return bool
+ * @throws NotAuthenticated
+ */
+ protected function validateUserPass($username, $password) {
+ $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION);
+
+ try {
+ $share = $this->getShare();
+ } catch (ShareNotFound $e) {
+ $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
+ return false;
+ }
+
+ \OC_User::setIncognitoMode(true);
+
+ // check if the share is password protected
+ if ($share->getPassword() !== null) {
+ if ($share->getShareType() === IShare::TYPE_LINK
+ || $share->getShareType() === IShare::TYPE_EMAIL
+ || $share->getShareType() === IShare::TYPE_CIRCLE) {
+ if ($this->shareManager->checkPassword($share, $password)) {
+ // If not set, set authenticated session cookie
+ if (!$this->session->exists(self::DAV_AUTHENTICATED)
+ || $this->session->get(self::DAV_AUTHENTICATED) !== $share->getId()) {
+ $this->session->set(self::DAV_AUTHENTICATED, $share->getId());
+ }
+ return true;
+ }
+
+ if ($this->session->exists(PublicAuth::DAV_AUTHENTICATED)
+ && $this->session->get(PublicAuth::DAV_AUTHENTICATED) === $share->getId()) {
+ return true;
+ }
+
+ if (in_array('XMLHttpRequest', explode(',', $this->request->getHeader('X-Requested-With')))) {
+ // do not re-authenticate over ajax, use dummy auth name to prevent browser popup
+ http_response_code(401);
+ header('WWW-Authenticate: DummyBasic realm="' . $this->realm . '"');
+ throw new NotAuthenticated('Cannot authenticate over ajax calls');
+ }
+
+ $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
+ return false;
+ } elseif ($share->getShareType() === IShare::TYPE_REMOTE) {
+ return true;
+ }
+
+ $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
+ return false;
+ }
+
+ return true;
+ }
+
+ public function getShare(): IShare {
+ $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 f2b652e3320..bbb378edc9b 100644
--- a/apps/dav/lib/Connector/Sabre/QuotaPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/QuotaPlugin.php
@@ -1,40 +1,22 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (C) 2012 entreCables S.L. All rights reserved.
- * @copyright Copyright (C) 2012 entreCables S.L. All rights reserved.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Felix Moeller <mail@felixmoeller.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author scambra <sergio@entrecables.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-FileCopyrightText: 2012 entreCables S.L. All rights reserved
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
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.
@@ -44,10 +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
*
@@ -56,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,
+ ) {
}
/**
@@ -78,7 +57,9 @@ 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 +71,19 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
* @param bool $modified modified
*/
public function beforeCreateFile($uri, $data, INode $parent, $modified) {
+ $request = $this->server->httpRequest;
+ if ($parent instanceof UploadFolder && $request->getHeader('Destination')) {
+ // If chunked upload and Total-Length header is set, use that
+ // value for quota check. This allows us to also check quota while
+ // uploading chunks and not only when the file is assembled.
+ $length = $request->getHeader('OC-Total-Length');
+ $destinationPath = $this->server->calculateUri($request->getHeader('Destination'));
+ $quotaPath = $this->getPathForDestination($destinationPath);
+ if ($quotaPath && is_numeric($length)) {
+ return $this->checkQuota($quotaPath, (int)$length);
+ }
+ }
+
if (!$parent instanceof Node) {
return;
}
@@ -98,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
@@ -114,40 +133,76 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
}
/**
- * Check if we're moving a Futurefile in which case we need to check
+ * Check if we're moving a FutureFile in which case we need to check
* the quota on the target destination.
- *
- * @param string $source source path
- * @param string $destination destination path
*/
- public function beforeMove($source, $destination) {
- $sourceNode = $this->server->tree->getNodeForPath($source);
+ public function beforeMove(string $sourcePath, string $destinationPath): bool {
+ $sourceNode = $this->server->tree->getNodeForPath($sourcePath);
if (!$sourceNode instanceof FutureFile) {
- return;
+ return true;
}
- // get target node for proper path conversion
- if ($this->server->tree->nodeExists($destination)) {
- $destinationNode = $this->server->tree->getNodeForPath($destination);
- $path = $destinationNode->getPath();
- } else {
- $parentNode = $this->server->tree->getNodeForPath(dirname($destination));
- $path = $parentNode->getPath();
+ try {
+ // The final path is not known yet, we check the quota on the parent
+ $path = $this->getPathForDestination($destinationPath);
+ } catch (\Exception $e) {
+ return true;
}
return $this->checkQuota($path, $sourceNode->getSize());
}
+ /**
+ * Check quota on the target destination before a copy.
+ */
+ public function beforeCopy(string $sourcePath, string $destinationPath): bool {
+ $sourceNode = $this->server->tree->getNodeForPath($sourcePath);
+ if (!$sourceNode instanceof Node) {
+ return true;
+ }
+
+ try {
+ $path = $this->getPathForDestination($destinationPath);
+ } catch (\Exception $e) {
+ return true;
+ }
+
+ return $this->checkQuota($path, $sourceNode->getSize());
+ }
+
+ private function getPathForDestination(string $destinationPath): string {
+ // get target node for proper path conversion
+ if ($this->server->tree->nodeExists($destinationPath)) {
+ $destinationNode = $this->server->tree->getNodeForPath($destinationPath);
+ if (!$destinationNode instanceof Node) {
+ throw new \Exception('Invalid destination node');
+ }
+ return $destinationNode->getPath();
+ }
+
+ $parent = dirname($destinationPath);
+ if ($parent === '.') {
+ $parent = '';
+ }
+
+ $parentNode = $this->server->tree->getNodeForPath($parent);
+ if (!$parentNode instanceof Node) {
+ throw new \Exception('Invalid destination node');
+ }
+
+ return $parentNode->getPath();
+ }
+
/**
* This method is called before any HTTP method and validates there is enough free space to store the file
*
* @param string $path relative to the users home
- * @param int $length
+ * @param int|float|null $length
* @throws InsufficientStorage
* @return bool
*/
- public function checkQuota($path, $length = null) {
+ public function checkQuota(string $path, $length = null, $isDir = false) {
if ($length === null) {
$length = $this->getLength();
}
@@ -158,29 +213,21 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
$parentPath = '';
}
$req = $this->server->httpRequest;
- if ($req->getHeader('OC-Chunked')) {
- $info = \OC_FileChunking::decodeName($newName);
- $chunkHandler = $this->getFileChunking($info);
- // subtract the already uploaded size to see whether
- // there is still enough space for the remaining chunks
- $length -= $chunkHandler->getCurrentSize();
- // use target file name for free space check in case of shared files
- $path = rtrim($parentPath, '/') . '/' . $info['name'];
- }
+
+ // Strip any duplicate slashes
+ $path = str_replace('//', '/', $path);
+
$freeSpace = $this->getFreeSpace($path);
if ($freeSpace >= 0 && $length > $freeSpace) {
- if (isset($chunkHandler)) {
- $chunkHandler->cleanup();
+ 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");
}
}
- return true;
- }
- public function getFileChunking($info) {
- // FIXME: need a factory for better mocking support
- return new \OC_FileChunking($info);
+ return true;
}
public function getLength() {
@@ -192,11 +239,13 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
}
$ocLength = $req->getHeader('OC-Total-Length');
- if (is_numeric($length) && is_numeric($ocLength)) {
- return max($length, $ocLength);
+ if (!is_numeric($ocLength)) {
+ return $length;
}
-
- return $length;
+ if (!is_numeric($length)) {
+ return $ocLength;
+ }
+ return max($length, $ocLength);
}
/**
diff --git a/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php b/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php
index b281a1053b8..5484bab9237 100644
--- a/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php
@@ -1,22 +1,9 @@
-<?php declare(strict_types=1);
+<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Connector\Sabre;
@@ -26,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 6cf6fa954c8..dda9c29b763 100644
--- a/apps/dav/lib/Connector/Sabre/Server.php
+++ b/apps/dav/lib/Connector/Sabre/Server.php
@@ -1,30 +1,20 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author scolebrook <scolebrook@mac.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
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
*
@@ -36,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) {
@@ -43,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 b13dbd20ca9..a6a27057177 100644
--- a/apps/dav/lib/Connector/Sabre/ServerFactory.php
+++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php
@@ -1,137 +1,111 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
-use OCP\Files\Folder;
+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\ILogger;
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 Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Sabre\DAV\SimpleCollection;
class ServerFactory {
- /** @var IConfig */
- private $config;
- /** @var ILogger */
- private $logger;
- /** @var IDBConnection */
- private $databaseConnection;
- /** @var IUserSession */
- private $userSession;
- /** @var IMountManager */
- private $mountManager;
- /** @var ITagManager */
- private $tagManager;
- /** @var IRequest */
- private $request;
- /** @var IPreview */
- private $previewManager;
- /** @var EventDispatcherInterface */
- private $eventDispatcher;
- /** @var IL10N */
- private $l10n;
- /**
- * @param IConfig $config
- * @param ILogger $logger
- * @param IDBConnection $databaseConnection
- * @param IUserSession $userSession
- * @param IMountManager $mountManager
- * @param ITagManager $tagManager
- * @param IRequest $request
- * @param IPreview $previewManager
- */
public function __construct(
- IConfig $config,
- ILogger $logger,
- IDBConnection $databaseConnection,
- IUserSession $userSession,
- IMountManager $mountManager,
- ITagManager $tagManager,
- IRequest $request,
- IPreview $previewManager,
- EventDispatcherInterface $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 string $baseUri
- * @param string $requestUri
- * @param Plugin $authPlugin
* @param callable $viewCallBack callback that should return the view for the dav endpoint
- * @return Server
*/
- public function createServer($baseUri,
- $requestUri,
- Plugin $authPlugin,
- callable $viewCallBack) {
+ public function createServer(
+ bool $isPublicShare,
+ string $baseUri,
+ string $requestUri,
+ Plugin $authPlugin,
+ 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(\OC::$server->get(IRequest::class)));
+ $server->addPlugin(new RequestIdHeaderPlugin($this->request));
+
+ $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.
@@ -140,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)) {
@@ -148,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;
@@ -162,65 +137,108 @@ 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 \OCA\DAV\Connector\Sabre\File($view, $rootInfo);
+ $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 {
+ /** @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(
+ $userFolder,
+ ));
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(
- $objectTree,
+ new CustomPropertiesBackend(
+ $server,
+ $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->dispatch($event);
+ $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 6fbae0dee4a..0b66ed27576 100644
--- a/apps/dav/lib/Connector/Sabre/ShareTypeList.php
+++ b/apps/dav/lib/Connector/Sabre/ShareTypeList.php
@@ -1,24 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
@@ -35,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,
+ ) {
}
/**
@@ -79,7 +61,7 @@ class ShareTypeList implements Element {
}
/**
- * The xmlSerialize metod is called during xml writing.
+ * The xmlSerialize method is called during xml writing.
*
* @param Writer $writer
* @return void
diff --git a/apps/dav/lib/Connector/Sabre/ShareeList.php b/apps/dav/lib/Connector/Sabre/ShareeList.php
index db8c011cc45..909c29fc24b 100644
--- a/apps/dav/lib/Connector/Sabre/ShareeList.php
+++ b/apps/dav/lib/Connector/Sabre/ShareeList.php
@@ -3,27 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Tobias Kaminsky <tobias@kaminsky.me>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Connector\Sabre;
@@ -37,15 +18,14 @@ 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,
+ ) {
}
/**
- * The xmlSerialize metod is called during xml writing.
+ * The xmlSerialize method is called during xml writing.
*
* @param Writer $writer
* @return void
diff --git a/apps/dav/lib/Connector/Sabre/SharesPlugin.php b/apps/dav/lib/Connector/Sabre/SharesPlugin.php
index 57c91e05a8c..f49e85333f3 100644
--- a/apps/dav/lib/Connector/Sabre/SharesPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/SharesPlugin.php
@@ -1,39 +1,23 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Tobias Kaminsky <tobias@kaminsky.me>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
+use OC\Share20\Exception\BackendError;
use OCA\DAV\Connector\Sabre\Node as DavNode;
use OCP\Files\Folder;
+use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\IUserSession;
+use OCP\Share\IManager;
use OCP\Share\IShare;
use Sabre\DAV\PropFind;
+use Sabre\DAV\Server;
+use Sabre\DAV\Tree;
/**
* Sabre Plugin to provide share-related properties
@@ -50,40 +34,19 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
* @var \Sabre\DAV\Server
*/
private $server;
-
- /** @var \OCP\Share\IManager */
- private $shareManager;
-
- /** @var \Sabre\DAV\Tree */
- private $tree;
-
- /** @var string */
- private $userId;
-
- /** @var \OCP\Files\Folder */
- private $userFolder;
+ private string $userId;
/** @var IShare[][] */
- private $cachedShares = [];
-
+ private array $cachedShares = [];
/** @var string[] */
- private $cachedFolders = [];
+ private array $cachedFolders = [];
- /**
- * @param \Sabre\DAV\Tree $tree tree
- * @param IUserSession $userSession user session
- * @param \OCP\Files\Folder $userFolder user home folder
- * @param \OCP\Share\IManager $shareManager share manager
- */
public function __construct(
- \Sabre\DAV\Tree $tree,
- IUserSession $userSession,
- \OCP\Files\Folder $userFolder,
- \OCP\Share\IManager $shareManager
+ private Tree $tree,
+ private IUserSession $userSession,
+ private Folder $userFolder,
+ private IManager $shareManager,
) {
- $this->tree = $tree;
- $this->shareManager = $shareManager;
- $this->userFolder = $userFolder;
$this->userId = $userSession->getUser()->getUID();
}
@@ -95,9 +58,9 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
*
* This method should set up the required event subscriptions.
*
- * @param \Sabre\DAV\Server $server
+ * @return void
*/
- public function initialize(\Sabre\DAV\Server $server) {
+ public function initialize(Server $server) {
$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
$server->xml->elementMap[self::SHARETYPES_PROPERTYNAME] = ShareTypeList::class;
$server->protectedProperties[] = self::SHARETYPES_PROPERTYNAME;
@@ -108,10 +71,10 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
}
/**
- * @param \OCP\Files\Node $node
+ * @param Node $node
* @return IShare[]
*/
- private function getShare(\OCP\Files\Node $node): array {
+ private function getShare(Node $node): array {
$result = [];
$requestedShareTypes = [
IShare::TYPE_USER,
@@ -122,19 +85,31 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
IShare::TYPE_ROOM,
IShare::TYPE_CIRCLE,
IShare::TYPE_DECK,
+ IShare::TYPE_SCIENCEMESH,
];
+
foreach ($requestedShareTypes as $requestedShareType) {
- $shares = $this->shareManager->getSharesBy(
+ $result = array_merge($result, $this->shareManager->getSharesBy(
$this->userId,
$requestedShareType,
$node,
false,
-1
- );
- foreach ($shares as $share) {
- $result[] = $share;
+ ));
+
+ // Also check for shares where the user is the recipient
+ try {
+ $result = array_merge($result, $this->shareManager->getSharedWith(
+ $this->userId,
+ $requestedShareType,
+ $node,
+ -1
+ ));
+ } catch (BackendError $e) {
+ // ignore
}
}
+
return $result;
}
@@ -156,27 +131,29 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
*/
private function getShares(DavNode $sabreNode): array {
if (isset($this->cachedShares[$sabreNode->getId()])) {
- $shares = $this->cachedShares[$sabreNode->getId()];
- } else {
- [$parentPath,] = \Sabre\Uri\split($sabreNode->getPath());
- if ($parentPath === '') {
- $parentPath = '/';
- }
- // if we already cached the folder this file is in we know there are no shares for this file
- if (array_search($parentPath, $this->cachedFolders) === false) {
- try {
- $node = $sabreNode->getNode();
- } catch (NotFoundException $e) {
- return [];
- }
- $shares = $this->getShare($node);
- $this->cachedShares[$sabreNode->getId()] = $shares;
- } else {
+ return $this->cachedShares[$sabreNode->getId()];
+ }
+
+ [$parentPath,] = \Sabre\Uri\split($sabreNode->getPath());
+ if ($parentPath === '') {
+ $parentPath = '/';
+ }
+
+ // if we already cached the folder containing this file
+ // then we already know there are no shares here.
+ if (array_search($parentPath, $this->cachedFolders) === false) {
+ try {
+ $node = $sabreNode->getNode();
+ } catch (NotFoundException $e) {
return [];
}
+
+ $shares = $this->getShare($node);
+ $this->cachedShares[$sabreNode->getId()] = $shares;
+ return $shares;
}
- return $shares;
+ return [];
}
/**
@@ -187,18 +164,20 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
*/
public function handleGetProperties(
PropFind $propFind,
- \Sabre\DAV\INode $sabreNode
+ \Sabre\DAV\INode $sabreNode,
) {
if (!($sabreNode instanceof DavNode)) {
return;
}
- // need prefetch ?
+ // If the node is a directory and we are requesting share types or sharees
+ // then we get all the shares in the folder and cache them.
+ // This is more performant than iterating each files afterwards.
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();
@@ -209,7 +188,7 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
}
}
- $propFind->handle(self::SHARETYPES_PROPERTYNAME, function () use ($sabreNode) {
+ $propFind->handle(self::SHARETYPES_PROPERTYNAME, function () use ($sabreNode): ShareTypeList {
$shares = $this->getShares($sabreNode);
$shareTypes = array_unique(array_map(function (IShare $share) {
@@ -219,7 +198,7 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
return new ShareTypeList($shareTypes);
});
- $propFind->handle(self::SHAREES_PROPERTYNAME, function () use ($sabreNode) {
+ $propFind->handle(self::SHAREES_PROPERTYNAME, function () use ($sabreNode): ShareeList {
$shares = $this->getShares($sabreNode);
return new ShareeList($shares);
diff --git a/apps/dav/lib/Connector/Sabre/TagList.php b/apps/dav/lib/Connector/Sabre/TagList.php
index bbb938fb27d..9a5cd0d51cf 100644
--- a/apps/dav/lib/Connector/Sabre/TagList.php
+++ b/apps/dav/lib/Connector/Sabre/TagList.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
@@ -36,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,
+ ) {
}
/**
@@ -95,7 +76,7 @@ class TagList implements Element {
}
/**
- * The xmlSerialize metod is called during xml writing.
+ * The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
diff --git a/apps/dav/lib/Connector/Sabre/TagsPlugin.php b/apps/dav/lib/Connector/Sabre/TagsPlugin.php
index da5dd874905..25c1633df36 100644
--- a/apps/dav/lib/Connector/Sabre/TagsPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/TagsPlugin.php
@@ -1,31 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright 2014 Vincent Petry <pvince81@owncloud.com>
- * @copyright 2014 Vincent Petry <pvince81@owncloud.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Sergio Bertolín <sbertolin@solidgear.es>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
@@ -49,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;
@@ -69,12 +50,7 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
private $server;
/**
- * @var \OCP\ITagManager
- */
- private $tagManager;
-
- /**
- * @var \OCP\ITags
+ * @var ITags
*/
private $tagger;
@@ -87,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 = [];
}
@@ -120,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) {
@@ -139,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;
@@ -176,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
@@ -211,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;
@@ -272,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;
}
@@ -281,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 {
@@ -296,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;
+ }
+ }
+}