diff --git a/Annotations/AnnotationGenerator.php b/Annotations/AnnotationGenerator.php index 3024a45..b76ea2a 100644 --- a/Annotations/AnnotationGenerator.php +++ b/Annotations/AnnotationGenerator.php @@ -15,6 +15,7 @@ use Matomo\Dependencies\OpenApiDocs\phpDocumentor\Reflection\DocBlock\Tags\Param; use Matomo\Dependencies\OpenApiDocs\phpDocumentor\Reflection\DocBlock\Tags\TagWithType; use Matomo\Dependencies\OpenApiDocs\phpDocumentor\Reflection\DocBlockFactory; +use Piwik\Exception\PluginNotFoundException; use Piwik\API\DocumentationGenerator; use Piwik\API\NoDefaultValue; use Piwik\API\Proxy; @@ -98,13 +99,20 @@ public function __construct(DocumentationGenerator $generator) * * @return string[]|array[] The collection of all the lines which make up the generated annotations for the public API * endpoints defined by the plugin. - * @throws \Piwik\Exception\PluginDeactivatedException If the plugin is not activated. It should be loaded. + * @throws PluginNotFoundException If the plugin is not present in the filesystem. * @throws \Throwable */ public function generatePluginApiAnnotations(string $pluginName, bool $writeToFile = false): array { BaseValidator::check('plugin', $pluginName, [new NotEmpty()]); - Manager::getInstance()->checkIsPluginActivated($pluginName); + + if (in_array($pluginName, OpenApiDocs::PLUGIN_BLOCKLIST, true)) { + throw new \RuntimeException('OpenAPI doc generation is blocked for ' . $pluginName . '.'); + } + + if (!Manager::getInstance()->isPluginInFilesystem($pluginName)) { + throw new PluginNotFoundException($pluginName); + } $rules = require $this->currentPluginDir . '/Annotations/config.php'; $pluginAnnotationDir = $this->currentPluginDir . OpenApiDocs::GENERATED_ANNOTATIONS_PATH; @@ -1180,24 +1188,27 @@ protected function determineResponses(array $rules, string $plugin, string $meth $responseSchema = !empty($responseInfo['type']) ? $this->buildSchemaObjectArray($responseInfo['type']) : []; $mediaTypes = []; - // This simply reuses the example URLs used by the current documentation, but some endpoints don't work because authentication is required - $exampleUrls = $this->getApplicableDemoExampleUrls($plugin, $method, $paramsData); - foreach ($exampleUrls as $type => $url) { - $exampleValue = $this->getExampleIfAvailable($url); - // If the example lookup failed, try making the same request locally using a local token. - if (empty($exampleValue)) { - $exampleValue = $this->getExampleIfAvailable($url, true); - } - if (strlen($exampleValue) > self::EXAMPLE_CHAR_LIMIT) { - $exampleValue = $this->cutExampleCloseToCharLimit($exampleValue, $type); - } + $exampleUrls = []; + if (Manager::getInstance()->isPluginActivated($plugin)) { + // Only fetch live examples for activated plugins since their endpoints can be executed safely. + $exampleUrls = $this->getApplicableDemoExampleUrls($plugin, $method, $paramsData); + foreach ($exampleUrls as $type => $url) { + $exampleValue = $this->getExampleIfAvailable($url); + // If the example lookup failed, try making the same request locally using a local token. + if (empty($exampleValue)) { + $exampleValue = $this->getExampleIfAvailable($url, true); + } + if (strlen($exampleValue) > self::EXAMPLE_CHAR_LIMIT) { + $exampleValue = $this->cutExampleCloseToCharLimit($exampleValue, $type); + } - // Skip if there was no example response - if (empty($exampleValue)) { - continue; - } + // Skip if there was no example response + if (empty($exampleValue)) { + continue; + } - $mediaTypes[] = $this->buildMediaTypePropertiesArray($type, $exampleValue, $responseSchema); + $mediaTypes[] = $this->buildMediaTypePropertiesArray($type, $exampleValue, $responseSchema); + } } // Check if any example files exist even though there aren't any example URLs diff --git a/Annotations/ApiMethodInfoExtractor.php b/Annotations/ApiMethodInfoExtractor.php index d735a5b..23e41a2 100644 --- a/Annotations/ApiMethodInfoExtractor.php +++ b/Annotations/ApiMethodInfoExtractor.php @@ -11,6 +11,7 @@ namespace Piwik\Plugins\OpenApiDocs\Annotations; +use Piwik\Exception\PluginNotFoundException; use Piwik\API\Proxy; use Piwik\API\Request; use Piwik\Plugin\Manager; @@ -42,7 +43,9 @@ public function extractMethodInfo(string $pluginName, bool $writeToFile = false) $methodInfoArray = []; foreach ($pluginNames as $plugin) { BaseValidator::check('pluginName', $plugin, [new NotEmpty()]); - Manager::getInstance()->checkIsPluginActivated($plugin); + if (!Manager::getInstance()->isPluginInFilesystem($plugin)) { + throw new PluginNotFoundException($plugin); + } $className = Request::getClassNameAPI($plugin); diff --git a/CHANGELOG.md b/CHANGELOG.md index 893dcbf..595b416 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ 5.0.2-b1 - 2026-03-16 - Added support for string literal union types - Added API endpoint to retrieve static matomo swagger file +- Added support for deactivated plugins 5.0.1-b1 - 2026-02-16 - Added class and function level docs diff --git a/OpenApiDocs.php b/OpenApiDocs.php index 33b028e..804c677 100644 --- a/OpenApiDocs.php +++ b/OpenApiDocs.php @@ -18,6 +18,7 @@ class OpenApiDocs extends \Piwik\Plugin public const EXAMPLE_RESPONSES_PATH = '/tmp/responses/'; public const GENERATED_SPECS_PATH = '/tmp/specs/'; public const AVAILABLE_PROPERTY_TYPES = ['string', 'number', 'integer', 'boolean', 'array', 'object', 'null']; + public const PLUGIN_BLOCKLIST = ['Billing', 'Cloud', 'ConnectAccounts', 'CDN', 'ProxySite']; public function registerEvents() { diff --git a/Specs/SpecGenerator.php b/Specs/SpecGenerator.php index e7a2b78..efd272c 100644 --- a/Specs/SpecGenerator.php +++ b/Specs/SpecGenerator.php @@ -12,6 +12,7 @@ use Matomo\Dependencies\OpenApiDocs\OpenApi\Annotations\OpenApi; use Matomo\Dependencies\OpenApiDocs\OpenApi\Generator; use Piwik\Container\StaticContainer; +use Piwik\Exception\PluginNotFoundException; use Piwik\Log\LoggerInterface; use Piwik\Log\NullLogger; use Piwik\Plugin\Manager; @@ -45,6 +46,12 @@ public function generatePluginDoc(string $pluginName, string $format = 'json', s { BaseValidator::check('plugin', $pluginName, [new NotEmpty()]); + foreach (explode(',', $pluginName) as $currentPluginName) { + if (in_array($currentPluginName, OpenApiDocs::PLUGIN_BLOCKLIST, true)) { + throw new \RuntimeException('OpenAPI doc generation is blocked for ' . $currentPluginName . '.'); + } + } + return $this->generateSpec(explode(',', $pluginName), $format, $version, $writeToFile); } @@ -59,7 +66,7 @@ public function generatePluginDoc(string $pluginName, string $format = 'json', s * @return string * @throws \Piwik\Exception\DI\DependencyException * @throws \Piwik\Exception\DI\NotFoundException - * @throws \Piwik\Exception\PluginDeactivatedException + * @throws PluginNotFoundException * @throws \Exception */ public function generateSpec(array $pluginNames, string $format = 'json', string $version = OpenApiDocs::DEFAULT_SPEC_VERSION, bool $writeToFile = false): string @@ -70,7 +77,9 @@ public function generateSpec(array $pluginNames, string $format = 'json', string $pluginDirs = []; foreach ($pluginNames as $pluginName) { BaseValidator::check('pluginName', $pluginName, [new NotEmpty()]); - Manager::getInstance()->checkIsPluginActivated($pluginName); + if (!Manager::getInstance()->isPluginInFilesystem($pluginName)) { + throw new PluginNotFoundException($pluginName); + } $pluginAnnotationsSource = $currentPluginDir . '/tmp/annotations/' . $pluginName . 'GeneratedAnnotations.php'; try {