Skip to content

Commit 5e7456a

Browse files
committed
limited invalid ssl to development mode, gating responses behind view access
1 parent 7e39f13 commit 5e7456a

5 files changed

Lines changed: 198 additions & 29 deletions

File tree

API.php

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99

1010
namespace Piwik\Plugins\ApiReference;
1111

12+
use Piwik\Access;
1213
use Piwik\Piwik;
1314
use Piwik\Plugins\ApiReference\Generation\PluginListProvider;
1415
use Piwik\Plugin\Manager;
15-
use Piwik\Plugins\ApiReference\Specs\SpecGenerator;
1616
use Piwik\Plugins\ApiReference\Specs\PathResolver;
1717

1818
/**
@@ -87,9 +87,66 @@ public function getOpenApiSpec(string $pluginName, string $format = 'json'): arr
8787
throw new \Exception('OpenAPI spec file contains invalid JSON.');
8888
}
8989

90+
$canViewExampleSite = in_array(1, Access::getInstance()->getSitesIdWithAtLeastViewAccess(), true);
91+
if (!$canViewExampleSite) {
92+
$decodedSpec = $this->removeSuccessfulResponseExamples($decodedSpec);
93+
}
94+
9095
return $decodedSpec;
9196
}
9297

98+
/**
99+
* Remove embedded example payloads from successful 200 responses.
100+
*
101+
* @param array<string, mixed> $spec
102+
* @return array<string, mixed>
103+
*/
104+
protected function removeSuccessfulResponseExamples(array $spec): array
105+
{
106+
if (empty($spec['paths']) || !is_array($spec['paths'])) {
107+
return $spec;
108+
}
109+
110+
foreach ($spec['paths'] as &$pathItem) {
111+
if (!is_array($pathItem)) {
112+
continue;
113+
}
114+
115+
foreach ($pathItem as &$operation) {
116+
if (
117+
!is_array($operation)
118+
|| empty($operation['responses'])
119+
|| !is_array($operation['responses'])
120+
) {
121+
continue;
122+
}
123+
124+
foreach (['200', 200] as $responseCode) {
125+
if (
126+
empty($operation['responses'][$responseCode])
127+
|| !is_array($operation['responses'][$responseCode])
128+
|| empty($operation['responses'][$responseCode]['content'])
129+
|| !is_array($operation['responses'][$responseCode]['content'])
130+
) {
131+
continue;
132+
}
133+
134+
foreach ($operation['responses'][$responseCode]['content'] as &$content) {
135+
if (!is_array($content)) {
136+
continue;
137+
}
138+
139+
unset($content['example'], $content['examples']);
140+
}
141+
unset($content);
142+
}
143+
}
144+
unset($operation);
145+
}
146+
unset($pathItem);
147+
148+
return $spec;
149+
}
93150
protected function getSpecFilePath(string $pluginName): string
94151
{
95152
return $this->getSpecPathResolver()->getSpecFilePath($pluginName);
@@ -131,30 +188,4 @@ protected function getPluginListProvider(): PluginListProvider
131188
return new PluginListProvider();
132189
}
133190

134-
/**
135-
* Generates an OpenAPI specification for one or more plugins and returns it immediately.
136-
*
137-
* @param string $plugin The plugin name to generate, or a comma-separated list of plugin names.
138-
* @param string $format The response format to generate. Supported values are `json` and `yaml`.
139-
* @return array<string, mixed>|string The generated OpenAPI specification as decoded JSON data for
140-
* `json`, or as a YAML string for `yaml`.
141-
*/
142-
public function getGeneratedOpenApiSpec(string $plugin, string $format)
143-
{
144-
Piwik::checkUserHasSomeViewAccess();
145-
146-
// Return an error if format is something other than JSON or YAML
147-
$allowedFormats = ['json', 'yaml'];
148-
if (!in_array(strtolower($format), $allowedFormats)) {
149-
throw new \Exception(
150-
Piwik::translate(
151-
'General_ExceptionInvalidReportRendererFormat',
152-
[$format, implode(', ', $allowedFormats)]
153-
)
154-
);
155-
}
156-
157-
$docString = (new SpecGenerator())->generatePluginDoc($plugin, $format);
158-
return strtolower($format) === 'json' ? json_decode($docString, true) : $docString;
159-
}
160191
}

