diff --git a/Classes/Middleware/T3apiPageArgumentsRestorer.php b/Classes/Middleware/T3apiPageArgumentsRestorer.php new file mode 100644 index 00000000..97416053 --- /dev/null +++ b/Classes/Middleware/T3apiPageArgumentsRestorer.php @@ -0,0 +1,31 @@ +getAttribute(T3apiPageArgumentsValidationBypass::ORIGINAL_ROUTING_ATTRIBUTE); + + if ($originalPageArguments instanceof PageArguments) { + $request = $request + ->withAttribute('routing', $originalPageArguments) + ->withoutAttribute(T3apiPageArgumentsValidationBypass::ORIGINAL_ROUTING_ATTRIBUTE); + } + + return $handler->handle($request); + } +} diff --git a/Classes/Middleware/T3apiPageArgumentsValidationBypass.php b/Classes/Middleware/T3apiPageArgumentsValidationBypass.php new file mode 100644 index 00000000..0e9ac68c --- /dev/null +++ b/Classes/Middleware/T3apiPageArgumentsValidationBypass.php @@ -0,0 +1,50 @@ +getAttribute('routing'); + + if ( + $pageArguments instanceof PageArguments + && array_key_exists(ResourceEnhancer::PARAMETER_NAME, $pageArguments->getRouteArguments()) + ) { + $request = $request + ->withAttribute(self::ORIGINAL_ROUTING_ATTRIBUTE, $pageArguments) + ->withAttribute('routing', new PageArguments( + $pageArguments->getPageId(), + $pageArguments->getPageType(), + $pageArguments->getRouteArguments(), + array_replace($pageArguments->getStaticArguments(), $pageArguments->getRouteArguments()) + )); + } + + return $handler->handle($request); + } +} diff --git a/Configuration/RequestMiddlewares.php b/Configuration/RequestMiddlewares.php index cc8af67e..6427a17a 100644 --- a/Configuration/RequestMiddlewares.php +++ b/Configuration/RequestMiddlewares.php @@ -14,6 +14,28 @@ 'typo3/cms-frontend/shortcut-and-mountpoint-redirect', ], ], + // Keeps API query arguments (filters, pagination) away from cHash validation, see class docblock. + 'sourcebroker/t3api/page-arguments-validation-bypass' => [ + 'target' => \SourceBroker\T3api\Middleware\T3apiPageArgumentsValidationBypass::class, + 'after' => [ + 'typo3/cms-frontend/page-resolver', + ], + 'before' => [ + 'typo3/cms-frontend/page-argument-validator', + ], + ], + 'sourcebroker/t3api/page-arguments-restorer' => [ + 'target' => \SourceBroker\T3api\Middleware\T3apiPageArgumentsRestorer::class, + 'after' => [ + 'typo3/cms-frontend/page-argument-validator', + ], + 'before' => [ + // `typo3/cms-frontend/tsfe` (directly after the validator) does not exist since TYPO3 v14 + (new Typo3Version())->getMajorVersion() < 14 + ? 'typo3/cms-frontend/tsfe' + : 'typo3/cms-frontend/prepare-tsfe-rendering', + ], + ], ]; // Only required on TYPO3 v12: applies the X-Locale-style header language override before TSFE diff --git a/Tests/Unit/Middleware/T3apiPageArgumentsValidationBypassTest.php b/Tests/Unit/Middleware/T3apiPageArgumentsValidationBypassTest.php new file mode 100644 index 00000000..00a39fa5 --- /dev/null +++ b/Tests/Unit/Middleware/T3apiPageArgumentsValidationBypassTest.php @@ -0,0 +1,94 @@ + 'news/news'], [], ['istopnews' => '1', 'page' => '2']); + + $request = $this->process(new T3apiPageArgumentsValidationBypass(), $this->createRequest($pageArguments)); + + $validatedPageArguments = $request->getAttribute('routing'); + self::assertInstanceOf(PageArguments::class, $validatedPageArguments); + self::assertSame([], $validatedPageArguments->getDynamicArguments()); + self::assertSame(1, $validatedPageArguments->getPageId()); + self::assertSame($pageArguments, $request->getAttribute(T3apiPageArgumentsValidationBypass::ORIGINAL_ROUTING_ATTRIBUTE)); + } + + #[Test] + public function nonApiRequestIsNotModified(): void + { + $pageArguments = new PageArguments(1, '0', [], [], ['foo' => 'bar']); + $request = $this->createRequest($pageArguments); + + $processedRequest = $this->process(new T3apiPageArgumentsValidationBypass(), $request); + + self::assertSame($request, $processedRequest); + } + + #[Test] + public function restorerPutsOriginalPageArgumentsBack(): void + { + $pageArguments = new PageArguments(1, '0', ['t3apiResource' => 'news/news'], [], ['istopnews' => '1']); + $request = $this->process(new T3apiPageArgumentsValidationBypass(), $this->createRequest($pageArguments)); + + $restoredRequest = $this->process(new T3apiPageArgumentsRestorer(), $request); + + self::assertSame($pageArguments, $restoredRequest->getAttribute('routing')); + self::assertNull($restoredRequest->getAttribute(T3apiPageArgumentsValidationBypass::ORIGINAL_ROUTING_ATTRIBUTE)); + } + + #[Test] + public function restorerDoesNotModifyRequestWithoutOriginalPageArguments(): void + { + $request = $this->createRequest(new PageArguments(1, '0', [], [], ['foo' => 'bar'])); + + self::assertSame($request, $this->process(new T3apiPageArgumentsRestorer(), $request)); + } + + private function createRequest(PageArguments $pageArguments): ServerRequestInterface + { + return (new ServerRequest('https://example.com/_api/news/news'))->withAttribute('routing', $pageArguments); + } + + /** + * Runs the middleware and returns the request it passed to the next handler. + */ + private function process( + T3apiPageArgumentsValidationBypass|T3apiPageArgumentsRestorer $middleware, + ServerRequestInterface $request + ): ServerRequestInterface { + $handler = new class () implements RequestHandlerInterface { + public ?ServerRequestInterface $request = null; + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $this->request = $request; + + return new Response(); + } + }; + + $middleware->process($request, $handler); + + self::assertInstanceOf(ServerRequestInterface::class, $handler->request); + + return $handler->request; + } +}