Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
66 changes: 66 additions & 0 deletions API.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,81 @@
namespace Piwik\Plugins\OpenApiDocs;

use Piwik\Piwik;
use Piwik\Plugin\Manager;
use Piwik\Plugins\OpenApiDocs\Specs\SpecGenerator;

/**
* API for plugin OpenApiDocs
*
* Exposes endpoints to fetch pre-generated OpenAPI specs or generate plugin-specific
* OpenAPI docs on demand.
*
* @method static \Piwik\Plugins\OpenApiDocs\API getInstance()
*/
class API extends \Piwik\Plugin\API
{
/**
* Get the pre-generated single OpenAPI spec file if it exists. This endpoint only reads
* the generated JSON file and does not trigger spec generation.
*
* /index.php?module=API&method=OpenApiDocs.getMatomoOpenApiSpec
*
* @return array<string, mixed> The decoded OpenAPI specification payload.
Comment thread
lachiebol marked this conversation as resolved.
* @throws \Exception If the file is missing, unreadable, or contains invalid JSON.
*/
public function getMatomoOpenApiSpec(string $format = 'json'): array
{
Comment thread
lachiebol marked this conversation as resolved.
Piwik::checkUserHasSomeViewAccess();

if (strtolower($format) !== 'json') {
throw new \Exception(
Piwik::translate(
'General_ExceptionInvalidReportRendererFormat',
[$format, 'json']
)
);
}

$filePath = $this->getMatomoSpecFilePath();
Comment thread
lachiebol marked this conversation as resolved.

if (!$this->isSpecFileReadable($filePath)) {
throw new \Exception('OpenAPI spec file was not found. Generate it first via openapidocs:generate-spec-file.');
}

$specContents = $this->readSpecFile($filePath);
if ($specContents === false) {
throw new \Exception('OpenAPI spec file could not be read.');
}

$decodedSpec = json_decode($specContents, true);
if (!is_array($decodedSpec) || json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('OpenAPI spec file contains invalid JSON.');
}

return $decodedSpec;
}

protected function getMatomoSpecFilePath(): string
{
$currentPluginDir = Manager::getInstance()::getPluginDirectory('OpenApiDocs');

return $currentPluginDir . OpenApiDocs::GENERATED_SPECS_PATH . 'matomo_openapi_spec_v' . OpenApiDocs::DEFAULT_SPEC_VERSION . '.json';
}

protected function isSpecFileReadable(string $filePath): bool
{
return is_file($filePath) && is_readable($filePath);
}

/**
* @param string $filePath
* @return string|false
*/
protected function readSpecFile(string $filePath)
{
return file_get_contents($filePath);
}

/**
* Get the generated API documentation data for the specified plugin.
*
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
## Changelog

5.0.2-b1 - 2026-02-16
5.0.2-b1 - 2026-03-16
- Added support for string literal union types
- Added API endpoint to retrieve static matomo swagger file

5.0.1-b1 - 2026-02-16
- Added class and function level docs
Expand Down
96 changes: 96 additions & 0 deletions tests/Unit/APITest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/

declare(strict_types=1);

namespace Piwik\Plugins\OpenApiDocs\tests\Unit;

require_once PIWIK_INCLUDE_PATH . '/plugins/OpenApiDocs/vendor/autoload.php';

use PHPUnit\Framework\TestCase;
use Piwik\Access;
use Piwik\Container\StaticContainer;
use Piwik\Plugins\OpenApiDocs\API;
use Piwik\Tests\Framework\Mock\FakeAccess;

/**
* @group OpenApiDocs
* @group OpenApiDocs_Unit
* @group OpenApiDocs_APITest
*/
class APITest extends TestCase
{
private $originalAccess;

protected function setUp(): void
{
parent::setUp();

$this->originalAccess = Access::getInstance();
StaticContainer::getContainer()->set(Access::class, new FakeAccess(false, [], [1], 'viewUser'));
}

protected function tearDown(): void
{
StaticContainer::getContainer()->set(Access::class, $this->originalAccess);

parent::tearDown();
}

public function testGetMatomoOpenApiSpecReturnsDecodedJson()
{
$expectedSpec = [
'openapi' => '3.1.0',
'info' => [
'title' => 'Matomo Reporting API',
'version' => '1.0.0',
],
];

$api = $this->buildApiMock(true, json_encode($expectedSpec));

$result = $api->getMatomoOpenApiSpec();

$this->assertSame($expectedSpec, $result);
}

public function testGetMatomoOpenApiSpecThrowsExceptionWhenFileMissing()
{
$api = $this->buildApiMock(false);

$this->expectException(\Exception::class);
$this->expectExceptionMessage('OpenAPI spec file was not found');

$api->getMatomoOpenApiSpec();
}

public function testGetMatomoOpenApiSpecThrowsExceptionWhenJsonIsInvalid()
{
$api = $this->buildApiMock(true, '{invalid json}');

$this->expectException(\Exception::class);
$this->expectExceptionMessage('OpenAPI spec file contains invalid JSON');

$api->getMatomoOpenApiSpec();
}


private function buildApiMock(bool $isReadable, $fileContents = false): API
{
$api = $this->getMockBuilder(API::class)
->onlyMethods(['getMatomoSpecFilePath', 'isSpecFileReadable', 'readSpecFile'])
->getMock();

$api->method('getMatomoSpecFilePath')->willReturn('/tmp/matomo_openapi_spec_v1.0.0.json');
$api->method('isSpecFileReadable')->willReturn($isReadable);
$api->method('readSpecFile')->willReturn($fileContents);

return $api;
}
}