Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 17 additions & 13 deletions Classes/EventListener/EnrichSerializationContextEventListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
5 changes: 3 additions & 2 deletions Classes/Service/SiteService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Expand Down
38 changes: 38 additions & 0 deletions Classes/Utility/NormalizedParamsUtility.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace SourceBroker\T3api\Utility;

use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Http\NormalizedParams;

/**
* Replaces `GeneralUtility::getIndpEnv()`, deprecated since TYPO3 14.3.
*
* @internal
*/
class NormalizedParamsUtility
{
/**
* Returns normalized params of `$GLOBALS['TYPO3_REQUEST']`. Without them (e.g. CLI or a request created by hand)
* they are created from `$_SERVER`, like `GeneralUtility::getIndpEnv()` did.
*/
public static function get(): NormalizedParams
{
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
$normalizedParams = $request instanceof ServerRequestInterface
? $request->getAttribute('normalizedParams')
: null;

return $normalizedParams instanceof NormalizedParams
? $normalizedParams
: new NormalizedParams(
$_SERVER,
$GLOBALS['TYPO3_CONF_VARS']['SYS'] ?? [],
Environment::getCurrentScript(),
Environment::getPublicPath()
);
}
}
3 changes: 2 additions & 1 deletion Classes/ViewHelpers/InlineViewHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -47,7 +48,7 @@ protected function getRequestUri(): string
return implode(
'/',
[
GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST'),
NormalizedParamsUtility::get()->getRequestHost(),
RouteService::getFullApiBasePath(),
$this->arguments['route'],
]
Expand Down
1 change: 1 addition & 0 deletions Documentation/Miscellaneous/Changelog/Index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion Documentation/Serialization/Customization/Index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

declare(strict_types=1);

namespace SourceBroker\T3api\Tests\Unit\EventListener;

use JMS\Serializer\SerializationContext;
use PHPUnit\Framework\Attributes\Test;
use SourceBroker\T3api\Domain\Model\OperationInterface;
use SourceBroker\T3api\Event\AfterCreateContextForOperationEvent;
use SourceBroker\T3api\EventListener\EnrichSerializationContextEventListener;
use Symfony\Component\HttpFoundation\Request;
use TYPO3\CMS\Core\Http\NormalizedParams;
use TYPO3\CMS\Core\Http\ServerRequest;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;

class EnrichSerializationContextEventListenerTest extends UnitTestCase
{
private const SERVER_PARAMS = [
'HTTP_HOST' => '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');
}
}
Loading