aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav')
-rw-r--r--apps/dav/composer/composer/autoload_classmap.php1
-rw-r--r--apps/dav/composer/composer/autoload_static.php1
-rw-r--r--apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php2
-rw-r--r--apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php78
-rw-r--r--apps/dav/lib/Connector/Sabre/Server.php112
-rw-r--r--apps/dav/lib/Connector/Sabre/ServerFactory.php10
-rw-r--r--apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php5
-rw-r--r--apps/dav/lib/DAV/CustomPropertiesBackend.php66
-rw-r--r--apps/dav/lib/Server.php7
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php123
10 files changed, 360 insertions, 45 deletions
diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php
index 764f94ef665..b9708ea5589 100644
--- a/apps/dav/composer/composer/autoload_classmap.php
+++ b/apps/dav/composer/composer/autoload_classmap.php
@@ -216,6 +216,7 @@ return array(
'OCA\\DAV\\Connector\\Sabre\\Node' => $baseDir . '/../lib/Connector/Sabre/Node.php',
'OCA\\DAV\\Connector\\Sabre\\ObjectTree' => $baseDir . '/../lib/Connector/Sabre/ObjectTree.php',
'OCA\\DAV\\Connector\\Sabre\\Principal' => $baseDir . '/../lib/Connector/Sabre/Principal.php',
+ 'OCA\\DAV\\Connector\\Sabre\\PropFindMonitorPlugin' => $baseDir . '/../lib/Connector/Sabre/PropFindMonitorPlugin.php',
'OCA\\DAV\\Connector\\Sabre\\PropfindCompressionPlugin' => $baseDir . '/../lib/Connector/Sabre/PropfindCompressionPlugin.php',
'OCA\\DAV\\Connector\\Sabre\\PublicAuth' => $baseDir . '/../lib/Connector/Sabre/PublicAuth.php',
'OCA\\DAV\\Connector\\Sabre\\QuotaPlugin' => $baseDir . '/../lib/Connector/Sabre/QuotaPlugin.php',
diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php
index f3d1eacfcd0..75ac3350160 100644
--- a/apps/dav/composer/composer/autoload_static.php
+++ b/apps/dav/composer/composer/autoload_static.php
@@ -231,6 +231,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Connector\\Sabre\\Node' => __DIR__ . '/..' . '/../lib/Connector/Sabre/Node.php',
'OCA\\DAV\\Connector\\Sabre\\ObjectTree' => __DIR__ . '/..' . '/../lib/Connector/Sabre/ObjectTree.php',
'OCA\\DAV\\Connector\\Sabre\\Principal' => __DIR__ . '/..' . '/../lib/Connector/Sabre/Principal.php',
+ 'OCA\\DAV\\Connector\\Sabre\\PropFindMonitorPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/PropFindMonitorPlugin.php',
'OCA\\DAV\\Connector\\Sabre\\PropfindCompressionPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/PropfindCompressionPlugin.php',
'OCA\\DAV\\Connector\\Sabre\\PublicAuth' => __DIR__ . '/..' . '/../lib/Connector/Sabre/PublicAuth.php',
'OCA\\DAV\\Connector\\Sabre\\QuotaPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/QuotaPlugin.php',
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/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);
diff --git a/apps/dav/lib/DAV/CustomPropertiesBackend.php b/apps/dav/lib/DAV/CustomPropertiesBackend.php
index f3fff11b3da..f9a4f8ee986 100644
--- a/apps/dav/lib/DAV/CustomPropertiesBackend.php
+++ b/apps/dav/lib/DAV/CustomPropertiesBackend.php
@@ -10,9 +10,9 @@ namespace OCA\DAV\DAV;
use Exception;
use OCA\DAV\CalDAV\Calendar;
+use OCA\DAV\CalDAV\CalendarObject;
use OCA\DAV\CalDAV\DefaultCalendarValidator;
use OCA\DAV\Connector\Sabre\Directory;
-use OCA\DAV\Connector\Sabre\FilesPlugin;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IUser;
@@ -66,38 +66,16 @@ class CustomPropertiesBackend implements BackendInterface {
'{DAV:}getetag',
'{DAV:}quota-used-bytes',
'{DAV:}quota-available-bytes',
- '{http://owncloud.org/ns}permissions',
- '{http://owncloud.org/ns}downloadURL',
- '{http://owncloud.org/ns}dDC',
- '{http://owncloud.org/ns}size',
- '{http://nextcloud.org/ns}is-encrypted',
-
- // Currently, returning null from any propfind handler would still trigger the backend,
- // so we add all known Nextcloud custom properties in here to avoid that
-
- // text app
- '{http://nextcloud.org/ns}rich-workspace',
- '{http://nextcloud.org/ns}rich-workspace-file',
- // groupfolders
- '{http://nextcloud.org/ns}acl-enabled',
- '{http://nextcloud.org/ns}acl-can-manage',
- '{http://nextcloud.org/ns}acl-list',
- '{http://nextcloud.org/ns}inherited-acl-list',
- '{http://nextcloud.org/ns}group-folder-id',
- // files_lock
- '{http://nextcloud.org/ns}lock',
- '{http://nextcloud.org/ns}lock-owner-type',
- '{http://nextcloud.org/ns}lock-owner',
- '{http://nextcloud.org/ns}lock-owner-displayname',
- '{http://nextcloud.org/ns}lock-owner-editor',
- '{http://nextcloud.org/ns}lock-time',
- '{http://nextcloud.org/ns}lock-timeout',
- '{http://nextcloud.org/ns}lock-token',
- // photos
- '{http://nextcloud.org/ns}realpath',
- '{http://nextcloud.org/ns}nbItems',
- '{http://nextcloud.org/ns}face-detections',
- '{http://nextcloud.org/ns}face-preview-image',
+ ];
+
+ /**
+ * Allowed properties for the oc/nc namespace, all other properties in the namespace are ignored
+ *
+ * @var string[]
+ */
+ private const ALLOWED_NC_PROPERTIES = [
+ '{http://owncloud.org/ns}calendar-enabled',
+ '{http://owncloud.org/ns}enabled',
];
/**
@@ -155,14 +133,9 @@ class CustomPropertiesBackend implements BackendInterface {
public function propFind($path, PropFind $propFind) {
$requestedProps = $propFind->get404Properties();
- // these might appear
- $requestedProps = array_diff(
- $requestedProps,
- self::IGNORED_PROPERTIES,
- );
$requestedProps = array_filter(
$requestedProps,
- fn ($prop) => !str_starts_with($prop, FilesPlugin::FILE_METADATA_PREFIX),
+ $this->isPropertyAllowed(...),
);
// substr of calendars/ => path is inside the CalDAV component
@@ -224,6 +197,11 @@ class CustomPropertiesBackend implements BackendInterface {
$this->cacheDirectory($path, $node);
}
+ if ($node instanceof CalendarObject) {
+ // No custom properties supported on individual events
+ return;
+ }
+
// First fetch the published properties (set by another user), then get the ones set by
// the current user. If both are set then the latter as priority.
foreach ($this->getPublishedProperties($path, $requestedProps) as $propName => $propValue) {
@@ -244,6 +222,16 @@ class CustomPropertiesBackend implements BackendInterface {
}
}
+ private function isPropertyAllowed(string $property): bool {
+ if (in_array($property, self::IGNORED_PROPERTIES)) {
+ return false;
+ }
+ if (str_starts_with($property, '{http://owncloud.org/ns}') || str_starts_with($property, '{http://nextcloud.org/ns}')) {
+ return in_array($property, self::ALLOWED_NC_PROPERTIES);
+ }
+ return true;
+ }
+
/**
* Updates properties for a path
*
diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php
index f81c7fa6f29..a92e162f1b0 100644
--- a/apps/dav/lib/Server.php
+++ b/apps/dav/lib/Server.php
@@ -45,6 +45,7 @@ use OCA\DAV\Connector\Sabre\FilesReportPlugin;
use OCA\DAV\Connector\Sabre\LockPlugin;
use OCA\DAV\Connector\Sabre\MaintenancePlugin;
use OCA\DAV\Connector\Sabre\PropfindCompressionPlugin;
+use OCA\DAV\Connector\Sabre\PropFindMonitorPlugin;
use OCA\DAV\Connector\Sabre\QuotaPlugin;
use OCA\DAV\Connector\Sabre\RequestIdHeaderPlugin;
use OCA\DAV\Connector\Sabre\SharesPlugin;
@@ -108,6 +109,7 @@ class Server {
private IRequest $request,
private string $baseUri,
) {
+ $debugEnabled = \OCP\Server::get(IConfig::class)->getSystemValue('debug', false);
$this->profiler = \OCP\Server::get(IProfiler::class);
if ($this->profiler->isEnabled()) {
/** @var IEventLogger $eventLogger */
@@ -120,6 +122,7 @@ class Server {
$root = new RootCollection();
$this->server = new \OCA\DAV\Connector\Sabre\Server(new CachingTree($root));
+ $this->server->setLogger($logger);
// Add maintenance plugin
$this->server->addPlugin(new MaintenancePlugin(\OCP\Server::get(IConfig::class), \OC::$server->getL10N('dav')));
@@ -167,7 +170,9 @@ class Server {
$authPlugin->addBackend($authBackend);
// debugging
- if (\OCP\Server::get(IConfig::class)->getSystemValue('debug', false)) {
+ if ($debugEnabled) {
+ $this->server->debugEnabled = true;
+ $this->server->addPlugin(new PropFindMonitorPlugin());
$this->server->addPlugin(new \Sabre\DAV\Browser\Plugin());
} else {
$this->server->addPlugin(new DummyGetResponsePlugin());
diff --git a/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php
new file mode 100644
index 00000000000..b528c3d731c
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php
@@ -0,0 +1,123 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace unit\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\PropFindMonitorPlugin;
+use OCA\DAV\Connector\Sabre\Server;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Sabre\HTTP\Request;
+use Sabre\HTTP\Response;
+use Test\TestCase;
+
+class PropFindMonitorPluginTest extends TestCase {
+
+ private PropFindMonitorPlugin $plugin;
+ private Server&MockObject $server;
+ private LoggerInterface&MockObject $logger;
+ private Request&MockObject $request;
+ private Response&MockObject $response;
+
+ public static function dataTest(): array {
+ $minQueriesTrigger = PropFindMonitorPlugin::THRESHOLD_QUERY_FACTOR
+ * PropFindMonitorPlugin::THRESHOLD_NODES;
+ return [
+ 'No queries logged' => [[], 0],
+ 'Plugins with queries in less than threshold nodes should not be logged' => [
+ [
+ [
+ 'PluginName' => ['queries' => 100, 'nodes'
+ => PropFindMonitorPlugin::THRESHOLD_NODES - 1]
+ ],
+ [],
+ ],
+ 0
+ ],
+ 'Plugins with query-to-node ratio less than threshold should not be logged' => [
+ [
+ [
+ 'PluginName' => [
+ 'queries' => $minQueriesTrigger - 1,
+ 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES ],
+ ],
+ [],
+ ],
+ 0
+ ],
+ 'Plugins with more nodes scanned than queries executed should not be logged' => [
+ [
+ [
+ 'PluginName' => [
+ 'queries' => $minQueriesTrigger,
+ 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES * 2],
+ ],
+ [],
+ ],
+ 0
+ ],
+ 'Plugins with queries only in highest depth level should not be logged' => [
+ [
+ [
+ 'PluginName' => [
+ 'queries' => $minQueriesTrigger,
+ 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES - 1
+ ]
+ ],
+ [
+ 'PluginName' => [
+ 'queries' => $minQueriesTrigger * 2,
+ 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES
+ ]
+ ]
+ ],
+ 0
+ ],
+ 'Plugins with too many queries should be logged' => [
+ [
+ [
+ 'FirstPlugin' => [
+ 'queries' => $minQueriesTrigger,
+ 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES,
+ ],
+ 'SecondPlugin' => [
+ 'queries' => $minQueriesTrigger,
+ 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES,
+ ]
+ ],
+ []
+ ],
+ 2
+ ]
+ ];
+ }
+
+ /**
+ * @dataProvider dataTest
+ */
+ public function test(array $queries, $expectedLogCalls): void {
+ $this->plugin->initialize($this->server);
+ $this->server->expects($this->once())->method('getPluginQueries')
+ ->willReturn($queries);
+
+ $this->server->expects(empty($queries) ? $this->never() : $this->once())
+ ->method('getLogger')
+ ->willReturn($this->logger);
+
+ $this->logger->expects($this->exactly($expectedLogCalls))->method('error');
+ $this->plugin->afterResponse($this->request, $this->response);
+ }
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->plugin = new PropFindMonitorPlugin();
+ $this->server = $this->createMock(Server::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->request = $this->createMock(Request::class);
+ $this->response = $this->createMock(Response::class);
+ }
+}