diff --git a/typo3/sysext/core/Classes/TypoScript/FrontendTypoScript.php b/typo3/sysext/core/Classes/TypoScript/FrontendTypoScript.php index aa3bcdd9ce4863a251543cee63fc65bb8f8078be..3fd6fb3c1f5c658ff18893a3fcbce544f4bf5a0f 100644 --- a/typo3/sysext/core/Classes/TypoScript/FrontendTypoScript.php +++ b/typo3/sysext/core/Classes/TypoScript/FrontendTypoScript.php @@ -18,6 +18,7 @@ declare(strict_types=1); namespace TYPO3\CMS\Core\TypoScript; use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode; +use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude; /** * This class contains the TypoScript set up by the PrepareTypoScriptFrontendRendering @@ -27,19 +28,24 @@ use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode; */ final class FrontendTypoScript { - private RootNode|null $setupTree = null; - private array|null $setupArray = null; - private RootNode $configTree; - private array $configArray; - private RootNode $pageTree; - private array $pageArray; + private ?RootInclude $setupIncludeTree = null; + private ?RootNode $setupTree = null; + private ?array $setupArray = null; + private ?RootNode $configTree = null; + private ?array $configArray = null; + private ?RootNode $pageTree = null; + private ?array $pageArray = null; public function __construct( private readonly RootNode $settingsTree, + private readonly array $settingsConditionList, private readonly array $flatSettings, + private readonly array $setupConditionList, ) {} /** + * The settings ("constants") AST. + * * @internal Internal for now until the AST API stabilized. */ public function getSettingsTree(): RootNode @@ -48,9 +54,19 @@ final class FrontendTypoScript } /** - * This is *always* set up by the middleware: Current settings (aka "TypoScript constants") - * are needed for page cache identifier calculation. + * List of settings conditions with verdicts. Used internally for + * page cache identifier calculation. * + * @internal + */ + public function getSettingsConditionList(): array + { + return $this->settingsConditionList; + } + + /** + * This is *always* set up by the middleware / factory: Current settings ("constants") + * are needed for page cache identifier calculation. * This is a "flattened" array of all settings, as example, consider these settings TypoScript: * * ``` @@ -74,6 +90,37 @@ final class FrontendTypoScript return $this->flatSettings; } + /** + * List of setup conditions with verdicts. Used internally for + * page cache identifier calculation. + * + * @internal + */ + public function getSetupConditionList(): array + { + return $this->setupConditionList; + } + + /** + * @internal + */ + public function setSetupIncludeTree(RootInclude $setupIncludeTree): void + { + $this->setupIncludeTree = $setupIncludeTree; + } + + /** + * A tree of all TypoScript setup includes. Used internally within + * FrontendTypoScriptFactory to suppress calculating the include tree + * twice. + * + * @internal + */ + public function getSetupIncludeTree(): ?RootInclude + { + return $this->setupIncludeTree; + } + /** * @internal */ @@ -84,13 +131,15 @@ final class FrontendTypoScript /** * When a page is retrieved from cache and does not contain COA_INT or USER_INT objects, - * Frontend TypoScript setup is not calculated, so the AST and the array are not set. + * Frontend TypoScript setup is not calculated, AST and the array representation aren't set. * Calling getSetupTree() or getSetupArray() will then throw an exception. * * To avoid the exception, consumers can call hasSetup() beforehand. * * Note casual content objects do not need to do this, since setup TypoScript is always * set up when content objects need to be calculated. + * + * @internal */ public function hasSetup(): bool { @@ -124,7 +173,7 @@ final class FrontendTypoScript * The full Frontend TypoScript array. * * This is always set up as soon as the Frontend rendering needs to actually render something and - * can not get the full content from page cache. This is the case when a page cache entry does + * can not get the *full* content from page cache. This is the case when a page cache entry does * not exist, or when the page contains COA_INT or USER_INT objects. */ public function getSetupArray(): array @@ -148,10 +197,26 @@ final class FrontendTypoScript } /** - * @internal + * The merged TypoScript 'config.'. + * + * This is the result of the "global" TypoScript 'config' section, merged with + * the 'config' section of the determined PAGE object which can override + * "global" 'config' per type / typeNum. + * + * This is *always* needed within casual Frontend rendering by FrontendTypoScriptFactory and + * has a dedicated cache layer to be quick to retrieve. It is needed even in fully cached pages + * context to for instance know if debug headers should be added ("config.debug=1") to a response. + * + * @internal Internal for now until the AST API stabilized. */ public function getConfigTree(): RootNode { + if ($this->configTree === null) { + throw new \RuntimeException( + 'Setup "config." not initialized. FrontendTypoScriptFactory->createSetupConfigOrFullSetup() not called?', + 1710666154 + ); + } return $this->configTree; } @@ -164,14 +229,16 @@ final class FrontendTypoScript } /** - * The merged TypoScript 'config'. - * - * This is the result of the "global" TypoScript 'config' section, merged with - * the 'config' section of the determined PAGE object which can override - * "global" 'config' per type / typeNum. + * Array representation of getConfigTree(). */ public function getConfigArray(): array { + if ($this->configArray === null) { + throw new \RuntimeException( + 'Setup "config." not initialized. FrontendTypoScriptFactory->createSetupConfigOrFullSetup() not called?', + 1710666123 + ); + } return $this->configArray; } @@ -184,13 +251,35 @@ final class FrontendTypoScript } /** + * The determined PAGE object from main TypoScript 'setup' that depends + * on type / typeNum. + * + * This is used internally by RequestHandler for page generation. + * It is *not* set in full cached page scenarios without _INT object. + * * @internal */ public function getPageTree(): RootNode { + if ($this->pageTree === null) { + throw new \RuntimeException( + 'PAGE node has not been initialized. This happens in cached Frontend scope where full TypoScript' . + ' is not needed by the system, and if a PAGE object for given type could not be determined.' . + ' Test with hasPage().', + 1710399966 + ); + } return $this->pageTree; } + /** + * @internal + */ + public function hasPage(): bool + { + return $this->pageTree !== null; + } + /** * @internal */ @@ -200,19 +289,20 @@ final class FrontendTypoScript } /** - * The determined PAGE object from main TypoScript 'setup' that depends - * on type / typeNum. - * - * This is used internally by RequestHandler for page generation. - * It is *not* set in full cached page scenarios without _INT object. - * - * *If* this in made non-internal, a method "hasPage()" should be added - * for extensions to verify if page is actually set. + * Array representation of getPageTree(). * * @internal */ public function getPageArray(): array { + if ($this->pageArray === null) { + throw new \RuntimeException( + 'PAGE array has not been initialized. This happens in cached Frontend scope where full TypoScript' . + ' is not needed by the system, and if a PAGE object for given type could not be determined.' . + ' Test with hasPage().', + 1710399967 + ); + } return $this->pageArray; } } diff --git a/typo3/sysext/core/Classes/TypoScript/FrontendTypoScriptFactory.php b/typo3/sysext/core/Classes/TypoScript/FrontendTypoScriptFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..24fd0c8c3e05d9a443777b2516297eb435907c9e --- /dev/null +++ b/typo3/sysext/core/Classes/TypoScript/FrontendTypoScriptFactory.php @@ -0,0 +1,473 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Core\TypoScript; + +use Psr\Container\ContainerInterface; +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend; +use TYPO3\CMS\Core\Site\Entity\SiteInterface; +use TYPO3\CMS\Core\TypoScript\AST\Merger\SetupConfigMerger; +use TYPO3\CMS\Core\TypoScript\AST\Node\ChildNode; +use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode; +use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude; +use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateTreeBuilder; +use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser; +use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser; +use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeAstBuilderVisitor; +use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionIncludeListAccumulatorVisitor; +use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionMatcherVisitor; +use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeSetupConditionConstantSubstitutionVisitor; +use TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer; +use TYPO3\CMS\Frontend\Event\ModifyTypoScriptConfigEvent; +use TYPO3\CMS\Frontend\Event\ModifyTypoScriptConstantsEvent; + +/** + * Create FrontendTypoScript with its details. This is typically used by a Frontend middleware + * to calculate the TypoScript needed to satisfy rendering details of the specific Request. + * + * @internal Methods signatures and detail implementations are still subject to change. + */ +final readonly class FrontendTypoScriptFactory +{ + public function __construct( + private ContainerInterface $container, + private EventDispatcherInterface $eventDispatcher, + private SysTemplateTreeBuilder $treeBuilder, + private LossyTokenizer $tokenizer, + private IncludeTreeTraverser $includeTreeTraverser, + private ConditionVerdictAwareIncludeTreeTraverser $includeTreeTraverserConditionVerdictAware, + ) {} + + /** + * First step of TypoScript calculations. + * This is *always* called, even in FE fully cached pages context since the page + * cache entry depends on setup condition verdicts, which depends on settings. + * + * Returns the FrontendTypoScript object with these parameters set: + * * settingsTree: The full settings ("constants") AST + * * flatSettings: Flattened list of settings, derived from settings tree + * * settingsConditionList: Settings conditions with verdicts of this Request + * * setupConditionList: Setup conditions with verdicts of this Request + * * (sometimes) setupIncludeTree: The setup include tree *if* it had to be calculated + */ + public function createSettingsAndSetupConditions( + SiteInterface $site, + array $sysTemplateRows, + array $expressionMatcherVariables, + ?PhpFrontend $typoScriptCache, + ): FrontendTypoScript { + $settingsDetails = $this->createSettings( + $site, + $sysTemplateRows, + $expressionMatcherVariables, + $typoScriptCache + ); + $setupDetails = $this->createSetupConditionList( + $site, + $sysTemplateRows, + $expressionMatcherVariables, + $typoScriptCache, + $settingsDetails['flatSettings'], + $settingsDetails['settingsConditionList'], + ); + $frontendTypoScript = new FrontendTypoScript( + $settingsDetails['settingsTree'], + $settingsDetails['settingsConditionList'], + $settingsDetails['flatSettings'], + $setupDetails['setupConditionList'], + ); + if ($setupDetails['setupIncludeTree']) { + $frontendTypoScript->setSetupIncludeTree($setupDetails['setupIncludeTree']); + } + return $frontendTypoScript; + } + + /** + * Calculate settings (formerly "constants"). + * + * The page cache entry identifier depends on setup TypoScript: A single page with two different + * setup TypoScript AST will probably render different results, thus two page-cache entries. + * Setup TypoScript can be different when setup conditions match differently. + * Setup conditions can use settings "[{$foo} = 42]". + * + * All FE requests thus need the current list of settings, and settings can have conditions, too. + * We thus *always* need the current list of settings, even in fully cached pages context. + * + * The method calculates settings and uses caches as much as possible: + * * settingsTree: The full settings AST + * * flatSettings: Flattened list of settings, derived from settings AST + * * settingsConditionList: Settings conditions with verdicts of this Request + * + * @return array{settingsTree: RootNode, flatSettings: array, settingsConditionList: array} + */ + private function createSettings( + SiteInterface $site, + array $sysTemplateRows, + array $expressionMatcherVariables, + ?PhpFrontend $typoScriptCache, + ): array { + $conditionTreeCacheIdentifier = 'settings-condition-tree-' . hash('xxh3', json_encode($sysTemplateRows, JSON_THROW_ON_ERROR)); + + if ($conditionTree = $typoScriptCache?->require($conditionTreeCacheIdentifier)) { + // Got the (flat) include tree of all settings conditions for this TypoScript combination from cache. + // Good. Traverse this list to calculate "current" condition verdicts. Hash this list together with a + // hash of the TypoScript sys_templates, and try to retrieve the full settings TypoScript AST from cache. + // Note: Working with the derived condition tree that *only* contains conditions, but not the full + // include tree is a trick: We only need the condition verdicts to know the AST cache identifier, + // and traversing the flat condition tree is quicker than traversing the entire settings include tree, + // since it only scales with the number of settings conditions and not with the full amount of TypoScript + // settings. The same trick is used for the setup AST cache later. + $conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class); + $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables); + // It does not matter if we use IncludeTreeTraverser or ConditionVerdictAwareIncludeTreeTraverser here: + // Conditions list is flat, not nested. IncludeTreeTraverser has an if() less, so we use that one. + $this->includeTreeTraverser->traverse($conditionTree, [$conditionMatcherVisitor]); + $conditionList = $conditionMatcherVisitor->getConditionListWithVerdicts(); + $settings = $typoScriptCache->require( + 'settings-' . hash('xxh3', $conditionTreeCacheIdentifier . json_encode($conditionList, JSON_THROW_ON_ERROR)) + ); + if (is_array($settings)) { + return [ + 'settingsTree' => $settings['ast'], + 'flatSettings' => $settings['flatSettings'], + 'settingsConditionList' => $conditionList, + ]; + } + } + + // We did not get settings from cache, or are not allowed to use cache. Build settings from scratch. + // We fetch the full settings include tree (from cache if possible), register the condition + // matcher and register the AST builder and traverse include tree to retrieve settings AST and derive + // 'flat settings' from it. Both are cached if allowed afterward for the above 'if' to kick in next time. + $includeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $this->tokenizer, $site, $typoScriptCache); + $conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class); + $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables); + $visitors = []; + $visitors[] = $conditionMatcherVisitor; + $astBuilderVisitor = $this->container->get(IncludeTreeAstBuilderVisitor::class); + $visitors[] = $astBuilderVisitor; + // We must use ConditionVerdictAwareIncludeTreeTraverser here: This one does not walk into + // children for not matching conditions, which is important to create the correct AST. + $this->includeTreeTraverserConditionVerdictAware->traverse($includeTree, $visitors); + $tree = $astBuilderVisitor->getAst(); + // @internal Dispatch an experimental event allowing listeners to still change the settings AST, + // to for instance implement nested constants if really needed. Note this event may change + // or vanish later without further notice. + $tree = $this->eventDispatcher->dispatch(new ModifyTypoScriptConstantsEvent($tree))->getConstantsAst(); + $flatSettings = $tree->flatten(); + + // Prepare the full list of settings conditions in order to cache this list, avoiding the + // settings AST building next time. We need all conditions of the entire include tree, but the + // above ConditionVerdictAwareIncludeTreeTraverser did not find nested conditions if an upper + // condition did not match. We thus have to traverse include tree a second time with the + // IncludeTreeTraverser. This one does traverse into not matching conditions. + $visitors = []; + $conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class); + $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables); + $visitors[] = $conditionMatcherVisitor; + $conditionTreeAccumulatorVisitor = null; + if (!$conditionTree && $typoScriptCache) { + // If the settingsConditionTree did not come from cache above and if we are allowed to cache, + // register the visitor that creates the settings condition include tree, to cache it. + $conditionTreeAccumulatorVisitor = $this->container->get(IncludeTreeConditionIncludeListAccumulatorVisitor::class); + $visitors[] = $conditionTreeAccumulatorVisitor; + } + $this->includeTreeTraverser->traverse($includeTree, $visitors); + $conditionList = $conditionMatcherVisitor->getConditionListWithVerdicts(); + + if ($conditionTreeAccumulatorVisitor) { + // Cache the flat condition include tree for next run. + $conditionTree = $conditionTreeAccumulatorVisitor->getConditionIncludes(); + $typoScriptCache?->set( + $conditionTreeCacheIdentifier, + 'return unserialize(\'' . addcslashes(serialize($conditionTree), '\'\\') . '\');' + ); + } + $typoScriptCache?->set( + // Cache full AST and the derived 'flattened' variant for next run, which will kick in if + // the sys_templates and condition verdicts are identical with another Request. + 'settings-' . hash('xxh3', $conditionTreeCacheIdentifier . json_encode($conditionList, JSON_THROW_ON_ERROR)), + 'return unserialize(\'' . addcslashes(serialize(['ast' => $tree, 'flatSettings' => $flatSettings]), '\'\\') . '\');' + ); + + return [ + 'settingsTree' => $tree, + 'flatSettings' => $flatSettings, + 'settingsConditionList' => $conditionList, + ]; + } + + /** + * Calculate setup condition verdicts. + * + * With settings being done, the list of matching setup condition verdicts is calculated, + * which depend on settings. Setup conditions with their verdicts are part of the page + * cache identifier, they are *always* needed in the FE rendering chain. + * + * The cached variant uses a similar trick as with the settings calculation above: We + * calculate a flat tree of all conditions and cache this, so the traverser only needs + * to iterate the conditions to calculate there verdicts, but not the entire include + * tree next time. + * + * The method returns: + * * 'setupConditionList': Setup conditions with verdicts of this Request + * * (sometimes) setupIncludeTree: The setup include tree *if* it had to be calculated. Used internally + * to suppress a second calculation in createSetupConfigOrFullSetup(). + * + * @return array{setupConditionList: array, setupIncludeTree: RootInclude|null} + */ + private function createSetupConditionList( + SiteInterface $site, + array $sysTemplateRows, + array $expressionMatcherVariables, + ?PhpFrontend $typoScriptCache, + array $flatSettings, + array $settingsConditionList, + ): array { + $conditionTreeCacheIdentifier = 'setup-condition-tree-' + . hash('xxh3', json_encode($sysTemplateRows, JSON_THROW_ON_ERROR) . json_encode($settingsConditionList, JSON_THROW_ON_ERROR)); + + if ($conditionTree = $typoScriptCache?->require($conditionTreeCacheIdentifier)) { + // We got the flat list of all setup conditions for this TypoScript combination from cache. Good. We traverse + // this list to calculate "current" condition verdicts, which we need as hash to be part of page cache identifier. + // We're done and return. Note 'setupIncludeTree' is *not* returned in this case since it is not needed and + // may or may not be needed later, depending on if we can get a page cache entry later and if it has _INT objects. + $visitors = []; + $conditionConstantSubstitutionVisitor = $this->container->get(IncludeTreeSetupConditionConstantSubstitutionVisitor::class); + $conditionConstantSubstitutionVisitor->setFlattenedConstants($flatSettings); + $visitors[] = $conditionConstantSubstitutionVisitor; + $conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class); + $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables); + $visitors[] = $conditionMatcherVisitor; + // It does not matter if we use IncludeTreeTraverser or ConditionVerdictAwareIncludeTreeTraverser here: + // Condition list is flat, not nested. IncludeTreeTraverser has an if() less, so we use that one. + $this->includeTreeTraverser->traverse($conditionTree, $visitors); + return [ + 'setupConditionList' => $conditionMatcherVisitor->getConditionListWithVerdicts(), + 'setupIncludeTree' => null, + ]; + } + + // We did not get setup condition list from cache, or are not allowed to use cache. We have to build setup + // condition list from scratch. This means we'll fetch the full setup include tree (from cache if possible), + // register the constant substitution visitor, the condition matcher and the condition accumulator visitor. + $includeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $this->tokenizer, $site, $typoScriptCache); + $visitors = []; + $conditionConstantSubstitutionVisitor = $this->container->get(IncludeTreeSetupConditionConstantSubstitutionVisitor::class); + $conditionConstantSubstitutionVisitor->setFlattenedConstants($flatSettings); + $visitors[] = $conditionConstantSubstitutionVisitor; + $conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class); + $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables); + $visitors[] = $conditionMatcherVisitor; + $conditionTreeAccumulatorVisitor = $this->container->get(IncludeTreeConditionIncludeListAccumulatorVisitor::class); + $visitors[] = $conditionTreeAccumulatorVisitor; + // It is important to use IncludeTreeTraverser here: We need the condition verdicts of *all* conditions, and + // we want to accumulate all of them. The ConditionVerdictAwareIncludeTreeTraverser wouldn't walk into nested + // conditions if an upper one does not match, which defeats cache identifier calculations. + $this->includeTreeTraverser->traverse($includeTree, $visitors); + + $typoScriptCache?->set( + $conditionTreeCacheIdentifier, + 'return unserialize(\'' . addcslashes(serialize($conditionTreeAccumulatorVisitor->getConditionIncludes()), '\'\\') . '\');' + ); + + return [ + 'setupConditionList' => $conditionMatcherVisitor->getConditionListWithVerdicts(), + 'setupIncludeTree' => $includeTree, + ]; + } + + /** + * Enrich the given FrontendTypoScript object with TypoScript 'setup' relevant data. + * + * The method is called in FE after an attempt to retrieve page content from cache has + * been done. There are three possible outcomes: + * * The page has been retrieved from cache and the content *does not* contain uncached "_INT" objects + * * The page has been retrieved from cache and the content *does* contain uncached "_INT" objects + * * The page could not be retrieved from cache + * + * If the page could not be retrieved from cache, or if the cached page content contains "_INT" objects, + * flag $needsFullSetup is given true, and the full TypoScript is calculated since at least parts of + * the page content has to be rendered, which then needs full TypoScript. + * If the page could be retrieved from cache, and contains no "_INT" objects, $needsFullSetup in false, the + * rendering chain only needs the "config." part of TypoScript to satisfy the remaining middlewares. + * + * The method implements these variants and tries to add as little overhead as possible. + * + * Returns the FrontendTypoScript object: + * * configTree: Always set. Global TypoScript 'config.' merged with overrides from given type/typeNum "page.config.". + * * configArray: Always set. Array representation of configTree. + * * setupTree: Not set if $needsFullSetup=false and configTree could be retrieved from cache. Full TypoScript setup. + * * setupArray: Not set if $needsFullSetup=false and configTree could be retrieved from cache. + * Array representation of setupTree. + * * pageTree: Not set if $needsFullSetup=false and configTree could be retrieved from cache, or if no PAGE object + * could be determined. The 'PAGE' object tree for given type/typeNum. + * * pageArray: Not set if $needsFullSetup=false and configTree could be retrieved from cache, or if no PAGE object + * could be determined. Array representation of PageTree. + */ + public function createSetupConfigOrFullSetup( + bool $needsFullSetup, + FrontendTypoScript $frontendTypoScript, + SiteInterface $site, + array $sysTemplateRows, + array $expressionMatcherVariables, + string $type, + ?PhpFrontend $typoScriptCache, + ?ServerRequestInterface $request, + ): FrontendTypoScript { + $setupTypoScriptCacheIdentifier = 'setup-' . hash( + 'xxh3', + json_encode($sysTemplateRows, JSON_THROW_ON_ERROR) + . json_encode($frontendTypoScript->getSettingsConditionList(), JSON_THROW_ON_ERROR) + . json_encode($frontendTypoScript->getSetupConditionList(), JSON_THROW_ON_ERROR) + ); + $setupConfigTypoScriptCacheIdentifier = 'setup-config-' . hash('xxh3', $setupTypoScriptCacheIdentifier . $type); + + $gotSetupConfigFromCache = false; + if ($setupConfigTypoScriptCache = $typoScriptCache?->require($setupConfigTypoScriptCacheIdentifier)) { + $frontendTypoScript->setConfigTree($setupConfigTypoScriptCache['ast']); + $frontendTypoScript->setConfigArray($setupConfigTypoScriptCache['array']); + if (!$needsFullSetup) { + // Fully cached page context without _INT - only 'config' is needed. Return early. + return $frontendTypoScript; + } + $gotSetupConfigFromCache = true; + } + + $setupRawConfigAst = null; + if (!$typoScriptCache || $needsFullSetup || !$gotSetupConfigFromCache) { + // If caching is not allowed, if no page cache entry could be loaded or if the page cache entry has _INT + // object, we need the full setup AST. Try to use a cache entry for setup AST, which especially up _INT + // parsing. In unavailable, calculate full setup AST and cache it if allowed. + $gotSetupFromCache = false; + if ($setupTypoScriptCache = $typoScriptCache?->require($setupTypoScriptCacheIdentifier)) { + // We need AST, and we got it from cache. + $frontendTypoScript->setSetupTree($setupTypoScriptCache['ast']); + $frontendTypoScript->setSetupArray($setupTypoScriptCache['array']); + $setupRawConfigAst = $setupTypoScriptCache['ast']->getChildByName('config'); + $gotSetupFromCache = true; + } + if (!$typoScriptCache || !$gotSetupFromCache) { + // We need AST and couldn't get it from cache or are now allowed to. We thus need the full setup + // IncludeTree, which we can get from cache again if allowed, or is calculated a-new if not. + $setupIncludeTree = $frontendTypoScript->getSetupIncludeTree(); + if (!$typoScriptCache || $setupIncludeTree === null) { + // A previous method *may* have calculated setup include tree already. Calculate now if not. + $setupIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $this->tokenizer, $site, $typoScriptCache); + } + $visitors = []; + $conditionConstantSubstitutionVisitor = $this->container->get(IncludeTreeSetupConditionConstantSubstitutionVisitor::class); + $conditionConstantSubstitutionVisitor->setFlattenedConstants($frontendTypoScript->getFlatSettings()); + $visitors[] = $conditionConstantSubstitutionVisitor; + $conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class); + $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables); + $visitors[] = $conditionMatcherVisitor; + $astBuilderVisitor = $this->container->get(IncludeTreeAstBuilderVisitor::class); + $astBuilderVisitor->setFlatConstants($frontendTypoScript->getFlatSettings()); + $visitors[] = $astBuilderVisitor; + $this->includeTreeTraverserConditionVerdictAware->traverse($setupIncludeTree, $visitors); + $setupAst = $astBuilderVisitor->getAst(); + // @todo: It would be good to actively remove 'config' from AST and array here + // to prevent people from using the unmerged variant. The same + // is already done for the determined PAGE 'config' below. This works, but + // is currently blocked by functional tests that assert details? + // Also, we need to still cache with full 'config' to handle multiple types. + $setupRawConfigAst = $setupAst->getChildByName('config'); + // $setupAst->removeChildByName('config'); + $frontendTypoScript->setSetupTree($setupAst); + $frontendTypoScript->setSetupArray($setupAst->toArray()); + + // Write cache entry for AST and its array representation. + $typoScriptCache?->set( + $setupTypoScriptCacheIdentifier, + 'return unserialize(\'' . addcslashes(serialize(['ast' => $setupAst, 'array' => $setupAst->toArray()]), '\'\\') . '\');' + ); + } + + $setupAst = $frontendTypoScript->getSetupTree(); + $rawSetupPageNodeFromType = null; + $pageNodeFoundByType = false; + foreach ($setupAst->getNextChild() as $potentialPageNode) { + // Find the PAGE object that matches given type/typeNum + if ($potentialPageNode->getValue() === 'PAGE') { + // @todo: We could potentially remove *all* PAGE objects from setup here. This prevents people + // from accessing other ones than the determined one in $frontendTypoScript->getSetupArray(). + $typeNumChild = $potentialPageNode->getChildByName('typeNum'); + if ($typeNumChild && $type === $typeNumChild->getValue()) { + $rawSetupPageNodeFromType = $potentialPageNode; + $pageNodeFoundByType = true; + break; + } + if (!$typeNumChild && $type === '0') { + // The first PAGE node that has no typeNum is considered '0' automatically. + $rawSetupPageNodeFromType = $potentialPageNode; + $pageNodeFoundByType = true; + break; + } + } + } + if (!$pageNodeFoundByType) { + $rawSetupPageNodeFromType = new RootNode(); + } + $setupPageAst = new RootNode(); + foreach ($rawSetupPageNodeFromType->getNextChild() as $child) { + $setupPageAst->addChild($child); + } + + if (!$gotSetupConfigFromCache) { + // If we did not get merged 'config.' from cache above, create it now and cache it. + $mergedSetupConfigAst = (new SetupConfigMerger())->merge($setupRawConfigAst, $setupPageAst->getChildByName('config')); + if ($mergedSetupConfigAst->getChildByName('absRefPrefix') === null) { + // Make sure config.absRefPrefix is set, fallback to 'auto'. + $absRefPrefixNode = new ChildNode('absRefPrefix'); + $absRefPrefixNode->setValue('auto'); + $mergedSetupConfigAst->addChild($absRefPrefixNode); + } + if ($mergedSetupConfigAst->getChildByName('doctype') === null) { + // Make sure config.doctype is set, fallback to 'html5'. + $doctypeNode = new ChildNode('doctype'); + $doctypeNode->setValue('html5'); + $mergedSetupConfigAst->addChild($doctypeNode); + } + if ($request) { + // Dispatch ModifyTypoScriptConfigEvent before config is cached and if Request is given. + $mergedSetupConfigAst = $this->eventDispatcher + ->dispatch(new ModifyTypoScriptConfigEvent($request, $setupAst, $mergedSetupConfigAst))->getConfigTree(); + } + $frontendTypoScript->setConfigTree($mergedSetupConfigAst); + $setupConfigArray = $mergedSetupConfigAst->toArray(); + $frontendTypoScript->setConfigArray($setupConfigArray); + $typoScriptCache?->set( + $setupConfigTypoScriptCacheIdentifier, + 'return unserialize(\'' . addcslashes(serialize(['ast' => $mergedSetupConfigAst, 'array' => $setupConfigArray]), '\'\\') . '\');' + ); + } + + if ($pageNodeFoundByType) { + // Remove "page.config" to prevent people from working with the not merged variant. + // We do *not* set page if it could not be determined (important for hasPage() later + // to return an early "no PAGE for type found" Response. + $setupPageAst->removeChildByName('config'); + $frontendTypoScript->setPageTree($setupPageAst); + $frontendTypoScript->setPageArray($setupPageAst->toArray()); + } + } + return $frontendTypoScript; + } +} diff --git a/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeSetupConditionConstantSubstitutionVisitor.php b/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeSetupConditionConstantSubstitutionVisitor.php index 3d2c4e34c3604dcb5acbf209e20fe211e189047e..83cc53a579d511311717084dd1dc050bbc2ef885 100644 --- a/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeSetupConditionConstantSubstitutionVisitor.php +++ b/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeSetupConditionConstantSubstitutionVisitor.php @@ -53,8 +53,8 @@ final class IncludeTreeSetupConditionConstantSubstitutionVisitor implements Incl /** * Do the magic, see tests for details. - * Implementation within 'visitBeforeChilden()' since this allows running *both* this - * visitor first, and then TreeVisitorConditionMatcher directly afterwards in the same + * Implementation within 'visitBeforeChildren()' since this allows running *both* this + * visitor first, and then IncludeTreeConditionMatcherVisitor directly afterward in the same * traverser cycle! */ public function visitBeforeChildren(IncludeInterface $include, int $currentDepth): void diff --git a/typo3/sysext/core/Configuration/Services.yaml b/typo3/sysext/core/Configuration/Services.yaml index fe5314605e60bf96dba7deb922fdb9d4ae540bf1..7d38fbae51747057424f3249e9c005148fe19175 100644 --- a/typo3/sysext/core/Configuration/Services.yaml +++ b/typo3/sysext/core/Configuration/Services.yaml @@ -283,11 +283,21 @@ services: # This Ast builder visitor creates state and should not be re-used shared: false + TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionIncludeListAccumulatorVisitor: + public: true + # This visitor creates state and should not be re-used + shared: false + TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionMatcherVisitor: public: true # This visitor creates state and should not be re-used shared: false + TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeSetupConditionConstantSubstitutionVisitor: + public: true + # This visitor creates state and should not be re-used + shared: false + TYPO3\CMS\Core\TypoScript\Tokenizer\TokenizerInterface: alias: TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer diff --git a/typo3/sysext/extbase/Classes/Configuration/BackendConfigurationManager.php b/typo3/sysext/extbase/Classes/Configuration/BackendConfigurationManager.php index b4a6f425de4898663148c91b43cb3e2e0b623bd4..dec4d7f844b76bb40ab9b4e3b4fdb1398aaad98f 100644 --- a/typo3/sysext/extbase/Classes/Configuration/BackendConfigurationManager.php +++ b/typo3/sysext/extbase/Classes/Configuration/BackendConfigurationManager.php @@ -31,13 +31,8 @@ use TYPO3\CMS\Core\SingletonInterface; use TYPO3\CMS\Core\Site\Entity\NullSite; use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\Type\Bitmask\Permission; +use TYPO3\CMS\Core\TypoScript\FrontendTypoScriptFactory; use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository; -use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateTreeBuilder; -use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser; -use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeAstBuilderVisitor; -use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionMatcherVisitor; -use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeSetupConditionConstantSubstitutionVisitor; -use TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer; use TYPO3\CMS\Core\TypoScript\TypoScriptService; use TYPO3\CMS\Core\Utility\ArrayUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -102,10 +97,8 @@ class BackendConfigurationManager implements SingletonInterface private readonly PhpFrontend $typoScriptCache, private readonly FrontendInterface $runtimeCache, private readonly SysTemplateRepository $sysTemplateRepository, - private readonly SysTemplateTreeBuilder $treeBuilder, - private readonly LossyTokenizer $lossyTokenizer, - private readonly ConditionVerdictAwareIncludeTreeTraverser $includeTreeTraverserConditionVerdictAware, private readonly SiteFinder $siteFinder, + private readonly FrontendTypoScriptFactory $frontendTypoScriptFactory, ) {} public function setRequest(ServerRequestInterface $request): void @@ -205,6 +198,10 @@ class BackendConfigurationManager implements SingletonInterface // Keep null / NullSite when no site could be determined for whatever reason. } } + if ($site === null) { + // If still no site object, have NullSite (usually pid 0). + $site = new NullSite(); + } $rootLine = []; $sysTemplateFakeRow = [ @@ -236,10 +233,6 @@ class BackendConfigurationManager implements SingletonInterface $sysTemplateRows[] = $sysTemplateFakeRow; } - // We do cache tree and tokens, but don't cache full ast in this backend context for now: - // That's a possible improvement to further speed up extbase backend modules, but a bit of - // hassle. See the Frontend TypoScript calculation on how to do this. - $constantIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $this->lossyTokenizer, $site, $this->typoScriptCache); $expressionMatcherVariables = [ 'request' => $this->request, 'pageId' => $pageId, @@ -247,34 +240,11 @@ class BackendConfigurationManager implements SingletonInterface 'fullRootLine' => $rootLine, 'site' => $site, ]; - $conditionMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class); - $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables); - $includeTreeTraverserConditionVerdictAwareVisitors = []; - $includeTreeTraverserConditionVerdictAwareVisitors[] = $conditionMatcherVisitor; - $constantAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeAstBuilderVisitor::class); - $includeTreeTraverserConditionVerdictAwareVisitors[] = $constantAstBuilderVisitor; - $this->includeTreeTraverserConditionVerdictAware->traverse($constantIncludeTree, $includeTreeTraverserConditionVerdictAwareVisitors); - $constantsAst = $constantAstBuilderVisitor->getAst(); - $flatConstants = $constantsAst->flatten(); - - $setupIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $this->lossyTokenizer, $site, $this->typoScriptCache); - $includeTreeTraverserConditionVerdictAwareVisitors = []; - $setupConditionConstantSubstitutionVisitor = new IncludeTreeSetupConditionConstantSubstitutionVisitor(); - $setupConditionConstantSubstitutionVisitor->setFlattenedConstants($flatConstants); - $includeTreeTraverserConditionVerdictAwareVisitors[] = $setupConditionConstantSubstitutionVisitor; - $setupMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class); - $setupMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables); - $includeTreeTraverserConditionVerdictAwareVisitors[] = $setupMatcherVisitor; - $setupAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeAstBuilderVisitor::class); - $setupAstBuilderVisitor->setFlatConstants($flatConstants); - $includeTreeTraverserConditionVerdictAwareVisitors[] = $setupAstBuilderVisitor; - $this->includeTreeTraverserConditionVerdictAware->traverse($setupIncludeTree, $includeTreeTraverserConditionVerdictAwareVisitors); - $setupAst = $setupAstBuilderVisitor->getAst(); - - $setupArray = $setupAst->toArray(); + $typoScript = $this->frontendTypoScriptFactory->createSettingsAndSetupConditions($site, $sysTemplateRows, $expressionMatcherVariables, $this->typoScriptCache); + $typoScript = $this->frontendTypoScriptFactory->createSetupConfigOrFullSetup(true, $typoScript, $site, $sysTemplateRows, $expressionMatcherVariables, '0', $this->typoScriptCache, null); + $setupArray = $typoScript->getSetupArray(); $this->runtimeCache->set($cacheIdentifier, $setupArray); - return $setupArray; } diff --git a/typo3/sysext/extbase/Tests/Functional/Configuration/FrontendConfigurationManagerTest.php b/typo3/sysext/extbase/Tests/Functional/Configuration/FrontendConfigurationManagerTest.php index f2198087f81133471ad7cd726c70a3572b175d8f..b37038551089767252036d77b4438e41260563f6 100644 --- a/typo3/sysext/extbase/Tests/Functional/Configuration/FrontendConfigurationManagerTest.php +++ b/typo3/sysext/extbase/Tests/Functional/Configuration/FrontendConfigurationManagerTest.php @@ -56,7 +56,7 @@ final class FrontendConfigurationManagerTest extends FunctionalTestCase </data> </T3FlexForms>'; - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray($typoScript); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())->withAttribute('frontend.typoscript', $frontendTypoScript); @@ -75,7 +75,7 @@ final class FrontendConfigurationManagerTest extends FunctionalTestCase $contentObject = new ContentObjectRenderer(); $contentObject->data = ['pi_flexform' => $flexForm]; $request = (new ServerRequest())->withAttribute('currentContentObject', $contentObject); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $request = $request->withAttribute('frontend.typoscript', $frontendTypoScript); $frontendConfigurationManager = $this->get(FrontendConfigurationManager::class); @@ -356,7 +356,7 @@ final class FrontendConfigurationManagerTest extends FunctionalTestCase $contentObject = new ContentObjectRenderer(); $contentObject->data = ['pi_flexform' => $flexForm]; $request = (new ServerRequest())->withAttribute('currentContentObject', $contentObject); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray($typoScript); $request = $request->withAttribute('frontend.typoscript', $frontendTypoScript); $frontendConfigurationManager = $this->get(FrontendConfigurationManager::class); diff --git a/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/ActionControllerArgumentTest.php b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/ActionControllerArgumentTest.php index 8b35411d8fc334ca0c0140f09290055b72bf602b..955edaeec33085582f2e864f020583948986df97 100644 --- a/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/ActionControllerArgumentTest.php +++ b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/ActionControllerArgumentTest.php @@ -191,7 +191,7 @@ final class ActionControllerArgumentTest extends FunctionalTestCase private function buildRequest(string $actionName, array $arguments = null): Request { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $serverRequest = (new ServerRequest()) ->withAttribute('extbase', new ExtbaseRequestParameters()) diff --git a/typo3/sysext/extbase/Tests/Functional/Mvc/Web/RequestBuilderTest.php b/typo3/sysext/extbase/Tests/Functional/Mvc/Web/RequestBuilderTest.php index 0b093a2731adbc3ecad4f19c96818f6182d97a6d..e59be60434cde593f762a3d466720289fbc9613f 100644 --- a/typo3/sysext/extbase/Tests/Functional/Mvc/Web/RequestBuilderTest.php +++ b/typo3/sysext/extbase/Tests/Functional/Mvc/Web/RequestBuilderTest.php @@ -678,7 +678,7 @@ final class RequestBuilderTest extends FunctionalTestCase { $pageArguments = new PageArguments(1, '0', ['tx_blog_example_blog' => ['action' => 'show']]); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $mainRequest = $this->prepareServerRequest('https://example.com/'); $mainRequest = $mainRequest diff --git a/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbBackendTest.php b/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbBackendTest.php index cadc67b8019a67f0635ea6db78fe83bf94d8a192..bbe377817f92b349e06bb01cee81b37d9a47554d 100644 --- a/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbBackendTest.php +++ b/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbBackendTest.php @@ -55,7 +55,7 @@ final class Typo3DbBackendTest extends FunctionalTestCase public function getObjectDataByQueryChangesUidIfInPreview(): void { $this->importCSVDataSet(__DIR__ . '/Fixtures/Typo3DbBackendTestImport.csv'); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) diff --git a/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbQueryParserTest.php b/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbQueryParserTest.php index 7280dec7c025b643974c9dacfabc4261355b74ac..d98d9353f05e624d25a2a91e038a88f842e0354f 100644 --- a/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbQueryParserTest.php +++ b/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbQueryParserTest.php @@ -48,7 +48,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase #[Test] public function convertQueryToDoctrineQueryBuilderDoesNotAddAndWhereWithEmptyConstraint(): void { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) @@ -67,7 +67,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase #[Test] public function convertQueryToDoctrineQueryBuilderThrowsExceptionOnNotImplementedConstraint(): void { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) @@ -90,7 +90,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase #[Test] public function convertQueryToDoctrineQueryBuilderAddsSimpleAndWhere(): void { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) @@ -112,7 +112,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase #[Test] public function convertQueryToDoctrineQueryBuilderAddsNotConstraint(): void { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) @@ -134,7 +134,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase #[Test] public function convertQueryToDoctrineQueryBuilderAddsAndConstraint(): void { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) @@ -160,7 +160,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase #[Test] public function convertQueryToDoctrineQueryBuilderAddsOrConstraint(): void { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) @@ -186,7 +186,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase #[Test] public function languageStatementWorksForDefaultLanguage(): void { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) @@ -204,7 +204,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase #[Test] public function languageStatementWorksForNonDefaultLanguage(): void { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) @@ -267,7 +267,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase #[Test] public function addGetLanguageStatementWorksForForeignLanguageWithSubselectionTakesDeleteStatementIntoAccountIfNecessary(): void { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) @@ -312,7 +312,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase #[Test] public function orderStatementGenerationWorks(): void { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) @@ -339,7 +339,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase $this->expectException(UnsupportedOrderException::class); $this->expectExceptionCode(1242816074); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) @@ -360,7 +360,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase #[Test] public function orderStatementGenerationWorksWithMultipleOrderings(): void { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) @@ -473,7 +473,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase #[Test] public function expressionIsOmittedForIgnoreEnableFieldsAreAndDoNotIncludeDeletedInFrontendContext(): void { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) @@ -497,7 +497,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase #[Test] public function expressionIsGeneratedForIgnoreEnableFieldsAndDoNotIncludeDeletedInFrontendContext(): void { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) @@ -521,7 +521,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase #[Test] public function expressionIsGeneratedForIgnoreOnlyFeGroupAndDoNotIncludeDeletedInFrontendContext(): void { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) @@ -547,7 +547,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase #[Test] public function expressionIsGeneratedForDoNotIgnoreEnableFieldsAndDoNotIncludeDeletedInFrontendContext(): void { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) @@ -573,7 +573,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase public function respectEnableFieldsSettingGeneratesCorrectStatementWithOnlyEndTimeInFrontendContext(): void { $GLOBALS['TCA']['tx_blogexample_domain_model_blog']['ctrl']['enablecolumns']['endtime'] = 'endtime_column'; - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) @@ -601,7 +601,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase // simulate time for backend enable fields $GLOBALS['SIM_ACCESS_TIME'] = 1451779200; $GLOBALS['TCA']['tx_blogexample_domain_model_blog']['ctrl']['enablecolumns']['endtime'] = 'endtime_column'; - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); @@ -622,7 +622,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase #[Test] public function visibilityConstraintStatementGenerationThrowsExceptionIfTheQuerySettingsAreInconsistent(): void { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) @@ -684,7 +684,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase $GLOBALS['TCA']['tx_blogexample_domain_model_blog']['ctrl'] = [ 'rootLevel' => $rootLevel, ]; - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) diff --git a/typo3/sysext/extbase/Tests/Functional/Persistence/TranslationTest.php b/typo3/sysext/extbase/Tests/Functional/Persistence/TranslationTest.php index 9423e759d00ea32ee6cea6a8339d3279c83607f3..91844e502de26d998a5c24318190cde4e70b0bae 100644 --- a/typo3/sysext/extbase/Tests/Functional/Persistence/TranslationTest.php +++ b/typo3/sysext/extbase/Tests/Functional/Persistence/TranslationTest.php @@ -45,7 +45,7 @@ final class TranslationTest extends FunctionalTestCase $context = GeneralUtility::makeInstance(Context::class); $context->setAspect('language', new LanguageAspect(0, 0, LanguageAspect::OVERLAYS_OFF)); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) diff --git a/typo3/sysext/extbase/Tests/Functional/Persistence/WorkspaceTest.php b/typo3/sysext/extbase/Tests/Functional/Persistence/WorkspaceTest.php index c1deed47c62a4d5d6e5333f63bacad352ddd3caf..8088b0ea69ada6d158f38910b02d2ba7ce8cd758 100644 --- a/typo3/sysext/extbase/Tests/Functional/Persistence/WorkspaceTest.php +++ b/typo3/sysext/extbase/Tests/Functional/Persistence/WorkspaceTest.php @@ -58,7 +58,7 @@ final class WorkspaceTest extends FunctionalTestCase $context = new Context(); $context->setAspect('workspace', new WorkspaceAspect($workspaceId)); GeneralUtility::setSingletonInstance(Context::class, $context); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) @@ -86,7 +86,7 @@ final class WorkspaceTest extends FunctionalTestCase $context->setAspect('backend.user', new UserAspect($backendUser)); $context->setAspect('workspace', new WorkspaceAspect($workspaceId)); GeneralUtility::setSingletonInstance(Context::class, $context); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) diff --git a/typo3/sysext/extbase/Tests/Unit/Configuration/FrontendConfigurationManagerTest.php b/typo3/sysext/extbase/Tests/Unit/Configuration/FrontendConfigurationManagerTest.php index ca30813fedc6ddb6632cf4febb680d671cf5c1cf..b2d72e6b9830f55b829b2e79b9de90858c818a50 100644 --- a/typo3/sysext/extbase/Tests/Unit/Configuration/FrontendConfigurationManagerTest.php +++ b/typo3/sysext/extbase/Tests/Unit/Configuration/FrontendConfigurationManagerTest.php @@ -439,7 +439,7 @@ final class FrontendConfigurationManagerTest extends UnitTestCase #[Test] public function getTypoScriptSetupReturnsSetupFromRequest(): void { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray(['foo' => 'bar']); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())->withAttribute('frontend.typoscript', $frontendTypoScript); self::assertEquals(['foo' => 'bar'], $this->frontendConfigurationManager->_call('getTypoScriptSetup')); @@ -448,7 +448,7 @@ final class FrontendConfigurationManagerTest extends UnitTestCase #[Test] public function getPluginConfigurationReturnsEmptyArrayIfNoPluginConfigurationWasFound(): void { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray(['foo' => 'bar']); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())->withAttribute('frontend.typoscript', $frontendTypoScript); $expectedResult = []; @@ -479,7 +479,7 @@ final class FrontendConfigurationManagerTest extends UnitTestCase ], ]; $this->mockTypoScriptService->method('convertTypoScriptArrayToPlainArray')->with($testSettings)->willReturn($testSettingsConverted); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray($testSetup); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())->withAttribute('frontend.typoscript', $frontendTypoScript); $expectedResult = [ @@ -510,7 +510,7 @@ final class FrontendConfigurationManagerTest extends UnitTestCase ], ]; $this->mockTypoScriptService->method('convertTypoScriptArrayToPlainArray')->with($testSettings)->willReturn($testSettingsConverted); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray($testSetup); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())->withAttribute('frontend.typoscript', $frontendTypoScript); $expectedResult = [ @@ -577,7 +577,7 @@ final class FrontendConfigurationManagerTest extends UnitTestCase self::assertSame($settings, $arguments[0]); return $arguments[1]; }); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray($testSetup); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())->withAttribute('frontend.typoscript', $frontendTypoScript); $expectedResult = [ diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/CObjectViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/CObjectViewHelperTest.php index 7dd600abd23896298a827d57391554e85b829cc1..122e5ca601d13fc6e3e49aa6a7410fd37500bb44 100644 --- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/CObjectViewHelperTest.php +++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/CObjectViewHelperTest.php @@ -45,13 +45,6 @@ final class CObjectViewHelperTest extends FunctionalTestCase ); } - protected function tearDown(): void - { - // @todo: When a FE sub request throws an exception, as some of the below test do, TSFE does NOT release locks properly! - $GLOBALS['TSFE']->releaseLocks(); - parent::tearDown(); - } - #[Test] public function viewHelperAcceptsDataParameter(): void { diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/FormViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/FormViewHelperTest.php index 91143b603ca2e7e372e6f2fd97ecf249676089d3..b92ce8c982e385616af0ff1f61c0c5ef041a2b25 100644 --- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/FormViewHelperTest.php +++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/FormViewHelperTest.php @@ -244,7 +244,7 @@ final class FormViewHelperTest extends FunctionalTestCase protected function createRequest(): ServerRequestInterface { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupTree(new RootNode()); $frontendTypoScript->setSetupArray([]); $frontendTypoScript->setConfigArray([]); diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/ActionViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/ActionViewHelperTest.php index 79851ed366e98a5a2d70ec02e24db3a2f7754d19..b00a67d148e75f9bf824ef378e49ac778b2e198d 100644 --- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/ActionViewHelperTest.php +++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/ActionViewHelperTest.php @@ -136,7 +136,7 @@ final class ActionViewHelperTest extends FunctionalTestCase 'test', $this->buildSiteConfiguration(1, '/'), ); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setConfigArray($frontendTypoScriptConfigArray); $request = new ServerRequest(); $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE); @@ -236,7 +236,7 @@ final class ActionViewHelperTest extends FunctionalTestCase 'test', $this->buildSiteConfiguration(1, '/'), ); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $frontendTypoScript->setConfigArray($frontendTypoScriptConfigArray); $extbaseRequestParameters = new ExtbaseRequestParameters(); diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/PageViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/PageViewHelperTest.php index c5c73ea75a6468ae3291fa2bf5ea2fed892225e6..6148c85312a2fea5f24cba696317a5aa274c788f 100644 --- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/PageViewHelperTest.php +++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/PageViewHelperTest.php @@ -242,7 +242,7 @@ final class PageViewHelperTest extends FunctionalTestCase 'test', $this->buildSiteConfiguration(1, '/'), ); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $frontendTypoScript->setConfigArray($frontendTypoScriptConfigArray); $request = new ServerRequest('http://localhost/typo3/'); @@ -267,7 +267,7 @@ final class PageViewHelperTest extends FunctionalTestCase 'test', $this->buildSiteConfiguration(1, '/'), ); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray($frontendTypoScriptSetupArray); $frontendTypoScript->setConfigArray([]); $request = new ServerRequest(); diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/ActionViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/ActionViewHelperTest.php index 836331b473601d68b074c180a94a9ff9a0db236e..d102abdb080668ae7872d1bc6546ffbc9123109d 100644 --- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/ActionViewHelperTest.php +++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/ActionViewHelperTest.php @@ -187,7 +187,7 @@ final class ActionViewHelperTest extends FunctionalTestCase 'test', $this->buildSiteConfiguration(1, '/'), ); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $frontendTypoScript->setConfigArray([]); $extbaseRequestParameters = new ExtbaseRequestParameters(); diff --git a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php index f93bf5418c048426af33b6654d8b437c1780d29e..46e0d963c3bf7b3f7d1f67a8012bc5c2c8f27643 100644 --- a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php +++ b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php @@ -24,49 +24,25 @@ use Psr\Log\LogLevel; use TYPO3\CMS\Backend\FrontendBackendUserAuthentication; use TYPO3\CMS\Core\Cache\CacheManager; use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; -use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Domain\Repository\PageRepository; -use TYPO3\CMS\Core\Error\Http\StatusException; -use TYPO3\CMS\Core\Http\PropagateResponseException; use TYPO3\CMS\Core\Localization\LanguageService; use TYPO3\CMS\Core\Localization\LanguageServiceFactory; use TYPO3\CMS\Core\Localization\Locale; use TYPO3\CMS\Core\Localization\Locales; -use TYPO3\CMS\Core\Locking\ResourceMutex; use TYPO3\CMS\Core\Page\AssetCollector; use TYPO3\CMS\Core\Page\PageRenderer; use TYPO3\CMS\Core\PageTitle\PageTitleProviderManager; -use TYPO3\CMS\Core\Routing\PageArguments; use TYPO3\CMS\Core\TimeTracker\TimeTracker; use TYPO3\CMS\Core\Type\DocType; -use TYPO3\CMS\Core\TypoScript\AST\Merger\SetupConfigMerger; -use TYPO3\CMS\Core\TypoScript\AST\Node\ChildNode; -use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode; -use TYPO3\CMS\Core\TypoScript\FrontendTypoScript; -use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateTreeBuilder; -use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser; -use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser; -use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeAstBuilderVisitor; -use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionIncludeListAccumulatorVisitor; -use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionMatcherVisitor; -use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeSetupConditionConstantSubstitutionVisitor; -use TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Core\Utility\HttpUtility; use TYPO3\CMS\Core\Utility\PathUtility; use TYPO3\CMS\Frontend\Cache\CacheLifetimeCalculator; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent; use TYPO3\CMS\Frontend\Event\AfterCachedPageIsPersistedEvent; -use TYPO3\CMS\Frontend\Event\BeforePageCacheIdentifierIsHashedEvent; -use TYPO3\CMS\Frontend\Event\ModifyTypoScriptConfigEvent; -use TYPO3\CMS\Frontend\Event\ModifyTypoScriptConstantsEvent; -use TYPO3\CMS\Frontend\Event\ShouldUseCachedPageDataIfAvailableEvent; -use TYPO3\CMS\Frontend\Page\CacheHashCalculator; -use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons; /** * Main controller class of the TypoScript based frontend. @@ -147,14 +123,21 @@ class TypoScriptFrontendController implements LoggerAwareInterface /** * Set if cached content was fetched from the cache. + * + * @internal Used by a middleware. Will be removed. */ - protected bool $pageContentWasLoadedFromCache = false; + public bool $pageContentWasLoadedFromCache = false; /** * Set to the expiry time of cached content + * @internal Used by a middleware. Will be removed. + */ + public int $cacheExpires = 0; + + /** + * @internal Used by a middleware. Will be removed. */ - protected int $cacheExpires = 0; - private int $cacheGenerated = 0; + public int $cacheGenerated = 0; /** * This hash is unique to the page id, involved TS templates, TS condition verdicts, and @@ -244,14 +227,13 @@ class TypoScriptFrontendController implements LoggerAwareInterface protected LanguageService $languageService; - /** - * @internal Internal locking. May move to a middleware soon. - */ - public ?ResourceMutex $lock = null; - protected ?PageRenderer $pageRenderer = null; protected FrontendInterface $pageCache; - protected array $pageCacheTags = []; + + /** + * @internal Used by a middleware. Will be removed. + */ + public array $pageCacheTags = []; /** * Content type HTTP header being sent in the request. @@ -268,8 +250,9 @@ class TypoScriptFrontendController implements LoggerAwareInterface /** * If debug mode is enabled, this contains the information if a page is fetched from cache, * and sent as HTTP Response Header. + * @internal Used by a middleware. Will be removed. */ - protected ?string $debugInformationHeader = null; + public ?string $debugInformationHeader = null; /** * @internal Extensions should usually not need to create own instances of TSFE @@ -323,459 +306,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface $this->contentType = $contentType; } - /** - * Fetches the arguments that are relevant for creating the hash base from the given PageArguments object. - * Excluded parameters are not taken into account when calculating the hash base. - */ - protected function getRelevantParametersForCachingFromPageArguments(PageArguments $pageArguments): array - { - $queryParams = $pageArguments->getDynamicArguments(); - if (!empty($queryParams) && ($pageArguments->getArguments()['cHash'] ?? false)) { - $queryParams['id'] = $pageArguments->getPageId(); - return GeneralUtility::makeInstance(CacheHashCalculator::class) - ->getRelevantParameters(HttpUtility::buildQueryString($queryParams)); - } - return []; - } - - /** - * This is a central and quite early method called by PrepareTypoScriptFrontendRendering middleware: - * This code is *always* executed for *every* frontend call if a general page rendering has to be done, - * if there is no early redirect or eid call or similar. - * - * The goal is to calculate dependencies up to a point to see if a possible page cache can be used, - * and to prepare TypoScript as far as really needed. - * - * @throws PropagateResponseException - * @throws StatusException - * - * @internal This method may vanish from TypoScriptFrontendController without further notice. - * @todo: This method is typically called by PrepareTypoScriptFrontendRendering middleware. - * However, the RedirectService of (earlier) ext:redirects RedirectHandler middleware - * calls this as well. We may want to put this code into some helper class, reduce class - * state as much as possible and carry really needed state as request attributes around?! - */ - public function getFromCache(ServerRequestInterface $request): FrontendTypoScript - { - $pageInformation = $request->getAttribute('frontend.page.information'); - $rootLine = $pageInformation->getRootLine(); - $localRootline = $pageInformation->getLocalRootLine(); - $sysTemplateRows = $pageInformation->getSysTemplateRows(); - $serializedSysTemplateRows = serialize($sysTemplateRows); - $site = $request->getAttribute('site'); - $isCachingAllowed = $request->getAttribute('frontend.cache.instruction')->isCachingAllowed(); - - $tokenizer = new LossyTokenizer(); - $treeBuilder = GeneralUtility::makeInstance(SysTemplateTreeBuilder::class); - $includeTreeTraverser = new IncludeTreeTraverser(); - $includeTreeTraverserConditionVerdictAware = new ConditionVerdictAwareIncludeTreeTraverser(); - $cacheManager = GeneralUtility::makeInstance(CacheManager::class); - /** @var PhpFrontend|null $typoscriptCache */ - $typoscriptCache = null; - if ($isCachingAllowed) { - // disableCache() might have been called by earlier middlewares. This means we don't do fancy cache - // stuff, calculate full TypoScript and don't get() from nor set() to typoscript and page cache. - /** @var PhpFrontend|null $typoscriptCache */ - $typoscriptCache = $cacheManager->getCache('typoscript'); - } - - $topDownRootLine = $rootLine; - ksort($topDownRootLine); - $expressionMatcherVariables = [ - 'request' => $request, - 'pageId' => $pageInformation->getId(), - // @todo We're using the full page row here to provide all necessary fields (e.g. "backend_layout"), - // which are currently not included in the rows, RootlineUtility provides by default. We might - // want to switch to $pageInformation->getRootLine() as soon as it contains all fields. - 'page' => $pageInformation->getPageRecord(), - 'fullRootLine' => $topDownRootLine, - 'localRootLine' => $localRootline, - 'site' => $site, - 'siteLanguage' => $request->getAttribute('language'), - 'tsfe' => $this, - ]; - - // We *always* need the TypoScript constants, one way or the other: Setup conditions can use constants, - // so we need the constants to substitute their values within setup conditions. - $constantConditionIncludeListCacheIdentifier = 'constant-condition-include-list-' . sha1($serializedSysTemplateRows); - $constantConditionList = []; - $constantsAst = new RootNode(); - $flatConstants = []; - $serializedConstantConditionList = ''; - $gotConstantFromCache = false; - if ($isCachingAllowed && $constantConditionIncludeTree = $typoscriptCache->require($constantConditionIncludeListCacheIdentifier)) { - // We got the flat list of all constants conditions for this TypoScript combination from cache. Good. We traverse - // this list to calculate "current" condition verdicts. With a hash of this list together with a hash of the - // TypoScript sys_templates, we try to retrieve the full constants TypoScript from cache. - $conditionMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class); - $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables); - // It does not matter if we use IncludeTreeTraverser or ConditionVerdictAwareIncludeTreeTraverser here: - // Condition list is flat, not nested. IncludeTreeTraverser has an if() less, so we use that one. - $includeTreeTraverser->traverse($constantConditionIncludeTree, [$conditionMatcherVisitor]); - $constantConditionList = $conditionMatcherVisitor->getConditionListWithVerdicts(); - // Needed for cache identifier calculations. Put into a variable here to not serialize multiple times. - $serializedConstantConditionList = serialize($constantConditionList); - $constantCacheEntryIdentifier = 'constant-' . sha1($serializedSysTemplateRows . $serializedConstantConditionList); - $constantsCacheEntry = $typoscriptCache->require($constantCacheEntryIdentifier); - if (is_array($constantsCacheEntry)) { - $constantsAst = $constantsCacheEntry['ast']; - $flatConstants = $constantsCacheEntry['flatConstants']; - $gotConstantFromCache = true; - } - } - if (!$isCachingAllowed || !$gotConstantFromCache) { - // We did not get constants from cache, or are not allowed to use cache. We have to build constants from scratch. - // This means we'll fetch the full constants include tree (from cache if possible), register the condition - // matcher and register the AST builder and traverse include tree to retrieve constants AST and calculate - // 'flat constants' from it. Both are cached if allowed afterwards for the 'if' above to kick in next time. - $constantIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $tokenizer, $site, $typoscriptCache); - $conditionMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class); - $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables); - $includeTreeTraverserConditionVerdictAwareVisitors = []; - $includeTreeTraverserConditionVerdictAwareVisitors[] = $conditionMatcherVisitor; - $constantAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeAstBuilderVisitor::class); - $includeTreeTraverserConditionVerdictAwareVisitors[] = $constantAstBuilderVisitor; - // We must use ConditionVerdictAwareIncludeTreeTraverser here: This one does not walk into - // children for not matching conditions, which is important to create the correct AST. - $includeTreeTraverserConditionVerdictAware->traverse($constantIncludeTree, $includeTreeTraverserConditionVerdictAwareVisitors); - $constantsAst = $constantAstBuilderVisitor->getAst(); - // @internal Dispatch an experimental event allowing listeners to still change the constants AST, - // to for instance implement nested constants if really needed. Note this event may change - // or vanish later without further notice. - $constantsAst = GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch(new ModifyTypoScriptConstantsEvent($constantsAst))->getConstantsAst(); - $flatConstants = $constantsAst->flatten(); - if ($isCachingAllowed) { - // We are allowed to cache and can create both the full list of conditions, plus the constant AST and flat constant - // list cache entry. To do that, we need all (!) conditions, but the above ConditionVerdictAwareIncludeTreeTraverser - // did not find nested conditions if an upper condition did not match. We thus have to traverse include tree a - // second time with the IncludeTreeTraverser that does traverse into not matching conditions as well. - $includeTreeTraverserVisitors = []; - $conditionMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class); - $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables); - $includeTreeTraverserVisitors[] = $conditionMatcherVisitor; - $constantConditionIncludeListAccumulatorVisitor = new IncludeTreeConditionIncludeListAccumulatorVisitor(); - $includeTreeTraverserVisitors[] = $constantConditionIncludeListAccumulatorVisitor; - $includeTreeTraverser->traverse($constantIncludeTree, $includeTreeTraverserVisitors); - $constantConditionList = $conditionMatcherVisitor->getConditionListWithVerdicts(); - // Needed for cache identifier calculations. Put into a variable here to not serialize multiple times. - $serializedConstantConditionList = serialize($constantConditionList); - $typoscriptCache->set($constantConditionIncludeListCacheIdentifier, 'return unserialize(\'' . addcslashes(serialize($constantConditionIncludeListAccumulatorVisitor->getConditionIncludes()), '\'\\') . '\');'); - $constantCacheEntryIdentifier = 'constant-' . sha1($serializedSysTemplateRows . $serializedConstantConditionList); - $typoscriptCache->set($constantCacheEntryIdentifier, 'return unserialize(\'' . addcslashes(serialize(['ast' => $constantsAst, 'flatConstants' => $flatConstants]), '\'\\') . '\');'); - } - } - - $frontendTypoScript = new FrontendTypoScript($constantsAst, $flatConstants); - - // Next step: We have constants and fetch the setup include tree now. We then calculate setup condition verdicts - // and set the constants to allow substitution of constants within conditions. Next, we traverse include tree - // to calculate conditions verdicts and gather them along the way. A hash of these conditions with their verdicts - // is then part of the page cache identifier hash: When a condition on a page creates a different result, the hash - // is different from an existing page cache entry and a new one is created later. - $setupConditionIncludeListCacheIdentifier = 'setup-condition-include-list-' . sha1($serializedSysTemplateRows . $serializedConstantConditionList); - $setupConditionList = []; - $gotSetupConditionsFromCache = false; - if ($isCachingAllowed && $setupConditionIncludeTree = $typoscriptCache->require($setupConditionIncludeListCacheIdentifier)) { - // We got the flat list of all setup conditions for this TypoScript combination from cache. Good. We traverse - // this list to calculate "current" condition verdicts, which we need as hash to be part of page cache identifier. - $includeTreeTraverserVisitors = []; - $setupConditionConstantSubstitutionVisitor = new IncludeTreeSetupConditionConstantSubstitutionVisitor(); - $setupConditionConstantSubstitutionVisitor->setFlattenedConstants($flatConstants); - $includeTreeTraverserVisitors[] = $setupConditionConstantSubstitutionVisitor; - $setupMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class); - $setupMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables); - $includeTreeTraverserVisitors[] = $setupMatcherVisitor; - // It does not matter if we use IncludeTreeTraverser or ConditionVerdictAwareIncludeTreeTraverser here: - // Condition list is flat, not nested. IncludeTreeTraverser has an if() less, so we use that one. - $includeTreeTraverser->traverse($setupConditionIncludeTree, $includeTreeTraverserVisitors); - $setupConditionList = $setupMatcherVisitor->getConditionListWithVerdicts(); - $gotSetupConditionsFromCache = true; - } - $setupIncludeTree = null; - if (!$isCachingAllowed || !$gotSetupConditionsFromCache) { - // We did not get setup condition list from cache, or are not allowed to use cache. We have to build setup - // condition list from scratch. This means we'll fetch the full setup include tree (from cache if possible), - // register the constant substitution visitor, and register condition matcher and register the condition - // accumulator visitor. - $setupIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $tokenizer, $site, $typoscriptCache); - $includeTreeTraverserVisitors = []; - $setupConditionConstantSubstitutionVisitor = new IncludeTreeSetupConditionConstantSubstitutionVisitor(); - $setupConditionConstantSubstitutionVisitor->setFlattenedConstants($flatConstants); - $includeTreeTraverserVisitors[] = $setupConditionConstantSubstitutionVisitor; - $setupMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class); - $setupMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables); - $includeTreeTraverserVisitors[] = $setupMatcherVisitor; - $setupConditionIncludeListAccumulatorVisitor = new IncludeTreeConditionIncludeListAccumulatorVisitor(); - $includeTreeTraverserVisitors[] = $setupConditionIncludeListAccumulatorVisitor; - // It is important we use IncludeTreeTraverser here: We to have the condition verdicts of *all* conditions, plus - // want to accumulate all of them. The ConditionVerdictAwareIncludeTreeTraverser wouldn't walk into nested - // conditions if an upper one does not match. - $includeTreeTraverser->traverse($setupIncludeTree, $includeTreeTraverserVisitors); - $setupConditionList = $setupMatcherVisitor->getConditionListWithVerdicts(); - $typoscriptCache?->set($setupConditionIncludeListCacheIdentifier, 'return unserialize(\'' . addcslashes(serialize($setupConditionIncludeListAccumulatorVisitor->getConditionIncludes()), '\'\\') . '\');'); - } - - // We now gathered everything to calculate the page cache identifier: It depends on sys_template rows, the calculated - // constant condition verdicts, the setup condition verdicts, plus various not TypoScript related details like - // obviously the page id. - $this->lock = GeneralUtility::makeInstance(ResourceMutex::class); - $this->newHash = $this->createHashBase($request, $sysTemplateRows, $constantConditionList, $setupConditionList); - if ($isCachingAllowed) { - if ($this->shouldAcquireCacheData($request)) { - // Try to get a page cache row. - $pageCacheRow = $this->pageCache->get($this->newHash); - if (!is_array($pageCacheRow)) { - // Nothing in the cache, we acquire an exclusive lock now. - // There are two scenarios when locking: We're either the first process acquiring this lock. This means we'll - // "immediately" get it and can continue with page rendering. Or, another process acquired the lock already. In - // this case, the below call will wait until the lock is released again. The other process then probably wrote - // a page cache entry, which we can use. - // To handle the second case - if our process had to wait for another one creating the content for us - we - // simply query the page cache again to see if there is a page cache now. - $hadToWaitForLock = $this->lock->acquireLock('pages', $this->newHash); - // From this point on we're the only one working on that page. - if ($hadToWaitForLock) { - // Query the cache again to see if the data is there meanwhile: We did not get the lock - // immediately, chances are high the other process created a page cache for us. - // There is a small chance the other process actually pageCache->set() the content, - // but pageCache->get() still returns false, for instance when a database returned "done" - // for the INSERT, but SELECT still does not return the new row - may happen in multi-head - // DB instances, and with some other distributed cache backends as well. The worst that - // can happen here is the page generation is done too often, which we accept as trade-off. - $pageCacheRow = $this->pageCache->get($this->newHash); - if (is_array($pageCacheRow)) { - // We have the content, some other process did the work for us, release our lock again. - $this->releaseLocks(); - } - } - // We keep the lock set, because we are the ones generating the page now and filling the cache. - // This indicates that we have to release the lock later in releaseLocks()! - } - if (is_array($pageCacheRow)) { - $this->config['INTincScript'] = $pageCacheRow['INTincScript']; - $this->config['INTincScript_ext'] = $pageCacheRow['INTincScript_ext']; - $this->config['pageTitleCache'] = $pageCacheRow['pageTitleCache']; - $this->content = $pageCacheRow['content']; - $this->contentType = $pageCacheRow['contentType']; - $this->cacheExpires = $pageCacheRow['expires']; - $this->pageCacheTags = $pageCacheRow['cacheTags']; - $this->cacheGenerated = $pageCacheRow['tstamp']; - $this->pageContentWasLoadedFromCache = true; - } - } else { - // User forced page cache rebuilding. Get a lock for the page content so other processes can't interfere. - $this->lock->acquireLock('pages', $this->newHash); - } - } else { - // Caching is not allowed. We'll rebuild the page. Lock this. - $this->lock->acquireLock('pages', $this->newHash); - } - - $setupRawConfigAst = null; - if (!$isCachingAllowed || !$this->pageContentWasLoadedFromCache || $this->isINTincScript()) { - // We don't need the full setup AST in many cached scenarios. However, if caching is not allowed, if no page - // cache entry could be loaded or if the page cache entry has _INT object, then we still need the full setup AST. - // If there is "just" an _INT object, we can use a possible cache entry for the setup AST, which speeds up _INT - // parsing quite a bit. In other cases we calculate full setup AST and cache it if allowed. - $setupTypoScriptCacheIdentifier = 'setup-' . sha1($serializedSysTemplateRows . $serializedConstantConditionList . serialize($setupConditionList)); - $gotSetupFromCache = false; - if ($isCachingAllowed) { - // We need AST, but we are allowed to potentially get it from cache. - if ($setupTypoScriptCache = $typoscriptCache->require($setupTypoScriptCacheIdentifier)) { - $frontendTypoScript->setSetupTree($setupTypoScriptCache['ast']); - $frontendTypoScript->setSetupArray($setupTypoScriptCache['array']); - $setupRawConfigAst = $setupTypoScriptCache['ast']->getChildByName('config'); - $gotSetupFromCache = true; - } - } - if (!$isCachingAllowed || !$gotSetupFromCache) { - // We need AST and couldn't get it from cache or are now allowed to. We thus need the full setup - // IncludeTree, which we can get from cache again if allowed, or is calculated a-new if not. - if (!$isCachingAllowed || $setupIncludeTree === null) { - $setupIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $tokenizer, $site, $typoscriptCache); - } - $includeTreeTraverserConditionVerdictAwareVisitors = []; - $setupConditionConstantSubstitutionVisitor = new IncludeTreeSetupConditionConstantSubstitutionVisitor(); - $setupConditionConstantSubstitutionVisitor->setFlattenedConstants($flatConstants); - $includeTreeTraverserConditionVerdictAwareVisitors[] = $setupConditionConstantSubstitutionVisitor; - $setupMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class); - $setupMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables); - $includeTreeTraverserConditionVerdictAwareVisitors[] = $setupMatcherVisitor; - $setupAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeAstBuilderVisitor::class); - $setupAstBuilderVisitor->setFlatConstants($flatConstants); - $includeTreeTraverserConditionVerdictAwareVisitors[] = $setupAstBuilderVisitor; - $includeTreeTraverserConditionVerdictAware->traverse($setupIncludeTree, $includeTreeTraverserConditionVerdictAwareVisitors); - $setupAst = $setupAstBuilderVisitor->getAst(); - // @todo: It would be good to actively remove 'config' from AST and array here - // to prevent people from using the unmerged variant. The same - // is already done for the determined PAGE 'config' below. This works, but - // is currently blocked by functional tests that assert details? - $setupRawConfigAst = $setupAst->getChildByName('config'); - // $setupAst->removeChildByName('config'); - $frontendTypoScript->setSetupTree($setupAst); - $frontendTypoScript->setSetupArray($setupAst->toArray()); - - if ($isCachingAllowed) { - // Write cache entry for AST and its array representation, we're allowed to do it. - $typoscriptCache->set($setupTypoScriptCacheIdentifier, 'return unserialize(\'' . addcslashes(serialize(['ast' => $setupAst, 'array' => $setupAst->toArray()]), '\'\\') . '\');'); - } - } - } - - /** @var PageArguments $pageArguments */ - $pageArguments = $request->getAttribute('routing'); - $type = $pageArguments->getPageType(); - - $gotSetupConfigFromCache = false; - $setupConfigTypoScriptCacheIdentifier = 'setup-config-' . sha1($serializedSysTemplateRows . $serializedConstantConditionList . serialize($setupConditionList) . $type); - if ($isCachingAllowed) { - if ($setupConfigTypoScriptCache = $typoscriptCache->require($setupConfigTypoScriptCacheIdentifier)) { - $frontendTypoScript->setConfigTree($setupConfigTypoScriptCache['ast']); - $frontendTypoScript->setConfigArray($setupConfigTypoScriptCache['array']); - $gotSetupConfigFromCache = true; - } - } - - if (!$isCachingAllowed || !$this->pageContentWasLoadedFromCache || !$gotSetupConfigFromCache || $this->isINTincScript()) { - $setupAst = $frontendTypoScript->getSetupTree(); - $rawSetupPageNodeFromType = null; - foreach ($setupAst->getNextChild() as $potentialPageNode) { - if ($potentialPageNode->getValue() === 'PAGE') { - // @todo: We could potentially remove *all* PAGE objects from setup here. This prevents people - // from accessing other ones than the determined one in $frontendTypoScript->getSetupArray(). - $typeNumChild = $potentialPageNode->getChildByName('typeNum'); - if ($typeNumChild && $type === $typeNumChild->getValue()) { - $rawSetupPageNodeFromType = $potentialPageNode; - break; - } - if (!$typeNumChild && $type === '0') { - // The first PAGE node that has no typeNum is considered '0' automatically. - $rawSetupPageNodeFromType = $potentialPageNode; - break; - } - } - } - if (!$rawSetupPageNodeFromType) { - $this->logger->error('No page configured for type={type}. There is no TypoScript object of type PAGE with typeNum={type}.', ['type' => $type]); - $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction( - $request, - 'No page configured for type=' . $type . '.', - ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_CONFIGURED] - ); - throw new PropagateResponseException($response, 1533931374); - } - $setupPageAst = new RootNode(); - foreach ($rawSetupPageNodeFromType->getNextChild() as $child) { - $setupPageAst->addChild($child); - } - if (!$gotSetupConfigFromCache) { - $mergedSetupConfigAst = (new SetupConfigMerger())->merge($setupRawConfigAst, $setupPageAst->getChildByName('config')); - - if ($mergedSetupConfigAst->getChildByName('absRefPrefix') === null) { - // Make sure config.absRefPrefix is set, fallback to 'auto'. - $absRefPrefixNode = new ChildNode('absRefPrefix'); - $absRefPrefixNode->setValue('auto'); - $mergedSetupConfigAst->addChild($absRefPrefixNode); - } - if ($mergedSetupConfigAst->getChildByName('doctype') === null) { - // Make sure config.doctype is set, fallback to 'html5'. - $doctypeNode = new ChildNode('doctype'); - $doctypeNode->setValue('html5'); - $mergedSetupConfigAst->addChild($doctypeNode); - } - $mergedSetupConfigAst = GeneralUtility::makeInstance(EventDispatcherInterface::class) - ->dispatch(new ModifyTypoScriptConfigEvent($request, $setupAst, $mergedSetupConfigAst))->getConfigTree(); - $frontendTypoScript->setConfigTree($mergedSetupConfigAst); - $setupConfigArray = $mergedSetupConfigAst->toArray(); - $frontendTypoScript->setConfigArray($setupConfigArray); - if ($isCachingAllowed) { - $typoscriptCache->set($setupConfigTypoScriptCacheIdentifier, 'return unserialize(\'' . addcslashes(serialize(['ast' => $mergedSetupConfigAst, 'array' => $setupConfigArray]), '\'\\') . '\');'); - } - } - if (!$isCachingAllowed || !$this->pageContentWasLoadedFromCache || $this->isINTincScript()) { - // Remove "page.config" to prevent people from working with the not merged variant. - $setupPageAst->removeChildByName('config'); - $frontendTypoScript->setPageTree($setupPageAst); - $frontendTypoScript->setPageArray($setupPageAst->toArray()); - } - } - - $setupConfigAst = $frontendTypoScript->getConfigTree(); - - if ($this->pageContentWasLoadedFromCache - && ($setupConfigAst->getChildByName('debug')?->getValue() || !empty($GLOBALS['TYPO3_CONF_VARS']['FE']['debug'])) - ) { - // Prepare X-TYPO3-Debug-Cache HTTP header - $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy']; - $timeFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm']; - $this->debugInformationHeader = 'Cached page generated ' . date($dateFormat . ' ' . $timeFormat, $this->cacheGenerated) - . '. Expires ' . date($dateFormat . ' ' . $timeFormat, $this->cacheExpires); - } - - if ($setupConfigAst->getChildByName('no_cache')?->getValue()) { - // Disable cache if config.no_cache is set! - $cacheInstruction = $request->getAttribute('frontend.cache.instruction'); - $cacheInstruction->disableCache('EXT:frontend: Disabled cache due to TypoScript "config.no_cache = 1"'); - } - - return $frontendTypoScript; - } - - /** - * Detecting if shift-reload has been clicked. - * This option will have no effect if re-generation of page happens by other reasons (for instance that the page is not in cache yet). - * Also, a backend user MUST be logged in for the shift-reload to be detected due to DoS-attack-security reasons. - * - * @return bool If shift-reload in client browser has been clicked, disable getting cached page and regenerate the page content. - */ - protected function shouldAcquireCacheData(ServerRequestInterface $request): bool - { - // Trigger event for possible by-pass of requiring of page cache. - $event = new ShouldUseCachedPageDataIfAvailableEvent($request, $this, $request->getAttribute('frontend.cache.instruction')->isCachingAllowed()); - GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch($event); - return $event->shouldUseCachedPageData(); - } - - /** - * This creates a hash used as page cache entry identifier and as page generation lock. - * When multiple requests try to render the same page that will result in the same page cache entry, - * this lock allows creation by one request which typically puts the result into page cache, while - * the other requests wait until this finished and re-use the result. - * - * This hash is unique to the TS template and constant and setup condition verdict, - * the variables ->id, ->type, list of frontend user groups, mount points and cHash array. - * - * @return string Page cache entry identifier also used as page generation lock - */ - protected function createHashBase(ServerRequestInterface $request, array $sysTemplateRows, array $constantConditionList, array $setupConditionList): string - { - $pageInformation = $request->getAttribute('frontend.page.information'); - $pageId = $pageInformation->getId(); - $site = $request->getAttribute('site'); - $pageArguments = $request->getAttribute('routing'); - $pageCacheIdentifierParameters = [ - 'id' => $pageId, - 'type' => $pageArguments->getPageType(), - 'groupIds' => implode(',', $this->context->getAspect('frontend.user')->getGroupIds()), - 'MP' => $pageInformation->getMountPoint(), - 'site' => $site->getIdentifier(), - // Ensure the language base is used for the hash base calculation as well, otherwise TypoScript and page-related rendering - // is not cached properly as we don't have any language-specific conditions anymore - 'siteBase' => (string)$request->getAttribute('language', $site->getDefaultLanguage())->getBase(), - // additional variation trigger for static routes - 'staticRouteArguments' => $pageArguments->getStaticArguments(), - // dynamic route arguments (if route was resolved) - 'dynamicArguments' => $this->getRelevantParametersForCachingFromPageArguments($pageArguments), - 'sysTemplateRows' => $sysTemplateRows, - 'constantConditionList' => $constantConditionList, - 'setupConditionList' => $setupConditionList, - ]; - $pageCacheIdentifierParameters = GeneralUtility::makeInstance(EventDispatcherInterface::class) - ->dispatch(new BeforePageCacheIdentifierIsHashedEvent($request, $pageCacheIdentifierParameters)) - ->getPageCacheIdentifierParameters(); - return $pageId . '_' . sha1(serialize($pageCacheIdentifierParameters)); - } - /** * Returns TRUE if the page content should be generated. * @@ -1467,15 +997,12 @@ class TypoScriptFrontendController implements LoggerAwareInterface } /** - * Release the page specific lock. - * - * @throws \InvalidArgumentException - * @throws \RuntimeException * @internal + * @todo: Remove when not called in TF anymore. */ public function releaseLocks(): void { - $this->lock?->releaseLock('pages'); + // noop } /** diff --git a/typo3/sysext/frontend/Classes/Event/ShouldUseCachedPageDataIfAvailableEvent.php b/typo3/sysext/frontend/Classes/Event/ShouldUseCachedPageDataIfAvailableEvent.php index b5602b3d7f2db76b3d94325d71a38cc790f85f8c..6b31e7e790f2dd22be43f5c3e685a33be92016a2 100644 --- a/typo3/sysext/frontend/Classes/Event/ShouldUseCachedPageDataIfAvailableEvent.php +++ b/typo3/sysext/frontend/Classes/Event/ShouldUseCachedPageDataIfAvailableEvent.php @@ -28,13 +28,15 @@ final class ShouldUseCachedPageDataIfAvailableEvent { public function __construct( private readonly ServerRequestInterface $request, - private readonly TypoScriptFrontendController $controller, private bool $shouldUseCachedPageData ) {} + /** + * @todo: deprecate + */ public function getController(): TypoScriptFrontendController { - return $this->controller; + return $this->request->getAttribute('frontend.controller'); } public function getRequest(): ServerRequestInterface diff --git a/typo3/sysext/frontend/Classes/Http/RequestHandler.php b/typo3/sysext/frontend/Classes/Http/RequestHandler.php index 397cf9cb27d7e62ce21be65c4f3ac0d058fa23dc..3d9379501625b95a44f69bb3a314a1a48fb0a3d9 100644 --- a/typo3/sysext/frontend/Classes/Http/RequestHandler.php +++ b/typo3/sysext/frontend/Classes/Http/RequestHandler.php @@ -158,7 +158,6 @@ class RequestHandler implements RequestHandlerInterface $controller->generatePage_postProcessing($request); $this->timeTracker->pull(); } - $controller->releaseLocks(); // Render non-cached page parts by replacing placeholders which are taken from cache or added during page generation if ($controller->isINTincScript()) { diff --git a/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php b/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php index fd654d806e82a7634cdea29c8bb1317b8996efe1..636de88afdfcc01c1f708eb99870ce1d688e5012 100644 --- a/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php +++ b/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php @@ -22,56 +22,229 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use TYPO3\CMS\Core\TimeTracker\TimeTracker; +use Psr\Log\LoggerInterface; +use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; +use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Locking\ResourceMutex; +use TYPO3\CMS\Core\TypoScript\FrontendTypoScript; +use TYPO3\CMS\Core\TypoScript\FrontendTypoScriptFactory; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\HttpUtility; +use TYPO3\CMS\Frontend\Controller\ErrorController; use TYPO3\CMS\Frontend\Event\AfterTypoScriptDeterminedEvent; +use TYPO3\CMS\Frontend\Event\BeforePageCacheIdentifierIsHashedEvent; +use TYPO3\CMS\Frontend\Event\ShouldUseCachedPageDataIfAvailableEvent; +use TYPO3\CMS\Frontend\Page\CacheHashCalculator; +use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons; /** - * Initialization of TypoScriptFrontendController - * - * Do all necessary preparation steps for rendering + * Initialize TypoScript, get page content from cache if possible, lock + * rendering if needed and create more TypoScript data if needed. * * @internal this middleware might get removed later. */ -final class PrepareTypoScriptFrontendRendering implements MiddlewareInterface +final readonly class PrepareTypoScriptFrontendRendering implements MiddlewareInterface { public function __construct( - private readonly EventDispatcherInterface $eventDispatcher, - private readonly TimeTracker $timeTracker + private EventDispatcherInterface $eventDispatcher, + private FrontendTypoScriptFactory $frontendTypoScriptFactory, + private PhpFrontend $typoScriptCache, + private FrontendInterface $pageCache, + private ResourceMutex $lock, + private Context $context, + private LoggerInterface $logger, + private ErrorController $errorController, ) {} - /** - * Initialize TypoScriptFrontendController to the point right before rendering of the page is triggered - */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $controller = $request->getAttribute('frontend.controller'); + $site = $request->getAttribute('site'); + $sysTemplateRows = $request->getAttribute('frontend.page.information')->getSysTemplateRows(); + $isCachingAllowed = $request->getAttribute('frontend.cache.instruction')->isCachingAllowed(); + + // Create FrontendTypoScript with essential info for page cache identifier + $conditionMatcherVariables = $this->prepareConditionMatcherVariables($request); + $frontendTypoScript = $this->frontendTypoScriptFactory->createSettingsAndSetupConditions( + $site, + $sysTemplateRows, + $conditionMatcherVariables, + $isCachingAllowed ? $this->typoScriptCache : null, + ); - // as long as TSFE throws errors with the global object, this needs to be set, but - // should be removed later-on once TypoScript Condition Matcher is built with the current request object. - $GLOBALS['TYPO3_REQUEST'] = $request; + $isUsingPageCacheAllowed = $this->eventDispatcher + ->dispatch(new ShouldUseCachedPageDataIfAvailableEvent($request, $isCachingAllowed)) + ->shouldUseCachedPageData(); + $pageCacheIdentifier = $this->createPageCacheIdentifier($request, $frontendTypoScript); - $this->timeTracker->push('Get Page from cache'); - // Get from cache. Locks may be acquired here. After this, we should have a valid config-array ready. - $frontendTypoScript = $controller->getFromCache($request); - $this->eventDispatcher->dispatch(new AfterTypoScriptDeterminedEvent($frontendTypoScript)); - $request = $request->withAttribute('frontend.typoscript', $frontendTypoScript); - $this->timeTracker->pull(); + $pageCacheRow = null; + if (!$isUsingPageCacheAllowed) { + // Caching is not allowed. We'll rebuild the page. Lock this. + $this->lock->acquireLock('pages', $pageCacheIdentifier); + } else { + // Try to get a page cache row. + $pageCacheRow = $this->pageCache->get($pageCacheIdentifier); + if (!is_array($pageCacheRow)) { + // Nothing in the cache, we acquire an exclusive lock now. + // There are two scenarios when locking: We're either the first process acquiring this lock. This means we'll + // "immediately" get it and can continue with page rendering. Or, another process acquired the lock already. In + // this case, the below call will wait until the lock is released again. The other process then probably wrote + // a page cache entry, which we can use. + // To handle the second case - if our process had to wait for another one creating the content for us - we + // simply query the page cache again to see if there is a page cache now. + $hadToWaitForLock = $this->lock->acquireLock('pages', $pageCacheIdentifier); + // From this point on we're the only one working on that page. + if ($hadToWaitForLock) { + // Query the cache again to see if the data is there meanwhile: We did not get the lock + // immediately, chances are high the other process created a page cache for us. + // There is a small chance the other process actually pageCache->set() the content, + // but pageCache->get() still returns false, for instance when a database returned "done" + // for the INSERT, but SELECT still does not return the new row - may happen in multi-head + // DB instances, and with some other distributed cache backends as well. The worst that + // can happen here is the page generation is done too often, which we accept as trade-off. + $pageCacheRow = $this->pageCache->get($pageCacheIdentifier); + if (is_array($pageCacheRow)) { + // We have the content, some other process did the work for us, release our lock again. + $this->lock->releaseLock('pages'); + } + } + // Keep the lock set, because we are the ones generating the page now and filling the cache. + } + } - // b/w compat - $controller->config['config'] = $request->getAttribute('frontend.typoscript')->getConfigArray(); - // Set new request which now has the frontend.typoscript attribute - $GLOBALS['TYPO3_REQUEST'] = $request; + $controller = $request->getAttribute('frontend.controller'); + $controller->newHash = $pageCacheIdentifier; + $pageContentWasLoadedFromCache = false; + if (is_array($pageCacheRow)) { + $controller->config['INTincScript'] = $pageCacheRow['INTincScript']; + $controller->config['INTincScript_ext'] = $pageCacheRow['INTincScript_ext']; + $controller->config['pageTitleCache'] = $pageCacheRow['pageTitleCache']; + $controller->content = $pageCacheRow['content']; + $controller->setContentType($pageCacheRow['contentType']); + $controller->cacheExpires = $pageCacheRow['expires']; + $controller->pageCacheTags = $pageCacheRow['cacheTags']; + $controller->cacheGenerated = $pageCacheRow['tstamp']; + $controller->pageContentWasLoadedFromCache = true; + $pageContentWasLoadedFromCache = true; + } + + try { + $needsFullSetup = !$pageContentWasLoadedFromCache || $controller->isINTincScript(); + $pageType = $request->getAttribute('routing')->getPageType(); + $frontendTypoScript = $this->frontendTypoScriptFactory->createSetupConfigOrFullSetup( + $needsFullSetup, + $frontendTypoScript, + $site, + $sysTemplateRows, + $conditionMatcherVariables, + $pageType, + $isCachingAllowed ? $this->typoScriptCache : null, + $request, + ); + if ($needsFullSetup && !$frontendTypoScript->hasPage()) { + $this->logger->error('No page configured for type={type}. There is no TypoScript object of type PAGE with typeNum={type}.', ['type' => $pageType]); + return $this->errorController->internalErrorAction( + $request, + 'No page configured for type=' . $pageType . '.', + ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_CONFIGURED] + ); + } + $setupConfigAst = $frontendTypoScript->getConfigTree(); + if ($pageContentWasLoadedFromCache && ($setupConfigAst->getChildByName('debug')?->getValue() || !empty($GLOBALS['TYPO3_CONF_VARS']['FE']['debug']))) { + // Prepare X-TYPO3-Debug-Cache HTTP header + $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy']; + $timeFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm']; + $controller->debugInformationHeader = 'Cached page generated ' . date($dateFormat . ' ' . $timeFormat, $controller->cacheGenerated) + . '. Expires ' . date($dateFormat . ' ' . $timeFormat, $controller->cacheExpires); + } + if ($setupConfigAst->getChildByName('no_cache')?->getValue()) { + // Disable cache if config.no_cache is set! + $cacheInstruction = $request->getAttribute('frontend.cache.instruction'); + $cacheInstruction->disableCache('EXT:frontend: Disabled cache due to TypoScript "config.no_cache = 1"'); + } + $this->eventDispatcher->dispatch(new AfterTypoScriptDeterminedEvent($frontendTypoScript)); + $request = $request->withAttribute('frontend.typoscript', $frontendTypoScript); - $response = $handler->handle($request); + // b/w compat + $controller->config['config'] = $frontendTypoScript->getConfigArray(); + $GLOBALS['TYPO3_REQUEST'] = $request; - /** - * Release TSFE locks. They have been acquired in the above call to controller->getFromCache(). - * TSFE locks are usually released by the RequestHandler 'final' middleware. - * However, when some middlewares returns early (e.g. Shortcut and MountPointRedirect, - * which both skip inner middlewares), or due to Exceptions, locks still need to be released explicitly. - */ - $controller->releaseLocks(); + $response = $handler->handle($request); + } finally { + // Whatever happens in a below middleware, this finally is called, even when exceptions + // are raised by a lower middleware. This ensures locks are released no matter what. + $this->lock->releaseLock('pages'); + } return $response; } + + /** + * Data available in TypoScript "condition" matching. + */ + private function prepareConditionMatcherVariables(ServerRequestInterface $request): array + { + $pageInformation = $request->getAttribute('frontend.page.information'); + $topDownRootLine = $pageInformation->getRootLine(); + $localRootline = $pageInformation->getLocalRootLine(); + ksort($topDownRootLine); + return [ + 'request' => $request, + 'pageId' => $pageInformation->getId(), + 'page' => $pageInformation->getPageRecord(), + 'fullRootLine' => $topDownRootLine, + 'localRootLine' => $localRootline, + 'site' => $request->getAttribute('site'), + 'siteLanguage' => $request->getAttribute('language'), + 'tsfe' => $request->getAttribute('frontend.controller'), + ]; + } + + /** + * This creates a hash used as page cache entry identifier and as page generation lock. + * When multiple requests try to render the same page that will result in the same page cache entry, + * this lock allows creation by one request which typically puts the result into page cache, while + * the other requests wait until this finished and re-use the result. + */ + private function createPageCacheIdentifier(ServerRequestInterface $request, FrontendTypoScript $frontendTypoScript): string + { + $pageInformation = $request->getAttribute('frontend.page.information'); + $pageId = $pageInformation->getId(); + $pageArguments = $request->getAttribute('routing'); + $site = $request->getAttribute('site'); + + $dynamicArguments = []; + $queryParams = $pageArguments->getDynamicArguments(); + if (!empty($queryParams) && ($pageArguments->getArguments()['cHash'] ?? false)) { + // Fetch arguments relevant for creating the page cache identifier from the PageArguments object. + // Excluded parameters are not taken into account when calculating the hash base. + $queryParams['id'] = $pageArguments->getPageId(); + // @todo: Make CacheHashCalculator and CacheHashConfiguration stateless and get it injected. + $dynamicArguments = GeneralUtility::makeInstance(CacheHashCalculator::class) + ->getRelevantParameters(HttpUtility::buildQueryString($queryParams)); + } + + $pageCacheIdentifierParameters = [ + 'id' => $pageId, + 'type' => $pageArguments->getPageType(), + 'groupIds' => implode(',', $this->context->getAspect('frontend.user')->getGroupIds()), + 'MP' => $pageInformation->getMountPoint(), + 'site' => $site->getIdentifier(), + // Ensure the language base is used for the hash base calculation as well, otherwise TypoScript and page-related rendering + // is not cached properly as we don't have any language-specific conditions anymore + 'siteBase' => (string)$request->getAttribute('language', $site->getDefaultLanguage())->getBase(), + // additional variation trigger for static routes + 'staticRouteArguments' => $pageArguments->getStaticArguments(), + // dynamic route arguments (if route was resolved) + 'dynamicArguments' => $dynamicArguments, + 'sysTemplateRows' => $pageInformation->getSysTemplateRows(), + 'constantConditionList' => $frontendTypoScript->getSettingsConditionList(), + 'setupConditionList' => $frontendTypoScript->getSetupConditionList(), + ]; + $pageCacheIdentifierParameters = $this->eventDispatcher + ->dispatch(new BeforePageCacheIdentifierIsHashedEvent($request, $pageCacheIdentifierParameters)) + ->getPageCacheIdentifierParameters(); + + return $pageId . '_' . hash('xxh3', serialize($pageCacheIdentifierParameters)); + } } diff --git a/typo3/sysext/frontend/Configuration/Services.yaml b/typo3/sysext/frontend/Configuration/Services.yaml index c8ee38a0eeddb521ee4f6bacfccd6506b311dbbc..52305edbb6a709bfa3bc805a60a3a037754af578 100644 --- a/typo3/sysext/frontend/Configuration/Services.yaml +++ b/typo3/sysext/frontend/Configuration/Services.yaml @@ -15,6 +15,11 @@ services: arguments: $cache: '@cache.assets' + TYPO3\CMS\Frontend\Middleware\PrepareTypoScriptFrontendRendering: + arguments: + $pageCache: '@cache.pages' + $typoScriptCache: '@cache.typoscript' + TYPO3\CMS\Frontend\ContentObject\ContentDataProcessor: public: true diff --git a/typo3/sysext/frontend/Tests/Functional/ContentObject/ContentObjectRendererTest.php b/typo3/sysext/frontend/Tests/Functional/ContentObject/ContentObjectRendererTest.php index 22c1db10780b4d150b0ab26550019977bfeac916..14549e716d7a51283a1fd5afc8a8eedf485c306c 100644 --- a/typo3/sysext/frontend/Tests/Functional/ContentObject/ContentObjectRendererTest.php +++ b/typo3/sysext/frontend/Tests/Functional/ContentObject/ContentObjectRendererTest.php @@ -639,7 +639,7 @@ final class ContentObjectRendererTest extends FunctionalTestCase #[Test] public function typolinkReturnsCorrectLinkForSpamEncryptedEmails(array $tsfeConfig, string $linkText, string $parameter, string $expected): void { - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setConfigArray($tsfeConfig); $request = (new ServerRequest())->withAttribute('frontend.typoscript', $frontendTypoScript); $subject = new ContentObjectRenderer(); @@ -874,7 +874,7 @@ final class ContentObjectRendererTest extends FunctionalTestCase { $tsfe = $this->getMockBuilder(TypoScriptFrontendController::class)->disableOriginalConstructor()->getMock(); $subject = new ContentObjectRenderer($tsfe); - $typoScript = new FrontendTypoScript(new RootNode(), []); + $typoScript = new FrontendTypoScript(new RootNode(), [], [], []); $typoScript->setConfigArray([]); $request = $this->getPreparedRequest()->withAttribute('frontend.typoscript', $typoScript); $subject->setRequest($request); @@ -1163,7 +1163,7 @@ And another one'; $typoScriptFrontendController = GeneralUtility::makeInstance(TypoScriptFrontendController::class); $subject = GeneralUtility::makeInstance(ContentObjectRenderer::class, $typoScriptFrontendController); $subject->setCurrentFile($fileReference); - $typoScript = new FrontendTypoScript(new RootNode(), []); + $typoScript = new FrontendTypoScript(new RootNode(), [], [], []); $typoScript->setConfigArray([]); $request = $this->getPreparedRequest()->withAttribute('frontend.typoscript', $typoScript); $subject->setRequest($request); diff --git a/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php b/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php index 343b2f4b8b1d632fdf0ca4e810f7fee12d696e33..745fe16cc51fef58c22b7c26ee862d6d1fbc4997 100644 --- a/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php +++ b/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php @@ -1885,7 +1885,7 @@ final class ContentObjectRendererTest extends UnitTestCase { $this->expectException(\LogicException::class); $this->expectExceptionCode(1414513947); - $typoScript = new FrontendTypoScript(new RootNode(), []); + $typoScript = new FrontendTypoScript(new RootNode(), [], [], []); $typoScript->setConfigArray([]); $request = (new ServerRequest())->withAttribute('frontend.typoscript', $typoScript); $this->subject->setRequest($request); @@ -1907,7 +1907,7 @@ final class ContentObjectRendererTest extends UnitTestCase Environment::getPublicPath() . '/index.php', Environment::isWindows() ? 'WINDOWS' : 'UNIX' ); - $typoScript = new FrontendTypoScript(new RootNode(), []); + $typoScript = new FrontendTypoScript(new RootNode(), [], [], []); $typoScript->setConfigArray([]); $request = (new ServerRequest())->withAttribute('frontend.typoscript', $typoScript); $this->subject->setRequest($request); @@ -1922,7 +1922,7 @@ final class ContentObjectRendererTest extends UnitTestCase $configuration = [ 'exceptionHandler' => '1', ]; - $typoScript = new FrontendTypoScript(new RootNode(), []); + $typoScript = new FrontendTypoScript(new RootNode(), [], [], []); $typoScript->setConfigArray([]); $request = (new ServerRequest())->withAttribute('frontend.typoscript', $typoScript); $this->subject->setRequest($request); @@ -1933,7 +1933,7 @@ final class ContentObjectRendererTest extends UnitTestCase public function renderingContentObjectDoesNotThrowExceptionIfExceptionHandlerIsConfiguredGlobally(): void { $contentObjectFixture = $this->createContentObjectThrowingExceptionFixture($this->subject); - $typoScript = new FrontendTypoScript(new RootNode(), []); + $typoScript = new FrontendTypoScript(new RootNode(), [], [], []); $typoScript->setConfigArray([ 'contentObjectExceptionHandler' => '1', ]); @@ -1948,7 +1948,7 @@ final class ContentObjectRendererTest extends UnitTestCase $this->expectException(\LogicException::class); $this->expectExceptionCode(1414513947); $contentObjectFixture = $this->createContentObjectThrowingExceptionFixture($this->subject, false); - $typoScript = new FrontendTypoScript(new RootNode(), []); + $typoScript = new FrontendTypoScript(new RootNode(), [], [], []); $typoScript->setConfigArray([ 'contentObjectExceptionHandler' => '1', ]); @@ -1964,7 +1964,7 @@ final class ContentObjectRendererTest extends UnitTestCase public function renderedErrorMessageCanBeCustomized(): void { $contentObjectFixture = $this->createContentObjectThrowingExceptionFixture($this->subject); - $typoScript = new FrontendTypoScript(new RootNode(), []); + $typoScript = new FrontendTypoScript(new RootNode(), [], [], []); $typoScript->setConfigArray([]); $request = (new ServerRequest())->withAttribute('frontend.typoscript', $typoScript); $this->subject->setRequest($request); @@ -1981,7 +1981,7 @@ final class ContentObjectRendererTest extends UnitTestCase public function localConfigurationOverridesGlobalConfiguration(): void { $contentObjectFixture = $this->createContentObjectThrowingExceptionFixture($this->subject); - $typoScript = new FrontendTypoScript(new RootNode(), []); + $typoScript = new FrontendTypoScript(new RootNode(), [], [], []); $typoScript->setConfigArray([ 'contentObjectExceptionHandler.' => [ 'errorMessage' => 'Global message for testing', @@ -2011,7 +2011,7 @@ final class ContentObjectRendererTest extends UnitTestCase 'ignoreCodes.' => ['10.' => '1414513947'], ], ]; - $typoScript = new FrontendTypoScript(new RootNode(), []); + $typoScript = new FrontendTypoScript(new RootNode(), [], [], []); $typoScript->setConfigArray([]); $request = (new ServerRequest())->withAttribute('frontend.typoscript', $typoScript); $this->subject->setRequest($request); @@ -2093,7 +2093,7 @@ final class ContentObjectRendererTest extends UnitTestCase $linkFactory->method('create')->willReturn($linkResult); GeneralUtility::addInstance(LinkFactory::class, $linkFactory); } - $typoScript = new FrontendTypoScript(new RootNode(), []); + $typoScript = new FrontendTypoScript(new RootNode(), [], [], []); $typoScript->setConfigArray([]); $request = (new ServerRequest())->withAttribute('frontend.typoscript', $typoScript); $this->subject->setRequest($request); @@ -2365,7 +2365,7 @@ final class ContentObjectRendererTest extends UnitTestCase #[Test] public function parseFuncParsesNestedTagsProperly(string $value, array $configuration, string $expectedResult): void { - $typoScript = new FrontendTypoScript(new RootNode(), []); + $typoScript = new FrontendTypoScript(new RootNode(), [], [], []); $typoScript->setConfigArray([]); $request = (new ServerRequest())->withAttribute('frontend.typoscript', $typoScript); $this->subject->setRequest($request); @@ -5933,7 +5933,7 @@ final class ContentObjectRendererTest extends UnitTestCase int $times, string $will ): void { - $typoScript = new FrontendTypoScript(new RootNode(), []); + $typoScript = new FrontendTypoScript(new RootNode(), [], [], []); $typoScript->setConfigArray([ 'disablePrefixComment' => $disable, ]); @@ -7327,7 +7327,7 @@ final class ContentObjectRendererTest extends UnitTestCase "lib.bar.value = bar\n"; $lineStream = (new LossyTokenizer())->tokenize($typoScriptString); $typoScriptAst = (new AstBuilder(new NoopEventDispatcher()))->build($lineStream, new RootNode()); - $typoScriptAttribute = new FrontendTypoScript(new RootNode(), []); + $typoScriptAttribute = new FrontendTypoScript(new RootNode(), [], [], []); $typoScriptAttribute->setSetupTree($typoScriptAst); $typoScriptAttribute->setSetupArray($typoScriptAst->toArray()); $request = (new ServerRequest())->withAttribute('frontend.typoscript', $typoScriptAttribute); diff --git a/typo3/sysext/frontend/Tests/Unit/Http/RequestHandlerTest.php b/typo3/sysext/frontend/Tests/Unit/Http/RequestHandlerTest.php index b1f183b89b2c6563a6d1534d1d2c23c645090a47..62c6d1ce234acea83b4f9185007223ac2b0593b2 100644 --- a/typo3/sysext/frontend/Tests/Unit/Http/RequestHandlerTest.php +++ b/typo3/sysext/frontend/Tests/Unit/Http/RequestHandlerTest.php @@ -134,7 +134,7 @@ final class RequestHandlerTest extends UnitTestCase $pageRendererMock = $this->getMockBuilder(PageRenderer::class)->disableOriginalConstructor()->getMock(); $pageRendererMock->method('getDocType')->willReturn(DocType::html5); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $frontendTypoScript->setPageArray([ 'meta.' => [ @@ -295,7 +295,7 @@ final class RequestHandlerTest extends UnitTestCase $pageRendererMock = $this->getMockBuilder(PageRenderer::class)->disableOriginalConstructor()->getMock(); $pageRendererMock->method('getDocType')->willReturn(DocType::html5); $pageRendererMock->expects(self::once())->method('setMetaTag')->with($expectedTags['type'], $expectedTags['name'], $expectedTags['content'], [], false); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $frontendTypoScript->setPageArray([ 'meta.' => $typoScript, @@ -342,7 +342,7 @@ final class RequestHandlerTest extends UnitTestCase $pageRendererMock = $this->getMockBuilder(PageRenderer::class)->disableOriginalConstructor()->getMock(); $pageRendererMock->method('getDocType')->willReturn(DocType::html5); $pageRendererMock->expects(self::never())->method('setMetaTag'); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $frontendTypoScript->setPageArray([ 'meta.' => [ @@ -459,7 +459,7 @@ final class RequestHandlerTest extends UnitTestCase self::assertSame($expectedArgs[3], $subProperties); self::assertSame($expectedArgs[4], $replace); }); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray([]); $frontendTypoScript->setPageArray([ 'meta.' => $typoScript, diff --git a/typo3/sysext/frontend/Tests/Unit/Typolink/DatabaseRecordLinkBuilderTest.php b/typo3/sysext/frontend/Tests/Unit/Typolink/DatabaseRecordLinkBuilderTest.php index e681c8a246d5f10446a5685913c641e3481c2cbb..a116d585d39af1e9298198843c9a0bbafe80be47 100644 --- a/typo3/sysext/frontend/Tests/Unit/Typolink/DatabaseRecordLinkBuilderTest.php +++ b/typo3/sysext/frontend/Tests/Unit/Typolink/DatabaseRecordLinkBuilderTest.php @@ -157,7 +157,7 @@ final class DatabaseRecordLinkBuilderTest extends UnitTestCase $frontendControllerMock = $this->createMock(TypoScriptFrontendController::class); $pageRepositoryMock = $this->createMock(PageRepository::class); $contentObjectRendererMock = $this->createMock(ContentObjectRenderer::class); - $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); $frontendTypoScript->setSetupArray($typoScriptConfig); $request = (new ServerRequest())->withAttribute('frontend.typoscript', $frontendTypoScript); $contentObjectRendererMock->method('getRequest')->willReturn($request); diff --git a/typo3/sysext/indexed_search/Tests/Unit/IndexerTest.php b/typo3/sysext/indexed_search/Tests/Unit/IndexerTest.php index 34c24d924f84df715f8a69d0e28d571e8accdeb7..1225fb2134ba0efd76ba36357b01ede7fda2229f 100644 --- a/typo3/sysext/indexed_search/Tests/Unit/IndexerTest.php +++ b/typo3/sysext/indexed_search/Tests/Unit/IndexerTest.php @@ -76,7 +76,7 @@ final class IndexerTest extends UnitTestCase { $absRefPrefix = '/' . StringUtility::getUniqueId(); $html = 'test <a href="' . $absRefPrefix . 'index.php">test</a> test'; - $typoScript = new FrontendTypoScript(new RootNode(), []); + $typoScript = new FrontendTypoScript(new RootNode(), [], [], []); $typoScript->setConfigArray([ 'absRefPrefix' => $absRefPrefix, ]); diff --git a/typo3/sysext/redirects/Classes/Service/RedirectService.php b/typo3/sysext/redirects/Classes/Service/RedirectService.php index 88de1806ce0da80805a8e3dc14629d5ab54569e2..bb431a4545af490cffcb412d44f45acb5b0f612b 100644 --- a/typo3/sysext/redirects/Classes/Service/RedirectService.php +++ b/typo3/sysext/redirects/Classes/Service/RedirectService.php @@ -20,8 +20,8 @@ namespace TYPO3\CMS\Redirects\Service; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\UriInterface; -use Psr\Log\LoggerAwareInterface; -use Psr\Log\LoggerAwareTrait; +use Psr\Log\LoggerInterface; +use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Exception\SiteNotFoundException; use TYPO3\CMS\Core\Http\Uri; @@ -34,6 +34,7 @@ use TYPO3\CMS\Core\Routing\PageArguments; use TYPO3\CMS\Core\Site\Entity\NullSite; use TYPO3\CMS\Core\Site\Entity\SiteInterface; use TYPO3\CMS\Core\Site\SiteFinder; +use TYPO3\CMS\Core\TypoScript\FrontendTypoScriptFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\HttpUtility; use TYPO3\CMS\Frontend\Aspect\PreviewAspect; @@ -47,18 +48,19 @@ use TYPO3\CMS\Redirects\Event\BeforeRedirectMatchDomainEvent; /** * Creates a proper URL to redirect from a matched redirect of a request * - * @internal due to some possible refactorings in TYPO3 v9 + * @internal due to some possible refactorings */ -class RedirectService implements LoggerAwareInterface +class RedirectService { - use LoggerAwareTrait; - public function __construct( private readonly RedirectCacheService $redirectCacheService, private readonly LinkService $linkService, private readonly SiteFinder $siteFinder, private readonly EventDispatcherInterface $eventDispatcher, private readonly PageInformationFactory $pageInformationFactory, + private readonly FrontendTypoScriptFactory $frontendTypoScriptFactory, + private readonly PhpFrontend $typoScriptCache, + private readonly LoggerInterface $logger, ) {} /** @@ -402,9 +404,25 @@ class RedirectService implements LoggerAwareInterface $originalRequest = $originalRequest->withAttribute('frontend.page.information', $pageInformation); $controller = GeneralUtility::makeInstance(TypoScriptFrontendController::class); $controller->initializePageRenderer($originalRequest); - $frontendTypoScript = $controller->getFromCache($originalRequest); + $expressionMatcherVariables = $this->getExpressionMatcherVariables($site, $originalRequest, $controller); + $frontendTypoScript = $this->frontendTypoScriptFactory->createSettingsAndSetupConditions( + $site, + $pageInformation->getSysTemplateRows(), + // $originalRequest does not contain site ... + $expressionMatcherVariables, + $this->typoScriptCache, + ); + $frontendTypoScript = $this->frontendTypoScriptFactory->createSetupConfigOrFullSetup( + false, + $frontendTypoScript, + $site, + $pageInformation->getSysTemplateRows(), + $expressionMatcherVariables, + '0', + $this->typoScriptCache, + null + ); $newRequest = $originalRequest->withAttribute('frontend.typoscript', $frontendTypoScript); - $controller->releaseLocks(); $controller->newCObj($newRequest); if (!isset($GLOBALS['TSFE']) || !$GLOBALS['TSFE'] instanceof TypoScriptFrontendController) { $GLOBALS['TSFE'] = $controller; @@ -412,6 +430,24 @@ class RedirectService implements LoggerAwareInterface return $controller; } + private function getExpressionMatcherVariables(SiteInterface $site, ServerRequestInterface $request, TypoScriptFrontendController $controller): array + { + $pageInformation = $request->getAttribute('frontend.page.information'); + $topDownRootLine = $pageInformation->getRootLine(); + $localRootline = $pageInformation->getLocalRootLine(); + ksort($topDownRootLine); + return [ + 'request' => $request, + 'pageId' => $pageInformation->getId(), + 'page' => $pageInformation->getPageRecord(), + 'fullRootLine' => $topDownRootLine, + 'localRootLine' => $localRootline, + 'site' => $site, + 'siteLanguage' => $request->getAttribute('language'), + 'tsfe' => $controller, + ]; + } + protected function replaceRegExpCaptureGroup(array $matchedRedirect, UriInterface $uri, array $linkDetails): array { $uriToCheck = rawurldecode($uri->getPath()); diff --git a/typo3/sysext/redirects/Configuration/Services.yaml b/typo3/sysext/redirects/Configuration/Services.yaml index 0b021b02588be598db1cf45307624401bb910d1b..ff62488ec202f671f8af55ccc9aed13bda73f858 100644 --- a/typo3/sysext/redirects/Configuration/Services.yaml +++ b/typo3/sysext/redirects/Configuration/Services.yaml @@ -24,3 +24,7 @@ services: TYPO3\CMS\Redirects\Configuration\CheckIntegrityConfiguration: arguments: $extensionConfiguration: '@extension.configuration.redirects' + + TYPO3\CMS\Redirects\Service\RedirectService: + arguments: + $typoScriptCache: '@cache.typoscript' diff --git a/typo3/sysext/redirects/Tests/Functional/Service/IntegrityServiceTest.php b/typo3/sysext/redirects/Tests/Functional/Service/IntegrityServiceTest.php index 18559e081a09717145e13c1955f98cd5f78ec7f1..5053ca23e79ac5226726ce528578c027a6518341 100644 --- a/typo3/sysext/redirects/Tests/Functional/Service/IntegrityServiceTest.php +++ b/typo3/sysext/redirects/Tests/Functional/Service/IntegrityServiceTest.php @@ -19,10 +19,14 @@ namespace TYPO3\CMS\Redirects\Tests\Functional\Service; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\MockObject; +use TYPO3\CMS\Core\Cache\CacheManager; +use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend; use TYPO3\CMS\Core\EventDispatcher\NoopEventDispatcher; use TYPO3\CMS\Core\LinkHandling\LinkService; +use TYPO3\CMS\Core\Log\LogManager; use TYPO3\CMS\Core\Site\Entity\Site; use TYPO3\CMS\Core\Site\SiteFinder; +use TYPO3\CMS\Core\TypoScript\FrontendTypoScriptFactory; use TYPO3\CMS\Frontend\Page\PageInformationFactory; use TYPO3\CMS\Redirects\Service\IntegrityService; use TYPO3\CMS\Redirects\Service\RedirectCacheService; @@ -40,13 +44,18 @@ final class IntegrityServiceTest extends FunctionalTestCase { parent::setUp(); $siteFinder = $this->getSiteFinderMock(); + /** @var PhpFrontend $typoScriptCache */ + $typoScriptCache = $this->get(CacheManager::class)->getCache('typoscript'); $this->subject = new IntegrityService( new RedirectService( new RedirectCacheService(), $this->getMockBuilder(LinkService::class)->disableOriginalConstructor()->getMock(), $siteFinder, new NoopEventDispatcher(), - $this->get(PageInformationFactory::class) + $this->get(PageInformationFactory::class), + $this->get(FrontendTypoScriptFactory::class), + $typoScriptCache, + $this->get(LogManager::class)->getLogger('Testing'), ), $siteFinder ); diff --git a/typo3/sysext/redirects/Tests/Functional/Service/RedirectServiceTest.php b/typo3/sysext/redirects/Tests/Functional/Service/RedirectServiceTest.php index d44b39a52e90fe08cc0a62d8d3e818809ac546cb..ab5655fa3bead214dd0bcce04bf39cddae10018a 100644 --- a/typo3/sysext/redirects/Tests/Functional/Service/RedirectServiceTest.php +++ b/typo3/sysext/redirects/Tests/Functional/Service/RedirectServiceTest.php @@ -22,6 +22,8 @@ use PHPUnit\Framework\Attributes\Test; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\NullLogger; use Symfony\Component\DependencyInjection\Container; +use TYPO3\CMS\Core\Cache\CacheManager; +use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; use TYPO3\CMS\Core\EventDispatcher\ListenerProvider; @@ -29,8 +31,10 @@ use TYPO3\CMS\Core\EventDispatcher\NoopEventDispatcher; use TYPO3\CMS\Core\Http\ServerRequest; use TYPO3\CMS\Core\Http\Uri; use TYPO3\CMS\Core\LinkHandling\LinkService; +use TYPO3\CMS\Core\Log\LogManager; use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait; +use TYPO3\CMS\Core\TypoScript\FrontendTypoScriptFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication; use TYPO3\CMS\Frontend\Page\PageInformationFactory; @@ -98,14 +102,18 @@ final class RedirectServiceTest extends FunctionalTestCase ] ); + /** @var PhpFrontend $typoScriptCache */ + $typoScriptCache = $this->get(CacheManager::class)->getCache('typoscript'); $redirectService = new RedirectService( new RedirectCacheService(), $linkServiceMock, $siteFinder, new NoopEventDispatcher(), - $this->get(PageInformationFactory::class) + $this->get(PageInformationFactory::class), + $this->get(FrontendTypoScriptFactory::class), + $typoScriptCache, + $this->get(LogManager::class)->getLogger('Testing'), ); - $redirectService->setLogger($logger); // Assert correct redirect is matched $redirectMatch = $redirectService->matchRedirect($uri->getHost(), $uri->getPath(), $uri->getQuery()); @@ -850,12 +858,11 @@ final class RedirectServiceTest extends FunctionalTestCase $this->buildSiteConfiguration(1, 'https://acme.com/') ); - $logger = new NullLogger(); $frontendUserAuthentication = new FrontendUserAuthentication(); $siteFinder = GeneralUtility::makeInstance(SiteFinder::class); $uri = new Uri('https://acme.com/non-existing-page'); - $request = $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest($uri)) + $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest($uri)) ->withAttribute('site', $siteFinder->getSiteByRootPageId(1)) ->withAttribute('frontend.user', $frontendUserAuthentication) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE); @@ -878,14 +885,18 @@ final class RedirectServiceTest extends FunctionalTestCase $eventListener = $container->get(ListenerProvider::class); $eventListener->addListener(BeforeRedirectMatchDomainEvent::class, 'before-redirect-match-domain-event-is-triggered'); + /** @var PhpFrontend $typoScriptCache */ + $typoScriptCache = $this->get(CacheManager::class)->getCache('typoscript'); $redirectService = new RedirectService( new RedirectCacheService(), new LinkService(), $siteFinder, $this->get(EventDispatcherInterface::class), - $this->get(PageInformationFactory::class) + $this->get(PageInformationFactory::class), + $this->get(FrontendTypoScriptFactory::class), + $typoScriptCache, + $this->get(LogManager::class)->getLogger('Testing'), ); - $redirectService->setLogger($logger); $redirectMatch = $redirectService->matchRedirect($uri->getHost(), $uri->getPath(), $uri->getQuery()); diff --git a/typo3/sysext/redirects/Tests/Unit/Service/RedirectServiceTest.php b/typo3/sysext/redirects/Tests/Unit/Service/RedirectServiceTest.php index 5c136fa9ebd2f00e1a2a4ccd9f59225d19dee848..501394647bd7bee0646c860f010c4284a509e2fe 100644 --- a/typo3/sysext/redirects/Tests/Unit/Service/RedirectServiceTest.php +++ b/typo3/sysext/redirects/Tests/Unit/Service/RedirectServiceTest.php @@ -20,7 +20,10 @@ namespace TYPO3\CMS\Redirects\Tests\Unit\Service; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\MockObject; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; +use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Domain\Access\RecordAccessVoter; @@ -30,12 +33,20 @@ use TYPO3\CMS\Core\Http\Uri; use TYPO3\CMS\Core\LinkHandling\LinkService; use TYPO3\CMS\Core\LinkHandling\TypoLinkCodecService; use TYPO3\CMS\Core\Log\Logger; +use TYPO3\CMS\Core\Package\PackageManager; use TYPO3\CMS\Core\Resource\Exception\InvalidPathException; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Resource\Folder; +use TYPO3\CMS\Core\Resource\Security\FileNameValidator; use TYPO3\CMS\Core\Site\Entity\Site; use TYPO3\CMS\Core\Site\SiteFinder; +use TYPO3\CMS\Core\TypoScript\FrontendTypoScriptFactory; use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository; +use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateTreeBuilder; +use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser; +use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser; +use TYPO3\CMS\Core\TypoScript\IncludeTree\TreeFromLineStreamBuilder; +use TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication; use TYPO3\CMS\Frontend\Controller\ErrorController; @@ -78,9 +89,23 @@ final class RedirectServiceTest extends UnitTestCase new RecordAccessVoter(new NoopEventDispatcher()), new ErrorController(), new SysTemplateRepository(new NoopEventDispatcher(), $this->createMock(ConnectionPool::class), new Context()), - ) + ), + new FrontendTypoScriptFactory( + $this->createMock(ContainerInterface::class), + new NoopEventDispatcher(), + new SysTemplateTreeBuilder( + $this->createMock(ConnectionPool::class), + $this->createMock(PackageManager::class), + new Context(), + new TreeFromLineStreamBuilder(new FileNameValidator()) + ), + new LossyTokenizer(), + new IncludeTreeTraverser(), + new ConditionVerdictAwareIncludeTreeTraverser(), + ), + $this->createMock(PhpFrontend::class), + $this->createMock(LoggerInterface::class), ); - $this->redirectService->setLogger($logger); $GLOBALS['SIM_ACCESS_TIME'] = 42; } @@ -633,13 +658,25 @@ final class RedirectServiceTest extends UnitTestCase new ErrorController(), new SysTemplateRepository(new NoopEventDispatcher(), $this->createMock(ConnectionPool::class), new Context()), ), + new FrontendTypoScriptFactory( + $this->createMock(ContainerInterface::class), + new NoopEventDispatcher(), + new SysTemplateTreeBuilder( + $this->createMock(ConnectionPool::class), + $this->createMock(PackageManager::class), + new Context(), + new TreeFromLineStreamBuilder(new FileNameValidator()) + ), + new LossyTokenizer(), + new IncludeTreeTraverser(), + new ConditionVerdictAwareIncludeTreeTraverser(), + ), + $this->createMock(PhpFrontend::class), + $this->createMock(LoggerInterface::class), ], '', ); - $logger = new NullLogger(); - $redirectService->setLogger($logger); - $pageRecord = 't3://page?uid=13'; $redirectTargetMatch = [ 'target' => $pageRecord . ' - - - foo=bar', diff --git a/typo3/sysext/seo/Tests/Functional/Canonical/CanonicalGeneratorTest.php b/typo3/sysext/seo/Tests/Functional/Canonical/CanonicalGeneratorTest.php index ff699a7c2b8c6f04730bd12741fb29e6d8b18757..035d3c5e622fc6ae837fd5bb36a14296dd332701 100644 --- a/typo3/sysext/seo/Tests/Functional/Canonical/CanonicalGeneratorTest.php +++ b/typo3/sysext/seo/Tests/Functional/Canonical/CanonicalGeneratorTest.php @@ -180,7 +180,7 @@ final class CanonicalGeneratorTest extends FunctionalTestCase ]; $pageInformation->setPageRecord($pageRecord); $request = $request->withAttribute('frontend.page.information', $pageInformation); - $typoScript = new FrontendTypoScript(new RootNode(), []); + $typoScript = new FrontendTypoScript(new RootNode(), [], [], []); $typoScript->setConfigArray([]); $request = $request->withAttribute('frontend.typoscript', $typoScript); $this->get(CanonicalGenerator::class)->generate(['request' => $request]);