Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 29 additions & 18 deletions Annotations/AnnotationGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion Annotations/ApiMethodInfoExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions OpenApiDocs.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
13 changes: 11 additions & 2 deletions Specs/SpecGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand All @@ -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
Expand All @@ -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 {
Expand Down