From 829818c95c2ab99f5dc0bf859de89dba5d062ac8 Mon Sep 17 00:00:00 2001 From: Altamash Shaikh Date: Mon, 1 Jun 2026 16:01:52 +0530 Subject: [PATCH 1/8] Adds restrict access check for MultiSites.getAll report for non superusers, #AS-552 --- CHANGELOG.md | 1 + Processor.php | 18 ++++++++++++++ plugin.json | 2 +- tests/Integration/ProcessorTest.php | 38 +++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90e532c3..f47bdb99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## Changelog +* 5.3.1 - 2026-06-08 - Added restrict access check for MultiSites.getAll report for non superusers * 5.3.0 - 2026-05-11 - Added alert description and helptexts * 5.2.6 - 2026-04-27 - Updated API documentation * 5.2.5 - 2026-03-30 - Added escaping for report_matched value diff --git a/Processor.php b/Processor.php index 6fcdf7aa..6ca1349c 100755 --- a/Processor.php +++ b/Processor.php @@ -248,6 +248,8 @@ public function getValueForAlertInPast($alert, $idSite, $subPeriodN) $params = array_merge($params, $report['parameters']); } + $params = $this->restrictMultiSitesReportToAlertOwner($params, $report, $alert); + $subtableId = DataTable\Manager::getInstance()->getMostRecentTableId(); $table = ApiRequest::processRequest($report['module'] . '.' . $report['action'], $params, $default = []); @@ -264,6 +266,22 @@ public function getValueForAlertInPast($alert, $idSite, $subPeriodN) return $value; } + protected function restrictMultiSitesReportToAlertOwner(array $params, array $report, array $alert): array + { + if ( + $report['module'] !== 'MultiSites' + || $report['action'] !== 'getAll' + || empty($alert['login']) + || Piwik::hasTheUserSuperUserAccess($alert['login']) + ) { + return $params; + } + + $params['_restrictSitesToLogin'] = $alert['login']; + + return $params; + } + /** * Checks whether the archive status is complete. We throw an exception if the status is something other than * complete. If no status is found, we do nothing. diff --git a/plugin.json b/plugin.json index d0a992bd..d9bec1da 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "name": "CustomAlerts", "description": "Create custom Alerts to be notified of important changes on your website or app! ", - "version": "5.3.0", + "version": "5.3.1", "require": { "matomo": ">=5.0.0-b1,<6.0.0-b1" }, diff --git a/tests/Integration/ProcessorTest.php b/tests/Integration/ProcessorTest.php index 11b34da4..38e63fca 100644 --- a/tests/Integration/ProcessorTest.php +++ b/tests/Integration/ProcessorTest.php @@ -68,6 +68,11 @@ public function shouldBeTriggered($alert, $metricOne, $metricTwo) { return parent::shouldBeTriggered($alert, $metricOne, $metricTwo); } + + public function restrictMultiSitesReportToAlertOwner(array $params, array $report, array $alert): array + { + return parent::restrictMultiSitesReportToAlertOwner($params, $report, $alert); + } } /** @@ -619,6 +624,39 @@ public function test_processAlert_shouldOnlyBeTriggeredIfAlertMatches() $processorMock->processAlert($alert, 1); } + public function test_restrictMultiSitesReportToAlertOwner_shouldRestrictMultiSitesGetAllForAlertOwner() + { + $params = $this->processor->restrictMultiSitesReportToAlertOwner( + [], + ['module' => 'MultiSites', 'action' => 'getAll'], + ['login' => 'aUser'] + ); + + $this->assertSame('aUser', $params['_restrictSitesToLogin']); + } + + public function test_restrictMultiSitesReportToAlertOwner_shouldNotRestrictSuperUserAlertOwner() + { + $params = $this->processor->restrictMultiSitesReportToAlertOwner( + [], + ['module' => 'MultiSites', 'action' => 'getAll'], + ['login' => 'superUserLogin'] + ); + + $this->assertArrayNotHasKey('_restrictSitesToLogin', $params); + } + + public function test_restrictMultiSitesReportToAlertOwner_shouldNotRestrictOtherReports() + { + $params = $this->processor->restrictMultiSitesReportToAlertOwner( + [], + ['module' => 'VisitsSummary', 'action' => 'get'], + ['login' => 'aUser'] + ); + + $this->assertArrayNotHasKey('_restrictSitesToLogin', $params); + } + public function test_shouldBeTriggered_ShouldFail_IfInvalidConditionGiven() { $this->expectException(\Exception::class); From 0a1757bdf01c5e171c65fd9c855f6fd1eb42284c Mon Sep 17 00:00:00 2001 From: Altamash Shaikh Date: Mon, 1 Jun 2026 16:13:02 +0530 Subject: [PATCH 2/8] Fixes PHPstan error --- Processor.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Processor.php b/Processor.php index 6ca1349c..20198b81 100755 --- a/Processor.php +++ b/Processor.php @@ -214,7 +214,7 @@ private function reportExists($idSite, $report, $metric) * @param int $idSite * @param int $subPeriodN * - * @return array + * @return mixed|null * @throws RetryableException If the report has an archive status, and it's something other than complete */ public function getValueForAlertInPast($alert, $idSite, $subPeriodN) @@ -239,10 +239,7 @@ public function getValueForAlertInPast($alert, $idSite, $subPeriodN) 'filter_limit' => -1 ); - // Only include the archive state param for versions of Matomo that allow it - if (version_compare(\Piwik\Version::VERSION, '5.1.0-b1', '>=')) { - $params['fetch_archive_state'] = 1; - } + $params['fetch_archive_state'] = 1; if (!empty($report['parameters'])) { $params = array_merge($params, $report['parameters']); @@ -295,11 +292,6 @@ protected function restrictMultiSitesReportToAlertOwner(array $params, array $re */ protected function checkWhetherArchiveIsComplete(array $alert, DataTable $table): void { - // Don't bother checking older versions of Matomo since the data and constants won't be there - if (version_compare(\Piwik\Version::VERSION, '5.1.0-b1', '<')) { - return; - } - $archiveState = $table->getMetadata(DataTable::ARCHIVE_STATE_METADATA_NAME); if (empty($archiveState)) { return; From 78c04141b2dfe2d33513942db247d0bf27e50adf Mon Sep 17 00:00:00 2001 From: Altamash Shaikh Date: Mon, 1 Jun 2026 16:39:39 +0530 Subject: [PATCH 3/8] Fixes tests --- Processor.php | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Processor.php b/Processor.php index 20198b81..13289329 100755 --- a/Processor.php +++ b/Processor.php @@ -239,7 +239,9 @@ public function getValueForAlertInPast($alert, $idSite, $subPeriodN) 'filter_limit' => -1 ); - $params['fetch_archive_state'] = 1; + if ($this->supportsArchiveStateMetadata()) { + $params['fetch_archive_state'] = 1; + } if (!empty($report['parameters'])) { $params = array_merge($params, $report['parameters']); @@ -292,12 +294,18 @@ protected function restrictMultiSitesReportToAlertOwner(array $params, array $re */ protected function checkWhetherArchiveIsComplete(array $alert, DataTable $table): void { - $archiveState = $table->getMetadata(DataTable::ARCHIVE_STATE_METADATA_NAME); + if (!$this->supportsArchiveStateMetadata()) { + return; + } + + $archiveStateMetadataName = constant(DataTable::class . '::ARCHIVE_STATE_METADATA_NAME'); + $archiveState = $table->getMetadata($archiveStateMetadataName); if (empty($archiveState)) { return; } - if ($archiveState === \Piwik\Archive\ArchiveState::COMPLETE) { + $completeArchiveState = constant(\Piwik\Archive\ArchiveState::class . '::COMPLETE'); + if ($archiveState === $completeArchiveState) { return; } @@ -305,6 +313,12 @@ protected function checkWhetherArchiveIsComplete(array $alert, DataTable $table) throw new RetryableException('This alert is not ready to process due to incomplete archiving'); } + private function supportsArchiveStateMetadata(): bool + { + return defined(DataTable::class . '::ARCHIVE_STATE_METADATA_NAME') + && defined(\Piwik\Archive\ArchiveState::class . '::COMPLETE'); + } + private function getDateForAlertInPast($idSite, $period, $subPeriodN) { $timezone = Site::getTimezoneFor($idSite); From 717d79cdbd347e139a32270830d1946b79d7663a Mon Sep 17 00:00:00 2001 From: Altamash Shaikh Date: Mon, 1 Jun 2026 16:41:50 +0530 Subject: [PATCH 4/8] Revert to minimal fix --- Processor.php | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/Processor.php b/Processor.php index 13289329..d04a9587 100755 --- a/Processor.php +++ b/Processor.php @@ -239,7 +239,7 @@ public function getValueForAlertInPast($alert, $idSite, $subPeriodN) 'filter_limit' => -1 ); - if ($this->supportsArchiveStateMetadata()) { + if (version_compare(\Piwik\Version::VERSION, '5.1.0-b1', '>=')) { $params['fetch_archive_state'] = 1; } @@ -294,18 +294,16 @@ protected function restrictMultiSitesReportToAlertOwner(array $params, array $re */ protected function checkWhetherArchiveIsComplete(array $alert, DataTable $table): void { - if (!$this->supportsArchiveStateMetadata()) { + if (version_compare(\Piwik\Version::VERSION, '5.1.0-b1', '<')) { return; } - $archiveStateMetadataName = constant(DataTable::class . '::ARCHIVE_STATE_METADATA_NAME'); - $archiveState = $table->getMetadata($archiveStateMetadataName); + $archiveState = $table->getMetadata(DataTable::ARCHIVE_STATE_METADATA_NAME); if (empty($archiveState)) { return; } - $completeArchiveState = constant(\Piwik\Archive\ArchiveState::class . '::COMPLETE'); - if ($archiveState === $completeArchiveState) { + if ($archiveState === \Piwik\Archive\ArchiveState::COMPLETE) { return; } @@ -313,12 +311,6 @@ protected function checkWhetherArchiveIsComplete(array $alert, DataTable $table) throw new RetryableException('This alert is not ready to process due to incomplete archiving'); } - private function supportsArchiveStateMetadata(): bool - { - return defined(DataTable::class . '::ARCHIVE_STATE_METADATA_NAME') - && defined(\Piwik\Archive\ArchiveState::class . '::COMPLETE'); - } - private function getDateForAlertInPast($idSite, $period, $subPeriodN) { $timezone = Site::getTimezoneFor($idSite); From 703679ac34d9a2004df9c80ca1e61c6e65687049 Mon Sep 17 00:00:00 2001 From: Altamash Shaikh Date: Mon, 1 Jun 2026 16:46:50 +0530 Subject: [PATCH 5/8] fixes phpstan --- Processor.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Processor.php b/Processor.php index d04a9587..1fc770c2 100755 --- a/Processor.php +++ b/Processor.php @@ -239,6 +239,7 @@ public function getValueForAlertInPast($alert, $idSite, $subPeriodN) 'filter_limit' => -1 ); + // @phpstan-ignore if.alwaysTrue if (version_compare(\Piwik\Version::VERSION, '5.1.0-b1', '>=')) { $params['fetch_archive_state'] = 1; } @@ -294,6 +295,7 @@ protected function restrictMultiSitesReportToAlertOwner(array $params, array $re */ protected function checkWhetherArchiveIsComplete(array $alert, DataTable $table): void { + // @phpstan-ignore if.alwaysFalse if (version_compare(\Piwik\Version::VERSION, '5.1.0-b1', '<')) { return; } From 46a0ac1f88d2ddd6549fc85a784805ddebe83787 Mon Sep 17 00:00:00 2001 From: Altamash Shaikh Date: Mon, 1 Jun 2026 17:12:59 +0530 Subject: [PATCH 6/8] fixes phpstan --- Processor.php | 2 -- phpstan/phpstan.modified.neon | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Processor.php b/Processor.php index 1fc770c2..d04a9587 100755 --- a/Processor.php +++ b/Processor.php @@ -239,7 +239,6 @@ public function getValueForAlertInPast($alert, $idSite, $subPeriodN) 'filter_limit' => -1 ); - // @phpstan-ignore if.alwaysTrue if (version_compare(\Piwik\Version::VERSION, '5.1.0-b1', '>=')) { $params['fetch_archive_state'] = 1; } @@ -295,7 +294,6 @@ protected function restrictMultiSitesReportToAlertOwner(array $params, array $re */ protected function checkWhetherArchiveIsComplete(array $alert, DataTable $table): void { - // @phpstan-ignore if.alwaysFalse if (version_compare(\Piwik\Version::VERSION, '5.1.0-b1', '<')) { return; } diff --git a/phpstan/phpstan.modified.neon b/phpstan/phpstan.modified.neon index 1cd88dfa..ddb9ff95 100644 --- a/phpstan/phpstan.modified.neon +++ b/phpstan/phpstan.modified.neon @@ -2,4 +2,5 @@ includes: - ../phpstan.neon parameters: level: 5 - tmpDir: /tmp/phpstan/CustomAlerts/modified \ No newline at end of file + tmpDir: /tmp/phpstan/CustomAlerts/modified + treatPhpDocTypesAsCertain: false From 78752e7841af15b22e1cf69a968f87c8361fcef3 Mon Sep 17 00:00:00 2001 From: Altamash Shaikh Date: Mon, 1 Jun 2026 17:18:40 +0530 Subject: [PATCH 7/8] Minimal fix --- Processor.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Processor.php b/Processor.php index d04a9587..90b1387a 100755 --- a/Processor.php +++ b/Processor.php @@ -214,7 +214,7 @@ private function reportExists($idSite, $report, $metric) * @param int $idSite * @param int $subPeriodN * - * @return mixed|null + * @return array * @throws RetryableException If the report has an archive status, and it's something other than complete */ public function getValueForAlertInPast($alert, $idSite, $subPeriodN) @@ -239,6 +239,7 @@ public function getValueForAlertInPast($alert, $idSite, $subPeriodN) 'filter_limit' => -1 ); + // Only include the archive state param for versions of Matomo that allow it if (version_compare(\Piwik\Version::VERSION, '5.1.0-b1', '>=')) { $params['fetch_archive_state'] = 1; } From 772c44d9f0b7825a3cecd9d68d2edcaeb6526be3 Mon Sep 17 00:00:00 2001 From: Altamash Shaikh Date: Mon, 1 Jun 2026 17:19:54 +0530 Subject: [PATCH 8/8] comment added back --- Processor.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Processor.php b/Processor.php index 90b1387a..6ca1349c 100755 --- a/Processor.php +++ b/Processor.php @@ -295,6 +295,7 @@ protected function restrictMultiSitesReportToAlertOwner(array $params, array $re */ protected function checkWhetherArchiveIsComplete(array $alert, DataTable $table): void { + // Don't bother checking older versions of Matomo since the data and constants won't be there if (version_compare(\Piwik\Version::VERSION, '5.1.0-b1', '<')) { return; }