diff options
Diffstat (limited to 'apps/dav/lib/Connector')
-rw-r--r-- | apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php | 2 | ||||
-rw-r--r-- | apps/dav/lib/Connector/Sabre/File.php | 12 | ||||
-rw-r--r-- | apps/dav/lib/Connector/Sabre/Principal.php | 3 | ||||
-rw-r--r-- | apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php | 78 | ||||
-rw-r--r-- | apps/dav/lib/Connector/Sabre/Server.php | 112 | ||||
-rw-r--r-- | apps/dav/lib/Connector/Sabre/ServerFactory.php | 10 | ||||
-rw-r--r-- | apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php | 5 |
7 files changed, 211 insertions, 11 deletions
diff --git a/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php b/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php index 44430b0004e..21358406a4a 100644 --- a/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php +++ b/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php @@ -49,7 +49,7 @@ class BlockLegacyClientPlugin extends ServerPlugin { return; } - $minimumSupportedDesktopVersion = $this->config->getSystemValueString('minimum.supported.desktop.version', '2.7.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 diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php index 218d38e1c4b..d2a71eb3e7b 100644 --- a/apps/dav/lib/Connector/Sabre/File.php +++ b/apps/dav/lib/Connector/Sabre/File.php @@ -204,6 +204,9 @@ class File extends Node implements IFile { } } + $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 { @@ -215,7 +218,7 @@ class File extends Node implements IFile { $count = -1; try { /** @var IWriteStreamStorage $partStorage */ - $count = $partStorage->writeStream($internalPartPath, $wrappedData); + $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']); @@ -235,10 +238,7 @@ class File extends Node implements IFile { [$count, $result] = Files::streamCopy($data, $target, true); fclose($target); } - - $lengthHeader = $this->request->getHeader('content-length'); - $expected = $lengthHeader !== '' ? (int)$lengthHeader : -1; - if ($result === false && $expected >= 0) { + 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)', @@ -253,7 +253,7 @@ class File extends Node implements IFile { // if content length is sent by client: // double check if the file was fully received // compare expected and actual size - if ($expected >= 0 + if ($expected !== null && $expected !== $count && $this->request->getMethod() === 'PUT' ) { diff --git a/apps/dav/lib/Connector/Sabre/Principal.php b/apps/dav/lib/Connector/Sabre/Principal.php index b61cabedf5f..d6ea9fd887d 100644 --- a/apps/dav/lib/Connector/Sabre/Principal.php +++ b/apps/dav/lib/Connector/Sabre/Principal.php @@ -186,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()); } } 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/Server.php b/apps/dav/lib/Connector/Sabre/Server.php index f3bfac1d6e0..dda9c29b763 100644 --- a/apps/dav/lib/Connector/Sabre/Server.php +++ b/apps/dav/lib/Connector/Sabre/Server.php @@ -7,7 +7,11 @@ */ 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; @@ -22,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) { @@ -30,6 +42,97 @@ class Server extends \Sabre\DAV\Server { $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 @@ -115,4 +218,13 @@ class Server extends \Sabre\DAV\Server { $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 3749b506d16..a6a27057177 100644 --- a/apps/dav/lib/Connector/Sabre/ServerFactory.php +++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php @@ -68,6 +68,7 @@ class ServerFactory { Plugin $authPlugin, callable $viewCallBack, ): Server { + $debugEnabled = $this->config->getSystemValue('debug', false); // Fire up server if ($isPublicShare) { $rootCollection = new SimpleCollection('root'); @@ -89,6 +90,10 @@ class ServerFactory { )); $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 DummyGetResponsePlugin()); $server->addPlugin(new ExceptionLoggerPlugin('webdav', $this->logger)); @@ -117,7 +122,8 @@ class ServerFactory { } // wait with registering these until auth is handled and the filesystem is setup - $server->on('beforeMethod:*', function () use ($server, $tree, $viewCallBack, $isPublicShare, $rootCollection): void { + $server->on('beforeMethod:*', function () use ($server, $tree, + $viewCallBack, $isPublicShare, $rootCollection, $debugEnabled): void { // ensure the skeleton is copied $userFolder = \OC::$server->getUserFolder(); @@ -181,7 +187,7 @@ class ServerFactory { \OCP\Server::get(IFilenameValidator::class), \OCP\Server::get(IAccountManager::class), false, - !$this->config->getSystemValue('debug', false) + !$debugEnabled ) ); $server->addPlugin(new QuotaPlugin($view)); diff --git a/apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php b/apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php index 220988caba0..f198519b454 100644 --- a/apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php +++ b/apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php @@ -67,15 +67,16 @@ class ZipFolderPlugin extends ServerPlugin { // 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(), $node->getMTime()); + $streamer->addFileFromStream($resource, $filename, $node->getSize(), $mtime); } elseif ($node instanceof NcFolder) { - $streamer->addEmptyDir($filename); + $streamer->addEmptyDir($filename, $mtime); $content = $node->getDirectoryListing(); foreach ($content as $subNode) { $this->streamNode($streamer, $subNode, $rootPath); |