From 30d7f01fe39ef070e9050ad13039400d312cb550 Mon Sep 17 00:00:00 2001 From: Krystian Szymukowicz Date: Tue, 3 Nov 2026 17:03:44 +0100 Subject: [PATCH] [TASK] Replace deprecated GeneralUtility::getIndpEnv() with NormalizedParams GeneralUtility::getIndpEnv() is deprecated since TYPO3 14.3. TYPO3_* attributes of the serialization context, SiteService and InlineViewHelper read NormalizedParams of the current request, or $_SERVER when the request has none (like getIndpEnv() did). TYPO3_PROXY contains whether the request comes through a configured reverse proxy (it was always empty). --- ...nrichSerializationContextEventListener.php | 30 ++--- Classes/Service/SiteService.php | 5 +- Classes/Utility/NormalizedParamsUtility.php | 38 ++++++ Classes/ViewHelpers/InlineViewHelper.php | 3 +- .../Miscellaneous/Changelog/Index.rst | 1 + .../Serialization/Customization/Index.rst | 2 +- ...hSerializationContextEventListenerTest.php | 109 ++++++++++++++++++ 7 files changed, 171 insertions(+), 17 deletions(-) create mode 100644 Classes/Utility/NormalizedParamsUtility.php create mode 100644 Tests/Unit/EventListener/EnrichSerializationContextEventListenerTest.php diff --git a/Classes/EventListener/EnrichSerializationContextEventListener.php b/Classes/EventListener/EnrichSerializationContextEventListener.php index f29e684f..0f3cef49 100644 --- a/Classes/EventListener/EnrichSerializationContextEventListener.php +++ b/Classes/EventListener/EnrichSerializationContextEventListener.php @@ -5,25 +5,29 @@ namespace SourceBroker\T3api\EventListener; use SourceBroker\T3api\Event\AfterCreateContextForOperationEvent; -use TYPO3\CMS\Core\Utility\GeneralUtility; +use SourceBroker\T3api\Utility\NormalizedParamsUtility; class EnrichSerializationContextEventListener { public function __invoke(AfterCreateContextForOperationEvent $createContextForOperationEvent): void { + $normalizedParams = NormalizedParamsUtility::get(); + $port = $normalizedParams->getRequestPort(); + $attributes = [ - 'TYPO3_HOST_ONLY' => GeneralUtility::getIndpEnv('TYPO3_HOST_ONLY'), - 'TYPO3_PORT' => GeneralUtility::getIndpEnv('TYPO3_PORT'), - 'TYPO3_REQUEST_HOST' => GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST'), - 'TYPO3_REQUEST_URL' => GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'), - 'TYPO3_REQUEST_SCRIPT' => GeneralUtility::getIndpEnv('TYPO3_REQUEST_SCRIPT'), - 'TYPO3_REQUEST_DIR' => GeneralUtility::getIndpEnv('TYPO3_REQUEST_DIR'), - 'TYPO3_SITE_URL' => GeneralUtility::getIndpEnv('TYPO3_SITE_URL'), - 'TYPO3_SITE_PATH' => GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'), - 'TYPO3_SITE_SCRIPT' => GeneralUtility::getIndpEnv('TYPO3_SITE_SCRIPT'), - 'TYPO3_DOCUMENT_ROOT' => GeneralUtility::getIndpEnv('TYPO3_DOCUMENT_ROOT'), - 'TYPO3_SSL' => GeneralUtility::getIndpEnv('TYPO3_SSL'), - 'TYPO3_PROXY' => GeneralUtility::getIndpEnv('TYPO3_PROXY'), + 'TYPO3_HOST_ONLY' => $normalizedParams->getRequestHostOnly(), + // string as returned by `GeneralUtility::getIndpEnv()`, empty when the host has no port + 'TYPO3_PORT' => $port > 0 ? (string)$port : '', + 'TYPO3_REQUEST_HOST' => $normalizedParams->getRequestHost(), + 'TYPO3_REQUEST_URL' => $normalizedParams->getRequestUrl(), + 'TYPO3_REQUEST_SCRIPT' => $normalizedParams->getRequestScript(), + 'TYPO3_REQUEST_DIR' => $normalizedParams->getRequestDir(), + 'TYPO3_SITE_URL' => $normalizedParams->getSiteUrl(), + 'TYPO3_SITE_PATH' => $normalizedParams->getSitePath(), + 'TYPO3_SITE_SCRIPT' => $normalizedParams->getSiteScript(), + 'TYPO3_DOCUMENT_ROOT' => $normalizedParams->getDocumentRoot(), + 'TYPO3_SSL' => $normalizedParams->isHttps(), + 'TYPO3_PROXY' => $normalizedParams->isBehindReverseProxy(), ]; foreach ($attributes as $name => $value) { diff --git a/Classes/Service/SiteService.php b/Classes/Service/SiteService.php index f9010fc4..5e115b31 100644 --- a/Classes/Service/SiteService.php +++ b/Classes/Service/SiteService.php @@ -6,6 +6,7 @@ use Psr\Http\Message\ServerRequestInterface; use SourceBroker\T3api\Routing\Enhancer\ResourceEnhancer; +use SourceBroker\T3api\Utility\NormalizedParamsUtility; use TYPO3\CMS\Core\Exception\SiteNotFoundException; use TYPO3\CMS\Core\Http\ServerRequestFactory; use TYPO3\CMS\Core\Routing\SiteMatcher; @@ -112,9 +113,9 @@ protected static function getResolvedByTypo3(): ?SiteInterface protected static function getFirstMatchingCurrentUrl(): ?Site { + $requestHost = NormalizedParamsUtility::get()->getRequestHost(); foreach (self::getAll() as $site) { - if (rtrim(trim((string)$site->getBase()), '/') - === GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST')) { + if (rtrim(trim((string)$site->getBase()), '/') === $requestHost) { return $site; } } diff --git a/Classes/Utility/NormalizedParamsUtility.php b/Classes/Utility/NormalizedParamsUtility.php new file mode 100644 index 00000000..00f7a561 --- /dev/null +++ b/Classes/Utility/NormalizedParamsUtility.php @@ -0,0 +1,38 @@ +getAttribute('normalizedParams') + : null; + + return $normalizedParams instanceof NormalizedParams + ? $normalizedParams + : new NormalizedParams( + $_SERVER, + $GLOBALS['TYPO3_CONF_VARS']['SYS'] ?? [], + Environment::getCurrentScript(), + Environment::getPublicPath() + ); + } +} diff --git a/Classes/ViewHelpers/InlineViewHelper.php b/Classes/ViewHelpers/InlineViewHelper.php index 376efbe3..914a7a54 100644 --- a/Classes/ViewHelpers/InlineViewHelper.php +++ b/Classes/ViewHelpers/InlineViewHelper.php @@ -6,6 +6,7 @@ use SourceBroker\T3api\Dispatcher\HeadlessDispatcher; use SourceBroker\T3api\Service\RouteService; +use SourceBroker\T3api\Utility\NormalizedParamsUtility; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\RequestContext; use TYPO3\CMS\Core\Routing\RouteNotFoundException; @@ -47,7 +48,7 @@ protected function getRequestUri(): string return implode( '/', [ - GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST'), + NormalizedParamsUtility::get()->getRequestHost(), RouteService::getFullApiBasePath(), $this->arguments['route'], ] diff --git a/Documentation/Miscellaneous/Changelog/Index.rst b/Documentation/Miscellaneous/Changelog/Index.rst index 4967c1bc..9621b668 100644 --- a/Documentation/Miscellaneous/Changelog/Index.rst +++ b/Documentation/Miscellaneous/Changelog/Index.rst @@ -6,6 +6,7 @@ Changelog 5.0.0 ===== +- Read ``TYPO3_*`` attributes of the serialization context from ``NormalizedParams`` of the current request instead of ``GeneralUtility::getIndpEnv()`` (deprecated since TYPO3 14.3). ``TYPO3_PROXY`` contains whether the request comes through a configured reverse proxy (before it was always empty). See :ref:`serialization_expression-language`. - [!!!] Apply ``allowedFileExtensions`` of the resource ``upload`` settings to its operations. Before, an operation without its own ``allowedFileExtensions`` used ``$GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']``. Review the list when you rely on the previous behaviour. Configured extensions are compared case-insensitively. - OpenAPI specification documents ``403 Forbidden`` (instead of ``404 Not Found``) for operations with ``security`` or ``security_post_denormalize``, the same status as returned by the API. - [!!!] A new file reference created by ``uidLocal`` requires ``fileToken`` returned for the file by the API, otherwise ``400 Bad Request`` is returned. The same applies to a new file of a property of type ``File`` or a class extending it (``{"uid": 101, "fileToken": "..."}``). The token is returned in the response of upload endpoints and of operations with ``"fileTokens"=true`` in ``normalizationContext`` (files offered for selection, ``file.fileToken`` for file references). Pass ``fileToken`` when saving a reference. Custom upload code has to grant tokens with ``FileTokenService::grant()``. See :ref:`handling_file_upload_file_token`. diff --git a/Documentation/Serialization/Customization/Index.rst b/Documentation/Serialization/Customization/Index.rst index 19f296ea..78d6d710 100644 --- a/Documentation/Serialization/Customization/Index.rst +++ b/Documentation/Serialization/Customization/Index.rst @@ -33,7 +33,7 @@ Context attributes - ``groups`` - serialization groups of the operation (from ``normalizationContext``). - any other key set in ``normalizationContext`` of the operation. -- ``TYPO3_SITE_URL``, ``TYPO3_REQUEST_HOST``, ``TYPO3_REQUEST_URL``, ``TYPO3_HOST_ONLY``, ``TYPO3_PORT``, ``TYPO3_SSL``, ``TYPO3_PROXY``, ``TYPO3_SITE_PATH``, ``TYPO3_SITE_SCRIPT``, ``TYPO3_REQUEST_SCRIPT``, ``TYPO3_REQUEST_DIR``, ``TYPO3_DOCUMENT_ROOT`` - values of ``GeneralUtility::getIndpEnv()`` for the current request. +- ``TYPO3_SITE_URL``, ``TYPO3_REQUEST_HOST``, ``TYPO3_REQUEST_URL``, ``TYPO3_HOST_ONLY``, ``TYPO3_PORT``, ``TYPO3_SSL``, ``TYPO3_PROXY``, ``TYPO3_SITE_PATH``, ``TYPO3_SITE_SCRIPT``, ``TYPO3_REQUEST_SCRIPT``, ``TYPO3_REQUEST_DIR``, ``TYPO3_DOCUMENT_ROOT`` - values of ``NormalizedParams`` of the current request (``$_SERVER`` when the request has none, e.g. on CLI). ``TYPO3_SSL`` and ``TYPO3_PROXY`` (request comes through a configured reverse proxy) are booleans, ``TYPO3_PORT`` is empty when the host has no port. Use ``context.hasAttribute('name')`` before reading an attribute which may not exist - ``context.getAttribute()`` of a missing attribute triggers an error. diff --git a/Tests/Unit/EventListener/EnrichSerializationContextEventListenerTest.php b/Tests/Unit/EventListener/EnrichSerializationContextEventListenerTest.php new file mode 100644 index 00000000..a0376c93 --- /dev/null +++ b/Tests/Unit/EventListener/EnrichSerializationContextEventListenerTest.php @@ -0,0 +1,109 @@ + 'example.com:8080', + 'HTTPS' => 'on', + 'SCRIPT_NAME' => '/index.php', + 'REQUEST_URI' => '/_api/news?page=2', + ]; + + private array $serverBackup; + + protected function setUp(): void + { + parent::setUp(); + $this->serverBackup = $_SERVER; + } + + protected function tearDown(): void + { + unset($GLOBALS['TYPO3_REQUEST']); + $_SERVER = $this->serverBackup; + parent::tearDown(); + } + + #[Test] + public function attributesAreReadFromNormalizedParamsOfCurrentRequest(): void + { + $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest('https://example.com:8080/_api/news?page=2')) + ->withAttribute('normalizedParams', $this->createNormalizedParams(self::SERVER_PARAMS)); + + $context = $this->dispatch(); + + self::assertSame('example.com', $context->getAttribute('TYPO3_HOST_ONLY')); + self::assertSame('8080', $context->getAttribute('TYPO3_PORT')); + self::assertSame('https://example.com:8080', $context->getAttribute('TYPO3_REQUEST_HOST')); + self::assertSame('https://example.com:8080/_api/news?page=2', $context->getAttribute('TYPO3_REQUEST_URL')); + self::assertSame('https://example.com:8080/index.php', $context->getAttribute('TYPO3_REQUEST_SCRIPT')); + self::assertSame('https://example.com:8080/', $context->getAttribute('TYPO3_REQUEST_DIR')); + self::assertSame('https://example.com:8080/', $context->getAttribute('TYPO3_SITE_URL')); + self::assertSame('/', $context->getAttribute('TYPO3_SITE_PATH')); + self::assertSame('_api/news?page=2', $context->getAttribute('TYPO3_SITE_SCRIPT')); + self::assertSame('/var/www/html/public', $context->getAttribute('TYPO3_DOCUMENT_ROOT')); + self::assertTrue($context->getAttribute('TYPO3_SSL')); + self::assertFalse($context->getAttribute('TYPO3_PROXY')); + } + + #[Test] + public function portIsEmptyStringWhenHostHasNoPort(): void + { + $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest('https://example.com/_api/news')) + ->withAttribute( + 'normalizedParams', + $this->createNormalizedParams(['HTTP_HOST' => 'example.com'] + self::SERVER_PARAMS) + ); + + self::assertSame('', $this->dispatch()->getAttribute('TYPO3_PORT')); + } + + /** + * Like `GeneralUtility::getIndpEnv()` - a request created by hand usually has no server params. + */ + #[Test] + public function attributesAreReadFromGlobalServerWhenRequestHasNoNormalizedParams(): void + { + $_SERVER = self::SERVER_PARAMS + $_SERVER; + $GLOBALS['TYPO3_REQUEST'] = new ServerRequest('https://other.example.com/_api/news'); + + self::assertSame('https://example.com:8080', $this->dispatch()->getAttribute('TYPO3_REQUEST_HOST')); + } + + #[Test] + public function attributesAreReadFromGlobalServerWithoutRequest(): void + { + $_SERVER = self::SERVER_PARAMS + $_SERVER; + + self::assertSame('https://example.com:8080', $this->dispatch()->getAttribute('TYPO3_REQUEST_HOST')); + } + + private function dispatch(): SerializationContext + { + $context = SerializationContext::create(); + (new EnrichSerializationContextEventListener())( + new AfterCreateContextForOperationEvent(self::createStub(OperationInterface::class), Request::create('/'), $context) + ); + + return $context; + } + + private function createNormalizedParams(array $serverParams): NormalizedParams + { + return new NormalizedParams($serverParams, [], '/var/www/html/public/index.php', '/var/www/html/public'); + } +}