Annotations/AnnotationGenerator.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Piwik\API\NoDefaultValue;
2121
use Piwik\API\Proxy;
2222
use Piwik\API\Request;
23+
use Piwik\Development;
2324
use Piwik\Http;
2425
use Piwik\Piwik;
2526
use Piwik\Plugin\Manager;
@@ -1026,7 +1027,7 @@ protected function getDemoReportMetadata(): array
10261027
$file = null,
10271028
$followDepth = 0,
10281029
$acceptLanguage = false,
1029-
$acceptInvalidSslCertificate = true,
1030+
$acceptInvalidSslCertificate = $this->shouldAcceptInvalidSslCertificate(),
10301031
$byteRange = false,
10311032
$getExtendedInfo = true,
10321033
$httpMethod = 'GET'
@@ -1102,7 +1103,7 @@ protected function getExampleIfAvailable(string $url, bool $useLocalToken = fals
11021103
$file = null,
11031104
$followDepth = 0,
11041105
$acceptLanguage = false,
1105-
$acceptInvalidSslCertificate = true,
1106+
$acceptInvalidSslCertificate = $this->shouldAcceptInvalidSslCertificate(),
11061107
$byteRange = false,
11071108
$getExtendedInfo = true,
11081109
$httpMethod = 'GET'
@@ -1192,6 +1193,11 @@ protected function writeFile(string $filePath, string $contents)
11921193
return $this->artifactWriter->writeFile($filePath, $contents);
11931194
}
11941195

1196+
protected function shouldAcceptInvalidSslCertificate(): bool
1197+
{
1198+
return Development::isEnabled();
1199+
}
1200+
11951201
protected function getInstanceUrl(): string
11961202
{
11971203
return rtrim(SettingsPiwik::getPiwikUrl(), '/') . '/';

tests/Resources/MockAnnotationGenerator.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,9 @@ public function shouldUseParameterLevelExample(array $typesMap, string $example)
110110
{
111111
return parent::shouldUseParameterLevelExample($typesMap, $example);
112112
}
113+
114+
public function shouldAcceptInvalidSslCertificate(): bool
115+
{
116+
return parent::shouldAcceptInvalidSslCertificate();
117+
}
113118
}

tests/Unit/APITest.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,46 @@ public function testGetOpenApiSpecReturnsDecodedJsonForPlugin()
6262
$this->assertSame($expectedSpec, $result);
6363
}
6464

