diff --git a/app/helpers/DemoProxy.php b/app/helpers/DemoProxy.php new file mode 100644 index 000000000..86ae6d3b9 --- /dev/null +++ b/app/helpers/DemoProxy.php @@ -0,0 +1,111 @@ + $proxiedResponse['body'], + 'statusCode' => $statusCode, + ]; + } + + /** + * Builds a validated demo API URL for proxying. + * + * @param string $path Relative request path. + * @param array $queryParams Query parameters to append to the URL. + * @return string The validated absolute demo API URL. + * @throws \InvalidArgumentException If the path is not index.php. + * @throws \InvalidArgumentException If the module query parameter is not API. + */ + public static function buildValidatedApiUrl(string $path, array $queryParams): string + { + $normalizedPath = trim($path, '/'); + if ($normalizedPath !== 'index.php') { + throw new \InvalidArgumentException('Path must be index.php'); + } + + if (($queryParams['module'] ?? null) !== 'API') { + throw new \InvalidArgumentException('Module must be API'); + } + + $targetUrl = rtrim(self::MATOMO_SWAGGER_PROXY_TARGET, '/') . '/' . $normalizedPath; + $query = http_build_query($queryParams); + + if ($query !== '') { + $targetUrl .= '?' . $query; + } + + return $targetUrl; + } + + private static function createContext() + { + return stream_context_create([ + 'http' => [ + 'method' => 'GET', + 'header' => self::buildHeaders(), + 'ignore_errors' => true, + 'timeout' => 30, + ], + ]); + } + + private static function buildHeaders(): string + { + return 'Authorization: ' . self::DEMO_AUTHORIZATION_HEADER; + } + + /** + * @return array{body: string, responseHeaders: array} + */ + private static function fetchResponse(string $url, $context): array + { + $body = @file_get_contents($url, false, $context); + if ($body === false) { + throw new \RuntimeException('Could not proxy HTTP request'); + } + + return [ + 'body' => $body, + 'responseHeaders' => $http_response_header ?? [], + ]; + } + + private static function parseStatusCode(array $responseHeaders): int + { + $statusLine = $responseHeaders[0] ?? ''; + + if (preg_match('#^HTTP/\S+\s+(\d{3})#', $statusLine, $matches)) { + return (int) $matches[1]; + } + + return 200; + } +} diff --git a/app/routes/page.php b/app/routes/page.php index e1c8903ce..02043e0b2 100644 --- a/app/routes/page.php +++ b/app/routes/page.php @@ -20,6 +20,7 @@ use helpers\Content\Category\TracTicketArchiveCategory; use helpers\Content\Guide; use helpers\Content\PhpDoc; +use helpers\DemoProxy; use helpers\DocumentNotExistException; use helpers\Environment; use helpers\OpenApiSpecRegistry; @@ -31,6 +32,7 @@ use Slim\Exception\HttpNotFoundException; + function renderGuide(Slim\Views\Twig $view, Response $response, Psr\Http\Message\UriInterface $uri, Guide $guide, Category $category, string $template = 'guide.twig') { return $view->render($response, $template, [ @@ -265,6 +267,27 @@ function renderGuide(Slim\Views\Twig $view, Response $response, Psr\Http\Message ->withStatus(200); }); +$app->get('/demo/{path:.*}', function (Request $request, Response $response, $args) { + $path = $args['path'] ?? ''; + + try { + $targetUrl = DemoProxy::buildValidatedApiUrl($path, $request->getQueryParams()); + } catch (\InvalidArgumentException $e) { + $response->getBody()->write($e->getMessage()); + return $response->withStatus(400); + } + + try { + $proxiedResponse = DemoProxy::get($targetUrl); + } catch (\RuntimeException $e) { + $response->getBody()->write('Could not proxy demo request'); + return $response->withStatus(502); + } + + $response->getBody()->write($proxiedResponse['body']); + return $response->withStatus($proxiedResponse['statusCode']); +}); + $app->post('/receive-commit-hook', function (Request $request, Response $response, $args) { $params = $request->getQueryParams(); if (empty($params["token"]) || !password_verify($params["token"], WEBHOOK_TOKEN)) { diff --git a/app/templates/api-swagger.twig b/app/templates/api-swagger.twig index de5fc5512..bfa5954d5 100644 --- a/app/templates/api-swagger.twig +++ b/app/templates/api-swagger.twig @@ -19,7 +19,8 @@