diff --git a/Annotations/AnnotationGenerator.php b/Annotations/AnnotationGenerator.php index 28f1118..beca23c 100644 --- a/Annotations/AnnotationGenerator.php +++ b/Annotations/AnnotationGenerator.php @@ -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', @@ -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 @@ -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(), @@ -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 @@ -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( @@ -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 @@ -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; } } diff --git a/Annotations/GlobalApiComponents.php b/Annotations/GlobalApiComponents.php index 8bf2d3a..033a735 100644 --- a/Annotations/GlobalApiComponents.php +++ b/Annotations/GlobalApiComponents.php @@ -36,7 +36,7 @@ * 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( @@ -44,11 +44,6 @@ * description="Current Matomo instance" * ) * - * @OA\Server( - * url="https://demo.matomo.cloud/", - * description="Matomo demo server" - * ) - * * Generic Error object * @OA\Schema( * schema="GenericSuccess", diff --git a/Specs/PathResolver.php b/Specs/PathResolver.php index 1a012d2..6ccb8ae 100644 --- a/Specs/PathResolver.php +++ b/Specs/PathResolver.php @@ -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( @@ -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 @@ -69,7 +62,7 @@ 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 @@ -77,45 +70,26 @@ public function getExampleResponseFilePath(string $pluginName, string $methodNam 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]); } } diff --git a/tests/Resources/MockAnnotationGenerator.php b/tests/Resources/MockAnnotationGenerator.php index b7e376c..b56f7b1 100644 --- a/tests/Resources/MockAnnotationGenerator.php +++ b/tests/Resources/MockAnnotationGenerator.php @@ -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); } /** @@ -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 */ diff --git a/tests/Unit/AnnotationGeneratorTest.php b/tests/Unit/AnnotationGeneratorTest.php index 9790de7..d91dffd 100644 --- a/tests/Unit/AnnotationGeneratorTest.php +++ b/tests/Unit/AnnotationGeneratorTest.php @@ -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 @@ -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 diff --git a/tests/Unit/Specs/PathResolverTest.php b/tests/Unit/Specs/PathResolverTest.php index 45d983a..8867e3f 100644 --- a/tests/Unit/Specs/PathResolverTest.php +++ b/tests/Unit/Specs/PathResolverTest.php @@ -14,7 +14,6 @@ require_once PIWIK_INCLUDE_PATH . '/plugins/OpenApiDocs/vendor/autoload.php'; use PHPUnit\Framework\TestCase; -use Piwik\Container\Container; use Piwik\Plugins\OpenApiDocs\Specs\PathResolver; /** @@ -26,34 +25,34 @@ class PathResolverTest extends TestCase { public function testReturnsPluginLocalPathsWhenCloudIsDisabled(): void { - $resolver = new PathResolver('/plugins/OpenApiDocs', false); + $resolver = new PathResolver('/plugins/OpenApiDocs'); $this->assertSame('/plugins/OpenApiDocs/tmp/specs/', $resolver->getSpecDirectory()); $this->assertSame('/plugins/OpenApiDocs/tmp/annotations/', $resolver->getAnnotationsDirectory()); $this->assertSame('/plugins/OpenApiDocs/tmp/responses/', $resolver->getResponsesDirectory()); } - public function testReturnsSharedPathsWhenCloudIsEnabledAndDistributedCachePathExists(): void + public function testReturnsOverriddenPathsWhenArtifactBasePathEventProvidesOne(): void { - $resolver = $this->buildPathResolverWithSharedPathValidationResult(true, '/cache/distributed', true); + $resolver = $this->buildPathResolverWithArtifactBasePath('/cache/distributed/OpenApiDocs'); $this->assertSame('/cache/distributed/OpenApiDocs/specs/', $resolver->getSpecDirectory()); $this->assertSame('/cache/distributed/OpenApiDocs/annotations/', $resolver->getAnnotationsDirectory()); $this->assertSame('/cache/distributed/OpenApiDocs/responses/', $resolver->getResponsesDirectory()); } - public function testFallsBackToPluginLocalPathsWhenCloudCachePathIsMissing(): void + public function testTrimsTrailingSlashesFromOverriddenArtifactBasePath(): void { - $resolver = new PathResolver('/plugins/OpenApiDocs', true, $this->buildContainerStub(false)); + $resolver = $this->buildPathResolverWithArtifactBasePath('/cache/distributed/OpenApiDocs/'); - $this->assertSame('/plugins/OpenApiDocs/tmp/specs/', $resolver->getSpecDirectory()); - $this->assertSame('/plugins/OpenApiDocs/tmp/annotations/', $resolver->getAnnotationsDirectory()); - $this->assertSame('/plugins/OpenApiDocs/tmp/responses/', $resolver->getResponsesDirectory()); + $this->assertSame('/cache/distributed/OpenApiDocs/specs/', $resolver->getSpecDirectory()); + $this->assertSame('/cache/distributed/OpenApiDocs/annotations/', $resolver->getAnnotationsDirectory()); + $this->assertSame('/cache/distributed/OpenApiDocs/responses/', $resolver->getResponsesDirectory()); } - public function testFallsBackToPluginLocalPathsWhenCloudCachePathIsEmpty(): void + public function testKeepsDefaultLocalPathsWhenEventDoesNotProvideOverride(): void { - $resolver = new PathResolver('/plugins/OpenApiDocs', true, $this->buildContainerStub(true, ' ')); + $resolver = $this->buildPathResolverWithArtifactBasePath(null); $this->assertSame('/plugins/OpenApiDocs/tmp/specs/', $resolver->getSpecDirectory()); $this->assertSame('/plugins/OpenApiDocs/tmp/annotations/', $resolver->getAnnotationsDirectory()); @@ -62,7 +61,7 @@ public function testFallsBackToPluginLocalPathsWhenCloudCachePathIsEmpty(): void public function testBuildsFilePathsUsingExpectedNamingConventions(): void { - $resolver = $this->buildPathResolverWithSharedPathValidationResult(true, '/cache/distributed/', true); + $resolver = $this->buildPathResolverWithArtifactBasePath('/cache/distributed/OpenApiDocs/'); $this->assertSame( '/cache/distributed/OpenApiDocs/specs/CustomAlerts_openapi_spec_v2.0.0.yaml', @@ -82,39 +81,17 @@ public function testBuildsFilePathsUsingExpectedNamingConventions(): void ); } - private function buildContainerStub(bool $hasDistributedCachePath, string $distributedCachePath = ''): Container + private function buildPathResolverWithArtifactBasePath(?string $artifactBasePath): PathResolver { - $container = $this->getMockBuilder(Container::class) - ->disableOriginalConstructor() - ->onlyMethods(['has', 'get']) - ->getMock(); - - $container->method('has') - ->with('CloudDistributedCachePath') - ->willReturn($hasDistributedCachePath); - - if ($hasDistributedCachePath) { - $container->method('get') - ->with('CloudDistributedCachePath') - ->willReturn($distributedCachePath); - } - - return $container; - } - - private function buildPathResolverWithSharedPathValidationResult( - bool $hasDistributedCachePath, - string $distributedCachePath, - bool $isUsableSharedBasePath - ): PathResolver { $resolver = $this->getMockBuilder(PathResolver::class) - ->setConstructorArgs(['/plugins/OpenApiDocs', true, $this->buildContainerStub($hasDistributedCachePath, $distributedCachePath)]) - ->onlyMethods(['isUsableSharedBasePath']) + ->setConstructorArgs(['/plugins/OpenApiDocs']) + ->onlyMethods(['dispatchArtifactBasePathEvent']) ->getMock(); - $resolver->method('isUsableSharedBasePath') - ->with(trim($distributedCachePath)) - ->willReturn($isUsableSharedBasePath); + $resolver->method('dispatchArtifactBasePathEvent') + ->willReturnCallback(static function (?string &$resolvedArtifactBasePath) use ($artifactBasePath): void { + $resolvedArtifactBasePath = $artifactBasePath; + }); return $resolver; }