65+
public function testGetOpenApiSpecKeepsSuccessfulExamplesForUsersWithSiteOneAccess(): void
66+
{
67+
StaticContainer::getContainer()->set(Access::class, new FakeAccess(false, [], [1], 'siteOneViewer'));
68+
69+
$expectedSpec = $this->getSpecFixtureWithResponseExamples();
70+
$api = $this->buildApiMock('/tmp/CustomAlerts_openapi_spec_v1.0.0.json', true, json_encode($expectedSpec));
71+
72+
$result = $api->getOpenApiSpec('CustomAlerts');
73+
74+
$this->assertSame($expectedSpec, $result);
75+
}
76+
77+
public function testGetOpenApiSpecRemovesOnlySuccessfulExamplesForUsersWithoutSiteOneAccess(): void
78+
{
79+
StaticContainer::getContainer()->set(Access::class, new FakeAccess(false, [], [2], 'otherViewer'));
80+
81+
$api = $this->buildApiMock(
82+
'/tmp/CustomAlerts_openapi_spec_v1.0.0.json',
83+
true,
84+
json_encode($this->getSpecFixtureWithResponseExamples())
85+
);
86+
87+
$result = $api->getOpenApiSpec('CustomAlerts');
88+
89+
$this->assertArrayNotHasKey('example', $result['paths']['/endpoint']['get']['responses']['200']['content']['application/json']);
90+
$this->assertArrayNotHasKey('examples', $result['paths']['/endpoint']['get']['responses']['200']['content']['application/json']);
91+
$this->assertSame(
92+
'kept error example',
93+
$result['paths']['/endpoint']['get']['responses']['400']['content']['application/json']['example']
94+
);
95+
$this->assertSame(
96+
['type' => 'string', 'example' => 'stay put'],
97+
$result['paths']['/endpoint']['get']['parameters'][0]['schema']
98+
);
99+
$this->assertSame(
100+
['value' => ['id' => 99]],
101+
$result['paths']['/endpoint']['get']['requestBody']['content']['application/json']['examples']['request']
102+
);
103+
}
104+
65105
public function testGetAllowedPluginsReturnsProviderValues(): void
66106
{
67107
$provider = $this->createMock(PluginListProvider::class);
@@ -171,6 +211,65 @@ private function buildApiMock(string $filePath, bool $isReadable, $fileContents
171211
return $api;
172212
}
173213

214+
/**
215+
* @return array<string, mixed>
216+
*/
217+
private function getSpecFixtureWithResponseExamples(): array
218+
{
219+
return [
220+
'openapi' => '3.1.0',
221+
'paths' => [
222+
'/endpoint' => [
223+
'get' => [
224+
'parameters' => [
225+
[
226+
'name' => 'label',
227+
'schema' => [
228+
'type' => 'string',
229+
'example' => 'stay put',
230+
],
231+
],
232+
],
233+
'requestBody' => [
234+
'content' => [
235+
'application/json' => [
236+
'examples' => [
237+
'request' => [
238+
'value' => ['id' => 99],
239+
],
240+
],
241+
],
242+
],
243+
],
244+
'responses' => [
245+
'200' => [
246+
'description' => 'Success',
247+
'content' => [
248+
'application/json' => [
249+
'example' => ['value' => 'remove me'],
250+
'examples' => [
251+
'success' => [
252+
'value' => ['another' => 'remove me'],
253+
],
254+
],
255+
],
256+
],
257+
],
258+
'400' => [
259+
'description' => 'Error',
260+
'content' => [
261+
'application/json' => [
262+
'example' => 'kept error example',
263+
],
264+
],
265+
],
266+
],
267+
],
268+
],
269+
],
270+
];
271+
}
272+
174273
/**
175274
* @param object $object
176275
* @param string $methodName

tests/Unit/AnnotationGeneratorTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
use PHPUnit\Framework\TestCase;
1717
use Piwik\API\DocumentationGenerator;
1818
use Piwik\API\NoDefaultValue;
19+
use Piwik\Config;
20+
use Piwik\Development;
1921
use Piwik\Plugins\ApiReference\Annotations\AnnotationGenerator;
2022
use Piwik\Plugins\ApiReference\ApiReference;
2123
use Piwik\Plugins\ApiReference\tests\Resources\MockAnnotationGenerator;
@@ -1319,6 +1321,25 @@ public function testShouldUseParameterLevelExampleForScalarArrayUnions(): void
13191321
$this->assertFalse($annotationGenerator->shouldUseParameterLevelExample(['string' => null, 'array' => 'string'], 'one'));
13201322
}
13211323

1324+
public function testShouldAcceptInvalidSslCertificateMatchesDevelopmentMode(): void
1325+
{
1326+
$annotationGenerator = new MockAnnotationGenerator(new DocumentationGenerator());
1327+
$defaultValue = Config::getInstance()->Development['enabled'] ?? 0;
1328+
1329+
try {
1330+
Config::getInstance()->Development['enabled'] = 0;
1331+
$this->resetDevelopmentModeCache();
1332+
$this->assertFalse($annotationGenerator->shouldAcceptInvalidSslCertificate());
1333+
1334+
Config::getInstance()->Development['enabled'] = 1;
1335+
$this->resetDevelopmentModeCache();
1336+
$this->assertTrue($annotationGenerator->shouldAcceptInvalidSslCertificate());
1337+
} finally {
1338+
Config::getInstance()->Development['enabled'] = $defaultValue;
1339+
$this->resetDevelopmentModeCache();
1340+
}
1341+
}
1342+
13221343
/**
13231344
* @dataProvider getTestDataForWrapStringWithQuotes
13241345
*
@@ -1415,4 +1436,11 @@ public function testCompileOperationLines(): void
14151436
// TODO - compileOperationLines method
14161437
$this->expectNotToPerformAssertions();
14171438
}
1439+
1440+
private function resetDevelopmentModeCache(): void
1441+
{
1442+
$reflection = new \ReflectionProperty(Development::class, 'isEnabled');
1443+
$reflection->setAccessible(true);
1444+
$reflection->setValue(null, null);
1445+
}
14181446
}

0 commit comments

Comments
 (0)