From f777d01abdef67a3f897cc38a48999a94b9a331c Mon Sep 17 00:00:00 2001 From: Krystian Szymukowicz Date: Sat, 3 Oct 2026 18:40:40 +0200 Subject: [PATCH] [BUGFIX] Skip cHash validation for API query arguments With cacheHash.enforceValidation enabled, any API request with query arguments (filters, pagination, ordering) returned 404 "cHash empty". T3apiPageArgumentsValidationBypass passes API requests to core's PageArgumentValidator without query arguments. T3apiPageArgumentsRestorer restores the original routing afterwards. Normal pages keep full cHash validation. --- .../Middleware/T3apiPageArgumentsRestorer.php | 31 ++++++ .../T3apiPageArgumentsValidationBypass.php | 50 ++++++++++ Configuration/RequestMiddlewares.php | 22 +++++ ...T3apiPageArgumentsValidationBypassTest.php | 94 +++++++++++++++++++ 4 files changed, 197 insertions(+) create mode 100644 Classes/Middleware/T3apiPageArgumentsRestorer.php create mode 100644 Classes/Middleware/T3apiPageArgumentsValidationBypass.php create mode 100644 Tests/Unit/Middleware/T3apiPageArgumentsValidationBypassTest.php 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; + } +}