diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7e653757..d778f9a2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -5515,11 +5515,6 @@ parameters: count: 1 path: tests/bundle/Routing/OptionsLoader/MapperTest.php - - - message: "#^Method Ibexa\\\\Tests\\\\Bundle\\\\Rest\\\\Routing\\\\OptionsLoader\\\\RouteCollectionMapperTest\\:\\:createRoute\\(\\) has parameter \\$methods with no value type specified in iterable type array\\.$#" - count: 1 - path: tests/bundle/Routing/OptionsLoader/RouteCollectionMapperTest.php - - message: "#^Method Ibexa\\\\Tests\\\\Bundle\\\\Rest\\\\Routing\\\\OptionsLoader\\\\RouteCollectionMapperTest\\:\\:testAddRestRoutesCollection\\(\\) has no return type specified\\.$#" count: 1 diff --git a/src/bundle/Resources/config/services.yml b/src/bundle/Resources/config/services.yml index e3f80340..ca2f412e 100644 --- a/src/bundle/Resources/config/services.yml +++ b/src/bundle/Resources/config/services.yml @@ -390,3 +390,13 @@ services: parent: hautelook.router.template calls: - [ setOption, [ strict_requirements, ~ ] ] + + Ibexa\Bundle\Rest\Routing\ExpressionLanguage\ContentTypeHeaderMatcherExpressionFunction: + arguments: + $mediaTypeParser: '@Ibexa\Contracts\Rest\Input\MediaTypeParserInterface' + tags: + - { name: routing.expression_language_function, function: 'ibexa_get_media_type' } + + Ibexa\Contracts\Rest\Input\MediaTypeParser: ~ + + Ibexa\Contracts\Rest\Input\MediaTypeParserInterface: '@Ibexa\Contracts\Rest\Input\MediaTypeParser' diff --git a/src/bundle/Routing/ExpressionLanguage/ContentTypeHeaderMatcherExpressionFunction.php b/src/bundle/Routing/ExpressionLanguage/ContentTypeHeaderMatcherExpressionFunction.php new file mode 100644 index 00000000..53f51d97 --- /dev/null +++ b/src/bundle/Routing/ExpressionLanguage/ContentTypeHeaderMatcherExpressionFunction.php @@ -0,0 +1,30 @@ +headers->get('Content-Type'); + if ($contentTypeHeaderValue === null) { + return null; + } + + return $this->mediaTypeParser->parseContentTypeHeader($contentTypeHeaderValue); + } +} diff --git a/src/bundle/Routing/OptionsLoader/Mapper.php b/src/bundle/Routing/OptionsLoader/Mapper.php index 196a7765..33a6f323 100644 --- a/src/bundle/Routing/OptionsLoader/Mapper.php +++ b/src/bundle/Routing/OptionsLoader/Mapper.php @@ -65,15 +65,23 @@ public function mergeMethodsDefault(Route $optionsRoute, Route $restRoute) /** * Returns the OPTIONS name of a REST route. - * - * @param $route Route - * - * @return string */ - public function getOptionsRouteName(Route $route) + public function getOptionsRouteName(Route $route): string { $name = str_replace('/', '_', $route->getPath()); - return 'ibexa.rest.options.' . trim($name, '_'); + $parts = [ + 'ibexa.rest.options', + trim($name, '_'), + ]; + + // Routes that share path 1-to-1 can result in overwrite. + // Use "options_route_suffix" to ensure uniqueness. + $routeSuffix = $route->getOption('options_route_suffix'); + if ($routeSuffix !== null) { + $parts[] = $routeSuffix; + } + + return implode('.', $parts); } } diff --git a/src/contracts/Input/MediaTypeParser.php b/src/contracts/Input/MediaTypeParser.php new file mode 100644 index 00000000..3e1feace --- /dev/null +++ b/src/contracts/Input/MediaTypeParser.php @@ -0,0 +1,23 @@ + $expectedMethods */ - public function testHttpOptions(string $route, array $expectedMethods): void - { + public function testHttpOptions( + string $route, + array $expectedMethods, + ?string $contentType = null + ): void { $restAPIPrefix = '/api/ibexa/v2'; $response = $this->sendHttpRequest( - $this->createHttpRequest('OPTIONS', "{$restAPIPrefix}{$route}") + $this->createHttpRequest( + 'OPTIONS', + "{$restAPIPrefix}{$route}", + $contentType ?? '', + ) ); self::assertHttpResponseCodeEquals($response, 200); diff --git a/tests/bundle/Routing/ExpressionLanguage/ContentTypeHeaderMatcherExpressionFunctionTest.php b/tests/bundle/Routing/ExpressionLanguage/ContentTypeHeaderMatcherExpressionFunctionTest.php new file mode 100644 index 00000000..90b6a754 --- /dev/null +++ b/tests/bundle/Routing/ExpressionLanguage/ContentTypeHeaderMatcherExpressionFunctionTest.php @@ -0,0 +1,51 @@ +contentTypeHeaderMatcher = $contentTypeHeaderMatcher; + } + + public function testGetMediaType(): void + { + $request = new Request(); + $request->headers->add([ + 'Content-Type' => 'application/vnd.ibexa.api.CopyContentTypeInput+json', + ]); + + $closure = $this->contentTypeHeaderMatcher; + + self::assertSame('CopyContentTypeInput', $closure($request)); + } + + public function testRequestContentTypeDoesNotMatchRoute(): void + { + $request = new Request(); + $request->headers->add([ + 'Content-Type' => 'application.CreateContentTypeInput+xml', + ]); + + $closure = $this->contentTypeHeaderMatcher; + + self::assertNull($closure($request)); + } +} diff --git a/tests/bundle/Routing/OptionsLoader/RouteCollectionMapperTest.php b/tests/bundle/Routing/OptionsLoader/RouteCollectionMapperTest.php index ec103522..b9235e4b 100644 --- a/tests/bundle/Routing/OptionsLoader/RouteCollectionMapperTest.php +++ b/tests/bundle/Routing/OptionsLoader/RouteCollectionMapperTest.php @@ -63,14 +63,40 @@ public function testAddRestRoutesCollection() ); } + public function testAddRestRoutesCollectionWithConditionAndSuffix(): void + { + $restRoutesCollection = new RouteCollection(); + $restRoutesCollection->add( + 'ibexa.rest.route_three_post', + $this->createRoute( + '/route/three', + ['POST'], + 'ibexa_get_media_type(request) === "RouteThreeInput"', + ['options_route_suffix' => 'RouteThreeInput'], + ), + ); + + $optionsRouteCollection = $this->collectionMapper->mapCollection($restRoutesCollection); + + self::assertCount(1, $optionsRouteCollection); + + $optionsRoute = $optionsRouteCollection->get('ibexa.rest.options.route_three.RouteThreeInput'); + + self::assertInstanceOf(Route::class, $optionsRoute); + + self::assertEquals('POST', $optionsRoute->getDefault('allowedMethods')); + } + /** - * @param string $path - * @param array $methods - * - * @return \Symfony\Component\Routing\Route + * @param array $methods + * @param array $options */ - private function createRoute($path, array $methods) - { - return new Route($path, [], [], [], '', [], $methods); + private function createRoute( + string $path, + array $methods, + ?string $condition = null, + array $options = [], + ): Route { + return new Route($path, [], [], $options, '', [], $methods, $condition); } } diff --git a/tests/lib/Input/MediaTypeParserTest.php b/tests/lib/Input/MediaTypeParserTest.php new file mode 100644 index 00000000..a1146eda --- /dev/null +++ b/tests/lib/Input/MediaTypeParserTest.php @@ -0,0 +1,49 @@ +mediaTypeParser = new MediaTypeParser(); + } + + public function testParsingSuccesses(): void + { + $header = 'application/vnd.ibexa.api.CopyContentTypeInput+json'; + + self::assertSame('CopyContentTypeInput', $this->mediaTypeParser->parseContentTypeHeader($header)); + } + + /** + * @dataProvider providerForParsingFails + */ + public function testParsingFails(string $header): void + { + self::assertNull($this->mediaTypeParser->parseContentTypeHeader($header)); + } + + /** + * @return iterable> + */ + public function providerForParsingFails(): iterable + { + yield 'a' => ['application.CopyContentTypeInput+json']; + yield 'b' => ['application.CopyContentTypeInput']; + yield 'c' => ['CopyContentTypeInput+json']; + yield 'd' => ['CopyContentTypeInput']; + } +}