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
32 changes: 24 additions & 8 deletions Annotations/AnnotationGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -845,7 +845,7 @@ protected function getApplicableDemoExampleUrls(string $pluginName, string $meth
}
}

$exampleUrl = 'https://demo.matomo.cloud/' . $exampleUrl;
$exampleUrl = $this->prependInstanceUrl($exampleUrl);
return [
'xml' => $exampleUrl . '&format=xml&token_auth=anonymous',
'json' => $exampleUrl . '&format=JSON&token_auth=anonymous',
Expand All @@ -854,7 +854,7 @@ protected function getApplicableDemoExampleUrls(string $pluginName, string $meth
}

/**
* Query demo.matomo.cloud for report metadata which can later be used to help determine good example URLs for
* Query for report metadata which can later be used to help determine good example URLs for
* specific API endpoints. This method is only used when the example URL can't be determined using the default
* method. This only works for endpoints associated with reports and have metadata provided by the containing
* plugin. The response is cached as a property so the request is only made once regardless of how many times this
Expand All @@ -871,7 +871,7 @@ protected function getDemoReportMetadata(): array
return $this->reportMetadata;
}

$url = 'https://demo.matomo.cloud/index.php?module=API&method=API.getReportMetadata&format=JSON&idSite=1&hideMetricsDoc=0&showSubtableReports=0&filter_limit=-1&period=day';
$url = $this->getReportMetadataUrl();
try {
$response = Http::sendHttpRequestBy(
Http::getTransportMethod(),
Expand Down Expand Up @@ -903,11 +903,10 @@ protected function getDemoReportMetadata(): array

/**
* Take the example URL and query the endpoint for an example response, hiding subtables. If a response isn't
* received from demo.matomo.cloud, it can try using a temporary token to make the request against the current
* received, it can try using a temporary token to make the request against the current
* instance of Matomo.
*
* @param string $url The full example URL. E.g.
* https://demo.matomo.cloud/?module=API&method=CustomReports.getConfiguredReports&idSite=1&format=xml&token_auth=anonymous
* @param string $url The full example URL.
* @param bool $useLocalToken A boolean indicating whether to get a temporary token and try the request against the
* currently running Matomo instance.
* @param bool $ignoreCached A boolean indicating whether the cached response file should be ignored. Default is
Expand Down Expand Up @@ -948,7 +947,6 @@ protected function getExampleIfAvailable(string $url, bool $useLocalToken = fals
if ($useLocalToken) {
$token = Piwik::requestTemporarySystemAuthToken('OpenApiDocs', 24);
$tempUrl = str_replace('&token_auth=anonymous', '&token_auth=' . $token, $tempUrl);
$tempUrl = str_replace('https://demo.matomo.cloud/', SettingsPiwik::getPiwikUrl(), $tempUrl);
}
try {
$response = Http::sendHttpRequestBy(
Expand Down Expand Up @@ -1050,6 +1048,24 @@ protected function writeFile(string $filePath, string $contents)
return $this->artifactWriter->writeFile($filePath, $contents);
}

protected function getInstanceUrl(): string
{
return rtrim(SettingsPiwik::getPiwikUrl(), '/') . '/';
}

protected function prependInstanceUrl(string $path): string
{
return $this->getInstanceUrl() . ltrim($path, '/');
}

protected function getReportMetadataUrl(): string
{
return $this->prependInstanceUrl(
'index.php?module=API&method=API.getReportMetadata&format=JSON&idSite=1&hideMetricsDoc=0'
. '&showSubtableReports=0&filter_limit=-1&period=day'
);
}

/**
* Try to build an example URL for a specific API method using report metadata. This queries the demo server for
* report metadata to get examples of existing reports which can be used as example URLS. If no metadata matches the
Expand Down Expand Up @@ -1094,7 +1110,7 @@ protected function getReportExampleUrlFromMetadata(string $pluginName, string $m
);

// Use the JSON format for the test. If we get a valid response, return the URL without format.
if (!empty($this->getExampleIfAvailable('https://demo.matomo.cloud/' . $url . '&format=JSON'))) {
if (!empty($this->getExampleIfAvailable($this->prependInstanceUrl($url . '&format=JSON')))) {
return $url;
}
}
Expand Down
7 changes: 1 addition & 6 deletions Annotations/GlobalApiComponents.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,14 @@
* securityScheme="MatomoToken",
* type="http",
* scheme="bearer",
* description="Matomo API token passed in the Authorization header as a bearer token. For demo.matomo.cloud requests, use the anonymous token value: anonymous."
* description="Matomo API token passed in the Authorization header as a bearer token."
* )
*
* @OA\Server(
* url=LOCAL_MATOMO_SERVER_URL,
* description="Current Matomo instance"
* )
*
* @OA\Server(
* url="https://demo.matomo.cloud/",
* description="Matomo demo server"
* )
*
* Generic Error object
* @OA\Schema(
* schema="GenericSuccess",
Expand Down
66 changes: 20 additions & 46 deletions Specs/PathResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,30 @@

namespace Piwik\Plugins\OpenApiDocs\Specs;

use Piwik\Container\Container;
use Piwik\Container\StaticContainer;
use Piwik\Plugin\Manager;
use Piwik\Piwik;
use Piwik\Plugins\OpenApiDocs\OpenApiDocs;

class PathResolver
{
private const SHARED_BASE_SUBDIRECTORY = '/OpenApiDocs/';
private const ARTIFACT_BASE_SUBDIRECTORY = '/tmp/';

private const SHARED_SPECS_SUBDIRECTORY = '/OpenApiDocs/specs/';
private const SPECS_SUBDIRECTORY = 'specs/';

private const SHARED_ANNOTATIONS_SUBDIRECTORY = '/OpenApiDocs/annotations/';
private const ANNOTATIONS_SUBDIRECTORY = 'annotations/';

private const SHARED_RESPONSES_SUBDIRECTORY = '/OpenApiDocs/responses/';
private const RESPONSES_SUBDIRECTORY = 'responses/';

private $pluginDirectory;

private $isCloudActivated;

private $container;

public function __construct(?string $pluginDirectory = null, ?bool $isCloudActivated = null, ?Container $container = null)
public function __construct(?string $pluginDirectory = null)
{
$this->pluginDirectory = $pluginDirectory ?? Manager::getInstance()::getPluginDirectory('OpenApiDocs');
$this->isCloudActivated = $isCloudActivated ?? Manager::getInstance()->isPluginActivated('Cloud');
$this->container = $container ?? $this->getStaticContainer();
}

public function getSpecDirectory(): string
{
return $this->getArtifactDirectory(self::SHARED_SPECS_SUBDIRECTORY, OpenApiDocs::GENERATED_SPECS_PATH);
return $this->getArtifactDirectory(self::SPECS_SUBDIRECTORY);
}

public function getSpecFilePath(
Expand All @@ -54,7 +47,7 @@ public function getSpecFilePath(

public function getAnnotationsDirectory(): string
{
return $this->getArtifactDirectory(self::SHARED_ANNOTATIONS_SUBDIRECTORY, OpenApiDocs::GENERATED_ANNOTATIONS_PATH);
return $this->getArtifactDirectory(self::ANNOTATIONS_SUBDIRECTORY);
}

public function getAnnotationFilePath(string $pluginName): string
Expand All @@ -69,53 +62,34 @@ public function getApiMethodInfoFilePath(string $fileBaseName): string

public function getResponsesDirectory(): string
{
return $this->getArtifactDirectory(self::SHARED_RESPONSES_SUBDIRECTORY, OpenApiDocs::EXAMPLE_RESPONSES_PATH);
return $this->getArtifactDirectory(self::RESPONSES_SUBDIRECTORY);
}

public function getExampleResponseFilePath(string $pluginName, string $methodName, string $format): string
{
return $this->getResponsesDirectory() . $pluginName . '.' . $methodName . '.' . strtolower($format);
}

private function getArtifactDirectory(string $sharedSubdirectory, string $fallbackPath): string
private function getArtifactDirectory(string $subdirectory): string
{
$sharedPath = $this->getSharedArtifactDirectory($sharedSubdirectory);
if ($sharedPath !== null) {
return $sharedPath;
}

return $this->pluginDirectory . $fallbackPath;
return $this->getArtifactBasePath() . $subdirectory;
}

private function getSharedArtifactDirectory(string $sharedSubdirectory): ?string
private function getArtifactBasePath(): string
{
if (!$this->isCloudActivated || $this->container === null || !$this->container->has('CloudDistributedCachePath')) {
return null;
}
$defaultArtifactBasePath = $this->pluginDirectory . self::ARTIFACT_BASE_SUBDIRECTORY;
$artifactBasePath = $defaultArtifactBasePath;
$this->dispatchArtifactBasePathEvent($artifactBasePath);

$sharedBasePath = trim((string) $this->container->get('CloudDistributedCachePath'));
if ($sharedBasePath === '') {
return null;
if (empty($artifactBasePath)) {
$artifactBasePath = $defaultArtifactBasePath;
}

if (!$this->isUsableSharedBasePath($sharedBasePath)) {
return null;
}

return rtrim($sharedBasePath, '/\\') . self::SHARED_BASE_SUBDIRECTORY . ltrim(substr($sharedSubdirectory, strlen(self::SHARED_BASE_SUBDIRECTORY)), '/\\');
return rtrim($artifactBasePath, '/\\') . '/';
}

protected function isUsableSharedBasePath(string $sharedBasePath): bool
protected function dispatchArtifactBasePathEvent(?string &$artifactBasePath): void
{
return is_dir($sharedBasePath) && is_writable($sharedBasePath);
}

private function getStaticContainer(): ?Container
{
try {
return StaticContainer::getContainer();
} catch (\Throwable $e) {
return null;
}
Piwik::postEvent('OpenApiDocs.getArtifactBasePath', [&$artifactBasePath]);
}
}
14 changes: 12 additions & 2 deletions tests/Resources/MockAnnotationGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ public function getDemoReportMetadata(): array
/**
* @inheritDoc
*/
public function getExampleIfAvailable(string $url, bool $useLocalToken = false): string
public function getExampleIfAvailable(string $url, bool $useLocalToken = false, bool $ignoreCached = false): string
{
return parent::getExampleIfAvailable($url, $useLocalToken);
return parent::getExampleIfAvailable($url, $useLocalToken, $ignoreCached);
}

/**
Expand All @@ -73,6 +73,16 @@ public function getReportExampleUrlFromMetadata(string $pluginName, string $meth
return parent::getReportExampleUrlFromMetadata($pluginName, $methodName);
}

public function getReportMetadataUrl(): string
{
return parent::getReportMetadataUrl();
}

public function prependInstanceUrl(string $path): string
{
return parent::prependInstanceUrl($path);
}

/**
* @inheritDoc
*/
Expand Down
84 changes: 84 additions & 0 deletions tests/Unit/AnnotationGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Piwik\API\NoDefaultValue;
use Piwik\Plugins\OpenApiDocs\Annotations\AnnotationGenerator;
use Piwik\Plugins\OpenApiDocs\OpenApiDocs;
use Piwik\Plugins\OpenApiDocs\tests\Resources\MockAnnotationGenerator;

/**
* @group OpenApiDocs
Expand Down Expand Up @@ -286,6 +287,89 @@ public function testBuildAnnotationForMethod(): void
$this->expectNotToPerformAssertions();
}

public function testGetApplicableDemoExampleUrlsUsesCurrentInstanceUrl(): void
{
$generator = $this->getMockBuilder(DocumentationGenerator::class)
->disableOriginalConstructor()
->onlyMethods(['getExampleUrl'])
->getMock();
$generator->expects($this->once())
->method('getExampleUrl')
->with('\\Piwik\\Plugins\\API\\API', 'get', [
'idSite' => 1,
'period' => 'day',
'date' => 'today',
])
->willReturn('index.php?module=API&method=API.get&idSite=1&period=day&date=today');

$annotationGenerator = new class ($generator) extends MockAnnotationGenerator {
protected function getInstanceUrl(): string
{
return 'https://local.matomo.test/';
}
};

$this->assertSame([
'xml' => 'https://local.matomo.test/index.php?module=API&method=API.get&idSite=1&period=day&date=today&format=xml&token_auth=anonymous',
'json' => 'https://local.matomo.test/index.php?module=API&method=API.get&idSite=1&period=day&date=today&format=JSON&token_auth=anonymous',
'tsv' => 'https://local.matomo.test/index.php?module=API&method=API.get&idSite=1&period=day&date=today&format=Tsv&token_auth=anonymous',
], $annotationGenerator->getApplicableDemoExampleUrls('API', 'get', []));
}

public function testGetReportMetadataUrlUsesCurrentInstanceUrl(): void
{
$annotationGenerator = new class (new DocumentationGenerator()) extends MockAnnotationGenerator {
protected function getInstanceUrl(): string
{
return 'https://local.matomo.test/';
}
};

$this->assertSame(
'https://local.matomo.test/index.php?module=API&method=API.getReportMetadata&format=JSON&idSite=1'
. '&hideMetricsDoc=0&showSubtableReports=0&filter_limit=-1&period=day',
$annotationGenerator->getReportMetadataUrl()
);
}

public function testGetReportExampleUrlFromMetadataUsesCurrentInstanceUrl(): void
{
$annotationGenerator = new class (new DocumentationGenerator()) extends MockAnnotationGenerator {
public $receivedUrl = null;

protected function getInstanceUrl(): string
{
return 'https://local.matomo.test/';
}

public function getDemoReportMetadata(): array
{
return [[
'module' => 'VisitsSummary',
'action' => 'get',
'imageGraphUrl' => 'index.php?module=API&method=ImageGraph.get&apiModule=VisitsSummary&apiAction=get&idSite=1&period=day&date=today',
]];
}

public function getExampleIfAvailable(string $url, bool $useLocalToken = false, bool $ignoreCached = false): string
{
$this->receivedUrl = $url;
return '{"result":"ok"}';
}
};

$result = $annotationGenerator->getReportExampleUrlFromMetadata('VisitsSummary', 'get');

$this->assertSame(
'index.php?module=API&method=VisitsSummary.get&idSite=1&period=day&date=today',
$result
);
$this->assertSame(
'https://local.matomo.test/index.php?module=API&method=VisitsSummary.get&idSite=1&period=day&date=today&format=JSON',
$annotationGenerator->receivedUrl
);
}

public function testGetParamInfoFromDocBlock(): void
{
// TODO - Update to use resource file and/or dataprovider to test more than one comment block
Expand Down
Loading
Loading