From e332b1c9776fc865292e6cb72cc961c54b72999d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 4 Mar 2025 18:49:49 +0000 Subject: [PATCH 1/6] Proof of content, error in alias --- composer.lock | 91 ++++++++++++++++---------------------- tests/e2e/Client.php | 24 +++++++++- tests/e2e/ResponseTest.php | 22 +++++++++ tests/e2e/server.php | 12 +++++ 4 files changed, 96 insertions(+), 53 deletions(-) diff --git a/composer.lock b/composer.lock index b726c908..0156c108 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "brick/math", - "version": "0.12.1", + "version": "0.12.3", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "f510c0a40911935b77b86859eb5223d58d660df1" + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", - "reference": "f510c0a40911935b77b86859eb5223d58d660df1", + "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba", + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba", "shasum": "" }, "require": { @@ -26,7 +26,7 @@ "require-dev": { "php-coveralls/php-coveralls": "^2.2", "phpunit/phpunit": "^10.1", - "vimeo/psalm": "5.16.0" + "vimeo/psalm": "6.8.8" }, "type": "library", "autoload": { @@ -56,7 +56,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.12.1" + "source": "https://github.com/brick/math/tree/0.12.3" }, "funding": [ { @@ -64,7 +64,7 @@ "type": "github" } ], - "time": "2023-11-29T23:19:16+00:00" + "time": "2025-02-28T13:11:00+00:00" }, { "name": "composer/semver", @@ -1082,16 +1082,16 @@ }, { "name": "ramsey/collection", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/ramsey/collection.git", - "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" + "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", - "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "url": "https://api.github.com/repos/ramsey/collection/zipball/3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", + "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", "shasum": "" }, "require": { @@ -1099,25 +1099,22 @@ }, "require-dev": { "captainhook/plugin-composer": "^5.3", - "ergebnis/composer-normalize": "^2.28.3", - "fakerphp/faker": "^1.21", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", "hamcrest/hamcrest-php": "^2.0", - "jangregor/phpstan-prophecy": "^1.0", - "mockery/mockery": "^1.5", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3", - "phpcsstandards/phpcsutils": "^1.0.0-rc1", - "phpspec/prophecy-phpunit": "^2.0", - "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "^1.9", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5", - "psalm/plugin-mockery": "^1.1", - "psalm/plugin-phpunit": "^0.18.4", - "ramsey/coding-standard": "^2.0.3", - "ramsey/conventional-commits": "^1.3", - "vimeo/psalm": "^5.4" + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" }, "type": "library", "extra": { @@ -1155,19 +1152,9 @@ ], "support": { "issues": "https://github.com/ramsey/collection/issues", - "source": "https://github.com/ramsey/collection/tree/2.0.0" + "source": "https://github.com/ramsey/collection/tree/2.1.0" }, - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", - "type": "tidelift" - } - ], - "time": "2022-12-31T21:50:55+00:00" + "time": "2025-03-02T04:48:29+00:00" }, { "name": "ramsey/uuid", @@ -1330,16 +1317,16 @@ }, { "name": "symfony/http-client", - "version": "v7.2.3", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "7ce6078c79a4a7afff931c413d2959d3bffbfb8d" + "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/7ce6078c79a4a7afff931c413d2959d3bffbfb8d", - "reference": "7ce6078c79a4a7afff931c413d2959d3bffbfb8d", + "url": "https://api.github.com/repos/symfony/http-client/zipball/78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", + "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", "shasum": "" }, "require": { @@ -1405,7 +1392,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.2.3" + "source": "https://github.com/symfony/http-client/tree/v7.2.4" }, "funding": [ { @@ -1421,7 +1408,7 @@ "type": "tidelift" } ], - "time": "2025-01-28T15:51:35+00:00" + "time": "2025-02-13T10:27:23+00:00" }, { "name": "symfony/http-client-contracts", @@ -4650,16 +4637,16 @@ }, { "name": "symfony/process", - "version": "v7.2.0", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e" + "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", + "url": "https://api.github.com/repos/symfony/process/zipball/d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf", + "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf", "shasum": "" }, "require": { @@ -4691,7 +4678,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.2.0" + "source": "https://github.com/symfony/process/tree/v7.2.4" }, "funding": [ { @@ -4707,7 +4694,7 @@ "type": "tidelift" } ], - "time": "2024-11-06T14:24:19+00:00" + "time": "2025-02-05T08:33:46+00:00" }, { "name": "symfony/string", diff --git a/tests/e2e/Client.php b/tests/e2e/Client.php index 4052689b..86b96a02 100644 --- a/tests/e2e/Client.php +++ b/tests/e2e/Client.php @@ -54,17 +54,35 @@ public function __construct() public function call(string $method, string $path = '', array $headers = [], array $params = []) { usleep(50000); + $url = $this->baseUrl.$path.(($method == self::METHOD_GET && !empty($params)) ? '?'.http_build_query($params) : ''); $ch = curl_init($this->baseUrl.$path.(($method == self::METHOD_GET && !empty($params)) ? '?'.http_build_query($params) : '')); $responseHeaders = []; $responseStatus = -1; $responseType = ''; $responseBody = ''; + $query = match ($headers['content-type']) { + 'application/json' => \json_encode($params), + 'text/plain' => $params, + default => \http_build_query($params), + }; + + $formattedHeaders = []; + foreach ($headers as $key => $value) { + if (strtolower($key) === 'accept-encoding') { + curl_setopt($ch, CURLOPT_ENCODING, $value); + continue; + } else { + $formattedHeaders[] = $key . ': ' . $value; + } + } + + curl_setopt($ch, CURLOPT_PATH_AS_IS, 1); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_HTTPHEADER, $formattedHeaders); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0); curl_setopt($ch, CURLOPT_TIMEOUT, 15); curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { @@ -80,6 +98,10 @@ public function call(string $method, string $path = '', array $headers = [], arr return $len; }); + if ($method !== self::METHOD_GET) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $query); + } + $responseBody = curl_exec($ch); $responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); diff --git a/tests/e2e/ResponseTest.php b/tests/e2e/ResponseTest.php index bff46a55..87ce8168 100644 --- a/tests/e2e/ResponseTest.php +++ b/tests/e2e/ResponseTest.php @@ -56,4 +56,26 @@ public function testEarlyResponse() $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('Init response. Actioned before: no', $response['body']); } + + public function testAliasWithParameter(): void + { + $response = $this->client->call(Client::METHOD_POST, '/functions/deployment', [ + 'content-type' => 'application/json' + ], [ + 'deploymentId' => 'deployment1' + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('ID:deployment1', $response['body']); + + $response = $this->client->call(Client::METHOD_POST, '/functions/deployment', [ + 'content-type' => 'application/json' + ]); + $this->assertEquals(204, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, '/functions/deployment/deployment2', [ + 'content-type' => 'application/json' + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('ID:deployment2', $response['body']); + } } diff --git a/tests/e2e/server.php b/tests/e2e/server.php index a10d58ba..7a591238 100644 --- a/tests/e2e/server.php +++ b/tests/e2e/server.php @@ -46,6 +46,18 @@ $response->noContent(); }); +App::post('/functions/deployment') + ->alias('/functions/deployment/:deploymentId') + ->param('deploymentId', '', new Text(64, 0), '', true) + ->inject('response') + ->action(function (string $deploymentId, Response $response) { + if (empty($deploymentId)) { + $response->noContent(); + return; + } + + $response->send('ID:' . $deploymentId); + }); // Endpoints for early response // Meant to run twice, so init hook can know if action ran From 5771262c147b5bc5cb1e2a7942abc929c1e09b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 4 Mar 2025 20:30:12 +0000 Subject: [PATCH 2/6] Fix edge-case --- src/Router.php | 6 +++++- tests/e2e/Client.php | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Router.php b/src/Router.php index a2ad9b99..9e5c2772 100644 --- a/src/Router.php +++ b/src/Router.php @@ -101,12 +101,16 @@ public static function addRoute(Route $route): void */ public static function addRouteAlias(string $path, Route $route): void { - [$alias] = self::preparePath($path); + [$alias, $params] = self::preparePath($path); if (array_key_exists($alias, self::$routes[$route->getMethod()]) && !self::$allowOverride) { throw new Exception("Route for ({$route->getMethod()}:{$alias}) already registered."); } + foreach ($params as $key => $index) { + $route->setPathParam($key, $index); + } + self::$routes[$route->getMethod()][$alias] = $route; } diff --git a/tests/e2e/Client.php b/tests/e2e/Client.php index 86b96a02..cedd4cf8 100644 --- a/tests/e2e/Client.php +++ b/tests/e2e/Client.php @@ -61,7 +61,7 @@ public function call(string $method, string $path = '', array $headers = [], arr $responseType = ''; $responseBody = ''; - $query = match ($headers['content-type']) { + $query = match ($headers['content-type'] ?? '') { 'application/json' => \json_encode($params), 'text/plain' => $params, default => \http_build_query($params), From 1a515465257eeaa6c3bff4175f0c790c40387bbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 4 Mar 2025 22:15:32 +0000 Subject: [PATCH 3/6] Fix new edge case --- src/Route.php | 8 ++++---- src/Router.php | 10 +++++----- tests/e2e/ResponseTest.php | 16 ++++++++++------ tests/e2e/server.php | 9 +++++++++ 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/Route.php b/src/Route.php index fa4b25fa..aa0591be 100755 --- a/src/Route.php +++ b/src/Route.php @@ -28,7 +28,7 @@ class Route extends Hook /** * Path params. * - * @var array + * @var array> */ protected array $pathParams = []; @@ -141,9 +141,9 @@ public function getHook(): bool * @param int $index * @return void */ - public function setPathParam(string $key, int $index): void + public function setPathParam(string $path, string $key, int $index): void { - $this->pathParams[$key] = $index; + $this->pathParams[$path][$key] = $index; } /** @@ -157,7 +157,7 @@ public function getPathValues(Request $request): array $pathValues = []; $parts = explode('/', ltrim($request->getURI(), '/')); - foreach ($this->pathParams as $key => $index) { + foreach (($this->pathParams[$this->getPath()] ?? []) as $key => $index) { if (array_key_exists($index, $parts)) { $pathValues[$key] = $parts[$index]; } diff --git a/src/Router.php b/src/Router.php index 9e5c2772..76662fdb 100644 --- a/src/Router.php +++ b/src/Router.php @@ -86,7 +86,7 @@ public static function addRoute(Route $route): void } foreach ($params as $key => $index) { - $route->setPathParam($key, $index); + $route->setPathParam($path, $key, $index); } self::$routes[$route->getMethod()][$path] = $route; @@ -108,7 +108,7 @@ public static function addRouteAlias(string $path, Route $route): void } foreach ($params as $key => $index) { - $route->setPathParam($key, $index); + $route->setPathParam($alias, $key, $index); } self::$routes[$route->getMethod()][$alias] = $route; @@ -142,7 +142,7 @@ public static function match(string $method, string $path): Route|null ); if (array_key_exists($match, self::$routes[$method])) { - return self::$routes[$method][$match]; + return (self::$routes[$method][$match])->path($match); } } @@ -151,7 +151,7 @@ public static function match(string $method, string $path): Route|null */ $match = self::WILDCARD_TOKEN; if (array_key_exists($match, self::$routes[$method])) { - return self::$routes[$method][$match]; + return (self::$routes[$method][$match])->path($match); } /** @@ -161,7 +161,7 @@ public static function match(string $method, string $path): Route|null $current = ($current ?? '') . "{$part}/"; $match = $current . self::WILDCARD_TOKEN; if (array_key_exists($match, self::$routes[$method])) { - return self::$routes[$method][$match]; + return (self::$routes[$method][$match])->path($match); } } diff --git a/tests/e2e/ResponseTest.php b/tests/e2e/ResponseTest.php index 87ce8168..ecbb9761 100644 --- a/tests/e2e/ResponseTest.php +++ b/tests/e2e/ResponseTest.php @@ -67,15 +67,19 @@ public function testAliasWithParameter(): void $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('ID:deployment1', $response['body']); - $response = $this->client->call(Client::METHOD_POST, '/functions/deployment', [ - 'content-type' => 'application/json' - ]); + $response = $this->client->call(Client::METHOD_POST, '/functions/deployment'); $this->assertEquals(204, $response['headers']['status-code']); - $response = $this->client->call(Client::METHOD_POST, '/functions/deployment/deployment2', [ - 'content-type' => 'application/json' - ]); + $response = $this->client->call(Client::METHOD_POST, '/functions/deployment/deployment2'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('ID:deployment2', $response['body']); + + $response = $this->client->call(Client::METHOD_POST, '/database/collections/col1'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(';col1', $response['body']); + + $response = $this->client->call(Client::METHOD_POST, '/databases/db2/collections/col2'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('db2;col2', $response['body']); } } diff --git a/tests/e2e/server.php b/tests/e2e/server.php index 7a591238..179767cd 100644 --- a/tests/e2e/server.php +++ b/tests/e2e/server.php @@ -59,6 +59,15 @@ $response->send('ID:' . $deploymentId); }); +App::post('/databases/:databaseId/collections/:collectionId') + ->alias('/database/collections/:collectionId') + ->param('databaseId', '', new Text(64, 0), '', true) + ->param('collectionId', '', new Text(64, 0), '', true) + ->inject('response') + ->action(function (string $databaseId, string $collectionId, Response $response) { + $response->send($databaseId . ';' . $collectionId); + }); + // Endpoints for early response // Meant to run twice, so init hook can know if action ran $earlyResponseAction = 'no'; From 1d31f755348f78004f870693b4f072e600da2e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 4 Mar 2025 22:31:03 +0000 Subject: [PATCH 4/6] Fix another edge case --- src/App.php | 4 +++- src/Route.php | 17 +++++++++++++++-- src/Router.php | 14 ++++++++++---- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/App.php b/src/App.php index 63965ecf..aa2249a9 100755 --- a/src/App.php +++ b/src/App.php @@ -579,7 +579,9 @@ public function execute(Route $route, Request $request, Response $response): sta { $arguments = []; $groups = $route->getGroups(); - $pathValues = $route->getPathValues($request); + + $preparedPath = Router::preparePath($route->getMatchedPath()); + $pathValues = $route->getPathValues($request, $preparedPath[0]); try { if ($route->getHook()) { diff --git a/src/Route.php b/src/Route.php index aa0591be..f89139b0 100755 --- a/src/Route.php +++ b/src/Route.php @@ -46,6 +46,8 @@ class Route extends Hook */ protected int $order; + protected string $getMatchedPath = ''; + public function __construct(string $method, string $path) { $this->path($path); @@ -55,6 +57,17 @@ public function __construct(string $method, string $path) }; } + public function setMatchedPath(string $path): self + { + $this->getMatchedPath = $path; + return $this; + } + + public function getMatchedPath(): string + { + return $this->getMatchedPath; + } + /** * Get Route Order ID * @@ -152,12 +165,12 @@ public function setPathParam(string $path, string $key, int $index): void * @param \Utopia\Request $request * @return array */ - public function getPathValues(Request $request): array + public function getPathValues(Request $request, string $path): array { $pathValues = []; $parts = explode('/', ltrim($request->getURI(), '/')); - foreach (($this->pathParams[$this->getPath()] ?? []) as $key => $index) { + foreach (($this->pathParams[$path] ?? []) as $key => $index) { if (array_key_exists($index, $parts)) { $pathValues[$key] = $parts[$index]; } diff --git a/src/Router.php b/src/Router.php index 76662fdb..bc6e63cc 100644 --- a/src/Router.php +++ b/src/Router.php @@ -142,7 +142,9 @@ public static function match(string $method, string $path): Route|null ); if (array_key_exists($match, self::$routes[$method])) { - return (self::$routes[$method][$match])->path($match); + $route = self::$routes[$method][$match]; + $route->setMatchedPath($match); + return $route; } } @@ -151,7 +153,9 @@ public static function match(string $method, string $path): Route|null */ $match = self::WILDCARD_TOKEN; if (array_key_exists($match, self::$routes[$method])) { - return (self::$routes[$method][$match])->path($match); + $route = self::$routes[$method][$match]; + $route->setMatchedPath($match); + return $route; } /** @@ -161,7 +165,9 @@ public static function match(string $method, string $path): Route|null $current = ($current ?? '') . "{$part}/"; $match = $current . self::WILDCARD_TOKEN; if (array_key_exists($match, self::$routes[$method])) { - return (self::$routes[$method][$match])->path($match); + $route = self::$routes[$method][$match]; + $route->setMatchedPath($match); + return $route; } } @@ -196,7 +202,7 @@ protected static function combinations(array $set): iterable * @param string $path * @return array */ - protected static function preparePath(string $path): array + public static function preparePath(string $path): array { $parts = array_values(array_filter(explode('/', $path))); $prepare = ''; From c42db6d059fe10601a743ef8d6ff7141fb627be7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 6 Mar 2025 08:17:09 +0100 Subject: [PATCH 5/6] Apply suggestions from code review --- src/Route.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Route.php b/src/Route.php index f89139b0..7a64c732 100755 --- a/src/Route.php +++ b/src/Route.php @@ -46,7 +46,7 @@ class Route extends Hook */ protected int $order; - protected string $getMatchedPath = ''; + protected string $matchedPath = ''; public function __construct(string $method, string $path) { @@ -65,7 +65,7 @@ public function setMatchedPath(string $path): self public function getMatchedPath(): string { - return $this->getMatchedPath; + return $this->matchedPath; } /** From 897e9e8e22c34da6fe844a8a323dbdc5ebbf9eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 6 Mar 2025 08:17:27 +0100 Subject: [PATCH 6/6] Apply suggestions from code review --- src/Route.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Route.php b/src/Route.php index 7a64c732..aa06a29c 100755 --- a/src/Route.php +++ b/src/Route.php @@ -59,7 +59,7 @@ public function __construct(string $method, string $path) public function setMatchedPath(string $path): self { - $this->getMatchedPath = $path; + $this->matchedPath = $path; return $this; }