Skip to content

Commit 4846351

Browse files
Merge pull request #876 from matomo-org/PG-5029-fix-cors
Switch to proxy URL to handle API requests to demo, #PG-5029
2 parents 18f4932 + 59f2d5c commit 4846351

3 files changed

Lines changed: 142 additions & 2 deletions

File tree

app/helpers/DemoProxy.php

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
/**
3+
* Piwik - Open source web analytics
4+
*
5+
* @link http://piwik.org
6+
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7+
*/
8+
9+
namespace helpers;
10+
11+
/**
12+
* Proxy for demo.matomo.cloud, allows us to make single origin requests by doing them through this class
13+
*/
14+
class DemoProxy
15+
{
16+
17+
private const MATOMO_SWAGGER_PROXY_TARGET = 'https://demo.matomo.cloud';
18+
private const DEMO_AUTHORIZATION_HEADER = 'Bearer anonymous';
19+
20+
/**
21+
* Fetches a demo API GET response and returns the body and status code.
22+
*
23+
* @param string $url Demo API URL to request.
24+
* @return array{body: string, statusCode: int}
25+
*/
26+
public static function get(string $url): array
27+
{
28+
$context = self::createContext();
29+
$proxiedResponse = self::fetchResponse($url, $context);
30+
$statusCode = self::parseStatusCode($proxiedResponse['responseHeaders']);
31+
32+
return [
33+
'body' => $proxiedResponse['body'],
34+
'statusCode' => $statusCode,
35+
];
36+
}
37+
38+
/**
39+
* Builds a validated demo API URL for proxying.
40+
*
41+
* @param string $path Relative request path.
42+
* @param array $queryParams Query parameters to append to the URL.
43+
* @return string The validated absolute demo API URL.
44+
* @throws \InvalidArgumentException If the path is not index.php.
45+
* @throws \InvalidArgumentException If the module query parameter is not API.
46+
*/
47+
public static function buildValidatedApiUrl(string $path, array $queryParams): string
48+
{
49+
$normalizedPath = trim($path, '/');
50+
if ($normalizedPath !== 'index.php') {
51+
throw new \InvalidArgumentException('Path must be index.php');
52+
}
53+
54+
if (($queryParams['module'] ?? null) !== 'API') {
55+
throw new \InvalidArgumentException('Module must be API');
56+
}
57+
58+
$targetUrl = rtrim(self::MATOMO_SWAGGER_PROXY_TARGET, '/') . '/' . $normalizedPath;
59+
$query = http_build_query($queryParams);
60+
61+
if ($query !== '') {
62+
$targetUrl .= '?' . $query;
63+
}
64+
65+
return $targetUrl;
66+
}
67+
68+
private static function createContext()
69+
{
70+
return stream_context_create([
71+
'http' => [
72+
'method' => 'GET',
73+
'header' => self::buildHeaders(),
74+
'ignore_errors' => true,
75+
'timeout' => 30,
76+
],
77+
]);
78+
}
79+
80+
private static function buildHeaders(): string
81+
{
82+
return 'Authorization: ' . self::DEMO_AUTHORIZATION_HEADER;
83+
}
84+
85+
/**
86+
* @return array{body: string, responseHeaders: array}
87+
*/
88+
private static function fetchResponse(string $url, $context): array
89+
{
90+
$body = @file_get_contents($url, false, $context);
91+
if ($body === false) {
92+
throw new \RuntimeException('Could not proxy HTTP request');
93+
}
94+
95+
return [
96+
'body' => $body,
97+
'responseHeaders' => $http_response_header ?? [],
98+
];
99+
}
100+
101+
private static function parseStatusCode(array $responseHeaders): int
102+
{
103+
$statusLine = $responseHeaders[0] ?? '';
104+
105+
if (preg_match('#^HTTP/\S+\s+(\d{3})#', $statusLine, $matches)) {
106+
return (int) $matches[1];
107+
}
108+
109+
return 200;
110+
}
111+
}

app/routes/page.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use helpers\Content\Category\TracTicketArchiveCategory;
2121
use helpers\Content\Guide;
2222
use helpers\Content\PhpDoc;
23+
use helpers\DemoProxy;
2324
use helpers\DocumentNotExistException;
2425
use helpers\Environment;
2526
use helpers\OpenApiSpecRegistry;
@@ -31,6 +32,7 @@
3132
use Slim\Exception\HttpNotFoundException;
3233

3334

35+
3436
function renderGuide(Slim\Views\Twig $view, Response $response, Psr\Http\Message\UriInterface $uri, Guide $guide, Category $category, string $template = 'guide.twig')
3537
{
3638
return $view->render($response, $template, [
@@ -265,6 +267,27 @@ function renderGuide(Slim\Views\Twig $view, Response $response, Psr\Http\Message
265267
->withStatus(200);
266268
});
267269

270+
$app->get('/demo/{path:.*}', function (Request $request, Response $response, $args) {
271+
$path = $args['path'] ?? '';
272+
273+
try {
274+
$targetUrl = DemoProxy::buildValidatedApiUrl($path, $request->getQueryParams());
275+
} catch (\InvalidArgumentException $e) {
276+
$response->getBody()->write($e->getMessage());
277+
return $response->withStatus(400);
278+
}
279+
280+
try {
281+
$proxiedResponse = DemoProxy::get($targetUrl);
282+
} catch (\RuntimeException $e) {
283+
$response->getBody()->write('Could not proxy demo request');
284+
return $response->withStatus(502);
285+
}
286+
287+
$response->getBody()->write($proxiedResponse['body']);
288+
return $response->withStatus($proxiedResponse['statusCode']);
289+
});
290+
268291
$app->post('/receive-commit-hook', function (Request $request, Response $response, $args) {
269292
$params = $request->getQueryParams();
270293
if (empty($params["token"]) || !password_verify($params["token"], WEBHOOK_TOKEN)) {

app/templates/api-swagger.twig

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
<script src="/vendor/swagger-ui/swagger-ui-bundle.js?{{ revision|e('html_attr') }}"></script>
2020
<script type="text/javascript">
2121
window.onload = function () {
22-
let pluginSpec = {{ pluginSpecJson|raw }};
22+
let pluginSpecJson = {{ pluginSpecJson|raw }};
23+
let proxyBaseUrl = '/demo';
2324
let summaryPrefix = '/index.php?module=API&method=';
2425
2526
function clonePluginSpec(spec) {
@@ -50,10 +51,15 @@
5051
}
5152
5253
if (typeof SwaggerUIBundle === 'function') {
54+
let pluginSpec = clonePluginSpec(pluginSpecJson);
55+
56+
// CORs on demo not allowing authorization: Bearer anonymous in preflight, so we need a proxy into demo
57+
pluginSpec.servers = [{ url: proxyBaseUrl }];
58+
5359
SwaggerUIBundle({
5460
dom_id: '#swagger-ui',
5561
url: null,
56-
spec: clonePluginSpec(pluginSpec),
62+
spec: pluginSpec,
5763
docExpansion: 'list',
5864
tagsSorter: 'alpha',
5965
presets: [

0 commit comments

Comments
 (0)