diff --git a/API.php b/API.php index c5edaa9..13e6eb1 100644 --- a/API.php +++ b/API.php @@ -10,6 +10,7 @@ namespace Piwik\Plugins\OpenApiDocs; use Piwik\Piwik; +use Piwik\Plugins\OpenApiDocs\Generation\PluginListProvider; use Piwik\Plugin\Manager; use Piwik\Plugins\OpenApiDocs\Specs\SpecGenerator; use Piwik\Plugins\OpenApiDocs\Specs\PathResolver; @@ -17,7 +18,7 @@ /** * Provides Reporting API endpoints for reading OpenAPI plugin configuration and specifications. * - * Exposes endpoints to return the configured plugin whitelist, read pre-generated spec files, + * Exposes endpoints to return the effective plugin list for spec generation, read pre-generated spec files, * or generate plugin OpenAPI specifications on demand. * * @method static \Piwik\Plugins\OpenApiDocs\API getInstance() @@ -25,21 +26,15 @@ class API extends \Piwik\Plugin\API { /** - * Returns the plugin names configured for OpenApiDocs spec generation. + * Returns the plugin names used for OpenApiDocs spec generation. * - * @return array The configured whitelist of plugin names from - * `config/plugins.php`. + * @return array */ - public function getPluginWhitelist(): array + public function getAllowedPlugins(): array { Piwik::checkUserHasSomeViewAccess(); - $pluginWhitelist = $this->loadPluginWhitelist(); - if (!is_array($pluginWhitelist)) { - throw new \Exception('OpenApiDocs plugin whitelist config is invalid.'); - } - - return $pluginWhitelist; + return $this->getPluginListProvider()->getAllowedPlugins(); } /** @@ -88,14 +83,6 @@ protected function getSpecFilePath(string $pluginName): string return $this->getSpecPathResolver()->getSpecFilePath($pluginName); } - /** - * @return mixed - */ - protected function loadPluginWhitelist() - { - return require __DIR__ . '/config/plugins.php'; - } - protected function isSpecFileReadable(string $filePath): bool { return is_file($filePath) && is_readable($filePath); @@ -127,6 +114,11 @@ protected function getSpecPathResolver(): PathResolver return new PathResolver(); } + protected function getPluginListProvider(): PluginListProvider + { + return new PluginListProvider(); + } + /** * Generates an OpenAPI specification for one or more plugins and returns it immediately. * diff --git a/Generation/PluginListProvider.php b/Generation/PluginListProvider.php new file mode 100644 index 0000000..19d024c --- /dev/null +++ b/Generation/PluginListProvider.php @@ -0,0 +1,73 @@ +pluginManager = $pluginManager ?? Manager::getInstance(); + } + + /** + * @return string[] + */ + public function getAllowedPlugins(): array + { + $pluginNames = array_values($this->pluginManager->getActivatedPlugins()); + + $this->dispatchUpdatePluginListEvent($pluginNames); + + $pluginNames = array_values(array_unique($pluginNames)); + + return array_values(array_filter($pluginNames, function ($pluginName): bool { + return is_string($pluginName) && $this->shouldIncludeEventProvidedPlugin($pluginName); + })); + } + + private function shouldIncludeEventProvidedPlugin(string $pluginName): bool + { + if (!$this->pluginManager->isPluginInFilesystem($pluginName)) { + return false; + } + return $this->pluginHasApiFile($pluginName); + } + + protected function pluginHasApiFile(string $pluginName): bool + { + return is_file(Manager::getPluginDirectory($pluginName) . '/API.php'); + } + + /** + * @param string[] $pluginNames + */ + private function dispatchUpdatePluginListEvent(array &$pluginNames): void + { + $this->postEvent('OpenApiDocs.updatePluginList', [&$pluginNames]); + } + + /** + * @param array $params + */ + protected function postEvent(string $eventName, array $params): void + { + Piwik::postEvent($eventName, $params); + } +} diff --git a/Tasks.php b/Tasks.php index 9228512..9d4728b 100644 --- a/Tasks.php +++ b/Tasks.php @@ -13,6 +13,7 @@ use Piwik\Config; use Piwik\Log\LoggerInterface; +use Piwik\Plugins\OpenApiDocs\Generation\PluginListProvider; use Piwik\Plugins\OpenApiDocs\Generation\SpecGenerationService; class Tasks extends \Piwik\Plugin\Tasks @@ -27,10 +28,19 @@ class Tasks extends \Piwik\Plugin\Tasks */ private $logger; - public function __construct(SpecGenerationService $specGenerationService, LoggerInterface $logger) - { + /** + * @var PluginListProvider + */ + private $pluginListProvider; + + public function __construct( + SpecGenerationService $specGenerationService, + LoggerInterface $logger, + ?PluginListProvider $pluginListProvider = null + ) { $this->specGenerationService = $specGenerationService; $this->logger = $logger; + $this->pluginListProvider = $pluginListProvider ?? new PluginListProvider(); } public function schedule() @@ -42,7 +52,7 @@ public function schedule() public function generateConfiguredPluginSpecs(): void { - $pluginNames = require __DIR__ . '/config/plugins.php'; + $pluginNames = $this->pluginListProvider->getAllowedPlugins(); foreach ($pluginNames as $pluginName) { try { diff --git a/config/plugins.php b/config/plugins.php deleted file mode 100644 index c50e627..0000000 --- a/config/plugins.php +++ /dev/null @@ -1,74 +0,0 @@ -assertSame($expectedSpec, $result); } - public function testGetPluginWhitelistReturnsConfigValuesInOrder() + public function testGetAllowedPluginsReturnsProviderValues(): void { - $expectedWhitelist = ['RollUpReporting', 'Login', 'ActivityLog']; + $provider = $this->createMock(PluginListProvider::class); + $provider->expects($this->once()) + ->method('getAllowedPlugins') + ->willReturn(['Login', 'ActivityLog']); $api = $this->getMockBuilder(API::class) - ->onlyMethods(['loadPluginWhitelist']) + ->onlyMethods(['getPluginListProvider']) ->getMock(); - $api->method('loadPluginWhitelist')->willReturn($expectedWhitelist); + $api->method('getPluginListProvider')->willReturn($provider); - $this->assertSame($expectedWhitelist, $api->getPluginWhitelist()); - } - - public function testGetPluginWhitelistThrowsExceptionWhenConfigIsInvalid() - { - $api = $this->getMockBuilder(API::class) - ->onlyMethods(['loadPluginWhitelist']) - ->getMock(); - $api->method('loadPluginWhitelist')->willReturn('invalid'); - - $this->expectException(\Exception::class); - $this->expectExceptionMessage('OpenApiDocs plugin whitelist config is invalid.'); - - $api->getPluginWhitelist(); + $this->assertSame(['Login', 'ActivityLog'], $api->getAllowedPlugins()); } public function testGetOpenApiSpecThrowsExceptionWhenFileMissing() diff --git a/tests/Unit/Generation/PluginListProviderTest.php b/tests/Unit/Generation/PluginListProviderTest.php new file mode 100644 index 0000000..3c0e1a2 --- /dev/null +++ b/tests/Unit/Generation/PluginListProviderTest.php @@ -0,0 +1,152 @@ +makeProvider( + ['HasApi'], + ['HasApi' => true], + true + ); + + $this->assertSame(['HasApi'], $provider->getAllowedPlugins()); + } + + public function testGetAllowedPluginsExcludesPluginNotInFilesystem(): void + { + $provider = $this->makeProvider( + ['InactivePlugin'], + ['InactivePlugin' => false], + true + ); + + $this->assertSame([], $provider->getAllowedPlugins()); + } + + public function testGetAllowedPluginsExcludesPluginWithoutApiFile(): void + { + $provider = $this->makeProvider( + ['NoApi'], + ['NoApi' => true], + false + ); + + $this->assertSame([], $provider->getAllowedPlugins()); + } + + public function testGetAllowedPluginsAppliesEventUpdates(): void + { + $provider = $this->makeProvider( + ['HasApi', 'Login'], + ['HasApi' => true, 'Login' => true], + ['HasApi' => true, 'Login' => true], + static function (string $eventName, array $params): void { + $pluginNames = &$params[0]; + $pluginNames[] = 'Login'; + } + ); + + $this->assertSame(['HasApi', 'Login'], $provider->getAllowedPlugins()); + } + + public function testGetAllowedPluginsReturnsEmptyListWhenNoPluginsInstalled(): void + { + $provider = $this->makeProvider([], [], false); + + $this->assertSame([], $provider->getAllowedPlugins()); + } + + public function testGetAllowedPluginsDropsInvalidEventUpdatesButKeepsEventAddedPlugins(): void + { + $provider = $this->makeProvider( + ['HasApi', 'Login', 'InactivePlugin', 'NoApi', 'ConnectAccounts'], + [ + 'HasApi' => true, + 'Login' => true, + 'InactivePlugin' => true, + 'NoApi' => true, + ], + [ + 'HasApi' => true, + 'Login' => true, + 'InactivePlugin' => true, + 'NoApi' => false, + ], + static function (string $eventName, array $params): void { + $pluginNames = &$params[0]; + $pluginNames[] = 'Login'; + $pluginNames[] = 'InactivePlugin'; + $pluginNames[] = 'NoApi'; + $pluginNames[] = 123; + } + ); + + $this->assertSame(['HasApi', 'Login', 'InactivePlugin'], $provider->getAllowedPlugins()); + } + + /** + * @param string[] $activatedPlugins + * @param array $inFilesystemByPlugin + * @param bool|array $hasApiFile + */ + private function makeProvider( + array $activatedPlugins, + array $inFilesystemByPlugin, + $hasApiFile, + ?callable $postEventCallback = null + ): PluginListProvider { + $pluginManager = $this->createConfiguredMock(Manager::class, [ + 'getActivatedPlugins' => $activatedPlugins, + 'getPluginsLoadedAndActivated' => [], + ]); + + $pluginManager->method('isPluginInFilesystem') + ->willReturnCallback(static function (string $pluginName) use ($inFilesystemByPlugin): bool { + return $inFilesystemByPlugin[$pluginName] ?? false; + }); + + $provider = $this->getMockBuilder(PluginListProvider::class) + ->setConstructorArgs([$pluginManager]) + ->onlyMethods(['pluginHasApiFile', 'postEvent']) + ->getMock(); + + $provider->method('pluginHasApiFile') + ->willReturnCallback(static function (string $pluginName) use ($hasApiFile): bool { + if (is_array($hasApiFile)) { + return $hasApiFile[$pluginName] ?? false; + } + + return $hasApiFile; + }); + + if ($postEventCallback) { + $provider->method('postEvent') + ->willReturnCallback($postEventCallback); + } + + return $provider; + } +} diff --git a/tests/Unit/TasksTest.php b/tests/Unit/TasksTest.php index ec5a945..29f07ae 100644 --- a/tests/Unit/TasksTest.php +++ b/tests/Unit/TasksTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase; use Piwik\Config; +use Piwik\Plugins\OpenApiDocs\Generation\PluginListProvider; use Piwik\Plugins\OpenApiDocs\Generation\SpecGenerationService; use Piwik\Plugins\OpenApiDocs\Tasks; use Piwik\Scheduler\Schedule\Daily; @@ -87,7 +88,11 @@ public function testGenerateConfiguredPluginSpecsLogsPerPluginFailuresAndContinu }); $logger = new FakeLogger(); - $tasks = new Tasks($service, $logger); + $pluginListProvider = $this->createMock(PluginListProvider::class); + $pluginListProvider->method('getAllowedPlugins') + ->willReturn(['RollUpReporting', 'Login']); + + $tasks = new Tasks($service, $logger, $pluginListProvider); $tasks->generateConfiguredPluginSpecs();