From 6d6e92bd7c7f8f13018f95151045feeefe37bf38 Mon Sep 17 00:00:00 2001 From: Benjamin Beck Date: Thu, 4 Jun 2026 13:55:00 +0200 Subject: [PATCH 01/12] [TASK] Define TYPO3 13.4 and 14.3 support matrix --- .github/workflows/build.yml | 73 +++++++++++++++++++++---------------- .gitignore | 1 + Tests/bootstrap.php | 21 ----------- Tests/preflight.php | 2 +- composer.json | 28 +++++++++----- ext_emconf.php | 4 +- phpstan-baseline.neon | 40 -------------------- phpstan-bootstrap.php | 2 +- phpstan.neon | 1 - phpunit-functional.xml.dist | 24 ++++++++++++ phpunit.xml.dist | 11 ++---- 11 files changed, 92 insertions(+), 115 deletions(-) create mode 100644 phpunit-functional.xml.dist diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 58db5fbd3..d7e328ac9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,9 +2,9 @@ name: build on: push: - branches: [ master, development, '[0-9]+.[0-9]+', '[0-9]+.[0-9]+.[0-9]+' ] + branches: [ master, main, development, '[0-9]+.[0-9]+', '[0-9]+.[0-9]+.[0-9]+' ] pull_request: - branches: [ master, development, '[0-9]+.[0-9]', '[0-9]+.[0-9]+.[0-9]+' ] + branches: [ master, main, development, '[0-9]+.[0-9]+', '[0-9]+.[0-9]+.[0-9]+' ] schedule: - cron: '30 4 * * *' @@ -17,8 +17,10 @@ jobs: fail-fast: false matrix: include: - - php: "8.1" - typo3: "^11.5" + - php: "8.2" + typo3: "^13.4" + - php: "8.3" + typo3: "^14.3" runs-on: ubuntu-latest @@ -35,7 +37,14 @@ jobs: tools: composer:v2.2 - name: Select TYPO3 version - run: "composer require typo3/cms-core:${{ matrix.typo3 }} -W --no-update" + run: > + composer require + typo3/cms-core:${{ matrix.typo3 }} + typo3/cms-extbase:${{ matrix.typo3 }} + typo3/cms-fluid:${{ matrix.typo3 }} + typo3/cms-frontend:${{ matrix.typo3 }} + typo3/cms-backend:${{ matrix.typo3 }} + -W --no-update - name: Composer install uses: ramsey/composer-install@v2 @@ -51,20 +60,10 @@ jobs: fail-fast: false matrix: include: - - php: "7.4" - typo3: "^10.4" - - php: "8.0" - typo3: "^11.5" - - php: "8.1" - typo3: "^11.5" - - php: "8.1" - typo3: "^12.4" - php: "8.2" - typo3: "^12.4" - - php: "8.3" - typo3: "^12.4" - - php: "8.3" typo3: "^13.4" + - php: "8.3" + typo3: "^14.3" runs-on: ubuntu-latest @@ -81,7 +80,14 @@ jobs: tools: composer:v2.2 - name: Select TYPO3 version - run: "composer require typo3/cms-core:${{ matrix.typo3 }} -W --no-update" + run: > + composer require + typo3/cms-core:${{ matrix.typo3 }} + typo3/cms-extbase:${{ matrix.typo3 }} + typo3/cms-fluid:${{ matrix.typo3 }} + typo3/cms-frontend:${{ matrix.typo3 }} + typo3/cms-backend:${{ matrix.typo3 }} + -W --no-update - name: Composer install uses: ramsey/composer-install@v2 @@ -97,20 +103,10 @@ jobs: fail-fast: false matrix: include: - - php: "7.4" - typo3: "^10.4" - - php: "8.0" - typo3: "^11.5" - - php: "8.1" - typo3: "^11.5" - - php: "8.1" - typo3: "^12.4" - php: "8.2" - typo3: "^12.4" - - php: "8.3" - typo3: "^12.4" - - php: "8.3" typo3: "^13.4" + - php: "8.3" + typo3: "^14.3" runs-on: ubuntu-latest @@ -122,19 +118,32 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: mbstring, json + extensions: mbstring, json, pdo_sqlite ini-values: date.timezone="Europe/Copenhagen", opcache.fast_shutdown=0 tools: composer:v2.2 - name: Select TYPO3 version - run: "composer require typo3/cms-core:${{ matrix.typo3 }} --no-update" + run: > + composer require + typo3/cms-core:${{ matrix.typo3 }} + typo3/cms-extbase:${{ matrix.typo3 }} + typo3/cms-fluid:${{ matrix.typo3 }} + typo3/cms-frontend:${{ matrix.typo3 }} + typo3/cms-backend:${{ matrix.typo3 }} + -W --no-update - name: Composer install uses: ramsey/composer-install@v2 + - name: Restore source Composer manifest + run: git checkout -- composer.json + - name: Run tests run: vendor/bin/phpunit --coverage-clover=build/logs/clover.xml + - name: Run functional tests + run: typo3DatabaseDriver=pdo_sqlite vendor/bin/phpunit -c phpunit-functional.xml.dist + - name: Upload test coverage run: | composer global require php-coveralls/php-coveralls diff --git a/.gitignore b/.gitignore index 75c5596c8..2686d15bb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ vendor .phpunit.result.cache composer.lock preflight +/var /build /Documentation-GENERATED-temp/ /index.php diff --git a/Tests/bootstrap.php b/Tests/bootstrap.php index 85add7559..7ebf62580 100644 --- a/Tests/bootstrap.php +++ b/Tests/bootstrap.php @@ -9,27 +9,6 @@ /** @var Composer\Autoload\ClassLoader $autoloader */ $autoloader = require __DIR__ . '/../vendor/autoload.php'; -\FluidTYPO3\Development\Bootstrap::initialize( - $autoloader, - [ - 'vhs_main' => \FluidTYPO3\Development\Bootstrap::CACHE_NULL, - 'vhs_markdown' => \FluidTYPO3\Development\Bootstrap::CACHE_NULL, - 'extbase_typo3dbbackend_tablecolumns' => \FluidTYPO3\Development\Bootstrap::CACHE_NULL, - 'extbase_typo3dbbackend_queries' => \FluidTYPO3\Development\Bootstrap::CACHE_NULL, - 'extbase_datamapfactory_datamap' => \FluidTYPO3\Development\Bootstrap::CACHE_NULL, - 'cache_rootline' => \FluidTYPO3\Development\Bootstrap::CACHE_NULL, - 'cache_pages' => \FluidTYPO3\Development\Bootstrap::CACHE_NULL, - 'cache_core' => \FluidTYPO3\Development\Bootstrap::CACHE_PHP_NULL, - 'extbase_object' => \FluidTYPO3\Development\Bootstrap::CACHE_NULL, - 'extbase_reflection' => \FluidTYPO3\Development\Bootstrap::CACHE_NULL, - 'l10n' => \FluidTYPO3\Development\Bootstrap::CACHE_NULL, - 'fluid_template' => \FluidTYPO3\Development\Bootstrap::CACHE_PHP_NULL - ], - [ - 'core' - ] -); - $forceErrorMode = getenv('FORCE_ERROR_MODE'); if ((string)$forceErrorMode === '') { $errorMode = E_ERROR | E_WARNING | E_PARSE; diff --git a/Tests/preflight.php b/Tests/preflight.php index d31321ccc..adca18b89 100644 --- a/Tests/preflight.php +++ b/Tests/preflight.php @@ -1,6 +1,6 @@ array ( - 'php' => '7.4.0-8.3.99', - 'typo3' => '10.4.0-13.4.99', + 'php' => '8.2.0-8.4.99', + 'typo3' => '13.4.0-14.3.99', ), 'conflicts' => array ( diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1a32c857a..e8d9605dc 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,25 +1,5 @@ parameters: ignoreErrors: - - - message: "#^Method TYPO3\\\\CMS\\\\Core\\\\Resource\\\\FileRepository\\:\\:findByRelation\\(\\) invoked with 4 parameters, 3 required\\.$#" - count: 1 - path: Classes/Proxy/FileRepositoryProxy.php - - - message: "#^Parameter \\#2 \\$response of method FluidTYPO3\\\\Vhs\\\\Proxy\\\\DispatcherProxy\\:\\:dispatch\\(\\) expects TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\Response\\|null, \\(TYPO3\\\\CMS\\\\Core\\\\Http\\\\Response\\&TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\Web\\\\Response\\)\\|\\(TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\ResponseInterface\\&TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\Web\\\\Response\\)\\|null given\\.$#" - count: 1 - path: Classes/ViewHelpers/Render/RequestViewHelper.php - - - message: "#^Parameter \\#2 \\$response of method FluidTYPO3\\\\Vhs\\\\Proxy\\\\DispatcherProxy\\:\\:dispatch\\(\\) expects TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\Response\\|null, \\(TYPO3\\\\CMS\\\\Core\\\\Http\\\\Response\\&TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\Web\\\\Response\\)\\|\\(TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\ResponseInterface\\&TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\Web\\\\Response\\)\\|null given\\.$#" - count: 1 - path: Classes/Proxy/DispatcherProxy.php - - - message: "#^Method TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\Dispatcher\\:\\:dispatch\\(\\) invoked with 2 parameters, 1 required\\.$#" - count: 1 - path: Classes/Proxy/DispatcherProxy.php - - - message: "#^Access to an undefined property TYPO3\\\\CMS\\\\Frontend\\\\Controller\\\\TypoScriptFrontendController\\:\\:\\$no_cache\\.$#" - count: 1 - path: Classes/Service/AssetService.php - message: "#^Call to an undefined static method TYPO3Fluid\\\\Fluid\\\\Core\\\\ViewHelper\\\\AbstractConditionViewHelper\\:\\:renderStatic\\(\\)\\.$#" count: 1 @@ -36,10 +16,6 @@ parameters: message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Core\\\\Site\\\\Entity\\\\SiteLanguage\\:\\:getTwoLetterIsoCode\\(\\)\\.$#" count: 1 path: Classes/ViewHelpers/Page/LanguageMenuViewHelper.php - - - message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Core\\\\Database\\\\Query\\\\QueryBuilder\\:\\:execute\\(\\)\\.$#" - count: 1 - path: Classes/Proxy/DoctrineQueryProxy.php - message: "#^Access to undefined constant TYPO3\\\\CMS\\\\Core\\\\Domain\\\\Repository\\\\PageRepository\\:\\:DOKTYPE_RECYCLER\\.$#" count: 1 @@ -48,14 +24,6 @@ parameters: message: "#^Cannot call method getLanguageCode\\(\\) on string\\.$#" count: 1 path: Classes/ViewHelpers/Page/LanguageMenuViewHelper.php - - - message: "#Call to an undefined method Doctrine\\\\DBAL\\\\Driver\\\\ResultStatement\\|Doctrine\\\\DBAL\\\\Result\\:\\:fetch\\(\\)\\.$#" - count: 1 - path: Classes/Proxy/DoctrineQueryProxy.php - - - message: "#Call to an undefined method Doctrine\\\\DBAL\\\\Driver\\\\ResultStatement\\|Doctrine\\\\DBAL\\\\Result\\:\\:fetchAll\\(\\)\\.$#" - count: 1 - path: Classes/Proxy/DoctrineQueryProxy.php - message: "#^Access to protected property TYPO3\\\\CMS\\\\Core\\\\Domain\\\\Repository\\\\PageRepository\\:\\:\\$where_groupAccess\\.$#" count: 1 @@ -84,18 +52,10 @@ parameters: message: "#^Parameter \\#2 \\$response of method FluidTYPO3\\\\Vhs\\\\Utility\\\\DispatcherProxy\\:\\:dispatch\\(\\) expects TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\Response\\|null, \\(TYPO3\\\\CMS\\\\Core\\\\Http\\\\Response&TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\Web\\\\Response\\)\\|\\(TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\ResponseInterface&TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\Web\\\\Response\\)\\|null given\\.$#" count: 1 path: Classes/ViewHelpers/Render/RequestViewHelper.php - - - message: "#^Method TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\Dispatcher\\:\\:dispatch\\(\\) invoked with 2 parameters, 1 required\\.$#" - count: 1 - path: Classes/Proxy/DispatcherProxy.php - message: "#^Parameter \\#1 \\$controllerClassName of class TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\Request constructor expects string, Psr\\\\Http\\\\Message\\\\ServerRequestInterface given\\.$#" count: 1 path: Classes/ViewHelpers/Render/RequestViewHelper.php - - - message: "#^Parameter \\#2 \\$response of method TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\Dispatcher\\:\\:dispatch\\(\\) expects TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\ResponseInterface, TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\Response\\|null given\\.$#" - count: 1 - path: Classes/Proxy/DispatcherProxy.php - message: "#Right side of \\|\\| is always true\\.$#" count: 1 diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php index 49ed59a92..7b15c59d5 100644 --- a/phpstan-bootstrap.php +++ b/phpstan-bootstrap.php @@ -5,4 +5,4 @@ define('TYPO3_REQUESTTYPE', 1); define('TYPO3_REQUESTTYPE_FE', 1); define('TYPO3_REQUESTTYPE_CLI', 3); -define('TYPO3_version', '9.5.31'); +define('TYPO3_version', '14.3.0'); diff --git a/phpstan.neon b/phpstan.neon index 8961d7b25..b435c8d61 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,7 +4,6 @@ includes: parameters: level: 9 reportUnmatchedIgnoredErrors: false - checkGenericClassInNonGenericObjectType: false treatPhpDocTypesAsCertain: false ignoreErrors: - "#^Class TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\Controller\\\\ControllerContext not found\\.$#" diff --git a/phpunit-functional.xml.dist b/phpunit-functional.xml.dist new file mode 100644 index 000000000..7404ad7b0 --- /dev/null +++ b/phpunit-functional.xml.dist @@ -0,0 +1,24 @@ + + + + + Tests/Functional + + + + + Classes + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1b3b60924..284647fb0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,23 +2,18 @@ - + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.5/phpunit.xsd"> + Classes - + Tests/Unit From 0bad5866e520cde784a5c42a0dee07f0c49c4da6 Mon Sep 17 00:00:00 2001 From: Benjamin Beck Date: Thu, 4 Jun 2026 13:55:09 +0200 Subject: [PATCH 02/12] [BUGFIX] Add TYPO3 13 and 14 compatibility bridges --- .../Core/ViewHelper/AbstractViewHelper.php | 10 ++--- ...cheableContentIsGeneratedEventListener.php | 22 +++++++++- Classes/Middleware/AssetInclusion.php | 9 ++-- Classes/Middleware/RequestAvailability.php | 4 +- Classes/Proxy/DispatcherProxy.php | 7 ++-- Classes/Proxy/DoctrineQueryProxy.php | 41 +++---------------- Classes/Proxy/ResourceFactoryProxy.php | 19 +++++---- Classes/Proxy/SiteFinderProxy.php | 4 +- Classes/Traits/ArgumentOverride.php | 20 ++++++--- .../Traits/ArrayConsumingViewHelperTrait.php | 5 +-- ...pileWithContentArgumentAndRenderStatic.php | 20 +++++---- Classes/Traits/CompileWithRenderStatic.php | 10 ++++- Classes/Traits/SourceSetViewHelperTrait.php | 7 +++- Classes/Traits/TagViewHelperCompatibility.php | 15 ++----- Classes/Traits/TagViewHelperTrait.php | 10 +---- .../TemplateVariableViewHelperTrait.php | 10 ++++- ext_localconf.php | 18 -------- 17 files changed, 113 insertions(+), 118 deletions(-) diff --git a/Classes/Core/ViewHelper/AbstractViewHelper.php b/Classes/Core/ViewHelper/AbstractViewHelper.php index 110c6e959..9d0c13404 100644 --- a/Classes/Core/ViewHelper/AbstractViewHelper.php +++ b/Classes/Core/ViewHelper/AbstractViewHelper.php @@ -1,7 +1,7 @@ arguments instanceof ArgumentCollection ? $this->arguments->getArrayCopy() : $this->arguments, - $this->buildRenderChildrenClosure(), - $this->renderingContext - ); + return static::renderStatic($this->arguments, $this->buildRenderChildrenClosure(), $this->renderingContext); } } diff --git a/Classes/Events/AfterCacheableContentIsGeneratedEventListener.php b/Classes/Events/AfterCacheableContentIsGeneratedEventListener.php index 0ed11c3dc..8f62a1814 100644 --- a/Classes/Events/AfterCacheableContentIsGeneratedEventListener.php +++ b/Classes/Events/AfterCacheableContentIsGeneratedEventListener.php @@ -3,6 +3,7 @@ namespace FluidTYPO3\Vhs\Events; use FluidTYPO3\Vhs\Service\AssetService; +use TYPO3\CMS\Core\Utility\VersionNumberUtility; use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent; class AfterCacheableContentIsGeneratedEventListener @@ -16,6 +17,25 @@ public function __construct(AssetService $assetService) public function insertVhsAssetHeaderAndFooterCode(AfterCacheableContentIsGeneratedEvent $event): void { - $this->assetService->buildAll([], $event->getController(), $event->isCachingEnabled()); + if ($this->isAssetHandlingDisabled()) { + return; + } + if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '14.0', '<')) { + return; + } + // @phpstan-ignore-next-line TYPO3 14-only event content API. + $content = $event->getContent(); + $this->assetService->buildAll([], $event->getRequest(), $event->isCachingEnabled(), $content); + // @phpstan-ignore-next-line TYPO3 14-only event content API. + $event->setContent($content); + } + + private function isAssetHandlingDisabled(): bool + { + $disabled = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['vhs']['disableAssetHandling'] + ?? $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['vhs']['setup']['disableAssetHandling'] + ?? false; + + return filter_var($disabled, \FILTER_VALIDATE_BOOL); } } diff --git a/Classes/Middleware/AssetInclusion.php b/Classes/Middleware/AssetInclusion.php index 969bca267..35754b712 100644 --- a/Classes/Middleware/AssetInclusion.php +++ b/Classes/Middleware/AssetInclusion.php @@ -9,10 +9,13 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use TYPO3\CMS\Core\Http\Stream; -use TYPO3\CMS\Core\Utility\GeneralUtility; class AssetInclusion implements MiddlewareInterface { + public function __construct(private readonly AssetService $assetService) + { + } + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $response = $handler->handle($request); @@ -22,9 +25,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $contents = $body->getContents(); $contentsBefore = $contents; - /** @var AssetService $assetService */ - $assetService = GeneralUtility::makeInstance(AssetService::class); - $assetService->buildAllUncached([], $GLOBALS['TSFE'], $contents); + $this->assetService->buildAllUncached([], $request, $contents); if ($contentsBefore === $contents) { // Content is unchanged, return the original response since there is no need to modify it, or the diff --git a/Classes/Middleware/RequestAvailability.php b/Classes/Middleware/RequestAvailability.php index 5f1c0e6e9..5a1039e08 100644 --- a/Classes/Middleware/RequestAvailability.php +++ b/Classes/Middleware/RequestAvailability.php @@ -1,4 +1,7 @@ handle($request); } } diff --git a/Classes/Proxy/DispatcherProxy.php b/Classes/Proxy/DispatcherProxy.php index 2c8a3d237..852fd0273 100644 --- a/Classes/Proxy/DispatcherProxy.php +++ b/Classes/Proxy/DispatcherProxy.php @@ -8,10 +8,9 @@ * LICENSE.md file that was distributed with this source code. */ -use Psr\Http\Message\ResponseInterface; use TYPO3\CMS\Extbase\Mvc\Dispatcher; use TYPO3\CMS\Extbase\Mvc\RequestInterface; -use TYPO3\CMS\Extbase\Mvc\Response; +use Psr\Http\Message\ResponseInterface; class DispatcherProxy { @@ -22,8 +21,8 @@ public function __construct(Dispatcher $dispatcher) $this->dispatcher = $dispatcher; } - public function dispatch(RequestInterface $request, ?Response $response = null): ?ResponseInterface + public function dispatch(RequestInterface $request): ResponseInterface { - return $this->dispatcher->dispatch($request, $response); + return $this->dispatcher->dispatch($request); } } diff --git a/Classes/Proxy/DoctrineQueryProxy.php b/Classes/Proxy/DoctrineQueryProxy.php index 793a11f99..9dde57c96 100644 --- a/Classes/Proxy/DoctrineQueryProxy.php +++ b/Classes/Proxy/DoctrineQueryProxy.php @@ -1,52 +1,23 @@ executeQuery(); - } - /** @var Result $result */ - $result = $queryBuilder->execute(); - return $result; + return $queryBuilder->executeQuery(); } - /** - * @param Result|ResultStatement $result - */ - public static function fetchAssociative($result): ?array + public static function fetchAssociative(Result $result): ?array { - if (method_exists($result, 'fetchAssociative')) { - /** @var array|null $output */ - $output = $result->fetchAssociative() ?: null; - } else { - /** @var array|null $output */ - $output = $result->fetch(FetchMode::ASSOCIATIVE); - } - - return $output; + return $result->fetchAssociative() ?: null; } - /** - * @param Result|ResultStatement $result - */ - public static function fetchAllAssociative($result): array + public static function fetchAllAssociative(Result $result): array { - if (method_exists($result, 'fetchAllAssociative')) { - return $result->fetchAllAssociative() ?: []; - } - return $result->fetchAll(FetchMode::ASSOCIATIVE) ?: []; + return $result->fetchAllAssociative() ?: []; } } diff --git a/Classes/Proxy/ResourceFactoryProxy.php b/Classes/Proxy/ResourceFactoryProxy.php index e7474cd42..c9276d67b 100644 --- a/Classes/Proxy/ResourceFactoryProxy.php +++ b/Classes/Proxy/ResourceFactoryProxy.php @@ -1,4 +1,6 @@ resourceFactory = $resourceFactory; } - public function getFileReferenceObject(int $uid): FileReference - { - return $this->resourceFactory->getFileReferenceObject($uid); + public function getFileReferenceObject( + int|string $uid, + array $fileReferenceData = [], + bool $raw = false + ): FileReference { + return $this->resourceFactory->getFileReferenceObject((int) $uid, $fileReferenceData, $raw); } /** * @param int $uid */ - public function getFileObject($uid, array $fileData = []): File + public function getFileObject(int|string $uid, array $fileData = []): File { - return $this->resourceFactory->getFileObject($uid, $fileData); + return $this->resourceFactory->getFileObject((int) $uid, $fileData); } /** * @param string $identifier * @return File|ProcessedFile|null */ - public function getFileObjectFromCombinedIdentifier($identifier) + public function getFileObjectFromCombinedIdentifier(string $identifier): File|ProcessedFile|null { return $this->resourceFactory->getFileObjectFromCombinedIdentifier($identifier); } diff --git a/Classes/Proxy/SiteFinderProxy.php b/Classes/Proxy/SiteFinderProxy.php index 6b44e0283..6eb447a13 100644 --- a/Classes/Proxy/SiteFinderProxy.php +++ b/Classes/Proxy/SiteFinderProxy.php @@ -1,4 +1,6 @@ =')) { - return parent::registerArgument($name, $type, $description, $required, $defaultValue, $escape); - } - return parent::overrideArgument($name, $type, $description, $required, $defaultValue, $escape); + parent::registerArgument($name, $type, $description, $required, $defaultValue, $escape); + return $this; } } diff --git a/Classes/Traits/ArrayConsumingViewHelperTrait.php b/Classes/Traits/ArrayConsumingViewHelperTrait.php index 4a312b902..e3c59b9f4 100644 --- a/Classes/Traits/ArrayConsumingViewHelperTrait.php +++ b/Classes/Traits/ArrayConsumingViewHelperTrait.php @@ -60,7 +60,7 @@ protected static function getArgumentFromArgumentsOrTagContentAndConvertToArrayS /** * @param mixed $candidate */ - protected static function arrayFromArrayOrTraversableOrCSVStatic($candidate, bool $useKeys = true): array + protected static function arrayFromArrayOrTraversableOrCSVStatic(mixed $candidate, bool $useKeys = true): array { if ($candidate instanceof QueryResultInterface) { return $candidate->toArray(); @@ -75,7 +75,6 @@ protected static function arrayFromArrayOrTraversableOrCSVStatic($candidate, boo return GeneralUtility::trimExplode(',', $candidate, true); } ErrorUtility::throwViewHelperException('Unsupported input type; cannot convert to array!'); - return []; } protected function mergeArrays(array $array1, array $array2): array @@ -92,7 +91,7 @@ protected static function mergeArraysStatic(array $array1, array $array2): array /** * @param mixed $subject */ - protected static function assertIsArrayOrIterator($subject): bool + protected static function assertIsArrayOrIterator(mixed $subject): bool { return is_array($subject) || $subject instanceof \Traversable; } diff --git a/Classes/Traits/CompileWithContentArgumentAndRenderStatic.php b/Classes/Traits/CompileWithContentArgumentAndRenderStatic.php index 80d154cb1..e038bcbc2 100644 --- a/Classes/Traits/CompileWithContentArgumentAndRenderStatic.php +++ b/Classes/Traits/CompileWithContentArgumentAndRenderStatic.php @@ -1,6 +1,8 @@ renderingContext instanceof \TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface) { + throw new \RuntimeException('Unable to render ViewHelper without rendering context.', 1706067600); + } + return static::renderStatic( $this->arguments, $this->buildRenderChildrenClosure(), @@ -81,8 +87,6 @@ public function render() * @param string $argumentsName * @param string $closureName * @param string $initializationPhpCode - * @param ViewHelperNode $node - * @param TemplateCompiler $compiler * @return string */ public function compile( @@ -91,7 +95,7 @@ public function compile( &$initializationPhpCode, ViewHelperNode $node, TemplateCompiler $compiler - ) { + ): string { $initialization = ''; $execution = sprintf( '%s::renderStatic(%s, %s, $renderingContext)', @@ -124,7 +128,7 @@ public function compile( * * @return \Closure */ - protected function buildRenderChildrenClosure() + protected function buildRenderChildrenClosure(): Closure { $argumentName = $this->resolveContentArgumentName(); $arguments = $this->arguments; @@ -148,7 +152,7 @@ protected function buildRenderChildrenClosure() * @return mixed The finally rendered child nodes. * @api */ - public function renderChildren() + public function renderChildren(): mixed { if ($this->renderChildrenClosure !== null) { $closure = $this->renderChildrenClosure; @@ -160,7 +164,7 @@ public function renderChildren() /** * @return string */ - public function resolveContentArgumentName() + public function resolveContentArgumentName(): string { if (empty($this->contentArgumentName)) { $registeredArguments = $this->prepareArguments(); diff --git a/Classes/Traits/CompileWithRenderStatic.php b/Classes/Traits/CompileWithRenderStatic.php index aec174bfe..d656cf3c5 100644 --- a/Classes/Traits/CompileWithRenderStatic.php +++ b/Classes/Traits/CompileWithRenderStatic.php @@ -8,6 +8,8 @@ * LICENSE.md file that was distributed with this source code. */ +use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; + /** * Class CompilableWithRenderStatic * @@ -24,8 +26,12 @@ trait CompileWithRenderStatic * @return mixed Rendered result * @api */ - public function render() + public function render(): mixed { + if (!$this->renderingContext instanceof RenderingContextInterface) { + throw new \RuntimeException('Unable to render ViewHelper without rendering context.', 1706067600); + } + return static::renderStatic( $this->arguments, $this->buildRenderChildrenClosure(), @@ -35,6 +41,8 @@ public function render() /** * @return \Closure + * TYPO3 13 / Fluid 4 compatibility: keep this abstract signature untyped + * because Fluid 4's AbstractViewHelper method has no return type. */ abstract protected function buildRenderChildrenClosure(); } diff --git a/Classes/Traits/SourceSetViewHelperTrait.php b/Classes/Traits/SourceSetViewHelperTrait.php index e296ae40e..ebb6668e5 100644 --- a/Classes/Traits/SourceSetViewHelperTrait.php +++ b/Classes/Traits/SourceSetViewHelperTrait.php @@ -4,6 +4,7 @@ use FluidTYPO3\Vhs\Utility\ContentObjectFetcher; use FluidTYPO3\Vhs\Utility\ContextUtility; use FluidTYPO3\Vhs\Utility\FrontendSimulationUtility; +use TYPO3\CMS\Core\Imaging\ImageResource; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\MathUtility; use TYPO3Fluid\Fluid\Core\ViewHelper\Exception; @@ -118,7 +119,11 @@ public function getImgResource( if (ContextUtility::isBackend() && '../' === substr($src, 0, 3)) { $src = substr($src, 3); } - return (array) $contentObject->getImgResource($src, $setup); + $imageResource = $contentObject->getImgResource($src, $setup); + if ($imageResource instanceof ImageResource) { + return $imageResource->getLegacyImageResourceInformation(); + } + return (array) $imageResource; } /** diff --git a/Classes/Traits/TagViewHelperCompatibility.php b/Classes/Traits/TagViewHelperCompatibility.php index 1063c5c23..3ab62f1c6 100644 --- a/Classes/Traits/TagViewHelperCompatibility.php +++ b/Classes/Traits/TagViewHelperCompatibility.php @@ -8,8 +8,6 @@ * LICENSE.md file that was distributed with this source code. */ -use TYPO3\CMS\Core\Utility\VersionNumberUtility; - trait TagViewHelperCompatibility { /** @@ -24,13 +22,9 @@ trait TagViewHelperCompatibility * @return void * @api */ - protected function registerTagAttribute($name, $type, $description, $required = false, $defaultValue = null) + protected function registerTagAttribute($name, $type, $description, $required = false, $defaultValue = null): void { - if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '13.4', '>=')) { - $this->registerArgument($name, $type, $description, $required, $defaultValue); - return; - } - parent::registerTagAttribute($name, $type, $description, $required, $defaultValue); + $this->registerArgument($name, $type, $description, $required, $defaultValue); } /** @@ -40,11 +34,8 @@ protected function registerTagAttribute($name, $type, $description, $required = * @return void * @api */ - protected function registerUniversalTagAttributes() + protected function registerUniversalTagAttributes(): void { - if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '13.4', '>=')) { - return; - } $this->registerTagAttribute('class', 'string', 'CSS class(es) for this element'); $this->registerTagAttribute( 'dir', diff --git a/Classes/Traits/TagViewHelperTrait.php b/Classes/Traits/TagViewHelperTrait.php index 66461cb33..54ccf5c74 100644 --- a/Classes/Traits/TagViewHelperTrait.php +++ b/Classes/Traits/TagViewHelperTrait.php @@ -8,8 +8,6 @@ * LICENSE.md file that was distributed with this source code. */ -use TYPO3\CMS\Core\Utility\VersionNumberUtility; - /** * Class TagViewHelperTrait * @@ -45,13 +43,9 @@ public function registerArguments(): void * @return void * @api */ - protected function registerTagAttribute($name, $type, $description, $required = false, $defaultValue = null) + protected function registerTagAttribute($name, $type, $description, $required = false, $defaultValue = null): void { - if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '13.4', '>=')) { - $this->registerArgument($name, $type, $description, $required, $defaultValue); - return; - } - parent::registerTagAttribute($name, $type, $description, $required, $defaultValue); + $this->registerArgument($name, $type, $description, $required, $defaultValue); } /** diff --git a/Classes/Traits/TemplateVariableViewHelperTrait.php b/Classes/Traits/TemplateVariableViewHelperTrait.php index 1363df041..0007753d8 100644 --- a/Classes/Traits/TemplateVariableViewHelperTrait.php +++ b/Classes/Traits/TemplateVariableViewHelperTrait.php @@ -99,11 +99,19 @@ protected function renderChildrenWithVariables(array $variables) { return static::renderChildrenWithVariablesStatic( $variables, - $this->templateVariableContainer, + $this->getTemplateVariableContainerOrFail(), $this->buildRenderChildrenClosure() ); } + private function getTemplateVariableContainerOrFail(): VariableProviderInterface + { + if (!$this->templateVariableContainer instanceof VariableProviderInterface) { + throw new \RuntimeException('Template variable container missing', 1737807861); + } + return $this->templateVariableContainer; + } + /** * Renders tag content of ViewHelper and inserts variables * in $variables into $variableContainer while keeping backups diff --git a/ext_localconf.php b/ext_localconf.php index a302af213..17c70a3dd 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -5,14 +5,6 @@ \TYPO3\CMS\Core\Configuration\ExtensionConfiguration::class )->get('vhs'); - if (!isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['vhs']['setup']['disableAssetHandling']) || !$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['vhs']['setup']['disableAssetHandling']) { - if (version_compare(\TYPO3\CMS\Core\Utility\VersionNumberUtility::getCurrentTypo3Version(), '12.0', '<')) { - $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['usePageCache'][] = \FluidTYPO3\Vhs\Service\AssetService::class; - } - - $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['clearCachePostProc'][] = \FluidTYPO3\Vhs\Service\AssetService::class . '->clearCacheCommand'; - } - if (!is_array($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['vhs_main'] ?? null)) { $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['vhs_main'] = [ 'frontend' => \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class, @@ -35,14 +27,4 @@ } $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces']['v'] = ['FluidTYPO3\\Vhs\\ViewHelpers']; - - if (version_compare(\TYPO3\CMS\Core\Utility\VersionNumberUtility::getCurrentTypo3Version(), '13.0', '<')) { - // add navigtion hide to fix menu viewHelpers (e.g. breadcrumb) - $GLOBALS['TYPO3_CONF_VARS']['FE']['addRootLineFields'] .= (empty($GLOBALS['TYPO3_CONF_VARS']['FE']['addRootLineFields']) ? '' : ',') . 'nav_hide,shortcut,shortcut_mode'; - - // add and urltype to fix the rendering of external url doktypes - if (isset($GLOBALS['TCA']['pages']['columns']['urltype'])) { - $GLOBALS['TYPO3_CONF_VARS']['FE']['addRootLineFields'] .= ',url,urltype'; - } - } })(); From 5e1f50b3d4f757af315dca95928d26282af850d0 Mon Sep 17 00:00:00 2001 From: Benjamin Beck Date: Thu, 4 Jun 2026 13:55:22 +0200 Subject: [PATCH 03/12] [BUGFIX] Resolve frontend context from rendering requests --- Classes/Asset.php | 14 + Classes/Service/AssetService.php | 310 ++++++++++++------ Classes/Service/PageService.php | 181 +++++++--- Classes/Utility/ContentObjectFetcher.php | 23 +- Classes/Utility/ContextUtility.php | 17 +- Classes/Utility/ErrorUtility.php | 2 +- Classes/Utility/FrontendSimulationUtility.php | 179 +++++++--- Classes/Utility/RequestResolver.php | 83 +++-- Classes/View/UncacheTemplateView.php | 56 ++-- .../Asset/AbstractAssetViewHelper.php | 25 +- Classes/ViewHelpers/Asset/AssetInterface.php | 2 +- .../ViewHelpers/Asset/PrefetchViewHelper.php | 6 +- .../ViewHelpers/Asset/ScriptViewHelper.php | 4 + Classes/ViewHelpers/Asset/StyleViewHelper.php | 4 + Classes/ViewHelpers/AssetViewHelper.php | 5 + .../Condition/Context/IsBackendViewHelper.php | 2 +- .../Condition/Context/IsCliViewHelper.php | 6 +- .../Context/IsDevelopmentViewHelper.php | 2 +- .../Context/IsFrontendViewHelper.php | 2 +- .../Condition/Page/HasSubpagesViewHelper.php | 28 +- .../Condition/Page/IsChildPageViewHelper.php | 28 +- .../Content/AbstractContentViewHelper.php | 79 ++++- .../ViewHelpers/Content/InfoViewHelper.php | 34 +- .../Media/AbstractMediaViewHelper.php | 104 +++++- Classes/ViewHelpers/Media/AudioViewHelper.php | 5 +- .../ViewHelpers/Media/ExtensionViewHelper.php | 4 +- Classes/ViewHelpers/Media/FilesViewHelper.php | 2 +- .../ViewHelpers/Media/GravatarViewHelper.php | 2 +- .../Image/AbstractImageInfoViewHelper.php | 17 +- .../Media/Image/AbstractImageViewHelper.php | 8 - .../Media/Image/HeightViewHelper.php | 5 +- .../Media/Image/MimetypeViewHelper.php | 7 +- .../Media/Image/WidthViewHelper.php | 5 +- Classes/ViewHelpers/Media/ImageViewHelper.php | 4 +- .../Media/PdfThumbnailViewHelper.php | 2 +- .../ViewHelpers/Media/PictureViewHelper.php | 14 +- Classes/ViewHelpers/Media/SizeViewHelper.php | 4 +- .../ViewHelpers/Media/SourceViewHelper.php | 43 +-- .../ViewHelpers/Media/SpotifyViewHelper.php | 2 +- Classes/ViewHelpers/Media/VideoViewHelper.php | 5 +- Classes/ViewHelpers/Media/VimeoViewHelper.php | 2 +- .../ViewHelpers/Media/YoutubeViewHelper.php | 2 +- .../Menu/AbstractMenuViewHelper.php | 63 +++- Classes/ViewHelpers/Menu/BrowseViewHelper.php | 39 ++- .../ViewHelpers/Menu/DeferredViewHelper.php | 7 +- .../ViewHelpers/Menu/DirectoryViewHelper.php | 8 +- Classes/ViewHelpers/Menu/ListViewHelper.php | 9 +- Classes/ViewHelpers/Menu/SubViewHelper.php | 15 +- .../Once/AbstractOnceViewHelper.php | 24 +- .../ViewHelpers/Once/SessionViewHelper.php | 2 +- .../Page/AbsoluteUrlViewHelper.php | 29 +- .../ViewHelpers/Page/BreadCrumbViewHelper.php | 15 +- Classes/ViewHelpers/Page/FooterViewHelper.php | 8 +- .../Page/Header/AlternateViewHelper.php | 63 +++- .../Page/Header/CanonicalViewHelper.php | 52 ++- .../Page/Header/LinkViewHelper.php | 2 +- .../Page/Header/MetaViewHelper.php | 2 +- .../Page/Header/TitleViewHelper.php | 22 +- Classes/ViewHelpers/Page/HeaderViewHelper.php | 8 +- Classes/ViewHelpers/Page/InfoViewHelper.php | 26 +- .../Page/LanguageMenuViewHelper.php | 85 +++-- .../ViewHelpers/Page/LanguageViewHelper.php | 47 ++- Classes/ViewHelpers/Page/LinkViewHelper.php | 54 ++- .../Page/Resources/FalViewHelper.php | 25 +- .../ViewHelpers/Page/RootlineViewHelper.php | 11 +- .../Page/StaticPrefixViewHelper.php | 24 +- .../Render/AbstractRenderViewHelper.php | 64 +++- .../ViewHelpers/Render/AsciiViewHelper.php | 4 +- .../ViewHelpers/Render/CacheViewHelper.php | 4 +- .../ViewHelpers/Render/InlineViewHelper.php | 6 +- .../ViewHelpers/Render/RecordViewHelper.php | 6 +- .../ViewHelpers/Render/RequestViewHelper.php | 20 +- .../ViewHelpers/Render/TemplateViewHelper.php | 17 +- .../ViewHelpers/Render/UncacheViewHelper.php | 10 +- .../Resource/AbstractImageViewHelper.php | 141 +++++++- .../Resource/AbstractResourceViewHelper.php | 7 +- .../AbstractTagBasedResourceViewHelper.php | 212 ++++++++++++ .../Resource/CollectionViewHelper.php | 4 +- .../ViewHelpers/Resource/FileViewHelper.php | 5 +- .../ViewHelpers/Resource/ImageViewHelper.php | 19 +- .../Resource/LanguageViewHelper.php | 27 +- .../AbstractRecordResourceViewHelper.php | 9 +- .../Resource/Record/FalViewHelper.php | 26 +- .../Resource/ResourceViewHelperInterface.php | 2 +- .../Security/AbstractSecurityViewHelper.php | 64 ++-- Classes/ViewHelpers/Site/NameViewHelper.php | 38 ++- Classes/ViewHelpers/Site/UrlViewHelper.php | 31 +- .../ViewHelpers/Uri/GravatarViewHelper.php | 6 +- Classes/ViewHelpers/Uri/ImageViewHelper.php | 6 +- Classes/ViewHelpers/Uri/RequestViewHelper.php | 32 +- 90 files changed, 2020 insertions(+), 709 deletions(-) create mode 100644 Classes/ViewHelpers/Resource/AbstractTagBasedResourceViewHelper.php diff --git a/Classes/Asset.php b/Classes/Asset.php index bb49b0c3c..e87d907b1 100644 --- a/Classes/Asset.php +++ b/Classes/Asset.php @@ -1,4 +1,6 @@ defer; } + public function setCsp(?bool $csp): self + { + $this->csp = $csp; + return $this; + } + + public function getCsp(): ?bool + { + return $this->csp; + } + public function getName(): string { return $this->name; diff --git a/Classes/Service/AssetService.php b/Classes/Service/AssetService.php index d2eb1a4e9..529cd918d 100644 --- a/Classes/Service/AssetService.php +++ b/Classes/Service/AssetService.php @@ -14,19 +14,23 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; use TYPO3\CMS\Core\Cache\CacheManager; +use TYPO3\CMS\Core\Cache\Event\CacheFlushEvent; +use TYPO3\CMS\Core\Http\ApplicationType; use TYPO3\CMS\Core\Log\LogManager; use TYPO3\CMS\Core\Routing\PageArguments; use TYPO3\CMS\Core\Routing\RouteResultInterface; +use TYPO3\CMS\Core\Security\ContentSecurityPolicy\ConsumableNonce; +use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive; use TYPO3\CMS\Core\SingletonInterface; use TYPO3\CMS\Core\Utility\ArrayUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\PathUtility; use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; use TYPO3\CMS\Extbase\Utility\DebuggerUtility; -use TYPO3\CMS\Fluid\View\StandaloneView; +use TYPO3\CMS\Fluid\Core\Rendering\RenderingContextFactory; use TYPO3\CMS\Frontend\Cache\CacheInstruction; -use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; use TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder; +use TYPO3Fluid\Fluid\View\TemplateView; /** * Asset Handling Service @@ -49,7 +53,7 @@ class AssetService implements SingletonInterface protected $cacheManager; protected static bool $typoScriptAssetsBuilt = false; - protected static ?array $settingsCache = null; + protected static array $settingsCache = []; protected static array $cachedDependencies = []; protected static bool $cacheCleared = false; @@ -63,22 +67,24 @@ public function injectCacheManager(CacheManager $cacheManager): void $this->cacheManager = $cacheManager; } - public function usePageCache(object $caller, bool $shouldUsePageCache): bool + public function usePageCache(ServerRequestInterface $request, bool $shouldUsePageCache): bool { - $this->buildAll([], $caller); + $this->buildAll([], $request); return $shouldUsePageCache; } - public function buildAll(array $parameters, object $caller, bool $cached = true, ?string &$content = null): void - { - if ($content === null) { - $content = &$caller->content; - } + public function buildAll( + array $parameters, + ServerRequestInterface $request, + bool $cached = true, + ?string &$content = null + ): void { + $content = $content ?? ''; - $settings = $this->getSettings(); + $settings = $this->getSettings($request); $buildTypoScriptAssets = ( !static::$typoScriptAssetsBuilt - && ($cached || $this->readCacheDisabledInstructionFromContext()) + && ($cached || $this->readCacheDisabledInstructionFromContext($request)) ); if ($buildTypoScriptAssets && isset($settings['asset']) && is_array($settings['asset'])) { foreach ($settings['asset'] as $name => $typoScriptAsset) { @@ -103,7 +109,7 @@ public function buildAll(array $parameters, object $caller, bool $cached = true, } $assets = $GLOBALS['VhsAssets']; $assets = $this->sortAssetsByDependency($assets); - $assets = $this->manipulateAssetsByTypoScriptSettings($assets); + $assets = $this->manipulateAssetsByTypoScriptSettings($assets, $request); $buildDebugRequested = (isset($settings['asset']['debugBuild']) && $settings['asset']['debugBuild'] > 0); $assetDebugRequested = (isset($settings['asset']['debug']) && $settings['asset']['debug'] > 0); $useDebugUtility = (isset($settings['asset']['useDebugUtility']) && $settings['asset']['useDebugUtility'] > 0) @@ -115,22 +121,23 @@ public function buildAll(array $parameters, object $caller, bool $cached = true, echo var_export($assets, true); } } - $this->placeAssetsInHeaderAndFooter($assets, $cached, $content); + $this->placeAssetsInHeaderAndFooter($assets, $cached, $content, $request); } - public function buildAllUncached(array $parameters, object $caller, ?string &$content = null): void - { - if ($content === null) { - $content = &$caller->content; - } + public function buildAllUncached( + array $parameters, + ServerRequestInterface $request, + ?string &$content = null + ): void { + $content = $content ?? ''; $matches = []; - preg_match_all('/\<\![\-]+\ VhsAssetsDependenciesLoaded ([^ ]+) [\-]+\>/i', $content, $matches); + preg_match_all('/\<\![\-]+\ VhsAssetsDependenciesLoaded ([^ ]+) [\-]+\>/i', (string) $content, $matches); foreach ($matches[1] as $key => $match) { $extractedDependencies = explode(',', $matches[1][$key]); static::$cachedDependencies = array_merge(static::$cachedDependencies, $extractedDependencies); } - $this->buildAll($parameters, $caller, false, $content); + $this->buildAll($parameters, $request, false, $content); } public function isAlreadyDefined(string $assetName): bool @@ -143,19 +150,20 @@ public function isAlreadyDefined(string $assetName): bool * during inclusion. Public access allows later inspection * of the TypoScript values which were applied to the Asset. */ - public function getSettings(): array + public function getSettings(ServerRequestInterface $request): array { - if (null === static::$settingsCache) { - static::$settingsCache = $this->getTypoScript()['settings'] ?? []; + $cacheKey = $this->buildSettingsCacheKey($request); + if (!isset(static::$settingsCache[$cacheKey])) { + static::$settingsCache[$cacheKey] = $this->getTypoScript($request)['settings'] ?? []; } - $settings = (array) static::$settingsCache; + $settings = (array) static::$settingsCache[$cacheKey]; return $settings; } - protected function getTypoScript(): array + protected function getTypoScript(ServerRequestInterface $request): array { $cache = $this->cacheManager->getCache('vhs_main'); - $pageUid = $this->readPageUidFromContext(); + $pageUid = $this->readPageUidFromContext($request); $cacheId = 'vhs_asset_ts_' . $pageUid; $cacheTag = 'pageId_' . $pageUid; @@ -198,13 +206,17 @@ protected function getTypoScript(): array /** * @param AssetInterface[]|array[] $assets */ - protected function placeAssetsInHeaderAndFooter(array $assets, bool $cached, ?string &$content): void - { - $settings = $this->getSettings(); + protected function placeAssetsInHeaderAndFooter( + array $assets, + bool $cached, + ?string &$content, + ServerRequestInterface $request + ): void { + $settings = $this->getSettings($request); $header = []; $footer = []; - $footerRelocationEnabled = (isset($settings['enableFooterRelocation']) && $settings['relocateToFooter'] > 0) - || !isset($settings['enableFooterRelocation']); + $footerRelocationEnabled = !isset($settings['enableFooterRelocation']) + || (int) ($settings['relocateToFooter'] ?? $settings['enableFooterRelocation']) > 0; foreach ($assets as $name => $asset) { if ($asset instanceof AssetInterface) { $variables = $asset->getVariables(); @@ -226,22 +238,26 @@ protected function placeAssetsInHeaderAndFooter(array $assets, bool $cached, ?st } else { $uncachedSuffix = ''; $dependenciesString = ''; - $this->insertAssetsAtMarker('DependenciesLoaded', $dependenciesString, $content); + $this->insertAssetsAtMarker('DependenciesLoaded', $dependenciesString, $content, $request); } - $this->insertAssetsAtMarker('Header' . $uncachedSuffix, $header, $content); - $this->insertAssetsAtMarker('Footer' . $uncachedSuffix, $footer, $content); + $this->insertAssetsAtMarker('Header' . $uncachedSuffix, $header, $content, $request); + $this->insertAssetsAtMarker('Footer' . $uncachedSuffix, $footer, $content, $request); $GLOBALS['VhsAssets'] = []; } /** * @param AssetInterface[]|array[]|string $assets */ - protected function insertAssetsAtMarker(string $markerName, $assets, ?string &$content): void - { + protected function insertAssetsAtMarker( + string $markerName, + $assets, + ?string &$content, + ServerRequestInterface $request + ): void { $assetMarker = ''; if (is_array($assets)) { - $chunk = $this->buildAssetsChunk($assets); + $chunk = $this->buildAssetsChunk($assets, $request); } else { $chunk = $assets; } @@ -259,7 +275,7 @@ protected function insertAssetsAtMarker(string $markerName, $assets, ?string &$c } } - protected function buildAssetsChunk(array $assets): string + protected function buildAssetsChunk(array $assets, ServerRequestInterface $request): string { $spool = []; foreach ($assets as $name => $asset) { @@ -287,26 +303,45 @@ protected function buildAssetsChunk(array $assets): string $chunk[$name] = $asset; } else { if (0 < count($chunk)) { - $mergedFileTag = $this->writeCachedMergedFileAndReturnTag($chunk, $type); + $mergedFileTag = $this->writeCachedMergedFileAndReturnTag($chunk, $type, $request); $chunks[] = $mergedFileTag; $chunk = []; } if (empty($path)) { - $assetContent = $this->extractAssetContent($asset); - $chunks[] = $this->generateTagForAssetType($type, $assetContent, null, null, $assetSettings); + $assetContent = $this->extractAssetContent($asset, $request); + $chunks[] = $this->generateTagForAssetType( + $type, + $assetContent, + null, + null, + $assetSettings, + $request + ); } else { if ($external) { - $chunks[] = $this->generateTagForAssetType($type, null, $path, null, $assetSettings); + $chunks[] = $this->generateTagForAssetType( + $type, + null, + $path, + null, + $assetSettings, + $request + ); } else { if ($rewrite) { - $chunks[] = $this->writeCachedMergedFileAndReturnTag([$name => $asset], $type); + $chunks[] = $this->writeCachedMergedFileAndReturnTag( + [$name => $asset], + $type, + $request + ); } else { $chunks[] = $this->generateTagForAssetType( $type, null, $path, - $this->getFileIntegrity($path), - $assetSettings + $this->getFileIntegrity($path, $request), + $assetSettings, + $request ); } } @@ -314,21 +349,24 @@ protected function buildAssetsChunk(array $assets): string } } if (0 < count($chunk)) { - $mergedFileTag = $this->writeCachedMergedFileAndReturnTag($chunk, $type); + $mergedFileTag = $this->writeCachedMergedFileAndReturnTag($chunk, $type, $request); $chunks[] = $mergedFileTag; } } return implode(LF, $chunks); } - protected function writeCachedMergedFileAndReturnTag(array $assets, string $type): ?string - { + protected function writeCachedMergedFileAndReturnTag( + array $assets, + string $type, + ServerRequestInterface $request + ): ?string { $source = ''; $keys = array_keys($assets); sort($keys); $assetName = implode('-', $keys); unset($keys); - $typoScript = $this->getTypoScript(); + $typoScript = $this->getTypoScript($request); if (isset($typoScript['assets']['mergedAssetsUseHashedFilename'])) { if ($typoScript['assets']['mergedAssetsUseHashedFilename']) { $assetName = md5($assetName); @@ -338,8 +376,8 @@ protected function writeCachedMergedFileAndReturnTag(array $assets, string $type $fileAbsolutePathAndFilename = $this->resolveAbsolutePathForFile($fileRelativePathAndFilename); if (!file_exists($fileAbsolutePathAndFilename) || 0 === filemtime($fileAbsolutePathAndFilename) - || isset($GLOBALS['BE_USER']) - || $this->readCacheDisabledInstructionFromContext() + || ApplicationType::fromRequest($request)->isBackend() + || $this->readCacheDisabledInstructionFromContext($request) ) { foreach ($assets as $name => $asset) { $assetSettings = $this->extractAssetSettings($asset); @@ -347,7 +385,7 @@ protected function writeCachedMergedFileAndReturnTag(array $assets, string $type !isset($assetSettings['namedChunks'])) { $source .= '/* ' . $name . ' */' . LF; } - $source .= $this->extractAssetContent($asset) . LF; + $source .= $this->extractAssetContent($asset, $request) . LF; // Put a return carriage between assets preventing broken content. $source .= "\n"; } @@ -369,8 +407,8 @@ protected function writeCachedMergedFileAndReturnTag(array $assets, string $type } } } - $fileRelativePathAndFilename = $this->prefixPath($fileRelativePathAndFilename); - $integrity = $this->getFileIntegrity($fileAbsolutePathAndFilename); + $fileRelativePathAndFilename = $this->prefixPath($fileRelativePathAndFilename, $request); + $integrity = $this->getFileIntegrity($fileAbsolutePathAndFilename, $request); $assetSettings = null; if (count($assets) === 1) { @@ -380,7 +418,14 @@ protected function writeCachedMergedFileAndReturnTag(array $assets, string $type } } - return $this->generateTagForAssetType($type, null, $fileRelativePathAndFilename, $integrity, $assetSettings); + return $this->generateTagForAssetType( + $type, + null, + $fileRelativePathAndFilename, + $integrity, + $assetSettings, + $request + ); } protected function generateTagForAssetType( @@ -388,8 +433,12 @@ protected function generateTagForAssetType( ?string $content, ?string $file = null, ?string $integrity = null, - ?array $standaloneAssetSettings = null + ?array $standaloneAssetSettings = null, + ?ServerRequestInterface $request = null ): ?string { + if (null === $request) { + throw new \RuntimeException('Request must be provided for tag generation.'); + } /** @var TagBuilder $tagBuilder */ $tagBuilder = GeneralUtility::makeInstance(TagBuilder::class); if (null === $file && empty($content)) { @@ -400,14 +449,18 @@ protected function generateTagForAssetType( } if ($file !== null) { $file = PathUtility::getAbsoluteWebPath($file); - $file = $this->prefixPath($file); + $file = $this->prefixPath($file, $request); } - $settings = $this->getTypoScript(); + $settings = $this->getTypoScript($request); + $cspNonce = $this->consumeCspNonceForAsset($type, $file !== null, $standaloneAssetSettings, $request); switch ($type) { case 'js': $tagBuilder->setTagName('script'); $tagBuilder->forceClosingTag(true); $tagBuilder->addAttribute('type', 'text/javascript'); + if ($cspNonce !== null) { + $tagBuilder->addAttribute('nonce', $cspNonce); + } if (null === $file) { $tagBuilder->setContent((string) $content); } else { @@ -434,12 +487,18 @@ protected function generateTagForAssetType( $tagBuilder->setTagName('style'); $tagBuilder->forceClosingTag(true); $tagBuilder->addAttribute('type', 'text/css'); + if ($cspNonce !== null) { + $tagBuilder->addAttribute('nonce', $cspNonce); + } $tagBuilder->setContent((string) $content); } else { $tagBuilder->forceClosingTag(false); $tagBuilder->setTagName('link'); $tagBuilder->addAttribute('rel', 'stylesheet'); $tagBuilder->addAttribute('href', $file); + if ($cspNonce !== null) { + $tagBuilder->addAttribute('nonce', $cspNonce); + } } if (!empty($integrity)) { if (!empty($settings['prependPath'])) { @@ -464,13 +523,60 @@ protected function generateTagForAssetType( return $tagBuilder->render(); } + protected function consumeCspNonceForAsset( + string $type, + bool $fileBased, + ?array $standaloneAssetSettings, + ServerRequestInterface $request + ): ?string { + if (!in_array($type, ['css', 'js'], true)) { + return null; + } + if (!$this->resolveCspEnabledForAsset($fileBased, $standaloneAssetSettings)) { + return null; + } + $nonce = $request->getAttribute('nonce'); + if (!class_exists(ConsumableNonce::class) || !$nonce instanceof ConsumableNonce) { + return null; + } + $aspect = $this->resolveCspDirectiveAspect($type); + if ($fileBased && method_exists($nonce, 'consumeStatic')) { + return $nonce->consumeStatic($aspect); + } + if (!$fileBased && method_exists($nonce, 'consumeInline')) { + return $nonce->consumeInline($aspect); + } + return $nonce->consume(); + } + + protected function resolveCspEnabledForAsset(bool $fileBased, ?array $standaloneAssetSettings): bool + { + if (is_array($standaloneAssetSettings) && array_key_exists('csp', $standaloneAssetSettings)) { + return (bool) $standaloneAssetSettings['csp']; + } + return $fileBased; + } + + /** + * @return mixed Directive enum on TYPO3 versions which provide it, otherwise the directive name. + */ + protected function resolveCspDirectiveAspect(string $type) + { + if (enum_exists(Directive::class)) { + return $type === 'js' ? Directive::ScriptSrcElem : Directive::StyleSrcElem; + } + return $type === 'js' ? 'script-src-elem' : 'style-src-elem'; + } + /** * @param AssetInterface[] $assets * @return AssetInterface[] */ - protected function manipulateAssetsByTypoScriptSettings(array $assets): array - { - $settings = $this->getSettings(); + protected function manipulateAssetsByTypoScriptSettings( + array $assets, + ServerRequestInterface $request + ): array { + $settings = $this->getSettings($request); if (!(isset($settings['asset']) || isset($settings['assetGroup']))) { return $assets; } @@ -556,7 +662,7 @@ protected function sortAssetsByDependency(array $assets): array /** * @param AssetInterface|array $asset */ - protected function renderAssetAsFluidTemplate($asset): string + protected function renderAssetAsFluidTemplate($asset, ServerRequestInterface $request): string { $settings = $this->extractAssetSettings($asset); if (isset($settings['variables']) && is_array($settings['variables'])) { @@ -569,9 +675,11 @@ protected function renderAssetAsFluidTemplate($asset): string return ''; } $variables = GeneralUtility::removeDotsFromTS($variables); - /** @var StandaloneView $view */ - $view = GeneralUtility::makeInstance(StandaloneView::class); - $view->setTemplateSource($contents); + /** @var RenderingContextFactory $renderingContextFactory */ + $renderingContextFactory = GeneralUtility::makeInstance(RenderingContextFactory::class); + $renderingContext = $renderingContextFactory->create([], $request); + $renderingContext->getTemplatePaths()->setTemplateSource($contents); + $view = new TemplateView($renderingContext); $view->assignMultiple($variables); $content = $view->render(); return is_string($content) ? $content : ''; @@ -580,9 +688,9 @@ protected function renderAssetAsFluidTemplate($asset): string /** * Prefix a path according to "absRefPrefix" TS configuration. */ - protected function prefixPath(string $fileRelativePathAndFilename): string + protected function prefixPath(string $fileRelativePathAndFilename, ServerRequestInterface $request): string { - $settings = $this->getSettings(); + $settings = $this->getSettings($request); $prefixPath = $settings['prependPath'] ?? ''; if (!empty($prefixPath)) { $fileRelativePathAndFilename = $prefixPath . $fileRelativePathAndFilename; @@ -618,7 +726,7 @@ protected function copyReferencedFilesAndReplacePaths( $matches = []; $replacements = []; $wrap = explode('|', $wrap); - preg_match_all($regex, $contents, $matches); + preg_match_all($regex, (string) $contents, $matches); $logger = null; if (class_exists(LogManager::class)) { /** @var LogManager $logManager */ @@ -711,7 +819,7 @@ protected function buildAsset($asset): ?string /** * @param AssetInterface|array $asset */ - protected function extractAssetContent($asset): ?string + protected function extractAssetContent($asset, ServerRequestInterface $request): ?string { $assetSettings = $this->extractAssetSettings($asset); $fileRelativePathAndFilename = $assetSettings['path'] ?? null; @@ -723,7 +831,7 @@ protected function extractAssetContent($asset): ?string throw new \RuntimeException('Asset "' . $absolutePathAndFilename . '" does not exist.'); } if ($isFluidTemplate) { - $content = $this->renderAssetAsFluidTemplate($asset); + $content = $this->renderAssetAsFluidTemplate($asset, $request); } else { $content = $this->buildAsset($asset); } @@ -739,10 +847,24 @@ protected function extractAssetContent($asset): ?string public function clearCacheCommand(array $parameters): void { - if (static::$cacheCleared) { + if ('all' !== ($parameters['cacheCmd'] ?? '')) { return; } - if ('all' !== ($parameters['cacheCmd'] ?? '')) { + $this->clearAssetCache(); + } + + public function clearCacheByEvent(CacheFlushEvent $event): void + { + if (!$event->hasGroup('all')) { + return; + } + $this->clearAssetCache(); + } + + protected function clearAssetCache(): void + { + static::$settingsCache = []; + if (static::$cacheCleared) { return; } $assetCacheFiles = glob(GeneralUtility::getFileAbsFileName($this->getTempPath() . 'vhs-assets-*')); @@ -761,6 +883,20 @@ public function clearCacheCommand(array $parameters): void static::$cacheCleared = true; } + protected function buildSettingsCacheKey(ServerRequestInterface $request): string + { + $site = $request->getAttribute('site'); + $language = $request->getAttribute('language'); + return sha1(json_encode([ + 'request' => spl_object_id($request), + 'pageUid' => $this->readPageUidFromContext($request), + 'site' => is_object($site) && method_exists($site, 'getIdentifier') ? $site->getIdentifier() : null, + 'language' => is_object($language) && method_exists($language, 'getLanguageId') + ? $language->getLanguageId() + : null, + ], JSON_THROW_ON_ERROR)); + } + protected function writeFile(string $file, string $contents): void { ///** @var Dispatcher $signalSlotDispatcher */ @@ -795,9 +931,9 @@ protected function mergeArrays(array $array1, array $array2): array return $array1; } - protected function getFileIntegrity(string $file): ?string + protected function getFileIntegrity(string $file, ServerRequestInterface $request): ?string { - $typoScript = $this->getTypoScript(); + $typoScript = $this->getTypoScript($request); if (isset($typoScript['assets']['tagsAddSubresourceIntegrity'])) { // Note: 3 predefined hashing strategies (the ones suggestes in the rfc sheet) if (0 < $typoScript['assets']['tagsAddSubresourceIntegrity'] @@ -818,9 +954,9 @@ protected function getFileIntegrity(string $file): ?string ); if (!file_exists($integrityFile) - || 0 === filemtime($integrityFile) - || isset($GLOBALS['BE_USER']) - || $this->readCacheDisabledInstructionFromContext() + || 0 === filemtime($integrityFile) + || ApplicationType::fromRequest($request)->isBackend() + || $this->readCacheDisabledInstructionFromContext($request) ) { if (extension_loaded('hash') && function_exists('hash_file')) { $integrity = base64_encode((string) hash_file($integrityMethod, $file, true)); @@ -854,11 +990,8 @@ protected function resolveAbsolutePathForFile(string $filename): string return GeneralUtility::getFileAbsFileName($filename); } - protected function readPageUidFromContext(): int + protected function readPageUidFromContext(ServerRequestInterface $serverRequest): int { - /** @var ServerRequestInterface $serverRequest */ - $serverRequest = $GLOBALS['TYPO3_REQUEST']; - /** @var RouteResultInterface $pageArguments */ $pageArguments = $serverRequest->getAttribute('routing'); if (!$pageArguments instanceof PageArguments) { @@ -867,25 +1000,16 @@ protected function readPageUidFromContext(): int return $pageArguments->getPageId(); } - protected function readCacheDisabledInstructionFromContext(): bool + protected function readCacheDisabledInstructionFromContext(ServerRequestInterface $serverRequest): bool { $hasDisabledInstructionInRequest = false; - /** @var ServerRequestInterface $serverRequest */ - $serverRequest = $GLOBALS['TYPO3_REQUEST']; $instruction = $serverRequest->getAttribute('frontend.cache.instruction'); if ($instruction instanceof CacheInstruction) { $hasDisabledInstructionInRequest = !$instruction->isCachingAllowed(); } - /** @var TypoScriptFrontendController $typoScriptFrontendController */ - $typoScriptFrontendController = $GLOBALS['TSFE']; - return $hasDisabledInstructionInRequest - || (property_exists($typoScriptFrontendController, 'no_cache') && $typoScriptFrontendController->no_cache) - || ( - is_array($typoScriptFrontendController->page) - && ($typoScriptFrontendController->page['no_cache'] ?? false) - ); + || (bool) ($serverRequest->getAttribute('frontend.cache.no_cache') ?? false); } } diff --git a/Classes/Service/PageService.php b/Classes/Service/PageService.php index 774f07a99..21cfaa66a 100644 --- a/Classes/Service/PageService.php +++ b/Classes/Service/PageService.php @@ -12,12 +12,16 @@ use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Context\LanguageAspect; +use TYPO3\CMS\Core\Http\NormalizedParams; +use TYPO3\CMS\Core\Routing\PageArguments; use TYPO3\CMS\Core\Domain\Repository\PageRepository; use TYPO3\CMS\Core\SingletonInterface; use TYPO3\CMS\Core\Type\Bitmask\PageTranslationVisibility; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\RootlineUtility; -use TYPO3\CMS\Core\Utility\VersionNumberUtility; +use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication; +use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; +use TYPO3\CMS\Frontend\Page\PageInformation; /** * Page Service @@ -33,6 +37,13 @@ class PageService implements SingletonInterface protected static array $cachedPages = []; protected static array $cachedMenus = []; + public const SHORTCUT_MODE_RANDOM_SUBPAGE = 2; + protected ?ServerRequestInterface $request = null; + + public function setRequest(?ServerRequestInterface $request): void + { + $this->request = $request; + } public function getMenu( int $pageUid, @@ -43,14 +54,8 @@ public function getMenu( ): array { $pageRepository = $this->getPageRepository(); $pageConstraints = $this->getPageConstraints($excludePages, $includeNotInMenu, $includeMenuSeparator); - $cacheKey = md5($pageUid . $pageConstraints . (int) $disableGroupAccessCheck); + $cacheKey = $this->buildContextualCacheKey([$pageUid, $pageConstraints, $disableGroupAccessCheck]); if (!isset(static::$cachedMenus[$cacheKey])) { - if ($disableGroupAccessCheck - && version_compare(VersionNumberUtility::getCurrentTypo3Version(), '12.1', '<=') - ) { - $pageRepository->where_groupAccess = ''; - } - static::$cachedMenus[$cacheKey] = array_filter( $pageRepository->getMenu($pageUid, '*', 'sorting', $pageConstraints, true, $disableGroupAccessCheck), function ($page) use ($includeNotInMenu) { @@ -65,7 +70,7 @@ function ($page) use ($includeNotInMenu) { public function getPage(int $pageUid, bool $disableGroupAccessCheck = false): array { - $cacheKey = md5($pageUid . (int) $disableGroupAccessCheck); + $cacheKey = $this->buildContextualCacheKey([$pageUid, $disableGroupAccessCheck]); if (!isset(static::$cachedPages[$cacheKey])) { static::$cachedPages[$cacheKey] = $this->getPageRepository()->getPage($pageUid, $disableGroupAccessCheck); } @@ -78,11 +83,7 @@ public function getRootLine( bool $reverse = false ): array { if (null === $pageUid) { - if (isset($GLOBALS['TSFE'])) { - $pageUid = $GLOBALS['TSFE']->id; - } else { - $pageUid = $this->getRequest()->getQueryParams()['id'] ?? null; - } + $pageUid = $this->getCurrentPageUid(); } if (!$pageUid) { @@ -110,10 +111,6 @@ protected function getPageConstraints( PageRepository::DOKTYPE_SYSFOLDER ]; - if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '12.4', '<=')) { - $types[] = PageRepository::DOKTYPE_RECYCLER; - } - $constraints[] = 'doktype NOT IN (' . implode(',', $types) . ')'; if ($includeNotInMenu === false) { @@ -140,19 +137,15 @@ public function hidePageForLanguageUid($page = null, int $languageUid = -1, bool $pageUid = $page['uid']; $pageRecord = $page; } else { - $pageUid = (0 === (int) $page) ? $GLOBALS['TSFE']->id : (int) $page; + $pageUid = (0 === (int) $page) ? (int) ($this->getCurrentPageUid() ?? 0) : (int) $page; $pageRecord = $this->getPage($pageUid); } if (-1 === $languageUid) { - if (class_exists(LanguageAspect::class)) { - /** @var Context $context */ - $context = GeneralUtility::makeInstance(Context::class); - /** @var LanguageAspect $languageAspect */ - $languageAspect = $context->getAspect('language'); - $languageUid = $languageAspect->getId(); - } else { - $languageUid = $GLOBALS['TSFE']->sys_language_uid; - } + /** @var Context $context */ + $context = GeneralUtility::makeInstance(Context::class); + /** @var LanguageAspect $languageAspect */ + $languageAspect = $context->getAspect('language'); + $languageUid = $languageAspect->getId(); } $l18nCfg = $pageRecord['l18n_cfg'] ?? 0; @@ -194,7 +187,7 @@ public function getItemLink(array $page, bool $forceAbsoluteUrl = false): string if (GeneralUtility::validEmail($redirectTo)) { $redirectTo = 'mailto:' . $redirectTo; } elseif ($redirectTo[0] !== '/') { - $redirectTo = GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . $redirectTo; + $redirectTo = $this->readSiteUrlFromRequest() . $redirectTo; } } $parameter = $redirectTo; @@ -207,7 +200,7 @@ public function getItemLink(array $page, bool $forceAbsoluteUrl = false): string 'forceAbsoluteUrl' => $forceAbsoluteUrl, ]; - return $GLOBALS['TSFE']->cObj->typoLink('', $config); + return $this->getContentObjectRenderer()->typoLink('', $config); } public function isAccessProtected(array $page): bool @@ -226,8 +219,10 @@ public function isAccessGranted(array $page): bool $hide = (in_array(-1, $groups)); $show = (in_array(-2, $groups)); - $userIsLoggedIn = (is_array($GLOBALS['TSFE']->fe_user->user)); - $userGroups = $GLOBALS['TSFE']->fe_user->groupData['uid']; + $frontendUser = $this->getFrontendUserAuthentication(); + $user = $frontendUser?->user; + $userGroups = (array) ($frontendUser?->groupData['uid'] ?? []); + $userIsLoggedIn = (is_array($user)); $userIsInGrantedGroups = (0 < count(array_intersect($userGroups, $groups))); return (!$userIsLoggedIn && $hide) || ($userIsLoggedIn && $show) || ($userIsLoggedIn && $userIsInGrantedGroups); @@ -235,7 +230,7 @@ public function isAccessGranted(array $page): bool public function isCurrent(int $pageUid): bool { - return ($pageUid === (int) $GLOBALS['TSFE']->id); + return $pageUid === $this->getCurrentPageUid(); } public function isActive(int $pageUid): bool @@ -286,7 +281,7 @@ public function getShortcutTargetPage(array $page): ?array case PageRepository::SHORTCUT_MODE_PARENT_PAGE: $targetPage = $this->getPage($page['pid']); break; - case PageRepository::SHORTCUT_MODE_RANDOM_SUBPAGE: + case self::SHORTCUT_MODE_RANDOM_SUBPAGE: $menu = $this->getMenu($page['shortcut'] > 0 ? $page['shortcut'] : $originalPageUid); $targetPage = (0 < count($menu)) ? $menu[array_rand($menu)] : $page; break; @@ -307,7 +302,7 @@ public function getShortcutTargetPage(array $page): ?array */ public function getPageRepository() { - return clone ($GLOBALS['TSFE']->sys_page ?? $this->getPageRepositoryForBackendContext()); + return clone $this->getPageRepositoryForBackendContext(); } /** @@ -324,8 +319,120 @@ protected function getPageRepositoryForBackendContext() return $instance; } - private function getRequest(): ServerRequestInterface + protected function getRequest(): ?ServerRequestInterface + { + if ($this->request instanceof ServerRequestInterface) { + return $this->request; + } + $request = $GLOBALS['TYPO3_REQUEST'] ?? null; + return $request instanceof ServerRequestInterface ? $request : null; + } + + protected function buildContextualCacheKey(array $parts): string + { + $request = $this->getRequest(); + $contextParts = [ + 'request' => null, + 'site' => null, + 'language' => null, + 'workspace' => null, + 'frontendUser' => null, + ]; + if ($request instanceof ServerRequestInterface) { + $site = $request->getAttribute('site'); + $frontendUser = $request->getAttribute('frontend.user'); + $contextParts['request'] = spl_object_id($request); + $contextParts['site'] = is_object($site) && method_exists($site, 'getIdentifier') + ? $site->getIdentifier() + : null; + if ($frontendUser instanceof FrontendUserAuthentication) { + $contextParts['frontendUser'] = [ + 'user' => $frontendUser->user['uid'] ?? null, + 'groups' => $frontendUser->groupData['uid'] ?? [], + ]; + } + } + + /** @var Context $context */ + $context = GeneralUtility::makeInstance(Context::class); + try { + /** @var LanguageAspect $languageAspect */ + $languageAspect = $context->getAspect('language'); + $contextParts['language'] = $languageAspect->getId(); + } catch (\Throwable) { + } + try { + $workspaceAspect = $context->getAspect('workspace'); + $contextParts['workspace'] = method_exists($workspaceAspect, 'getId') ? $workspaceAspect->getId() : null; + } catch (\Throwable) { + } + + return sha1(json_encode([$parts, $contextParts], JSON_THROW_ON_ERROR)); + } + + protected function getPageInformation(): ?PageInformation + { + $pageInformation = $this->getRequest()?->getAttribute('frontend.page.information'); + return $pageInformation instanceof PageInformation ? $pageInformation : null; + } + + protected function getCurrentPageUid(): ?int + { + $pageInformation = $this->getPageInformation(); + if ($pageInformation instanceof PageInformation) { + return $pageInformation->getId(); + } + $routing = $this->getRequest()?->getAttribute('routing'); + if ($routing instanceof PageArguments) { + return $routing->getPageId(); + } + return null; + } + + protected function getFrontendUserAuthentication(): ?FrontendUserAuthentication + { + $frontendUser = $this->getRequest()?->getAttribute('frontend.user'); + return $frontendUser instanceof FrontendUserAuthentication ? $frontendUser : null; + } + + protected function getContentObjectRenderer(): ContentObjectRenderer + { + $request = $this->getRequest(); + if (!$request instanceof ServerRequestInterface) { + throw new \UnexpectedValueException('PageService::getItemLink requires a frontend request', 1774448249); + } + /** @var ContentObjectRenderer $contentObjectRenderer */ + $contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class); + $contentObjectRenderer->setRequest($request); + return $contentObjectRenderer; + } + + protected function readSiteUrlFromRequest(): string + { + $request = $this->getRequest(); + if (!$request instanceof ServerRequestInterface) { + throw new \UnexpectedValueException( + 'PageService::readSiteUrlFromRequest requires a frontend request', + 1774448250 + ); + } + $normalizedParams = $request->getAttribute('normalizedParams'); + if ($normalizedParams instanceof NormalizedParams) { + return $normalizedParams->getSiteUrl(); + } + + $uri = $request->getUri(); + $path = (string) $uri->getPath(); + if ('' === $path || '/' === $path) { + $path = '/'; + } + $path = rtrim(dirname($path), '/'); + return $uri->withPath($path . '/')->withQuery('')->withFragment('')->__toString(); + } + + public static function resetCaches(): void { - return $GLOBALS['TYPO3_REQUEST']; + static::$cachedPages = []; + static::$cachedMenus = []; } } diff --git a/Classes/Utility/ContentObjectFetcher.php b/Classes/Utility/ContentObjectFetcher.php index 6a08cbf94..a28a626c1 100644 --- a/Classes/Utility/ContentObjectFetcher.php +++ b/Classes/Utility/ContentObjectFetcher.php @@ -11,18 +11,20 @@ use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; -use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; class ContentObjectFetcher { - public static function resolve(?ConfigurationManagerInterface $configurationManager = null): ?ContentObjectRenderer - { + public static function resolve( + ?ConfigurationManagerInterface $configurationManager = null, + ?ServerRequestInterface $request = null + ): ?ContentObjectRenderer { $contentObject = null; - $request = ($configurationManager !== null && method_exists($configurationManager, 'getRequest') + $request ??= $configurationManager !== null && method_exists($configurationManager, 'getRequest') ? $configurationManager->getRequest() - : ($GLOBALS['TYPO3_REQUEST'] ?? null)) ?? $GLOBALS['TYPO3_REQUEST'] ?? null; + : null; + $request ??= $GLOBALS['TYPO3_REQUEST'] ?? null; - if ($request) { + if ($request instanceof ServerRequestInterface) { $contentObject = static::resolveFromRequest($request); } @@ -41,8 +43,13 @@ protected static function resolveFromRequest(ServerRequestInterface $request): ? if (($cObject = $request->getAttribute('currentContentObject')) instanceof ContentObjectRenderer) { return $cObject; } - /** @var TypoScriptFrontendController $controller */ $controller = $request->getAttribute('frontend.controller'); - return $controller instanceof TypoScriptFrontendController ? $controller->cObj : null; + if (is_object($controller) + && property_exists($controller, 'cObj') + && $controller->cObj instanceof ContentObjectRenderer + ) { + return $controller->cObj; + } + return null; } } diff --git a/Classes/Utility/ContextUtility.php b/Classes/Utility/ContextUtility.php index eb4a3bc20..7d731554e 100644 --- a/Classes/Utility/ContextUtility.php +++ b/Classes/Utility/ContextUtility.php @@ -8,17 +8,26 @@ * LICENSE.md file that was distributed with this source code. */ +use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Http\ApplicationType; class ContextUtility { - public static function isFrontend(): bool + public static function isFrontend(?ServerRequestInterface $request = null): bool { - return ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend(); + $request ??= $GLOBALS['TYPO3_REQUEST'] ?? null; + if (!$request instanceof ServerRequestInterface) { + return false; + } + return ApplicationType::fromRequest($request)->isFrontend(); } - public static function isBackend(): bool + public static function isBackend(?ServerRequestInterface $request = null): bool { - return ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend(); + $request ??= $GLOBALS['TYPO3_REQUEST'] ?? null; + if (!$request instanceof ServerRequestInterface) { + return false; + } + return ApplicationType::fromRequest($request)->isBackend(); } } diff --git a/Classes/Utility/ErrorUtility.php b/Classes/Utility/ErrorUtility.php index 4a6e6aea3..89804df69 100644 --- a/Classes/Utility/ErrorUtility.php +++ b/Classes/Utility/ErrorUtility.php @@ -21,7 +21,7 @@ public static function throwViewHelperException( ?string $message = null, ?int $code = null, ?\Throwable $previous = null - ): void { + ): never { throw new Exception((string) $message, (int) $code, $previous); } } diff --git a/Classes/Utility/FrontendSimulationUtility.php b/Classes/Utility/FrontendSimulationUtility.php index db791dbbb..7afc044f9 100644 --- a/Classes/Utility/FrontendSimulationUtility.php +++ b/Classes/Utility/FrontendSimulationUtility.php @@ -8,14 +8,14 @@ * LICENSE.md file that was distributed with this source code. */ -use FluidTYPO3\Vhs\Proxy\SiteFinderProxy; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Domain\Repository\PageRepository; -use TYPO3\CMS\Core\Routing\PageArguments; -use TYPO3\CMS\Core\Site\Entity\Site; +use TYPO3\CMS\Core\Http\ApplicationType; +use TYPO3\CMS\Core\Imaging\ImageResource; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication; -use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; +use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; +use Psr\Http\Message\ServerRequestInterface; /** * Frontend Simulation Utility @@ -25,59 +25,150 @@ class FrontendSimulationUtility { /** - * Sets the global variable $GLOBALS['TSFE'] in Backend mode. + * @var ServerRequestInterface[] */ - public static function simulateFrontendEnvironment(): ?TypoScriptFrontendController + protected static array $requestBackupStack = []; + + /** + * Creates a backend-safe frontend-like request context and stores the + * previous frontend state so it can be restored with resetFrontendEnvironment(). + * + * @return null Kept for compatibility with older callers that pass the return value to resetFrontendEnvironment(). + */ + public static function simulateFrontendEnvironment(): null { - if (!ContextUtility::isBackend()) { + $request = $GLOBALS['TYPO3_REQUEST'] ?? null; + if (!$request instanceof ServerRequestInterface || !ApplicationType::fromRequest($request)->isBackend()) { return null; } - $tsfeBackup = $GLOBALS['TSFE'] ?? null; - $GLOBALS['TYPO3_CONF_VARS']['FE']['cookieName'] = $GLOBALS['TYPO3_CONF_VARS']['FE']['cookieName'] ?? 'fe_user'; + $requestBackup = $request; + + $contentObjectRenderer = self::getContentObjectRenderer(); + $routing = $request->getAttribute('routing'); + $frontendPageId = 0; + if (is_object($routing) && method_exists($routing, 'getPageId')) { + $frontendPageId = (int) $routing->getPageId(); + } + $frontendController = new \stdClass(); + $frontendController->id = $frontendPageId; + $frontendController->cObj = $contentObjectRenderer; + $frontendController->fe_user = GeneralUtility::makeInstance(FrontendUserAuthentication::class); + $frontendController->sys_page = self::getPageRepository(); + $frontendController->sys_language_uid = 0; + $frontendController->sys_language_content = 0; + $frontendController->sys_language_contentOL = 0; + $frontendController->absRefPrefix = '/'; + $frontendController->lastImageInfo = null; + $frontendController->imagesOnPage = []; + $frontendController->tmpl = (object) [ + 'setup' => [ + 'plugin.' => [ + 'tx_vhs.' => [ + 'settings.' => [] + ] + ] + ] + ]; + $frontendController->currentRecord = ''; - /** @var SiteFinderProxy $siteFinder */ - $siteFinder = GeneralUtility::makeInstance(SiteFinderProxy::class); - $sites = $siteFinder->getAllSites(); - /** @var Context $context */ $context = GeneralUtility::makeInstance(Context::class); - /** @var Site $site */ - $site = reset($sites); - $siteLanguage = $site->getDefaultLanguage(); - /** @var PageArguments $pageArguments */ - $pageArguments = GeneralUtility::makeInstance( - PageArguments::class, - 0, - (string) PageRepository::DOKTYPE_DEFAULT, - [] - ); - /** @var FrontendUserAuthentication $frontendUser */ - $frontendUser = GeneralUtility::makeInstance(FrontendUserAuthentication::class); - - $controller = GeneralUtility::makeInstance( - TypoScriptFrontendController::class, - $context, - $site, - $siteLanguage, - $pageArguments, - $frontendUser - ); - - $GLOBALS['TSFE'] = $controller; - - return $tsfeBackup; + $languageAspect = $context->getAspect('language'); + if (method_exists($languageAspect, 'getId')) { + $frontendController->sys_language_uid = (int) $languageAspect->getId(); + } + + if (method_exists($contentObjectRenderer, 'setRequest')) { + $contentObjectRenderer->setRequest($request); + } + + $request = $request->withAttribute('currentContentObject', $contentObjectRenderer); + $request = $request->withAttribute('frontend.controller', $frontendController); + + self::$requestBackupStack[] = $requestBackup; + + $GLOBALS['TYPO3_REQUEST'] = $request; + + return null; } /** - * Resets $GLOBALS['TSFE'] if it was previously changed by simulateFrontendEnvironment() - * - * @see simulateFrontendEnvironment() + * Restores the previous frontend context created by simulateFrontendEnvironment(). */ - public static function resetFrontendEnvironment(?TypoScriptFrontendController $tsfeBackup): void + public static function resetFrontendEnvironment(mixed $tsfeBackup = null): void { - if (!ContextUtility::isBackend()) { + $request = $GLOBALS['TYPO3_REQUEST'] ?? null; + $isBackendContext = $request instanceof ServerRequestInterface + && ApplicationType::fromRequest($request)->isBackend(); + + $hasRequestBackup = !empty(self::$requestBackupStack); + $requestBackup = null; + if ($hasRequestBackup) { + $requestBackup = array_pop(self::$requestBackupStack); + } + + if (!$isBackendContext || !$requestBackup instanceof ServerRequestInterface) { return; } - $GLOBALS['TSFE'] = $tsfeBackup; + + $GLOBALS['TYPO3_REQUEST'] = $requestBackup; + } + + /** + * @return ContentObjectRenderer + */ + protected static function getContentObjectRenderer(): ContentObjectRenderer + { + try { + /** @var ContentObjectRenderer $contentObjectRenderer */ + $contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class); + return $contentObjectRenderer; + } catch (\Throwable) { + return new class () extends ContentObjectRenderer { + public function __construct() + { + } + + /** + * TYPO3 13.4 compatibility: keep $fileArray untyped because the + * parent ContentObjectRenderer method accepts mixed there. + */ + public function getImgResource($file, $fileArray): ?ImageResource + { + return null; + } + }; + } + } + + /** + * @return object + */ + protected static function getPageRepository(): object + { + try { + return GeneralUtility::makeInstance(PageRepository::class); + } catch (\Throwable) { + return new class { + public function getRecordOverlay( + string $table, + array $record, + int $languageUid, + int $languageContentOL = 0 + ): ?array { + return null; + } + + public function getPage(int $pageUid): ?array + { + return null; + } + + public function __call(string $name, array $arguments): mixed + { + return null; + } + }; + } } } diff --git a/Classes/Utility/RequestResolver.php b/Classes/Utility/RequestResolver.php index f5f90d19b..45aed3c1c 100644 --- a/Classes/Utility/RequestResolver.php +++ b/Classes/Utility/RequestResolver.php @@ -10,28 +10,63 @@ use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters; +use TYPO3\CMS\Extbase\Mvc\Request; use TYPO3\CMS\Extbase\Mvc\RequestInterface; use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; class RequestResolver { - /** - * @return RequestInterface|ServerRequestInterface - */ - public static function resolveRequestFromRenderingContext(RenderingContextInterface $renderingContext) - { - $request = null; - if (method_exists($renderingContext, 'getRequest')) { - $request = $renderingContext->getRequest(); - } elseif (method_exists($renderingContext, 'getControllerContext')) { - $request = $renderingContext->getControllerContext()->getRequest(); - } - if (!$request) { + public static function resolveRequestFromRenderingContext( + ?RenderingContextInterface $renderingContext, + bool $allowGlobalFallback = true + ): ServerRequestInterface { + $request = self::tryResolveRequestFromRenderingContext($renderingContext, $allowGlobalFallback); + if (!$request instanceof ServerRequestInterface) { throw new \UnexpectedValueException('Unable to resolve request from RenderingContext', 1673191812); } return $request; } + public static function tryResolveRequestFromRenderingContext( + ?RenderingContextInterface $renderingContext, + bool $allowGlobalFallback = true + ): ?ServerRequestInterface { + $request = null; + if ($renderingContext instanceof RenderingContextInterface) { + if (method_exists($renderingContext, 'getRequest')) { + $request = $renderingContext->getRequest(); + } elseif (method_exists($renderingContext, 'hasAttribute') + && $renderingContext->hasAttribute(ServerRequestInterface::class) + ) { + $request = $renderingContext->getAttribute(ServerRequestInterface::class); + } elseif (method_exists($renderingContext, 'getControllerContext')) { + $request = $renderingContext->getControllerContext()->getRequest(); + } + } + + if ($allowGlobalFallback && null === $request && isset($GLOBALS['TYPO3_REQUEST'])) { + $request = $GLOBALS['TYPO3_REQUEST']; + } + + return $request instanceof ServerRequestInterface ? $request : null; + } + + public static function resolveExtbaseRequestFromRenderingContext( + ?RenderingContextInterface $renderingContext + ): ?RequestInterface { + $request = self::resolveRequestFromRenderingContext($renderingContext); + if ($request instanceof RequestInterface) { + return $request; + } + if ($request instanceof ServerRequestInterface + && $request->getAttribute('extbase') instanceof ExtbaseRequestParameters + ) { + return new Request($request); + } + + return null; + } + public static function resolveControllerNameFromRenderingContext(RenderingContextInterface $context): ?string { return self::resolveControllerNameFromRequest(self::resolveRequestFromRenderingContext($context)); @@ -40,7 +75,7 @@ public static function resolveControllerNameFromRenderingContext(RenderingContex /** * @param RequestInterface|ServerRequestInterface $request */ - public static function resolveControllerNameFromRequest($request): ?string + public static function resolveControllerNameFromRequest(RequestInterface|ServerRequestInterface $request): ?string { return self::proxyCall($request, 'getControllerName'); } @@ -53,8 +88,9 @@ public static function resolveControllerActionNameFromRenderingContext(Rendering /** * @param RequestInterface|ServerRequestInterface $request */ - public static function resolveControllerActionNameFromRequest($request): ?string - { + public static function resolveControllerActionNameFromRequest( + RequestInterface|ServerRequestInterface $request + ): ?string { return self::proxyCall($request, 'getControllerActionName'); } @@ -67,8 +103,9 @@ public static function resolveControllerExtensionNameFromRenderingContext( /** * @param RequestInterface|ServerRequestInterface $request */ - public static function resolveControllerExtensionNameFromRequest($request): ?string - { + public static function resolveControllerExtensionNameFromRequest( + RequestInterface|ServerRequestInterface $request + ): ?string { return self::proxyCall($request, 'getControllerExtensionName'); } @@ -80,8 +117,9 @@ public static function resolveControllerObjectNameFromRenderingContext(Rendering /** * @param RequestInterface|ServerRequestInterface $request */ - public static function resolveControllerObjectNameFromRequest($request): ?string - { + public static function resolveControllerObjectNameFromRequest( + RequestInterface|ServerRequestInterface $request + ): ?string { return self::proxyCall($request, 'getControllerObjectName'); } @@ -93,7 +131,7 @@ public static function resolvePluginNameFromRenderingContext(RenderingContextInt /** * @param RequestInterface|ServerRequestInterface $request */ - public static function resolvePluginNameFromRequest($request): ?string + public static function resolvePluginNameFromRequest(RequestInterface|ServerRequestInterface $request): ?string { return self::proxyCall($request, 'getPluginName'); } @@ -106,7 +144,7 @@ public static function resolveFormatFromRenderingContext(RenderingContextInterfa /** * @param RequestInterface|ServerRequestInterface $request */ - public static function resolveFormatFromRequest($request): ?string + public static function resolveFormatFromRequest(RequestInterface|ServerRequestInterface $request): ?string { return self::proxyCall($request, 'getFormat'); } @@ -114,12 +152,13 @@ public static function resolveFormatFromRequest($request): ?string /** * @param RequestInterface|ServerRequestInterface $request */ - private static function proxyCall($request, string $method): ?string + private static function proxyCall(RequestInterface|ServerRequestInterface $request, string $method): ?string { if ($request instanceof RequestInterface) { return $request->{$method}(); } if (($parameters = $request->getAttribute('extbase')) instanceof ExtbaseRequestParameters) { + /** @var ExtbaseRequestParameters $parameters */ return $parameters->{$method}(); } return null; diff --git a/Classes/View/UncacheTemplateView.php b/Classes/View/UncacheTemplateView.php index ca051ca8a..fb3807d92 100644 --- a/Classes/View/UncacheTemplateView.php +++ b/Classes/View/UncacheTemplateView.php @@ -9,16 +9,16 @@ */ use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Core\Utility\VersionNumberUtility; +use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; use TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext; use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters; use TYPO3\CMS\Extbase\Mvc\Request; use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder; -use TYPO3\CMS\Fluid\Compatibility\TemplateParserBuilder; +use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Fluid\Core\Rendering\RenderingContext; use TYPO3\CMS\Fluid\Core\Rendering\RenderingContextFactory; -use TYPO3\CMS\Fluid\View\TemplateView; use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; +use TYPO3Fluid\Fluid\View\TemplateView; class UncacheTemplateView extends TemplateView { @@ -37,18 +37,20 @@ public function callUserFunction(string $postUserFunc, array $conf): string } if (class_exists(RenderingContextFactory::class)) { - $renderingContext = $this->createRenderingContextWithRenderingContextFactory(); - if (method_exists($renderingContext, 'setRequest')) { - $request = $parameters instanceof ExtbaseRequestParameters - ? $GLOBALS['TYPO3_REQUEST']->withAttribute('extbase', $parameters) - : $GLOBALS['TYPO3_REQUEST']; - $renderingContext->setRequest( - // TYPO3 v11.x needs the ServerRequest wrapped in an Extbase Request. - version_compare(VersionNumberUtility::getCurrentTypo3Version(), '12.0', '<') - ? new Request($request) - : $request - ); + $request = null; + if (property_exists($this, 'renderingContext') + && $this->renderingContext instanceof RenderingContextInterface + && method_exists($this->renderingContext, 'getRequest') + ) { + $request = $this->renderingContext->getRequest(); } + if (!$request instanceof ServerRequestInterface && isset($GLOBALS['TYPO3_REQUEST'])) { + $request = $GLOBALS['TYPO3_REQUEST']; + } + if ($parameters instanceof ExtbaseRequestParameters && $request instanceof ServerRequestInterface) { + $request = $request->withAttribute('extbase', $parameters); + } + $renderingContext = $this->createRenderingContextWithRenderingContextFactory($request); } else { /** @var ControllerContext $controllerContext */ $controllerContext = GeneralUtility::makeInstance(ControllerContext::class); @@ -96,7 +98,9 @@ public function callUserFunction(string $postUserFunc, array $conf): string $renderingContext->getTemplatePaths()->setPartialRootPaths($conf['partialRootPaths']); } elseif ($extensionName) { $extensionKey = GeneralUtility::camelCaseToLowerCaseUnderscored($extensionName); - $renderingContext->getTemplatePaths()->fillDefaultsByPackageName($extensionKey); + $renderingContext->getTemplatePaths()->setPartialRootPaths([ + ExtensionManagementUtility::extPath($extensionKey, 'Resources/Private/Partials/'), + ]); } return $this->renderPartialUncached($renderingContext, $partial, $section, $arguments); } @@ -112,24 +116,24 @@ protected function renderPartialUncached( ?string $section = null, array $arguments = [] ): string { - $this->renderingStack[] = [ - 'type' => static::RENDERING_TEMPLATE, - 'parsedTemplate' => $this->getCurrentParsedTemplate(), - 'renderingContext' => $renderingContext, - ]; - /** @var string $rendered */ $rendered = $this->renderPartial($partial, $section, $arguments); - array_pop($this->renderingStack); - return $rendered; + if ($rendered === null) { + return ''; + } + if (is_scalar($rendered) || $rendered instanceof \Stringable) { + return (string) $rendered; + } + throw new \UnexpectedValueException('Rendered uncached partial must be string-compatible', 1774448258); } /** * @codeCoverageIgnore */ - protected function createRenderingContextWithRenderingContextFactory(): RenderingContextInterface - { + protected function createRenderingContextWithRenderingContextFactory( + ?ServerRequestInterface $request = null + ): RenderingContextInterface { /** @var RenderingContextFactory $renderingContextFactory */ $renderingContextFactory = GeneralUtility::makeInstance(RenderingContextFactory::class); - return $renderingContextFactory->create(); + return $renderingContextFactory->create([], $request); } } diff --git a/Classes/ViewHelpers/Asset/AbstractAssetViewHelper.php b/Classes/ViewHelpers/Asset/AbstractAssetViewHelper.php index 8ae594d90..6c606f21e 100644 --- a/Classes/ViewHelpers/Asset/AbstractAssetViewHelper.php +++ b/Classes/ViewHelpers/Asset/AbstractAssetViewHelper.php @@ -20,6 +20,11 @@ /** * Base class for ViewHelpers capable of registering assets * which will be included when rendering the page. + * New installations should prefer TYPO3's native `f:asset.*` + * ViewHelpers for regular JavaScript, CSS and JavaScript module + * registration. Use VHS assets when you need VHS-specific behavior + * such as merging, CSS URL rewriting, named chunks or marker-based + * movable/standalone placement. * * Note: building of all Assets takes place in the class * FluidTYPO3\Vhs\Service\AssetService with two reasons: @@ -186,28 +191,34 @@ public function initializeArguments(): void false, false ); + $this->registerArgument( + 'csp', + 'boolean', + 'Controls TYPO3 Content Security Policy handling. Defaults to TYPO3 native asset behavior: TRUE for ' . + 'file-based assets and FALSE for inline assets. Prefer TYPO3 native f:asset.* ViewHelpers for new ' . + 'installations unless VHS-specific asset processing is required.', + false, + null + ); } /** * @return string */ - public function __toString() + public function __toString(): string { return (string) $this->build(); } - /** - * Render method - * - * @return void - */ - public function render() + public function render(): mixed { if (!isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['vhs']['setup']['disableAssetHandling']) || !$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['vhs']['setup']['disableAssetHandling'] ) { $this->finalize(); } + + return null; } /** diff --git a/Classes/ViewHelpers/Asset/AssetInterface.php b/Classes/ViewHelpers/Asset/AssetInterface.php index b282d1bc5..740ba34ea 100644 --- a/Classes/ViewHelpers/Asset/AssetInterface.php +++ b/Classes/ViewHelpers/Asset/AssetInterface.php @@ -19,7 +19,7 @@ interface AssetInterface * * @return void */ - public function render(); + public function render(): mixed; /** * Build this asset. Override this method in the specific diff --git a/Classes/ViewHelpers/Asset/PrefetchViewHelper.php b/Classes/ViewHelpers/Asset/PrefetchViewHelper.php index 6ec48311f..633ee1687 100644 --- a/Classes/ViewHelpers/Asset/PrefetchViewHelper.php +++ b/Classes/ViewHelpers/Asset/PrefetchViewHelper.php @@ -79,9 +79,9 @@ public function initializeArguments(): void } /** - * @return void + * @return null */ - public function render() + public function render(): mixed { $this->arguments['standalone'] = true; $this->arguments['movable'] = false; @@ -90,6 +90,8 @@ public function render() $this->tagBuilder->addAttribute('href', ''); $this->tagBuilder->setTagName('link'); $this->finalize(); + + return null; } public function build(): string diff --git a/Classes/ViewHelpers/Asset/ScriptViewHelper.php b/Classes/ViewHelpers/Asset/ScriptViewHelper.php index 9a2f6e04e..16b31f910 100644 --- a/Classes/ViewHelpers/Asset/ScriptViewHelper.php +++ b/Classes/ViewHelpers/Asset/ScriptViewHelper.php @@ -13,6 +13,10 @@ * * Allows inserting a `