From a927bc00be4acac1c341285bf3bb82093255ae65 Mon Sep 17 00:00:00 2001 From: Christian Kuhn <lolli@schwarzbu.ch> Date: Wed, 13 Apr 2022 12:59:57 +0200 Subject: [PATCH] [!!!][TASK] Use new TypoScript parser in Frontend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This switches from TemplateService to new TypoScript parser logic in TypoScriptFrontendController. The central methods getFromcache() and getConfigArray() were called in PrepareTypoScriptFrontendRendering after each other: getConfigArray() is now merged into getFromcache() directly. One main goal is to get rid of the 'pagesection' cache and leverage the new cache strategy of the new TypoScript parser: This cache strategy is more effective and allows caching TypoScript between different pages. We essentially get rid of the pagesection query load, but instead need the list of relevant sys_template rows early, which is done with a single query. This code is moved out of IncludeTree/TreeBuilder to new class IncludeTree/SysTemplateRepository, since the result is now needed to build page cache identifiers and thus must be exposed. An event is added as well, for extensions like ext:bolt to manipulate sys_template rows resolving. The old runThroughTemplatesPostProcessing hook is marked @internal now and will vanish during further v12 development when testing-framework has been changed to deal with it. The central getFromcache() is much better documented and should be far easier to understand now. Some parts of the code are currently a bit naive and there is quite a bit potential to further optimize parsetime especially in "full cached" scenarios. We also have the potential to make USER_INT parsing significantly quicker. Dedicated patches will follow with continued v12 development. The patch also sets a couple of properties to @internal, and marks the old TypoScriptParser and TemplateService @deprecated, even though it is currently still used for instance for TSconfig parsing, which will switch to the new parser soon as well. Resolves: #98503 Related: #97816 Releases: main Change-Id: I904e9add4a425479df4a6768a1d63a54d7b252d8 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/75944 Tested-by: Stefan Bürk <stefan@buerk.tech> Tested-by: core-ci <typo3@b13.com> Tested-by: Benni Mack <benni@typo3.org> Reviewed-by: Stefan Bürk <stefan@buerk.tech> Reviewed-by: Benni Mack <benni@typo3.org> --- .../core/Classes/DataHandling/DataHandler.php | 2 +- .../AfterTemplatesHaveBeenDeterminedEvent.php | 55 ++ .../IncludeTree/SysTemplateRepository.php | 199 ++++++ .../TypoScript/IncludeTree/TreeBuilder.php | 199 +----- .../IncludeTreeConditionMatcherVisitor.php | 12 + .../TypoScript/Parser/TypoScriptParser.php | 5 +- .../Classes/TypoScript/TemplateService.php | 91 +-- .../Utility/ExtensionManagementUtility.php | 22 +- .../Configuration/DefaultConfiguration.php | 9 - typo3/sysext/core/Configuration/Services.yaml | 13 +- ...ng-97816-NewTypoScriptParserInFrontend.rst | 77 +++ ...wAfterTemplatesHaveBeenDeterminedEvent.rst | 33 + .../IncludeTree/SysTemplateRepositoryTest.php | 95 +++ .../IncludeTree/TreeBuilderTest.php | 45 +- .../AbstractRequestHandlingTest.php | 4 - .../TypoScriptFrontendController.php | 634 ++++++++++++------ .../frontend/Classes/Http/RequestHandler.php | 2 +- .../PrepareTypoScriptFrontendRendering.php | 26 +- .../TypoScriptFrontendInitialization.php | 15 +- .../TypoScriptFrontendControllerTest.php | 10 + typo3/sysext/frontend/ext_localconf.php | 13 + .../Configuration/Cache/CustomCachePreset.php | 1 - .../Cache/DatabaseCachePreset.php | 2 - .../Configuration/Cache/FileCachePreset.php | 2 - .../SilentConfigurationUpgradeService.php | 2 + .../Classes/Service/RedirectService.php | 9 +- .../Controller/ConstantEditorController.php | 13 +- .../Controller/ObjectBrowserController.php | 21 +- .../Controller/TemplateAnalyzerController.php | 13 +- 29 files changed, 1077 insertions(+), 547 deletions(-) create mode 100644 typo3/sysext/core/Classes/TypoScript/IncludeTree/Event/AfterTemplatesHaveBeenDeterminedEvent.php create mode 100644 typo3/sysext/core/Classes/TypoScript/IncludeTree/SysTemplateRepository.php create mode 100644 typo3/sysext/core/Documentation/Changelog/12.0/Breaking-97816-NewTypoScriptParserInFrontend.rst create mode 100644 typo3/sysext/core/Documentation/Changelog/12.0/Feature-97816-NewAfterTemplatesHaveBeenDeterminedEvent.rst create mode 100644 typo3/sysext/core/Tests/Functional/TypoScript/IncludeTree/SysTemplateRepositoryTest.php diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php index 79cf0becf0ac..f3e705bce1e0 100644 --- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php +++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php @@ -9179,7 +9179,7 @@ class DataHandler implements LoggerAwareInterface * Clears cache for the page pointed to by $cacheCmd (an integer). * * $cacheCmd='cacheTag:[string]' - * Flush page and pagesection cache by given tag + * Flush page cache by given tag * * $cacheCmd='cacheId:[string]' * Removes cache identifier from page and page section cache diff --git a/typo3/sysext/core/Classes/TypoScript/IncludeTree/Event/AfterTemplatesHaveBeenDeterminedEvent.php b/typo3/sysext/core/Classes/TypoScript/IncludeTree/Event/AfterTemplatesHaveBeenDeterminedEvent.php new file mode 100644 index 000000000000..bad16bea0dd1 --- /dev/null +++ b/typo3/sysext/core/Classes/TypoScript/IncludeTree/Event/AfterTemplatesHaveBeenDeterminedEvent.php @@ -0,0 +1,55 @@ +<?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\IncludeTree\Event; + +use TYPO3\CMS\Core\Site\Entity\SiteInterface; + +/** + * A PSR-14 event fired when sys_template rows have been fetched. + * + * This event is intended to add own rows based on given rows or site resolution. + */ +final class AfterTemplatesHaveBeenDeterminedEvent +{ + public function __construct( + private readonly array $rootline, + private readonly SiteInterface $site, + private array $templateRows, + ) { + } + + public function getRootline(): array + { + return $this->rootline; + } + + public function getSite(): SiteInterface + { + return $this->site; + } + + public function getTemplateRows(): array + { + return $this->templateRows; + } + + public function setTemplateRows(array $templateRows): void + { + $this->templateRows = $templateRows; + } +} diff --git a/typo3/sysext/core/Classes/TypoScript/IncludeTree/SysTemplateRepository.php b/typo3/sysext/core/Classes/TypoScript/IncludeTree/SysTemplateRepository.php new file mode 100644 index 000000000000..2e2d5894cd9d --- /dev/null +++ b/typo3/sysext/core/Classes/TypoScript/IncludeTree/SysTemplateRepository.php @@ -0,0 +1,199 @@ +<?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\IncludeTree; + +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Psr\EventDispatcher\EventDispatcherInterface; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer; +use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; +use TYPO3\CMS\Core\Site\Entity\SiteInterface; +use TYPO3\CMS\Core\TypoScript\IncludeTree\Event\AfterTemplatesHaveBeenDeterminedEvent; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Fetch relevant sys_template records from database by given page rootline. + * + * The result sys_template rows are fed to the TreeBuilder for processing. + * + * @internal: Internal structure. There is optimization potential and especially getSysTemplateRowsByRootline() will probably vanish later. + */ +final class SysTemplateRepository +{ + public function __construct( + private readonly EventDispatcherInterface $eventDispatcher, + private readonly ConnectionPool $connectionPool, + private readonly Context $context, + ) { + } + + /** + * To calculate the TS include tree, we have to find sys_template rows attached to all rootline pages. + * When there are multiple active sys_template rows on a page, we pick the one with the lower sorting + * value. + * The query implementation below does that with *one* query for all rootline pages at once, not + * one query per page. To handle the capabilities mentioned above, the query is a bit nifty, but + * the implementation should scale nearly O(1) instead of O(n) with the rootline depth. + * + * @todo: It's potentially possible to get rid of this method in the frontend by joining sys_template + * into the Page rootline resolving as soon as it uses a CTE. + */ + public function getSysTemplateRowsByRootline(array $rootline, SiteInterface $site): array + { + // Site-root node first! + $rootLinePageIds = array_reverse(array_column($rootline, 'uid')); + $sysTemplateRows = []; + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_template'); + $queryBuilder->setRestrictions($this->getSysTemplateQueryRestrictionContainer()); + $queryBuilder->select('sys_template.*')->from('sys_template'); + // Build a value list as joined table to have sorting based on list sorting + $valueList = []; + // @todo: Use type/int cast from expression builder to handle this dbms aware + // when support for this has been extracted from CTE PoC patch (sbuerk). + $isPostgres = $queryBuilder->getConnection()->getDatabasePlatform() instanceof PostgreSQLPlatform; + $pattern = $isPostgres ? '%s::int as uid, %s::int as sorting' : '%s as uid, %s as sorting'; + foreach ($rootLinePageIds as $sorting => $rootLinePageId) { + $valueList[] = sprintf( + $pattern, + $queryBuilder->createNamedParameter($rootLinePageId, \PDO::PARAM_INT), + $queryBuilder->createNamedParameter($sorting, \PDO::PARAM_INT) + ); + } + $valueList = 'SELECT ' . implode(' UNION ALL SELECT ', $valueList); + $queryBuilder->getConcreteQueryBuilder()->innerJoin( + $queryBuilder->quoteIdentifier('sys_template'), + sprintf('(%s)', $valueList), + $queryBuilder->quoteIdentifier('pidlist'), + '(' . $queryBuilder->expr()->eq( + 'sys_template.pid', + $queryBuilder->quoteIdentifier('pidlist.uid') + ) . ')' + ); + // Sort by rootline determined depth as sort criteria + $queryBuilder->orderBy('pidlist.sorting', 'ASC') + ->addOrderBy('sys_template.root', 'DESC') + ->addOrderBy('sys_template.sorting', 'ASC'); + $lastPid = null; + $queryResult = $queryBuilder->executeQuery(); + while ($sysTemplateRow = $queryResult->fetchAssociative()) { + // We're retrieving *all* templates per pid, but need the first one only. The + // order restriction above at least takes care they're after-each-other per pid. + if ($lastPid === (int)$sysTemplateRow['pid']) { + continue; + } + $lastPid = (int)$sysTemplateRow['pid']; + $sysTemplateRows[] = $sysTemplateRow; + } + $event = new AfterTemplatesHaveBeenDeterminedEvent($rootline, $site, $sysTemplateRows); + $this->eventDispatcher->dispatch($event); + return $event->getTemplateRows(); + } + + /** + * To calculate the TS include tree, we have to find sys_template rows attached to all rootline pages. + * When there are multiple active sys_template rows on a page, we pick the one with the lower sorting + * value. + * + * This variant is tailored for ext:tstemplate use. It allows "overriding" the sys_template uid of + * the deepest page, which is used when multiple sys_template records on one page are managed in the Backend. + * + * The query implementation below does that with *one* query for all rootline pages at once, not + * one query per page. To handle the capabilities mentioned above, the query is a bit nifty, but + * the implementation should scale nearly O(1) instead of O(n) with the rootline depth. + */ + public function getSysTemplateRowsByRootlineWithUidOverride(array $rootline, SiteInterface $site, int $templateUidOnDeepestRootline): array + { + // Site-root node first! + $rootLinePageIds = array_reverse(array_column($rootline, 'uid')); + $templatePidOnDeepestRootline = $rootline[array_key_first($rootline)]['uid']; + $sysTemplateRows = []; + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_template'); + $queryBuilder->setRestrictions($this->getSysTemplateQueryRestrictionContainer()); + $queryBuilder->select('sys_template.*')->from('sys_template'); + if ($templateUidOnDeepestRootline && $templatePidOnDeepestRootline) { + $queryBuilder->andWhere( + $queryBuilder->expr()->or( + $queryBuilder->expr()->neq('sys_template.pid', $queryBuilder->createNamedParameter($templatePidOnDeepestRootline, \PDO::PARAM_INT)), + $queryBuilder->expr()->and( + $queryBuilder->expr()->eq('sys_template.pid', $queryBuilder->createNamedParameter($templatePidOnDeepestRootline, \PDO::PARAM_INT)), + $queryBuilder->expr()->eq('sys_template.uid', $queryBuilder->createNamedParameter($templateUidOnDeepestRootline, \PDO::PARAM_INT)), + ), + ), + ); + } + // Build a value list as joined table to have sorting based on list sorting + $valueList = []; + // @todo: Use type/int cast from expression builder to handle this dbms aware + // when support for this has been extracted from CTE PoC patch (sbuerk). + $isPostgres = $queryBuilder->getConnection()->getDatabasePlatform() instanceof PostgreSQLPlatform; + $pattern = $isPostgres ? '%s::int as uid, %s::int as sorting' : '%s as uid, %s as sorting'; + foreach ($rootLinePageIds as $sorting => $rootLinePageId) { + $valueList[] = sprintf( + $pattern, + $queryBuilder->createNamedParameter($rootLinePageId, \PDO::PARAM_INT), + $queryBuilder->createNamedParameter($sorting, \PDO::PARAM_INT) + ); + } + $valueList = 'SELECT ' . implode(' UNION ALL SELECT ', $valueList); + $queryBuilder->getConcreteQueryBuilder()->innerJoin( + $queryBuilder->quoteIdentifier('sys_template'), + sprintf('(%s)', $valueList), + $queryBuilder->quoteIdentifier('pidlist'), + '(' . $queryBuilder->expr()->eq( + 'sys_template.pid', + $queryBuilder->quoteIdentifier('pidlist.uid') + ) . ')' + ); + // Sort by rootline determined depth as sort criteria + $queryBuilder->orderBy('pidlist.sorting', 'ASC') + ->addOrderBy('sys_template.root', 'DESC') + ->addOrderBy('sys_template.sorting', 'ASC'); + $lastPid = null; + $queryResult = $queryBuilder->executeQuery(); + while ($sysTemplateRow = $queryResult->fetchAssociative()) { + // We're retrieving *all* templates per pid, but need the first one only. The + // order restriction above at least takes care they're after-each-other per pid. + if ($lastPid === (int)$sysTemplateRow['pid']) { + continue; + } + $lastPid = (int)$sysTemplateRow['pid']; + $sysTemplateRows[] = $sysTemplateRow; + } + // @todo: This event should be able to be fired even if the sys_template resolving is + // merged into an early middleware like "SiteResolver" which could join / sub-select + // pages together with sys_template directly, which would be possible if we manage + // to switch away from RootlineUtility usage in SiteResolver by using a CTE instead. + $event = new AfterTemplatesHaveBeenDeterminedEvent($rootline, $site, $sysTemplateRows); + $this->eventDispatcher->dispatch($event); + return $event->getTemplateRows(); + } + + /** + * Get sys_template record query builder restrictions. + * Allows hidden records if enabled in context. + */ + private function getSysTemplateQueryRestrictionContainer(): DefaultRestrictionContainer + { + $restrictionContainer = GeneralUtility::makeInstance(DefaultRestrictionContainer::class); + if ($this->context->getPropertyFromAspect('visibility', 'includeHiddenContent', false)) { + $restrictionContainer->removeByType(HiddenRestriction::class); + } + return $restrictionContainer; + } +} diff --git a/typo3/sysext/core/Classes/TypoScript/IncludeTree/TreeBuilder.php b/typo3/sysext/core/Classes/TypoScript/IncludeTree/TreeBuilder.php index c26593baee9f..d7fc5cec2659 100644 --- a/typo3/sysext/core/Classes/TypoScript/IncludeTree/TreeBuilder.php +++ b/typo3/sysext/core/Classes/TypoScript/IncludeTree/TreeBuilder.php @@ -17,18 +17,15 @@ declare(strict_types=1); namespace TYPO3\CMS\Core\TypoScript\IncludeTree; -use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer; use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; -use TYPO3\CMS\Core\Exception\SiteNotFoundException; use TYPO3\CMS\Core\Package\PackageManager; use TYPO3\CMS\Core\Site\Entity\Site; use TYPO3\CMS\Core\Site\Entity\SiteInterface; -use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\DefaultTypoScriptInclude; use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\DefaultTypoScriptMagicKeyInclude; use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\ExtensionStaticInclude; @@ -45,7 +42,6 @@ use TYPO3\CMS\Core\Utility\ArrayUtility; use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\PathUtility; -use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; /** * Create a tree representing all TypoScript includes. @@ -102,30 +98,14 @@ final class TreeBuilder */ private array $includedSysTemplateUids = []; - /** - * Site of given rootline if possible. Used to resolve site based default constants. - */ - private ?SiteInterface $site = null; - /** * Either 'constants' or 'setup' */ private string $type; - /** - * To calculate full setup TypoScript, this class needs to be called twice: Once to retrieve - * "constants", and a second time to retrieve "setup" include tree. To suppress identical DB - * calls for the second cycle, getTreeByRootline() can be called with $cached argument. - */ - private bool $cached; - - private array $sysTemplateRows; - private array $basedOnTemplateRows; - public function __construct( private readonly ConnectionPool $connectionPool, private readonly PackageManager $packageManager, - private readonly SiteFinder $siteFinder, private readonly Context $context, private readonly TreeFromLineStreamBuilder $treeFromTokenStreamBuilder, private TokenizerInterface $tokenizer, @@ -141,10 +121,19 @@ final class TreeBuilder public function setTokenizer(TokenizerInterface $tokenizer): void { $this->tokenizer = $tokenizer; - $this->cache = null; + $this->disableCache(); $this->treeFromTokenStreamBuilder->setTokenizer($tokenizer); } + /** + * Used in FE in no_cache = true context. + * @todo: Maybe change this and simply hand over a cache to getTreeBySysTemplateRowsAndSite() when needed? + */ + public function disableCache(): void + { + $this->cache = null; + } + /** * This is a special case for extbase BE modules and should not be used otherwise. See property comment. */ @@ -153,32 +142,16 @@ final class TreeBuilder $this->forceProcessExtensionStatics = true; } - /** - * @param array $rootline The "reversed" rootline as coming from RootlineUtility: Deepest page with - * the highest key as first entry, site-root page with key 0 as last entry. - */ - public function getTreeByRootline(array $rootline, string $type, bool $cached, int $templateUidOnDeepestRootline = 0): RootInclude + public function getTreeBySysTemplateRowsAndSite(string $type, array $sysTemplateRows, ?SiteInterface $site = null): RootInclude { if (!in_array($type, ['constants', 'setup'])) { throw new \RuntimeException('type must be either constants or setup', 1653737656); } $this->type = $type; - $this->cached = $cached; $this->includedSysTemplateUids = []; $this->extensionStaticsProcessed = false; - if ($cached) { - // Note this fatales if calling getTreeByRootline() with $cached=true when it has not - // been called with $cached=false before. This is intended: We don't need check-code if - // it simply fatales on broken use. - $sysTemplateRows = $this->sysTemplateRows; - } else { - $this->site = $this->determineSite($rootline); - $sysTemplateRows = $this->getRootlineSysTemplateRowsFromDatabase($rootline, $templateUidOnDeepestRootline); - $this->sysTemplateRows = $sysTemplateRows; - } - $includeTree = new RootInclude(); foreach ($sysTemplateRows as $sysTemplateRow) { @@ -210,21 +183,22 @@ final class TreeBuilder ) { $includeNode->setClear(true); } - $this->handleSysTemplateRecordInclude($includeNode, $sysTemplateRow); + $this->handleSysTemplateRecordInclude($includeNode, $sysTemplateRow, $site); $this->treeFromTokenStreamBuilder->buildTree($includeNode, $this->type); $this->cache?->set($identifier, $this->prepareNodeForCache($includeNode)); $includeTree->addChild($includeNode); } - // @todo: b/w compat hook hack tailored for testing-framework TyposcriptInstruction runThroughTemplatesPostProcessing - // hook. Substitute with an event and look at usages like ext:bolt when doing this. - // Note we also don't cache this hook result, which we either won't want at all and rely on "sub-caches" by - // TreeFromLineStreamBuilder, or implement it? Unsure. + // @todo: b/w compat hook hack tailored for testing-framework TyposcriptInstruction runThroughTemplatesPostProcessing hook. + // This hook has already been marked as removed in v12. We should drop it without further notice in v12 + // stabilization phase and make TF cope with it, probably by switching to AfterTemplatesHaveBeenDeterminedEvent. + // @deprecated hook runThroughTemplatesPostProcessing, will vanish in v12. $hookParameters = []; $templateService = new TemplateService(); foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['Core/TypoScript/TemplateService']['runThroughTemplatesPostProcessing'] ?? [] as $listener) { GeneralUtility::callUserFunction($listener, $hookParameters, $templateService); if (!empty($templateService->constants)) { + // @todo: Aehm, we should check $this->type here, shouldn't we? $node = new DefaultTypoScriptInclude(); $node->setIdentifier('hook-constants'); $node->setName('Hook constants'); @@ -232,6 +206,7 @@ final class TreeBuilder $includeTree->addChild($node); } if (!empty($templateService->config)) { + // @todo: Aehm, we should check $this->type here, shouldn't we? $node = new DefaultTypoScriptInclude(); $node->setIdentifier('hook-setup'); $node->setName('Hook setup'); @@ -244,7 +219,7 @@ final class TreeBuilder // Extbase hack: See property description above. if ($this->type === 'constants') { $this->addDefaultTypoScriptFromGlobals($includeTree); - $this->addDefaultTypoScriptConstantsFromSite($includeTree); + $this->addDefaultTypoScriptConstantsFromSite($includeTree, $site); } else { $this->addDefaultTypoScriptFromGlobals($includeTree); } @@ -257,7 +232,7 @@ final class TreeBuilder /** * Add includes defined in a sys_template record. */ - private function handleSysTemplateRecordInclude(IncludeInterface $parentNode, array $row): void + private function handleSysTemplateRecordInclude(IncludeInterface $parentNode, array $row, ?SiteInterface $site): void { $this->includedSysTemplateUids[] = (int)$row['uid']; @@ -269,7 +244,7 @@ final class TreeBuilder if ($this->type === 'constants' && $clearConstants) { $this->addDefaultTypoScriptFromGlobals($parentNode); - $this->addDefaultTypoScriptConstantsFromSite($parentNode); + $this->addDefaultTypoScriptConstantsFromSite($parentNode, $site); } if ($this->type === 'setup' && $clearSetup) { $this->addDefaultTypoScriptFromGlobals($parentNode); @@ -281,7 +256,7 @@ final class TreeBuilder $this->handleIncludeStaticFileArray($parentNode, (string)$row['include_static_file']); } if (!empty($row['basedOn'])) { - $this->handleIncludeBasedOnTemplates($parentNode, (string)$row['basedOn']); + $this->handleIncludeBasedOnTemplates($parentNode, (string)$row['basedOn'], $site); } if ($includeStaticAfterBasedOn) { $this->handleIncludeStaticFileArray($parentNode, (string)$row['include_static_file']); @@ -330,7 +305,7 @@ final class TreeBuilder * Warning: Calls handleSysTemplateRecordInclude() recursive when another basedOn templates * record includes things again! */ - private function handleIncludeBasedOnTemplates(IncludeInterface $parentNode, string $basedOnList): void + private function handleIncludeBasedOnTemplates(IncludeInterface $parentNode, string $basedOnList, ?SiteInterface $site): void { $basedOnTemplateUids = GeneralUtility::intExplode(',', $basedOnList, true); // Filter uids that have been handled already. @@ -339,12 +314,7 @@ final class TreeBuilder return; } - if ($this->cached) { - $basedOnTemplateRows = $this->basedOnTemplateRows[implode('-', $basedOnTemplateUids)]; - } else { - $basedOnTemplateRows = $this->getBasedOnSysTemplateRowsFromDatabase($basedOnTemplateUids); - $this->basedOnTemplateRows[implode('-', $basedOnTemplateUids)] = $basedOnTemplateRows; - } + $basedOnTemplateRows = $this->getBasedOnSysTemplateRowsFromDatabase($basedOnTemplateUids); foreach ($basedOnTemplateUids as $basedOnTemplateUid) { if (is_array($basedOnTemplateRows[$basedOnTemplateUid] ?? false)) { @@ -372,7 +342,7 @@ final class TreeBuilder $includeNode->setClear(true); } $parentNode->addChild($includeNode); - $this->handleSysTemplateRecordInclude($includeNode, $sysTemplateRow); + $this->handleSysTemplateRecordInclude($includeNode, $sysTemplateRow, $site); } } } @@ -509,13 +479,13 @@ final class TreeBuilder /** * Load default TS constants from site configuration if that page has a site in rootline. */ - private function addDefaultTypoScriptConstantsFromSite(IncludeInterface $parentConstantNode): void + private function addDefaultTypoScriptConstantsFromSite(IncludeInterface $parentConstantNode, ?SiteInterface $site): void { - if (!$this->site instanceof Site) { + if (!$site instanceof Site) { return; } $siteConstants = ''; - $siteSettings = $this->site->getConfiguration()['settings'] ?? []; + $siteSettings = $site->getConfiguration()['settings'] ?? []; if (empty($siteSettings)) { return; } @@ -533,7 +503,7 @@ final class TreeBuilder } $node = new SiteInclude(); $node->setIdentifier($identifier); - $node->setName('Site constants settings of site ' . $this->site->getIdentifier()); + $node->setName('Site constants settings of site ' . $site->getIdentifier()); $node->setLineStream($this->tokenizer->tokenize($siteConstants)); $this->cache?->set($identifier, $this->prepareNodeForCache($node)); $parentConstantNode->addChild($node); @@ -569,103 +539,6 @@ final class TreeBuilder } } - /** - * Note this takes the rootline array from 'lowest' up to page tree root: Deepest page - * first on key 0, higher page 1, and so on. - */ - private function determineSite(array $rootline): ?SiteInterface - { - if ($this->getTypoScriptFrontendController() instanceof TypoScriptFrontendController) { - return $this->getTypoScriptFrontendController()->getSite(); - } - $possibleRoots = array_filter($rootline, static function (array $page) { - return $page['is_siteroot'] === 1; - }); - $possibleRoots[] = end($rootline); - foreach ($possibleRoots as $possibleRoot) { - try { - return $this->siteFinder->getSiteByPageId((int)($possibleRoot['uid'] ?? 0)); - } catch (SiteNotFoundException $_) { - // continue - } - } - return null; - } - - /** - * getTreeByRootline() receives the rootline of a page. To calculate the TS include tree, we have - * to find sys_template rows attached to all rootline pages. - * When there are multiple active sys_template rows on a page, we pick the one with the lower sorting - * value. Additionally, the backend 'template' module allows selecting a sys_template record on the - * deepest page, if there is more than one. - * The query implementation below does that with *one* query for all rootline pages at once, not - * one query per page. To handle the capabilities mentioned above, the query is a bit nifty, but - * the implementation should scale nearly O(1) instead of O(n) with the rootline depth. - * - * @todo: It's potentially possible to further optimize using a recursive CTE. Benefit - * won't be *that* huge though, and there are much more important CTE targets first. - */ - private function getRootlineSysTemplateRowsFromDatabase(array $rootline, int $templateUidOnDeepestRootline): array - { - // Site-root node first! - $rootLinePageIds = array_reverse(array_column($rootline, 'uid')); - $templatePidOnDeepestRootline = $rootline[array_key_first($rootline)]['uid']; - $sysTemplateRows = []; - $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_template'); - $queryBuilder->setRestrictions($this->getSysTemplateQueryRestrictionContainer()); - $queryBuilder->select('sys_template.*')->from('sys_template'); - if ($templateUidOnDeepestRootline && $templatePidOnDeepestRootline) { - $queryBuilder->andWhere( - $queryBuilder->expr()->or( - $queryBuilder->expr()->neq('sys_template.pid', $queryBuilder->createNamedParameter($templatePidOnDeepestRootline, \PDO::PARAM_INT)), - $queryBuilder->expr()->and( - $queryBuilder->expr()->eq('sys_template.pid', $queryBuilder->createNamedParameter($templatePidOnDeepestRootline, \PDO::PARAM_INT)), - $queryBuilder->expr()->eq('sys_template.uid', $queryBuilder->createNamedParameter($templateUidOnDeepestRootline, \PDO::PARAM_INT)), - ), - ), - ); - } - // Build a value list as joined table to have sorting based on list sorting - $valueList = []; - // @todo: Use type/int cast from expression builder to handle this dbms aware - // when support for this has been extracted from CTE PoC patch (sbuerk). - $isPostgres = $queryBuilder->getConnection()->getDatabasePlatform() instanceof PostgreSQLPlatform; - $pattern = $isPostgres ? '%s::int as uid, %s::int as sorting' : '%s as uid, %s as sorting'; - foreach ($rootLinePageIds as $sorting => $rootLinePageId) { - $valueList[] = sprintf( - $pattern, - $queryBuilder->createNamedParameter($rootLinePageId, \PDO::PARAM_INT), - $queryBuilder->createNamedParameter($sorting, \PDO::PARAM_INT) - ); - } - $valueList = 'SELECT ' . implode(' UNION ALL SELECT ', $valueList); - $queryBuilder->getConcreteQueryBuilder()->innerJoin( - $queryBuilder->quoteIdentifier('sys_template'), - sprintf('(%s)', $valueList), - $queryBuilder->quoteIdentifier('pidlist'), - '(' . $queryBuilder->expr()->eq( - 'sys_template.pid', - $queryBuilder->quoteIdentifier('pidlist.uid') - ) . ')' - ); - // Sort by rootline determined depth as sort criteria - $queryBuilder->orderBy('pidlist.sorting', 'ASC') - ->addOrderBy('sys_template.root', 'DESC') - ->addOrderBy('sys_template.sorting', 'ASC'); - $lastPid = null; - $queryResult = $queryBuilder->executeQuery(); - while ($sysTemplateRow = $queryResult->fetchAssociative()) { - // We're retrieving *all* templates per pid, but need the first one only. The - // order restriction above at least takes care they're after-each-other per pid. - if ($lastPid === (int)$sysTemplateRow['pid']) { - continue; - } - $lastPid = (int)$sysTemplateRow['pid']; - $sysTemplateRows[] = $sysTemplateRow; - } - return $sysTemplateRows; - } - /** * Get 'basedOn' sys_template sub-rows of sys_templates that use this. * Note the 'IN()' query implementation below delivers rows in *any* order. To preserve @@ -731,18 +604,4 @@ final class TreeBuilder } return $restrictionContainer; } - - /** - * It's ugly this class has this dependency. - * It is used within 'addDefaultTypoScriptConstantsFromSite()' to find the current site object. - * - * @todo: It would be better if site is either set() from outside, or the SiteFinder is used to grab - * current site. But SiteFinder looks expensive to call in FE scope?! So this get()'er is a - * shortcut? Note we currently *do* have the rootline available in this class ... - * Call for help here @benni, old reference: TemplateService->addDefaultTypoScript() - */ - private function getTypoScriptFrontendController(): ?TypoScriptFrontendController - { - return $GLOBALS['TSFE'] ?? null; - } } diff --git a/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeConditionMatcherVisitor.php b/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeConditionMatcherVisitor.php index dc68b0d21dc0..2ce8c8b188a1 100644 --- a/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeConditionMatcherVisitor.php +++ b/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeConditionMatcherVisitor.php @@ -35,11 +35,22 @@ final class IncludeTreeConditionMatcherVisitor implements IncludeTreeVisitorInte { private ConditionMatcherInterface $conditionMatcher; + private array $conditionList = []; + public function setConditionMatcher(ConditionMatcherInterface $conditionMatcher) { $this->conditionMatcher = $conditionMatcher; } + /** + * A list of all handled conditions with their verdicts. + * This is used in FE since condition verdicts influence page caches. + */ + public function getConditionList(): array + { + return $this->conditionList; + } + public function visitBeforeChildren(IncludeInterface $include, int $currentDepth): void { if (!$include instanceof IncludeConditionInterface) { @@ -48,6 +59,7 @@ final class IncludeTreeConditionMatcherVisitor implements IncludeTreeVisitorInte $conditionValue = $include->getConditionToken()->getValue(); // @todo: This bracket handling is stupid, it's removed in matcher again ... $verdict = $this->conditionMatcher->match('[' . $conditionValue . ']'); + $this->conditionList[$conditionValue] = $verdict; $include->setConditionVerdict($verdict); } diff --git a/typo3/sysext/core/Classes/TypoScript/Parser/TypoScriptParser.php b/typo3/sysext/core/Classes/TypoScript/Parser/TypoScriptParser.php index be2092a1c940..df4a800112fd 100644 --- a/typo3/sysext/core/Classes/TypoScript/Parser/TypoScriptParser.php +++ b/typo3/sysext/core/Classes/TypoScript/Parser/TypoScriptParser.php @@ -30,7 +30,10 @@ use TYPO3\CMS\Core\Utility\StringUtility; use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher as FrontendConditionMatcher; /** - * The TypoScript parser + * The TypoScript parser. + * + * @deprecated This class should not be used anymore, last core usages will be removed during v12. + * Using methods or properties of this class will start logging deprecation messages. */ class TypoScriptParser { diff --git a/typo3/sysext/core/Classes/TypoScript/TemplateService.php b/typo3/sysext/core/Classes/TypoScript/TemplateService.php index 7cde066a00ec..e3aa25d60ea3 100644 --- a/typo3/sysext/core/Classes/TypoScript/TemplateService.php +++ b/typo3/sysext/core/Classes/TypoScript/TemplateService.php @@ -38,8 +38,9 @@ use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; /** * Template object that is responsible for generating the TypoScript template based on template records. - * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser - * @see \TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\AbstractConditionMatcher + * + * @deprecated This class should not be used anymore, last core usages will be removed during v12. + * Using methods or properties of this class will start logging deprecation messages. */ class TemplateService { @@ -363,31 +364,6 @@ class TemplateService } } - /** - * Fetches the "currentPageData" array from cache - * - * NOTE about currentPageData: - * It holds information about the TypoScript conditions along with the list - * of template uid's which is used on the page. In the getFromCache() function - * in TSFE, currentPageData is used to evaluate if there is a template and - * if the matching conditions are alright. Unfortunately this does not take - * into account if the templates in the rowSum of currentPageData has - * changed composition, eg. due to hidden fields or start/end time. So if a - * template is hidden or times out, it'll not be discovered unless the page - * is regenerated - at least the this->start function must be called, - * because this will make a new portion of data in currentPageData string. - * - * @param int $pageId - * @param string $mountPointValue - * @return array Returns the unmatched array $currentPageData if found cached in "cache_pagesection". Otherwise FALSE is returned which means that the array must be generated and stored in the cache - * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException - * @internal - */ - public function getCurrentPageData(int $pageId, string $mountPointValue) - { - return GeneralUtility::makeInstance(CacheManager::class)->getCache('pagesection')->get($pageId . '_' . GeneralUtility::md5int($mountPointValue)); - } - /** * Fetches data about which TypoScript-matches there are at this page. Then it performs a matchingtest. * @@ -416,7 +392,6 @@ class TemplateService * Sets $this->setup to the parsed TypoScript template array * * @param array $theRootLine The rootline of the current page (going ALL the way to tree root) - * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::getConfigArray() */ public function start($theRootLine) { @@ -425,47 +400,18 @@ class TemplateService $constantsData = []; $setupData = []; $cacheIdentifier = ''; - // Flag that indicates that the existing data in cache_pagesection - // could be used (this is the case if $TSFE->all is set, and the - // rowSum still matches). Based on this we decide if cache_pagesection - // needs to be updated... - $isCached = false; $this->runThroughTemplates($theRootLine); - if ($this->getTypoScriptFrontendController()->all) { - $cc = $this->getTypoScriptFrontendController()->all; - // The two rowSums must NOT be different from each other - which they will be if start/endtime or hidden has changed! - if (serialize($this->rowSum) !== serialize($cc['rowSum'])) { - $cc = []; - } else { - // If $TSFE->all contains valid data, we don't need to update cache_pagesection (because this data was fetched from there already) - if (serialize($this->rootLine) === serialize($cc['rootLine'])) { - $isCached = true; - } - // When the data is serialized below (ROWSUM hash), it must not contain the rootline by concept. So this must be removed (and added again later)... - unset($cc['rootLine']); - } - } // This is about getting the hash string which is used to fetch the cached TypoScript template. // If there was some cached currentPageData ($cc) then that's good (it gives us the hash). - if (!empty($cc)) { - // If currentPageData was actually there, we match the result (if this wasn't done already in $TSFE->getFromCache()...) - if (!$cc['match']) { - // @todo check if this can ever be the case - otherwise remove - $cc = $this->matching($cc); - ksort($cc); - } + // If currentPageData was not there, we first find $rowSum (freshly generated). After that we try to see, if it is stored with a list of all conditions. If so we match the result. + $rowSumHash = md5('ROWSUM:' . serialize($this->rowSum)); + $result = $this->getCacheEntry($rowSumHash); + if (is_array($result)) { + $cc['all'] = $result; + $cc['rowSum'] = $this->rowSum; + $cc = $this->matching($cc); + ksort($cc); $cacheIdentifier = md5(serialize($cc)); - } else { - // If currentPageData was not there, we first find $rowSum (freshly generated). After that we try to see, if it is stored with a list of all conditions. If so we match the result. - $rowSumHash = md5('ROWSUM:' . serialize($this->rowSum)); - $result = $this->getCacheEntry($rowSumHash); - if (is_array($result)) { - $cc['all'] = $result; - $cc['rowSum'] = $this->rowSum; - $cc = $this->matching($cc); - ksort($cc); - $cacheIdentifier = md5(serialize($cc)); - } } if ($cacheIdentifier) { // Get TypoScript setup array @@ -509,19 +455,6 @@ class TemplateService // Add rootLine $cc['rootLine'] = $this->rootLine; ksort($cc); - // Make global and save - $this->getTypoScriptFrontendController()->all = $cc; - // Matching must be executed for every request, so this must never be part of the pagesection cache! - unset($cc['match']); - if (!$isCached && !$this->simulationHiddenOrTime && !$this->getTypoScriptFrontendController()->no_cache) { - // Only save the data if we're not simulating by hidden/starttime/endtime - $mpvarHash = GeneralUtility::md5int($this->getTypoScriptFrontendController()->MP); - $pageSectionCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('pagesection'); - $pageSectionCache->set($this->getTypoScriptFrontendController()->id . '_' . $mpvarHash, $cc, [ - 'pageId_' . $this->getTypoScriptFrontendController()->id, - 'mpvarHash_' . $mpvarHash, - ]); - } // If everything OK. if ($this->rootId && $this->rootLine && $this->setup) { $this->loaded = true; @@ -827,7 +760,7 @@ class TemplateService * @internal * @see includeStaticTypoScriptSources() */ - public function addExtensionStatics($idList, $templateID, $pid) + protected function addExtensionStatics($idList, $templateID, $pid) { $this->extensionStaticsProcessed = true; diff --git a/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php b/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php index 03127c2a8120..2859a57baca0 100644 --- a/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php +++ b/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php @@ -1159,31 +1159,35 @@ tt_content.' . $key . $suffix . ' { } /** - * Adds $content to the default TypoScript setup code as set in $GLOBALS['TYPO3_CONF_VARS'][FE]['defaultTypoScript_setup'] - * Prefixed with a [GLOBAL] line + * Adds $content to the default TypoScript setup code as set in $GLOBALS['TYPO3_CONF_VARS'][FE]['defaultTypoScript_setup']. + * NOT prefixed with a [GLOBAL] line, other calls MUST properly close their conditions! * FOR USE IN ext_localconf.php FILES * * @param string $content TypoScript Setup string */ public static function addTypoScriptSetup(string $content): void { - $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_setup'] .= ' -[GLOBAL] -' . $content; + $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_setup'] ??= ''; + if (!empty($GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_setup'])) { + $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_setup'] .= LF; + } + $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_setup'] .= $content; } /** * Adds $content to the default TypoScript constants code as set in $GLOBALS['TYPO3_CONF_VARS'][FE]['defaultTypoScript_constants'] - * Prefixed with a [GLOBAL] line + * NOT prefixed with a [GLOBAL] line, other calls MUST properly close their conditions! * FOR USE IN ext_localconf.php FILES * * @param string $content TypoScript Constants string */ public static function addTypoScriptConstants(string $content): void { - $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_constants'] .= ' -[GLOBAL] -' . $content; + $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_constants'] ??= ''; + if (!empty($GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_constants'])) { + $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_constants'] .= LF; + } + $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_constants'] .= $content; } /** diff --git a/typo3/sysext/core/Configuration/DefaultConfiguration.php b/typo3/sysext/core/Configuration/DefaultConfiguration.php index 1c204be9035c..6db2bf6208df 100644 --- a/typo3/sysext/core/Configuration/DefaultConfiguration.php +++ b/typo3/sysext/core/Configuration/DefaultConfiguration.php @@ -177,15 +177,6 @@ return [ ], 'groups' => ['pages'], ], - 'pagesection' => [ - 'frontend' => \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class, - 'backend' => \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class, - 'options' => [ - 'compression' => true, - 'defaultLifetime' => 2592000, // 30 days; set this to a lower value in case your cache gets too big - ], - 'groups' => ['pages'], - ], 'runtime' => [ 'frontend' => \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class, 'backend' => \TYPO3\CMS\Core\Cache\Backend\TransientMemoryBackend::class, diff --git a/typo3/sysext/core/Configuration/Services.yaml b/typo3/sysext/core/Configuration/Services.yaml index bda6eacab243..31be37e6be90 100644 --- a/typo3/sysext/core/Configuration/Services.yaml +++ b/typo3/sysext/core/Configuration/Services.yaml @@ -303,6 +303,9 @@ services: TYPO3\CMS\Core\TypoScript\AST\AstBuilderInterface: alias: TYPO3\CMS\Core\TypoScript\AST\AstBuilder + TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository: + public: true + TYPO3\CMS\Core\TypoScript\IncludeTree\TreeBuilder: public: true arguments: @@ -315,6 +318,11 @@ services: # Ast builder 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\Tokenizer\TokenizerInterface: alias: TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer @@ -330,11 +338,6 @@ services: factory: ['@TYPO3\CMS\Core\Cache\CacheManager', 'getCache'] arguments: ['pages'] - cache.pagesection: - class: TYPO3\CMS\Core\Cache\Frontend\FrontendInterface - factory: ['@TYPO3\CMS\Core\Cache\CacheManager', 'getCache'] - arguments: ['pagesection'] - cache.runtime: class: TYPO3\CMS\Core\Cache\Frontend\FrontendInterface factory: ['@TYPO3\CMS\Core\Cache\CacheManager', 'getCache'] diff --git a/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-97816-NewTypoScriptParserInFrontend.rst b/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-97816-NewTypoScriptParserInFrontend.rst new file mode 100644 index 000000000000..4dfa6eb3d026 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-97816-NewTypoScriptParserInFrontend.rst @@ -0,0 +1,77 @@ +.. include:: /Includes.rst.txt + +.. _breaking-97816-1664800747: + +==================================================== +Breaking: #97816 - New TypoScript parser in Frontend +==================================================== + +See :issue:`97816` + +Description +=========== + +The rewrite of the TypoScript parser has been enabled for Frontend +rendering. + +See :ref:`breaking-97816-1656350406` and :ref:`feature-97816-1656350667` +for more details on the new parser. + + +Impact +====== + +The change has impact on Frontend caching, hooks, some classes and properties. In detail: + +* Hook :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['Core/TypoScript/TemplateService']['runThroughTemplatesPostProcessing']` + is gone and substituted by :php:`AfterTemplatesHaveBeenDeterminedEvent`. See :ref:`feature-97816-1664801053` for more details. + +* The classes :php:`TYPO3\CMS\Core\TypoScript\TemplateService` and :php:`TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser` + have been marked as deprecated and shouldn't be used anymore. + An instance of :php:`TemplateService` is still kept as property :php:`TypoScriptFrontendController->tmpl` (:php:`$GLOBALS['TSFE']->tmpl) + as backwards compatible layer, and the most important properties within the class, namely especially :php:`TemplateService->setup` is + still set. To avoid using these properties, the Frontend request object will contain this state. + In rare cases, where extensions need to parse TypoScript on their own, they should switch to the Tokenizer and AstBuilder structures + of the new parser. Note these classes are still young and currently marked @internal, the API may still slightly change with further + v12 development. + +* The :php:`pagesection` cache has been removed. This was a helper cache that grew O(n) with the number of + called Frontend pages. The new :php:`typoscript` cache is used instead: This grows only O(n) with the + number of different sys_template and condition combinations and is a filesystem based :php:`PhpFrontend` implementation. + When upgrading, the database tables :sql:`cache_pagesection` and :sql:`cache_pagesections_tags` can be safely removed, the + install tool will also silently remove any existing entries from :file:`settings.php` that reconfigure the cache. + +* The following properties and methods in :php:`TypoScriptFrontendController` have been set to :php:`@internal` and should not + be used any longer since they may vanish without further notice: + + * :php:`TypoScriptFrontendController->no_cache` + * :php:`TypoScriptFrontendController->tmpl` + * :php:`TypoScriptFrontendController->pageContentWasLoadedFromCache` + * :php:`TypoScriptFrontendController->getFromCache_queryRow()` + * :php:`TypoScriptFrontendController->populatePageDataFromCache()` + * :php:`TypoScriptFrontendController->shouldAcquireCacheData()` + * :php:`TypoScriptFrontendController->acquireLock()` + * :php:`TypoScriptFrontendController->releaseLock()` + +* The following methods in :php:`TypoScriptFrontendController` have been removed: + + * :php:`TypoScriptFrontendController->getHash()` + * :php:`TypoScriptFrontendController->getLockHash()` + * :php:`TypoScriptFrontendController->getConfigArray()` + * :php:`TypoScriptFrontendController->()` + + +Affected installations +====================== + +Many instances will only recognize that the :php:`pagesection` cache is gone and should continue to work. +Instances with extensions that use :php:`TemplateService` or :php:`TypoScriptParser`, or access the +property :php:`TypoScriptFrontendController->tmpl` may need adaptions. + + +Migration +========= + +See the impact description above for some migration hints. + +.. index:: Database, Frontend, PHP-API, TypoScript, LocalConfiguration, PartiallyScanned, ext:frontend diff --git a/typo3/sysext/core/Documentation/Changelog/12.0/Feature-97816-NewAfterTemplatesHaveBeenDeterminedEvent.rst b/typo3/sysext/core/Documentation/Changelog/12.0/Feature-97816-NewAfterTemplatesHaveBeenDeterminedEvent.rst new file mode 100644 index 000000000000..f501ead1c3ae --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/12.0/Feature-97816-NewAfterTemplatesHaveBeenDeterminedEvent.rst @@ -0,0 +1,33 @@ +.. include:: /Includes.rst.txt + +.. _feature-97816-1664801053: + +=========================================================== +Feature: #97816 - New AfterTemplatesHaveBeenDeterminedEvent +=========================================================== + +See :issue:`97816` + +Description +=========== + +With switching to the new TypoScript parser, hook +:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['Core/TypoScript/TemplateService']['runThroughTemplatesPostProcessing']` +has been removed. + +The new event :php:`AfterTemplatesHaveBeenDeterminedEvent` can be used +to manipulate sys_template rows. The event receives the list of resolved +sys_template rows and the :php:`SiteInterface` and allows manipulating the +sys_template rows array. + + +Impact +====== + +The event is called in Backend EXT:tstemplate code, for example in the Template Analyzer, +and - more importantly - in the Frontend. + +Extensions using the old hook that want to stay compatible with both core v11 and v12 +can implement both. + +.. index:: PHP-API, TypoScript, ext:core diff --git a/typo3/sysext/core/Tests/Functional/TypoScript/IncludeTree/SysTemplateRepositoryTest.php b/typo3/sysext/core/Tests/Functional/TypoScript/IncludeTree/SysTemplateRepositoryTest.php new file mode 100644 index 000000000000..11d98065aa13 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/TypoScript/IncludeTree/SysTemplateRepositoryTest.php @@ -0,0 +1,95 @@ +<?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\Tests\Functional\TypoScript\IncludeTree; + +use TYPO3\CMS\Core\Site\Entity\NullSite; +use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +class SysTemplateRepositoryTest extends FunctionalTestCase +{ + /** + * @test + */ + public function singleRootTemplate(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/SysTemplate/singleRootTemplate.csv'); + $rootline = [ + [ + 'uid' => 1, + 'pid' => 0, + 'is_siteroot' => 0, + ], + ]; + /** @var SysTemplateRepository $sysTemplateRepository */ + $sysTemplateRepository = $this->get(SysTemplateRepository::class); + $result = $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite()); + self::assertSame(1, $result[0]['uid']); + $result = $sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootline, new NullSite(), 1); + self::assertSame(1, $result[0]['uid']); + } + + /** + * @test + */ + public function twoPagesTwoTemplates(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/SysTemplate/twoPagesTwoTemplates.csv'); + $rootline = [ + [ + 'uid' => 2, + 'pid' => 1, + 'is_siteroot' => 0, + ], + [ + 'uid' => 1, + 'pid' => 0, + 'is_siteroot' => 0, + ], + ]; + /** @var SysTemplateRepository $sysTemplateRepository */ + $sysTemplateRepository = $this->get(SysTemplateRepository::class); + $result = $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite()); + self::assertSame(1, $result[0]['uid']); + self::assertSame(2, $result[1]['uid']); + $result = $sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootline, new NullSite(), 2); + self::assertSame(1, $result[0]['uid']); + self::assertSame(2, $result[1]['uid']); + } + + /** + * @test + */ + public function twoTemplatesOnPagePrefersTheOneWithLowerSorting(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/SysTemplate/twoTemplatesOnPage.csv'); + $rootline = [ + [ + 'uid' => 1, + 'pid' => 0, + 'is_siteroot' => 0, + ], + ]; + /** @var SysTemplateRepository $sysTemplateRepository */ + $sysTemplateRepository = $this->get(SysTemplateRepository::class); + $result = $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite()); + self::assertSame(1, $result[0]['uid']); + $result = $sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootline, new NullSite(), 2); + self::assertSame(2, $result[0]['uid']); + } +} diff --git a/typo3/sysext/core/Tests/Functional/TypoScript/IncludeTree/TreeBuilderTest.php b/typo3/sysext/core/Tests/Functional/TypoScript/IncludeTree/TreeBuilderTest.php index cd72c116a3a2..0c5212b092ac 100644 --- a/typo3/sysext/core/Tests/Functional/TypoScript/IncludeTree/TreeBuilderTest.php +++ b/typo3/sysext/core/Tests/Functional/TypoScript/IncludeTree/TreeBuilderTest.php @@ -18,9 +18,12 @@ declare(strict_types=1); namespace TYPO3\CMS\Core\Tests\Functional\TypoScript\IncludeTree; use TYPO3\CMS\Core\Cache\CacheManager; +use TYPO3\CMS\Core\Site\Entity\NullSite; +use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait; use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode; use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude; +use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository; use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser; use TYPO3\CMS\Core\TypoScript\IncludeTree\TreeBuilder; use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeAstBuilderVisitor; @@ -72,9 +75,11 @@ class TreeBuilderTest extends FunctionalTestCase 'is_siteroot' => 0, ], ]; + /** @var SysTemplateRepository $sysTemplateRepository */ + $sysTemplateRepository = $this->get(SysTemplateRepository::class); /** @var TreeBuilder $treeBuilder */ $treeBuilder = $this->get(TreeBuilder::class); - $includeTree = $treeBuilder->getTreeByRootline($rootline, 'constants', false); + $includeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite())); $ast = $this->getAst($includeTree); self::assertSame('fooValue', $ast->getChildByName('foo')->getValue()); } @@ -93,9 +98,11 @@ class TreeBuilderTest extends FunctionalTestCase 'is_siteroot' => 0, ], ]; + /** @var SysTemplateRepository $sysTemplateRepository */ + $sysTemplateRepository = $this->get(SysTemplateRepository::class); /** @var TreeBuilder $treeBuilder */ $treeBuilder = $this->get(TreeBuilder::class); - $includeTree = $treeBuilder->getTreeByRootline($rootline, 'constants', false); + $includeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite())); $ast = $this->getAst($includeTree); self::assertSame('fooValue', $ast->getChildByName('foo')->getValue()); self::assertSame('barValue', $ast->getChildByName('bar')->getValue()); @@ -114,9 +121,17 @@ class TreeBuilderTest extends FunctionalTestCase 'is_siteroot' => 0, ], ]; + /** @var SysTemplateRepository $sysTemplateRepository */ + $sysTemplateRepository = $this->get(SysTemplateRepository::class); + /** @var SiteFinder $siteFinder */ + $siteFinder = $this->get(SiteFinder::class); /** @var TreeBuilder $treeBuilder */ $treeBuilder = $this->get(TreeBuilder::class); - $includeTree = $treeBuilder->getTreeByRootline($rootline, 'constants', false); + $includeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite( + 'constants', + $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite()), + $siteFinder->getSiteByPageId(1) + ); $ast = $this->getAst($includeTree); self::assertSame('testValueFromSite', $ast->getChildByName('testConstantFromSite')->getValue()); } @@ -139,9 +154,11 @@ class TreeBuilderTest extends FunctionalTestCase 'is_siteroot' => 0, ], ]; + /** @var SysTemplateRepository $sysTemplateRepository */ + $sysTemplateRepository = $this->get(SysTemplateRepository::class); /** @var TreeBuilder $treeBuilder */ $treeBuilder = $this->get(TreeBuilder::class); - $includeTree = $treeBuilder->getTreeByRootline($rootline, 'constants', false); + $includeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite())); $ast = $this->getAst($includeTree); self::assertSame('fooValue', $ast->getChildByName('foo')->getValue()); self::assertSame('barValue', $ast->getChildByName('bar')->getValue()); @@ -165,9 +182,11 @@ class TreeBuilderTest extends FunctionalTestCase 'is_siteroot' => 0, ], ]; + /** @var SysTemplateRepository $sysTemplateRepository */ + $sysTemplateRepository = $this->get(SysTemplateRepository::class); /** @var TreeBuilder $treeBuilder */ $treeBuilder = $this->get(TreeBuilder::class); - $includeTree = $treeBuilder->getTreeByRootline($rootline, 'constants', false); + $includeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite())); $ast = $this->getAst($includeTree); self::assertNull($ast->getChildByName('foo')); self::assertSame('barValue', $ast->getChildByName('bar')->getValue()); @@ -186,9 +205,11 @@ class TreeBuilderTest extends FunctionalTestCase 'is_siteroot' => 0, ], ]; + /** @var SysTemplateRepository $sysTemplateRepository */ + $sysTemplateRepository = $this->get(SysTemplateRepository::class); /** @var TreeBuilder $treeBuilder */ $treeBuilder = $this->get(TreeBuilder::class); - $includeTree = $treeBuilder->getTreeByRootline($rootline, 'constants', false); + $includeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite())); $ast = $this->getAst($includeTree); self::assertSame('fooValue', $ast->getChildByName('foo')->getValue()); self::assertNull($ast->getChildByName('bar')); @@ -207,9 +228,11 @@ class TreeBuilderTest extends FunctionalTestCase 'is_siteroot' => 0, ], ]; + /** @var SysTemplateRepository $sysTemplateRepository */ + $sysTemplateRepository = $this->get(SysTemplateRepository::class); /** @var TreeBuilder $treeBuilder */ $treeBuilder = $this->get(TreeBuilder::class); - $includeTree = $treeBuilder->getTreeByRootline($rootline, 'constants', false); + $includeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite())); $ast = $this->getAst($includeTree); self::assertSame('fooValue', $ast->getChildByName('foo')->getValue()); self::assertSame('loadedByBasedOn', $ast->getChildByName('bar')->getValue()); @@ -228,9 +251,11 @@ class TreeBuilderTest extends FunctionalTestCase 'is_siteroot' => 0, ], ]; + /** @var SysTemplateRepository $sysTemplateRepository */ + $sysTemplateRepository = $this->get(SysTemplateRepository::class); /** @var TreeBuilder $treeBuilder */ $treeBuilder = $this->get(TreeBuilder::class); - $includeTree = $treeBuilder->getTreeByRootline($rootline, 'constants', false); + $includeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite())); $ast = $this->getAst($includeTree); self::assertSame('fooValue', $ast->getChildByName('foo')->getValue()); self::assertSame('loadedByBasedOn', $ast->getChildByName('bar')->getValue()); @@ -249,9 +274,11 @@ class TreeBuilderTest extends FunctionalTestCase 'is_siteroot' => 0, ], ]; + /** @var SysTemplateRepository $sysTemplateRepository */ + $sysTemplateRepository = $this->get(SysTemplateRepository::class); /** @var TreeBuilder $treeBuilder */ $treeBuilder = $this->get(TreeBuilder::class); - $includeTree = $treeBuilder->getTreeByRootline($rootline, 'constants', false); + $includeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRepository->getSysTemplateRowsByRootline($rootline, new NullSite())); $ast = $this->getAst($includeTree); self::assertSame('fooValue', $ast->getChildByName('foo')->getValue()); self::assertSame('includeStaticTarget', $ast->getChildByName('bar')->getValue()); diff --git a/typo3/sysext/form/Tests/Functional/RequestHandling/AbstractRequestHandlingTest.php b/typo3/sysext/form/Tests/Functional/RequestHandling/AbstractRequestHandlingTest.php index 66e2a2c8a1ff..bedba3c82b4b 100644 --- a/typo3/sysext/form/Tests/Functional/RequestHandling/AbstractRequestHandlingTest.php +++ b/typo3/sysext/form/Tests/Functional/RequestHandling/AbstractRequestHandlingTest.php @@ -64,10 +64,6 @@ abstract class AbstractRequestHandlingTest extends FunctionalTestCase 'backend' => Typo3DatabaseBackend::class, 'frontend' => VariableFrontend::class, ], - 'pagesection' => [ - 'backend' => Typo3DatabaseBackend::class, - 'frontend' => VariableFrontend::class, - ], 'rootline' => [ 'backend' => Typo3DatabaseBackend::class, 'frontend' => VariableFrontend::class, diff --git a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php index a9a9967bb0d9..367a71b41219 100644 --- a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php +++ b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php @@ -57,6 +57,13 @@ use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\TimeTracker\TimeTracker; use TYPO3\CMS\Core\Type\Bitmask\PageTranslationVisibility; use TYPO3\CMS\Core\Type\Bitmask\Permission; +use TYPO3\CMS\Core\TypoScript\AST\Node\ChildNode; +use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository; +use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser; +use TYPO3\CMS\Core\TypoScript\IncludeTree\TreeBuilder; +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\TemplateService; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\HttpUtility; @@ -66,7 +73,7 @@ use TYPO3\CMS\Core\Utility\RootlineUtility; use TYPO3\CMS\Frontend\Aspect\PreviewAspect; use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication; use TYPO3\CMS\Frontend\Cache\CacheLifetimeCalculator; -use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher; +use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher as FrontendConditionMatcher; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent; use TYPO3\CMS\Frontend\Event\AfterCachedPageIsPersistedEvent; @@ -129,6 +136,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface * Page will not be cached. Write only TRUE. Never clear value (some other * code might have reasons to set it TRUE). * @var bool + * @internal */ public $no_cache = false; @@ -251,6 +259,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface * The TypoScript template object. Used to parse the TypoScript template * * @var TemplateService + * @internal: Will get a proper deprecation in v12.x. + * @deprecated: TemplateService is kept for b/w compat in v12 but will be removed in v13. */ public $tmpl; @@ -264,6 +274,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface /** * Set if cached content was fetched from the cache. + * @internal */ protected bool $pageContentWasLoadedFromCache = false; @@ -274,24 +285,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface protected int $cacheExpires = 0; /** - * Used by template fetching system. This array is an identification of - * the template. If $this->all is empty it's because the template-data is not - * cached, which it must be. - * @var array - * @internal - */ - public $all = []; - - /** - * Toplevel - objArrayName, eg 'page' - * @var string - * @internal should only be used by TYPO3 Core - */ - public $sPre = ''; - - /** - * TypoScript configuration of the page-object pointed to by sPre. - * $this->tmpl->setup[$this->sPre.'.'] + * TypoScript configuration of the page-object. * @var array|string * @internal should only be used by TYPO3 Core */ @@ -575,7 +569,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface */ protected function initCaches() { - $this->pageCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('pages'); + $cacheManager = GeneralUtility::makeInstance(CacheManager::class); + $this->pageCache = $cacheManager->getCache('pages'); } /** @@ -1119,103 +1114,401 @@ class TypoScriptFrontendController implements LoggerAwareInterface } /** - * See if page is in cache and get it if so, populates the page content to $this->content. - * Also fetches the raw cached pagesection information (TypoScript information) before. + * 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. * - * @param ServerRequestInterface $request + * 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 AbstractServerErrorException + * + * @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) + public function getFromCache(ServerRequestInterface $request): void { - // clearing the content-variable, which will hold the pagecontent + // Reset some state. + // @todo: Find out which resets are really needed here - Since this is called from a + // relatively early middleware, we can expect these properties to be not set already?! $this->content = ''; - // Unsetting the lowlevel config $this->config = []; $this->pageContentWasLoadedFromCache = false; - if ($this->no_cache) { - return; + // Very first thing, *always* executed: TypoScript is one factor that influences page content. + // There can be multiple cache entries per page, when TypoScript conditions on the same page + // create different TypoScript. We thus need the sys_template rows relevant for this page. + // @todo: Even though all rootline sys_template records are fetched with only one query + // in below implementation, we could potentially join or sub select sys_template + // records already when pages rootline is queried. This will safe one query + // and needs an implementation in getPageAndRootline() which is called via determineId() + // in TypoScriptFrontendInitialization. This could be done when getPageAndRootline() + // switches to a CTE query instead of using RootlineUtility. + $sysTemplateRepository = GeneralUtility::makeInstance(SysTemplateRepository::class); + $sysTemplateRows = $sysTemplateRepository->getSysTemplateRowsByRootline($this->rootLine, $this->getSite()); + + // Early exception if there is no sys_template at all. + if (empty($sysTemplateRows)) { + $message = 'No TypoScript template found!'; + $this->logger->alert($message); + try { + $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction( + $request, + $message, + ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_FOUND] + ); + throw new PropagateResponseException($response, 1533931380); + } catch (AbstractServerErrorException $e) { + $exceptionClass = get_class($e); + throw new $exceptionClass($message, 1294587218); + } } if (!$this->tmpl instanceof TemplateService) { + // @todo: Well, TemplateService should be deprecated entirely, soon. We're essentially not + // using the old parser anymore and some properties are later just set for b/w compat + // reasons. The if() probably only exists to allow early setting of this property to + // something in tests, other than that, we could expect it to be null at + // this point in the middleware stack. $this->tmpl = GeneralUtility::makeInstance(TemplateService::class, $this->context, null, $this); } - $pageSectionCacheContent = $this->tmpl->getCurrentPageData($this->id, (string)$this->MP); - if (!is_array($pageSectionCacheContent)) { - // Nothing in the cache, we acquire an "exclusive lock" for the key now. - // We use the Registry to store this lock centrally, - // but we protect the access again with a global exclusive lock to avoid race conditions - - $this->acquireLock('pagesection', $this->id . '::' . $this->MP); - // - // from this point on we're the only one working on that page ($key) - // - - // query the cache again to see if the page data are there meanwhile - $pageSectionCacheContent = $this->tmpl->getCurrentPageData($this->id, (string)$this->MP); - if (is_array($pageSectionCacheContent)) { - // we have the content, nice that some other process did the work for us already - $this->releaseLock('pagesection'); - } - // 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($pageSectionCacheContent)) { - // BE CAREFUL to change the content of the cc-array. This array is serialized and an md5-hash based on this is used for caching the page. - // If this hash is not the same in here in this section and after page-generation, then the page will not be properly cached! - // This array is an identification of the template. If $this->all is empty it's because the template-data is not cached, which it must be. - $pageSectionCacheContent = $this->tmpl->matching($pageSectionCacheContent); - ksort($pageSectionCacheContent); - $this->all = $pageSectionCacheContent; + if ($this->no_cache) { + // $this->no_cache = true might have been set by earlier TypoScriptFrontendInitialization middleware. + // This means we don't do any fancy cache stuff, calculate full TypoScript and ignore page cache. + $this->prepareUncachedRendering($request, $sysTemplateRows); + return; } - // Look for page in cache only if a shift-reload is not sent to the server. - $lockHash = $this->getLockHash(); - if ($this->shouldAcquireCacheData($request) && $this->all) { - // we got page section information (TypoScript), so lets see if there is also a cached version - // of this page in the pages cache. - $this->newHash = $this->getHash(); + // 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. + // @todo: This is currently a rather naive approach - we simply always create the full constant AST plus + // $flatConstants. This shouldn't be *that* bad since the include tree is already fetched from cache, + // so "only" the AST parsing is done, and constants are typically at least an order of magnitude smaller + // than setup and thus relatively quick to parse. + // There are however further optimization opportunities: The constant AST only depends on sys_template + // rows (which have to be *always* queried anyways), plus condition verdicts, plus maybe SIM_ACCESS_TIME?! + // We could thus fetch only the IncludeTree and have a traverser that calculates and gathers conditions verdicts. + // Then hash $sysTemplateRows together with conditions list and it's verdicts, and use that as cache identifier. + // We could then cache the constant AST, flat constants and maybe even the AST as array in one cache entry + // with this identifier. Potential improvement: A relatively small cache entry for the constants, no AST + // parsing, re-usable for many pages that have the same sys_template combination and condition verdicts. + // So probably not too many cache entries, either. This may squeeze some time out of this construct and is + // beneficial since the code below is *always* executed even in full cached context. + $treeBuilder = GeneralUtility::makeInstance(TreeBuilder::class); + $includeTreeTraverser = GeneralUtility::makeInstance(ConditionVerdictAwareIncludeTreeTraverser::class); + $constantIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $this->getSite()); + $includeTreeTraverser->resetVisitors(); + $conditionMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class); + $conditionMatcherVisitor->setConditionMatcher(GeneralUtility::makeInstance(FrontendConditionMatcher::class, $this->context, $this->id, $this->rootLine)); + $includeTreeTraverser->addVisitor($conditionMatcherVisitor); + $constantAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeAstBuilderVisitor::class); + $includeTreeTraverser->addVisitor($constantAstBuilderVisitor); + $includeTreeTraverser->traverse($constantIncludeTree); + $constantAst = $constantAstBuilderVisitor->getAst(); + $constantConditionList = $conditionMatcherVisitor->getConditionList(); + $flatConstants = $constantAst->flatten(); + + // 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. We then traverse the 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. + // @todo: There is quite some optimization potential here: First, we don't need the tokenized TS streams from the + // IncludeTree cache entries here. We could thus create an IncludeTree cache entry that does not have + // the token streams. This will create a significantly smaller cache entry that is quicker to require and has + // a better chance to end up in opcache, too. We could also cache the final AST and maybe the array representation + // with an identifier created from the constant hash plus the condition hash. This would allow us to not parse AST + // at all, but just require the final AST in many cases directly. Both above strategies would be caches shared and + // re-usable between multiple different pages when constants and conditions resolve identical, which will safe a ton + // of time in both not-yet-cached-page and user-int scenarios since AST parsing is gone. + $setupIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $this->getSite()); + $includeTreeTraverser->resetVisitors(); + $setupConditionConstantSubstitutionVisitor = GeneralUtility::makeInstance(IncludeTreeSetupConditionConstantSubstitutionVisitor::class); + $setupConditionConstantSubstitutionVisitor->setFlattenedConstants($flatConstants); + $includeTreeTraverser->addVisitor($setupConditionConstantSubstitutionVisitor); + // @todo: Maybe split IncludeTreeConditionMatcherVisitor into two implementations? One that gathers conditions and one that does not? + $setupMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class); + $setupMatcherVisitor->setConditionMatcher(GeneralUtility::makeInstance(FrontendConditionMatcher::class, $this->context, $this->id, $this->rootLine)); + $includeTreeTraverser->addVisitor($setupMatcherVisitor); + $includeTreeTraverser->traverse($setupIncludeTree); + $setupConditionList = $setupMatcherVisitor->getConditionList(); + + $lockHash = $this->createLockHash(); + $this->newHash = $this->id . '_' . md5($this->createHashBase($sysTemplateRows, $constantConditionList, $setupConditionList)); + if ($this->shouldAcquireCacheData($request)) { + // Try to get a page cache row. $this->getTimeTracker()->push('Cache Row'); - $row = $this->getFromCache_queryRow(); - if (!is_array($row)) { - // nothing in the cache, we acquire an exclusive lock now + $pageCacheRow = $this->getFromCache_queryRow(); + 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. This has the drawback that the page + // cache is queried twice if the lock did not had to wait for another process ... Maybe we could suppress this? $this->acquireLock('pages', $lockHash); - // - // from this point on we're the only one working on that page ($lockHash) - // - - // query the cache again to see if the data are there meanwhile - $row = $this->getFromCache_queryRow(); - if (is_array($row)) { - // we have the content, nice that some other process did the work for us + // From this point on we're the only one working on that page ($lockHash). + // Query the cache again to see if the data is there meanwhile. + $pageCacheRow = $this->getFromCache_queryRow(); + if (is_array($pageCacheRow)) { + // We have the content, some other process did the work for us, release our lock again. $this->releaseLock('pages'); } // 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() + // This indicates that we have to release the lock later in releaseLocks()! } - if (is_array($row)) { - $this->populatePageDataFromCache($row); + if (is_array($pageCacheRow)) { + // Note this especially populates $this->config! + $this->populatePageDataFromCache($pageCacheRow); } $this->getTimeTracker()->pull(); } else { - // the user forced rebuilding the page cache or there was no pagesection information - // get a lock for the page content so other processes will not interrupt the regeneration + // User forced page cache rebuilding. Get a lock for the page content so other processes can't interfere. $this->acquireLock('pages', $lockHash); } + + if (empty($this->config) || $this->isINTincScript() || $this->context->getPropertyFromAspect('typoscript', 'forcedTemplateParsing')) { + // We don't have a cache entry for this page, or the user force TS parsing (do we really need this feature?), or + // (more importantly), the cache page row contains "INT" objects. In these cases, we have to boot up the rest of + // TypoScript: We need the full AST / array! + // Since we get the IncludeTree above already, and calculated condition verdicts (that's not very expensive anyways), + // We just need to reset $includeTreeTraverser visitors and only add the AST builder visitor to create the final AST. + // @todo: This code part should change as soon as the above mentioned "more effective setup AST building" + // strategy is implemented: We could still fetch the final AST from cache without parsing here. + $includeTreeTraverser->resetVisitors(); + $setupAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeAstBuilderVisitor::class); + $setupAstBuilderVisitor->setFlatConstants($flatConstants); + $includeTreeTraverser->addVisitor($setupAstBuilderVisitor); + $includeTreeTraverser->traverse($setupIncludeTree); + $setupAst = $setupAstBuilderVisitor->getAst(); + + // Create top-level setup AST 'types' node from all top-level PAGE objects. + // This is essentially a preparation for type-lookup below and should vanish later. + // Previously, TemplateService->generateConfig() did that. + $typesNode = new ChildNode('types'); + $gotTypeNumZero = false; + foreach ($setupAst->getNextChild() as $setupChild) { + if ($setupChild->getValue() === 'PAGE') { + $typeNumChild = $setupChild->getChildByName('typeNum'); + if ($typeNumChild) { + $typeNumValue = $typeNumChild->getValue(); + $typesSubNode = new ChildNode($typeNumValue); + $typesSubNode->setValue($setupChild->getName()); + $typesNode->addChild($typesSubNode); + if ($typeNumValue === '0') { + $gotTypeNumZero = true; + } + } elseif (!$gotTypeNumZero) { + // The first PAGE node that has no typeNum = 0 is considered + // '0' automatically. + $typesSubNode = new ChildNode('0'); + $typesSubNode->setValue($setupChild->getName()); + $typesNode->addChild($typesSubNode); + $gotTypeNumZero = true; + } + } + } + if ($typesNode->hasChildren()) { + $setupAst->addChild($typesNode); + } + + $setupAstArray = $setupAst->toArray(); + + $typoScriptPageTypeName = $setupAstArray['types.'][$this->type] ?? ''; + $this->pSetup = $setupAstArray[$typoScriptPageTypeName . '.'] ?? ''; + + if (!is_array($this->pSetup)) { + $this->logger->alert('The page is not configured! [type={type}][{type_name}].', ['type' => $this->type, 'type_name' => $typoScriptPageTypeName]); + try { + $message = 'The page is not configured! [type=' . $this->type . '][' . $typoScriptPageTypeName . '].'; + $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction( + $request, + $message, + ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_CONFIGURED] + ); + throw new PropagateResponseException($response, 1533931374); + } catch (AbstractServerErrorException $e) { + $explanation = 'This means that there is no TypoScript object of type PAGE with typeNum=' . $this->type . ' configured.'; + $exceptionClass = get_class($e); + throw new $exceptionClass($message . ' ' . $explanation, 1294587217); + } + } + + if (!isset($this->config['config'])) { + $this->config['config'] = []; + } + // Filling the config-array, first with the main "config." part + if (is_array($setupAstArray['config.'] ?? null)) { + $setupAstArray['config.'] = array_replace_recursive($setupAstArray['config.'], $this->config['config']); + $this->config['config'] = $setupAstArray['config.']; + } + // override it with the page/type-specific "config." + if (is_array($this->pSetup['config.'] ?? null)) { + $this->config['config'] = array_replace_recursive($this->config['config'], $this->pSetup['config.']); + } + // Processing for the config_array: + $this->config['rootLine'] = array_reverse($this->rootLine); + + // b/w compat, especially for bootstrap_package which does a lot of magic with constants + $this->tmpl->setup = $setupAstArray; + $this->tmpl->loaded = true; + $this->tmpl->rootLine = array_reverse($this->rootLine); + $this->tmpl->flatSetup = $flatConstants; + } + + // @todo: To phase out TemplateService, we should attach $constantAst and $setupAst (and maybe their array + // representations as well for a transition phase) as attributes to the request here and also below + // in prepareUncachedRendering(). + + // Set $this->no_cache TRUE if the config.no_cache value is set! + if ($this->config['config']['no_cache'] ?? false) { + $this->set_no_cache('config.no_cache is set', true); + } + + // Auto-configure settings when a site is configured + $this->config['config']['absRefPrefix'] = $this->config['config']['absRefPrefix'] ?? 'auto'; + + // Hook for postProcessing the configuration array + $params = ['config' => &$this->config['config']]; + foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['configArrayPostProc'] ?? [] as $funcRef) { + GeneralUtility::callUserFunction($funcRef, $params, $this); + } + } + + /** + * An early method branching within getFromCache() if caching has been disabled. + * @todo: This is currently not cleaned up well and duplicates code from getFromCache(). + * This should be cleaned up and merged with getFromCache() in a more efficient way, for + * now it is a simple solution to keep the scenarios apart. + */ + private function prepareUncachedRendering(ServerRequestInterface $request, array $sysTemplateRows): void + { + $includeTreeTraverser = GeneralUtility::makeInstance(ConditionVerdictAwareIncludeTreeTraverser::class); + $treeBuilder = GeneralUtility::makeInstance(TreeBuilder::class); + $treeBuilder->disableCache(); + + $constantIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $this->getSite()); + $includeTreeTraverser->resetVisitors(); + $conditionMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class); + $conditionMatcherVisitor->setConditionMatcher(GeneralUtility::makeInstance(FrontendConditionMatcher::class, $this->context, $this->id, $this->rootLine)); + $includeTreeTraverser->addVisitor($conditionMatcherVisitor); + $constantAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeAstBuilderVisitor::class); + $includeTreeTraverser->addVisitor($constantAstBuilderVisitor); + $includeTreeTraverser->traverse($constantIncludeTree); + + $constantAst = $constantAstBuilderVisitor->getAst(); + $flatConstants = $constantAst->flatten(); + + $setupIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $this->getSite()); + $includeTreeTraverser->resetVisitors(); + $setupConditionConstantSubstitutionVisitor = GeneralUtility::makeInstance(IncludeTreeSetupConditionConstantSubstitutionVisitor::class); + $setupConditionConstantSubstitutionVisitor->setFlattenedConstants($flatConstants); + $includeTreeTraverser->addVisitor($setupConditionConstantSubstitutionVisitor); + $setupMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class); + $setupMatcherVisitor->setConditionMatcher(GeneralUtility::makeInstance(FrontendConditionMatcher::class, $this->context, $this->id, $this->rootLine)); + $includeTreeTraverser->addVisitor($setupMatcherVisitor); + $setupAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeAstBuilderVisitor::class); + $setupAstBuilderVisitor->setFlatConstants($flatConstants); + $includeTreeTraverser->addVisitor($setupAstBuilderVisitor); + $includeTreeTraverser->traverse($setupIncludeTree); + $setupAst = $setupAstBuilderVisitor->getAst(); + + // Create top-level setup AST 'types' node from all top-level PAGE objects. + // This is essentially a preparation for type-lookup below and should vanish later. + // Previously, TemplateService->generateConfig() did this. + $typesNode = new ChildNode('types'); + $gotTypeNumZero = false; + foreach ($setupAst->getNextChild() as $setupChild) { + if ($setupChild->getValue() === 'PAGE') { + $typeNumChild = $setupChild->getChildByName('typeNum'); + if ($typeNumChild) { + $typeNumValue = $typeNumChild->getValue(); + $typesSubNode = new ChildNode($typeNumValue); + $typesSubNode->setValue($setupChild->getName()); + $typesNode->addChild($typesSubNode); + if ($typeNumValue === '0') { + $gotTypeNumZero = true; + } + } elseif (!$gotTypeNumZero) { + // The first PAGE node that has no typeNum = 0 is considered '0' automatically. + $typesSubNode = new ChildNode('0'); + $typesSubNode->setValue($setupChild->getName()); + $typesNode->addChild($typesSubNode); + $gotTypeNumZero = true; + } + } + } + if ($typesNode->hasChildren()) { + $setupAst->addChild($typesNode); + } + + $setupAstArray = $setupAst->toArray(); + + $typoScriptPageTypeName = $setupAstArray['types.'][$this->type] ?? ''; + $this->pSetup = $setupAstArray[$typoScriptPageTypeName . '.'] ?? ''; + + if (!is_array($this->pSetup)) { + $this->logger->alert('The page is not configured! [type={type}][{type_name}].', ['type' => $this->type, 'type_name' => $typoScriptPageTypeName]); + try { + $message = 'The page is not configured! [type=' . $this->type . '][' . $typoScriptPageTypeName . '].'; + $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction( + $request, + $message, + ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_CONFIGURED] + ); + // @todo: use the timestamp from above same call when extracting this to a method. + throw new PropagateResponseException($response, 1664672015); + } catch (AbstractServerErrorException $e) { + $explanation = 'This means that there is no TypoScript object of type PAGE with typeNum=' . $this->type . ' configured.'; + $exceptionClass = get_class($e); + throw new $exceptionClass($message . ' ' . $explanation, 1664672036); + } + } + + $this->config['config'] = []; + // Filling the config-array, first with the main "config." part + if (is_array($setupAstArray['config.'] ?? null)) { + $setupAstArray['config.'] = array_replace_recursive($setupAstArray['config.'], $this->config['config']); + $this->config['config'] = $setupAstArray['config.']; + } + // override it with the page/type-specific "config." + if (is_array($this->pSetup['config.'] ?? null)) { + $this->config['config'] = array_replace_recursive($this->config['config'], $this->pSetup['config.']); + } + // Processing for the config_array: + $this->config['rootLine'] = array_reverse($this->rootLine); + // Auto-configure settings when a site is configured + $this->config['config']['absRefPrefix'] = $this->config['config']['absRefPrefix'] ?? 'auto'; + + // b/w compat, especially for bootstrap_package which does a lot of magic with constants + $this->tmpl->setup = $setupAstArray; + $this->tmpl->loaded = true; + $this->tmpl->rootLine = array_reverse($this->rootLine); + $this->tmpl->flatSetup = $flatConstants; + + // Hook for postProcessing the configuration array + $params = ['config' => &$this->config['config']]; + foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['configArrayPostProc'] ?? [] as $funcRef) { + GeneralUtility::callUserFunction($funcRef, $params, $this); + } } /** * Returning the cached version of page with hash = newHash * * @return array Cached row, if any. Otherwise void. + * @internal */ public function getFromCache_queryRow() { - $this->getTimeTracker()->push('Cache Query'); - $row = $this->pageCache->get($this->newHash); - $this->getTimeTracker()->pull(); - return $row; + return $this->pageCache->get($this->newHash); } /** @@ -1227,6 +1520,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface * @see getFromCache() * @see setPageCacheContent() * @param array $cachedData + * @internal */ protected function populatePageDataFromCache(array $cachedData): void { @@ -1268,6 +1562,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface * 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. + * @internal */ protected function shouldAcquireCacheData(ServerRequestInterface $request): bool { @@ -1285,31 +1580,43 @@ class TypoScriptFrontendController implements LoggerAwareInterface } /** - * Calculates the cache-hash - * This hash is unique to the template, the variables ->id, ->type, list of fe user groups, ->MP (Mount Points) and cHash array - * Used to get and later store the cached data. - * - * @return string MD5 hash of serialized hash base from createHashBase(), prefixed with page id - * @see getFromCache() - * @see getLockHash() - */ - protected function getHash(): string - { - return $this->id . '_' . md5($this->createHashBase(false)); - } - - /** - * Calculates the lock-hash - * This hash is unique to the above hash, except that it doesn't contain the template information in $this->all. - * - * @return string MD5 hash prefixed with page id - * @see getFromCache() - * @see getHash() + * This creates a hash used to lock generation for a specific page: When multiple requests try + * to render the same page, this lock allows creating 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 method is similar to createHashBase() which is used as identifier for the page cache entry, + * with the exception that it does not include TypoScript constant and constant / setup condition + * results. This means multiple "different" calls to the same page still block each other when their + * TypoScript conditions verdicts are different. + * @todo: Find out if we couldn't simply use createHashBase() instead. Why should different calls to + * the same page that will lead to different page cache entries block each other? */ - protected function getLockHash(): string + private function createLockHash(): string { - $lockHash = $this->createHashBase(true); - return $this->id . '_' . md5($lockHash); + /** @var UserAspect $userAspect */ + $userAspect = $this->context->getAspect('frontend.user'); + $hashParameters = [ + 'id' => $this->id, + 'type' => $this->type, + 'groupIds' => (string)implode(',', $userAspect->getGroupIds()), + 'MP' => (string)$this->MP, + 'site' => $this->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)$this->language->getBase(), + // additional variation trigger for static routes + 'staticRouteArguments' => $this->pageArguments->getStaticArguments(), + // dynamic route arguments (if route was resolved) + 'dynamicArguments' => $this->getRelevantParametersForCachingFromPageArguments($this->pageArguments), + ]; + // Call hook to influence the hash calculation + $_params = [ + 'hashParameters' => &$hashParameters, + 'createLockHashBase' => true, + ]; + foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['createHashBase'] ?? [] as $_funcRef) { + GeneralUtility::callUserFunction($_funcRef, $_params, $this); + } + return $this->id . '_' . md5(serialize($hashParameters)); } /** @@ -1319,10 +1626,9 @@ class TypoScriptFrontendController implements LoggerAwareInterface * ->MP (Mount Points) and cHash array * Used to get and later store the cached data. * - * @param bool $createLockHashBase Whether to create the lock hash, which doesn't contain the "this->all" (the template information) * @return string the serialized hash base */ - protected function createHashBase($createLockHashBase = false) + protected function createHashBase(array $sysTemplateRows, array $constantConditionList, array $setupConditionList): string { // Fetch the list of user groups /** @var UserAspect $userAspect */ @@ -1340,15 +1646,14 @@ class TypoScriptFrontendController implements LoggerAwareInterface 'staticRouteArguments' => $this->pageArguments->getStaticArguments(), // dynamic route arguments (if route was resolved) 'dynamicArguments' => $this->getRelevantParametersForCachingFromPageArguments($this->pageArguments), + 'sysTemplateRows' => $sysTemplateRows, + 'constantConditionList' => $constantConditionList, + 'setupConditionList' => $setupConditionList, ]; - // Include the template information if we shouldn't create a lock hash - if (!$createLockHashBase) { - $hashParameters['all'] = $this->all; - } // Call hook to influence the hash calculation $_params = [ 'hashParameters' => &$hashParameters, - 'createLockHashBase' => $createLockHashBase, + 'createLockHashBase' => false, ]; foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['createHashBase'] ?? [] as $_funcRef) { GeneralUtility::callUserFunction($_funcRef, $_params, $this); @@ -1356,100 +1661,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface return serialize($hashParameters); } - /** - * Checks if config-array exists already but if not, gets it - * - * @param ServerRequestInterface $request - * @throws \TYPO3\CMS\Core\Error\Http\InternalServerErrorException - * @throws \TYPO3\CMS\Core\Error\Http\ServiceUnavailableException - */ - public function getConfigArray(ServerRequestInterface $request): void - { - if (!$this->tmpl instanceof TemplateService) { - $this->tmpl = GeneralUtility::makeInstance(TemplateService::class, $this->context, null, $this); - } - - // If config is not set by the cache (which would be a major mistake somewhere) OR if INTincScripts-include-scripts have been registered, then we must parse the template in order to get it - if (empty($this->config) || $this->isINTincScript() || $this->context->getPropertyFromAspect('typoscript', 'forcedTemplateParsing')) { - $timeTracker = $this->getTimeTracker(); - $timeTracker->push('Parse template'); - // Start parsing the TS template. Might return cached version. - $this->tmpl->start($this->rootLine); - $timeTracker->pull(); - // At this point we have a valid pagesection_cache (generated in $this->tmpl->start()), - // so let all other processes proceed now. (They are blocked at the pagessection_lock in getFromCache()) - $this->releaseLock('pagesection'); - if ($this->tmpl->loaded) { - $timeTracker->push('Setting the config-array'); - // toplevel - objArrayName - $typoScriptPageTypeName = $this->tmpl->setup['types.'][$this->type] ?? ''; - $this->sPre = $typoScriptPageTypeName; - $this->pSetup = $this->tmpl->setup[$typoScriptPageTypeName . '.'] ?? ''; - if (!is_array($this->pSetup)) { - $this->logger->alert('The page is not configured! [type={type}][{type_name}].', ['type' => $this->type, 'type_name' => $typoScriptPageTypeName]); - try { - $message = 'The page is not configured! [type=' . $this->type . '][' . $typoScriptPageTypeName . '].'; - $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction( - $request, - $message, - ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_CONFIGURED] - ); - throw new PropagateResponseException($response, 1533931374); - } catch (AbstractServerErrorException $e) { - $explanation = 'This means that there is no TypoScript object of type PAGE with typeNum=' . $this->type . ' configured.'; - $exceptionClass = get_class($e); - throw new $exceptionClass($message . ' ' . $explanation, 1294587217); - } - } else { - if (!isset($this->config['config'])) { - $this->config['config'] = []; - } - // Filling the config-array, first with the main "config." part - if (is_array($this->tmpl->setup['config.'] ?? null)) { - $this->tmpl->setup['config.'] = array_replace_recursive($this->tmpl->setup['config.'], $this->config['config']); - $this->config['config'] = $this->tmpl->setup['config.']; - } - // override it with the page/type-specific "config." - if (is_array($this->pSetup['config.'] ?? null)) { - $this->config['config'] = array_replace_recursive($this->config['config'], $this->pSetup['config.']); - } - // Processing for the config_array: - $this->config['rootLine'] = $this->tmpl->rootLine; - } - $timeTracker->pull(); - } else { - $message = 'No TypoScript template found!'; - $this->logger->alert($message); - try { - $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction( - $request, - $message, - ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_FOUND] - ); - throw new PropagateResponseException($response, 1533931380); - } catch (AbstractServerErrorException $e) { - $exceptionClass = get_class($e); - throw new $exceptionClass($message, 1294587218); - } - } - } - - // No cache - // Set $this->no_cache TRUE if the config.no_cache value is set! - if ($this->config['config']['no_cache'] ?? false) { - $this->set_no_cache('config.no_cache is set', true); - } - - // Auto-configure settings when a site is configured - $this->config['config']['absRefPrefix'] = $this->config['config']['absRefPrefix'] ?? 'auto'; - - // Hook for postProcessing the configuration array - $params = ['config' => &$this->config['config']]; - foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['configArrayPostProc'] ?? [] as $funcRef) { - GeneralUtility::callUserFunction($funcRef, $params, $this); - } - } - /******************************************** * * Further initialization and data processing @@ -1779,7 +1990,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface */ public function releaseLocks() { - $this->releaseLock('pagesection'); $this->releaseLock('pages'); } @@ -1812,10 +2022,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface */ public function generatePage_preProcessing() { - // Same codeline as in getFromCache(). But $this->all has been changed by - // \TYPO3\CMS\Core\TypoScript\TemplateService::start() in the meantime, so this must be called again! - $this->newHash = $this->getHash(); - // Used as a safety check in case a PHP script is falsely disabling $this->no_cache during page generation. $this->no_cacheBeforePageGen = $this->no_cache; } @@ -2401,7 +2607,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface public function getPagesTSconfig(): array { if (!is_array($this->pagesTSconfig)) { - $matcher = GeneralUtility::makeInstance(ConditionMatcher::class, $this->context, $this->id, $this->rootLine); + $matcher = GeneralUtility::makeInstance(FrontendConditionMatcher::class, $this->context, $this->id, $this->rootLine); $this->pagesTSconfig = GeneralUtility::makeInstance(PageTsConfig::class) ->getForRootLine( array_reverse($this->rootLine), @@ -2573,7 +2779,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface * @param string $key * @throws \InvalidArgumentException * @throws \RuntimeException - * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException + * @internal */ protected function acquireLock($type, $key) { @@ -2618,7 +2824,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface * @param string $type * @throws \InvalidArgumentException * @throws \RuntimeException - * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException + * @internal */ protected function releaseLock($type) { diff --git a/typo3/sysext/frontend/Classes/Http/RequestHandler.php b/typo3/sysext/frontend/Classes/Http/RequestHandler.php index b0f12046d526..131d7cd60498 100644 --- a/typo3/sysext/frontend/Classes/Http/RequestHandler.php +++ b/typo3/sysext/frontend/Classes/Http/RequestHandler.php @@ -136,7 +136,7 @@ class RequestHandler implements RequestHandlerInterface // Content generation $this->timeTracker->incStackPointer(); - $this->timeTracker->push($controller->sPre, 'PAGE'); + $this->timeTracker->push('Page generation PAGE object'); $controller->content = $this->generatePageContent($controller, $request); diff --git a/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php b/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php index 9f18d6c1b31f..f756753ed496 100644 --- a/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php +++ b/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php @@ -29,26 +29,17 @@ use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; * * Do all necessary preparation steps for rendering * - * @internal this middleware might get removed in TYPO3 v10.x. + * @internal this middleware might get removed later. */ -class PrepareTypoScriptFrontendRendering implements MiddlewareInterface +final class PrepareTypoScriptFrontendRendering implements MiddlewareInterface { - /** - * @var TimeTracker - */ - protected $timeTracker; - - public function __construct(TimeTracker $timeTracker) - { - $this->timeTracker = $timeTracker; + public function __construct( + private readonly TimeTracker $timeTracker + ) { } /** * Initialize TypoScriptFrontendController to the point right before rendering of the page is triggered - * - * @param ServerRequestInterface $request - * @param RequestHandlerInterface $handler - * @return ResponseInterface */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { @@ -58,14 +49,11 @@ class PrepareTypoScriptFrontendRendering implements MiddlewareInterface // 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; - // Get from cache + $this->timeTracker->push('Get Page from cache'); - // Locks may be acquired here + // Get from cache. Locks may be acquired here. After this, we should have a valid config-array ready. $controller->getFromCache($request); $this->timeTracker->pull(); - // Get config if not already gotten - // After this, we should have a valid config-array ready - $controller->getConfigArray($request); $response = $handler->handle($request); diff --git a/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php b/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php index f8e909ef24e9..5308f90b257e 100644 --- a/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php +++ b/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php @@ -38,18 +38,13 @@ use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons; * In addition, determineId builds up the rootline based on a valid frontend-user authentication and * Backend permissions if previewing. * - * @internal this middleware might get removed in TYPO3 v11.0. + * @internal this middleware might get removed later. */ -class TypoScriptFrontendInitialization implements MiddlewareInterface +final class TypoScriptFrontendInitialization implements MiddlewareInterface { - /** - * @var Context - */ - protected $context; - - public function __construct(Context $context) - { - $this->context = $context; + public function __construct( + private readonly Context $context + ) { } /** diff --git a/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php b/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php index 45a7c6ec1d21..66ce9f8a88cc 100644 --- a/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php +++ b/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php @@ -25,6 +25,7 @@ use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ServerRequestInterface; 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\SystemEnvironmentBuilder; use TYPO3\CMS\Core\Domain\Repository\PageRepository; @@ -279,6 +280,7 @@ class TypoScriptFrontendControllerTest extends UnitTestCase /** * @test + * @todo: Turn into functional test or drop. */ public function pageRendererLanguageIsSetToSiteLanguageTypo3LanguageInConstructor(): void { @@ -289,6 +291,8 @@ class TypoScriptFrontendControllerTest extends UnitTestCase $cacheManagerProphecy = $this->prophesize(CacheManager::class); $cacheManagerProphecy->getCache('pages')->willReturn($cacheFrontendProphecy->reveal()); $cacheManagerProphecy->getCache('l10n')->willReturn($cacheFrontendProphecy->reveal()); + $cachePhpFrontendProphecy = $this->prophesize(PhpFrontend::class); + $cacheManagerProphecy->getCache('typoscript')->willReturn($cachePhpFrontendProphecy->reveal()); GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerProphecy->reveal()); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest('https://www.example.com/')) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE); @@ -318,6 +322,7 @@ class TypoScriptFrontendControllerTest extends UnitTestCase /** * @test + * @todo: Turn into functional test or drop */ public function languageServiceIsSetUpWithSiteLanguageTypo3LanguageInConstructor(): void { @@ -328,6 +333,8 @@ class TypoScriptFrontendControllerTest extends UnitTestCase $cacheManagerProphecy = $this->prophesize(CacheManager::class); $cacheManagerProphecy->getCache('pages')->willReturn($cacheFrontendProphecy->reveal()); $cacheManagerProphecy->getCache('l10n')->willReturn($cacheFrontendProphecy->reveal()); + $cachePhpFrontendProphecy = $this->prophesize(PhpFrontend::class); + $cacheManagerProphecy->getCache('typoscript')->willReturn($cachePhpFrontendProphecy->reveal()); GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerProphecy->reveal()); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest('https://www.example.com/')) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE); @@ -362,6 +369,7 @@ class TypoScriptFrontendControllerTest extends UnitTestCase /** * @test + * @todo: Turn into functional test or drop */ public function mountPointParameterContainsOnlyValidMPValues(): void { @@ -372,6 +380,8 @@ class TypoScriptFrontendControllerTest extends UnitTestCase $cacheManagerProphecy = $this->prophesize(CacheManager::class); $cacheManagerProphecy->getCache('pages')->willReturn($cacheFrontendProphecy->reveal()); $cacheManagerProphecy->getCache('l10n')->willReturn($cacheFrontendProphecy->reveal()); + $cachePhpFrontendProphecy = $this->prophesize(PhpFrontend::class); + $cacheManagerProphecy->getCache('typoscript')->willReturn($cachePhpFrontendProphecy->reveal()); GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerProphecy->reveal()); $languageService = new LanguageService(new Locales(), new LocalizationFactory(new LanguageStore($packageManagerProphecy->reveal()), $cacheManagerProphecy->reveal()), $cacheFrontendProphecy->reveal()); $languageServiceFactoryProphecy = $this->prophesize(LanguageServiceFactory::class); diff --git a/typo3/sysext/frontend/ext_localconf.php b/typo3/sysext/frontend/ext_localconf.php index 381413ec5d20..82d8454d097b 100644 --- a/typo3/sysext/frontend/ext_localconf.php +++ b/typo3/sysext/frontend/ext_localconf.php @@ -2,6 +2,8 @@ declare(strict_types=1); +use TYPO3\CMS\Core\Cache\Backend\SimpleFileBackend; +use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend; use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; use TYPO3\CMS\Frontend\Controller\ShowImageController; use TYPO3\CMS\Frontend\Hooks\TreelistCacheUpdateHooks; @@ -11,6 +13,17 @@ defined('TYPO3') or die(); // Register eID provider for showpic $GLOBALS['TYPO3_CONF_VARS']['FE']['eID_include']['tx_cms_showpic'] = ShowImageController::class . '::processRequest'; +// Register TypoScript caching +if (!is_array($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['typoscript'] ?? null)) { + $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['typoscript'] = [ + 'frontend' => PhpFrontend::class, + 'backend' => SimpleFileBackend::class, + 'groups' => [ + 'pages', + ], + ]; +} + ExtensionManagementUtility::addUserTSConfig(' options.saveDocView = 1 options.saveDocNew = 1 diff --git a/typo3/sysext/install/Classes/Configuration/Cache/CustomCachePreset.php b/typo3/sysext/install/Classes/Configuration/Cache/CustomCachePreset.php index c73093418adc..56c57a678ffa 100644 --- a/typo3/sysext/install/Classes/Configuration/Cache/CustomCachePreset.php +++ b/typo3/sysext/install/Classes/Configuration/Cache/CustomCachePreset.php @@ -31,7 +31,6 @@ class CustomCachePreset extends AbstractCustomPreset implements CustomPresetInte protected $configurationValues = [ 'SYS/caching/cacheConfigurations/hash/backend' => Typo3DatabaseBackend::class, 'SYS/caching/cacheConfigurations/pages/backend' => Typo3DatabaseBackend::class, - 'SYS/caching/cacheConfigurations/pagesection/backend' => Typo3DatabaseBackend::class, 'SYS/caching/cacheConfigurations/imagesizes/backend' => Typo3DatabaseBackend::class, 'SYS/caching/cacheConfigurations/rootline/backend' => Typo3DatabaseBackend::class, ]; diff --git a/typo3/sysext/install/Classes/Configuration/Cache/DatabaseCachePreset.php b/typo3/sysext/install/Classes/Configuration/Cache/DatabaseCachePreset.php index cc838f2df11f..681efefdce85 100644 --- a/typo3/sysext/install/Classes/Configuration/Cache/DatabaseCachePreset.php +++ b/typo3/sysext/install/Classes/Configuration/Cache/DatabaseCachePreset.php @@ -37,8 +37,6 @@ class DatabaseCachePreset extends AbstractPreset 'SYS/caching/cacheConfigurations/hash/backend' => Typo3DatabaseBackend::class, 'SYS/caching/cacheConfigurations/pages/backend' => Typo3DatabaseBackend::class, 'SYS/caching/cacheConfigurations/pages/options/compression' => true, - 'SYS/caching/cacheConfigurations/pagesection/backend' => Typo3DatabaseBackend::class, - 'SYS/caching/cacheConfigurations/pagesection/options/compression' => true, 'SYS/caching/cacheConfigurations/imagesizes/backend' => Typo3DatabaseBackend::class, 'SYS/caching/cacheConfigurations/imagesizes/options/compression' => true, 'SYS/caching/cacheConfigurations/rootline/backend' => Typo3DatabaseBackend::class, diff --git a/typo3/sysext/install/Classes/Configuration/Cache/FileCachePreset.php b/typo3/sysext/install/Classes/Configuration/Cache/FileCachePreset.php index b241ceaa934c..fd53b8ec4bd7 100644 --- a/typo3/sysext/install/Classes/Configuration/Cache/FileCachePreset.php +++ b/typo3/sysext/install/Classes/Configuration/Cache/FileCachePreset.php @@ -38,8 +38,6 @@ class FileCachePreset extends AbstractPreset 'SYS/caching/cacheConfigurations/hash/backend' => FileBackend::class, 'SYS/caching/cacheConfigurations/pages/backend' => FileBackend::class, 'SYS/caching/cacheConfigurations/pages/options/compression' => '__UNSET', - 'SYS/caching/cacheConfigurations/pagesection/backend' => FileBackend::class, - 'SYS/caching/cacheConfigurations/pagesection/options/compression' => '__UNSET', 'SYS/caching/cacheConfigurations/imagesizes/backend' => SimpleFileBackend::class, 'SYS/caching/cacheConfigurations/imagesizes/options/compression' => '__UNSET', 'SYS/caching/cacheConfigurations/rootline/backend' => FileBackend::class, diff --git a/typo3/sysext/install/Classes/Service/SilentConfigurationUpgradeService.php b/typo3/sysext/install/Classes/Service/SilentConfigurationUpgradeService.php index 9709c03e724b..0f80e1efc619 100644 --- a/typo3/sysext/install/Classes/Service/SilentConfigurationUpgradeService.php +++ b/typo3/sysext/install/Classes/Service/SilentConfigurationUpgradeService.php @@ -180,6 +180,8 @@ class SilentConfigurationUpgradeService // Please note that further migrations in this file are kept in order to remove the setting at the very end // #97797 'GFX/processor_path_lzw', + // #98503 + 'SYS/caching/cacheConfigurations/pagesection', ]; public function __construct(ConfigurationManager $configurationManager) diff --git a/typo3/sysext/redirects/Classes/Service/RedirectService.php b/typo3/sysext/redirects/Classes/Service/RedirectService.php index e3bf0335b52d..89b437662a86 100644 --- a/typo3/sysext/redirects/Classes/Service/RedirectService.php +++ b/typo3/sysext/redirects/Classes/Service/RedirectService.php @@ -350,6 +350,12 @@ class RedirectService implements LoggerAwareInterface * - TSFE->cObj * * So a link to a page can be generated. + * + * @todo: This messes quite a bit with dependencies here. RedirectService is called by an early middleware + * *before* TSFE has been set up at all. The code thus has to hop through various loops later middlewares + * would usually do. The overall scenario of needing a partially set up TSFE for target redirect calculation + * is quite unfortunate here and should be sorted out differently by further refactoring the link building + * and reducing TSFE dependencies. */ protected function bootFrontendController(SiteInterface $site, array $queryParams, ServerRequestInterface $originalRequest): TypoScriptFrontendController { @@ -363,7 +369,8 @@ class RedirectService implements LoggerAwareInterface ); $controller->determineId($originalRequest); $controller->calculateLinkVars($queryParams); - $controller->getConfigArray($originalRequest); + $controller->getFromCache($originalRequest); + $controller->releaseLocks(); $controller->newCObj($originalRequest); if (!isset($GLOBALS['TSFE']) || !$GLOBALS['TSFE'] instanceof TypoScriptFrontendController) { $GLOBALS['TSFE'] = $controller; diff --git a/typo3/sysext/tstemplate/Classes/Controller/ConstantEditorController.php b/typo3/sysext/tstemplate/Classes/Controller/ConstantEditorController.php index 64ef8554958e..89bd778cc506 100644 --- a/typo3/sysext/tstemplate/Classes/Controller/ConstantEditorController.php +++ b/typo3/sysext/tstemplate/Classes/Controller/ConstantEditorController.php @@ -25,10 +25,12 @@ use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\DataHandling\DataHandler; use TYPO3\CMS\Core\Http\RedirectResponse; use TYPO3\CMS\Core\Imaging\Icon; +use TYPO3\CMS\Core\Site\Entity\SiteInterface; use TYPO3\CMS\Core\TypoScript\AST\AstBuilderInterface; use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode; use TYPO3\CMS\Core\TypoScript\AST\Traverser\AstTraverser; use TYPO3\CMS\Core\TypoScript\AST\Visitor\AstConstantCommentVisitor; +use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository; use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser; use TYPO3\CMS\Core\TypoScript\IncludeTree\TreeBuilder; use TYPO3\CMS\Core\TypoScript\Tokenizer\LosslessTokenizer; @@ -45,6 +47,7 @@ class ConstantEditorController extends AbstractTemplateModuleController { public function __construct( protected readonly ModuleTemplateFactory $moduleTemplateFactory, + private readonly SysTemplateRepository $sysTemplateRepository, private readonly TreeBuilder $treeBuilder, private readonly IncludeTreeTraverser $treeTraverser, private readonly AstTraverser $astTraverser, @@ -126,7 +129,10 @@ class ConstantEditorController extends AbstractTemplateModuleController // Build the constant include tree $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageUid)->get(); - $constantIncludeTree = $this->treeBuilder->getTreeByRootline($rootLine, 'constants', false, $selectedTemplateUid); + /** @var SiteInterface|null $site */ + $site = $request->getAttribute('site'); + $sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootLine, $site, $selectedTemplateUid); + $constantIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $site); $constantAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeCommentAwareAstBuilderVisitor::class); $this->treeTraverser->resetVisitors(); $this->treeTraverser->addVisitor($constantAstBuilderVisitor); @@ -222,7 +228,10 @@ class ConstantEditorController extends AbstractTemplateModuleController } $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageUid)->get(); - $constantIncludeTree = $this->treeBuilder->getTreeByRootline($rootLine, 'constants', false, $selectedTemplateUid); + /** @var SiteInterface|null $site */ + $site = $request->getAttribute('site'); + $sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootLine, $site, $selectedTemplateUid); + $constantIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $site); $constantAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeCommentAwareAstBuilderVisitor::class); $this->treeTraverser->resetVisitors(); $this->treeTraverser->addVisitor($constantAstBuilderVisitor); diff --git a/typo3/sysext/tstemplate/Classes/Controller/ObjectBrowserController.php b/typo3/sysext/tstemplate/Classes/Controller/ObjectBrowserController.php index b4f9aa200340..449a795d6723 100644 --- a/typo3/sysext/tstemplate/Classes/Controller/ObjectBrowserController.php +++ b/typo3/sysext/tstemplate/Classes/Controller/ObjectBrowserController.php @@ -28,11 +28,13 @@ use TYPO3\CMS\Core\Http\RedirectResponse; use TYPO3\CMS\Core\Imaging\Icon; use TYPO3\CMS\Core\Messaging\FlashMessage; use TYPO3\CMS\Core\Messaging\FlashMessageService; +use TYPO3\CMS\Core\Site\Entity\SiteInterface; use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity; use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPath; use TYPO3\CMS\Core\TypoScript\AST\Traverser\AstTraverser; use TYPO3\CMS\Core\TypoScript\AST\Visitor\AstSortChildrenVisitor; use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude; +use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository; use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser; use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser; use TYPO3\CMS\Core\TypoScript\IncludeTree\TreeBuilder; @@ -57,6 +59,7 @@ final class ObjectBrowserController extends AbstractTemplateModuleController public function __construct( private readonly ModuleTemplateFactory $moduleTemplateFactory, private readonly FlashMessageService $flashMessageService, + private readonly SysTemplateRepository $sysTemplateRepository, private readonly IncludeTreeTraverser $treeTraverser, private readonly ConditionVerdictAwareIncludeTreeTraverser $treeTraverserConditionVerdictAware, private readonly TreeBuilder $treeBuilder, @@ -137,8 +140,12 @@ final class ObjectBrowserController extends AbstractTemplateModuleController $displayComments = $moduleData->get('displayComments'); $searchValue = $moduleData->get('searchValue'); + /** @var SiteInterface|null $site */ + $site = $request->getAttribute('site'); + $sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootLine, $site, $selectedTemplateUid); + // Build the constant include tree - $constantIncludeTree = $this->treeBuilder->getTreeByRootline($rootLine, 'constants', false, $selectedTemplateUid); + $constantIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $site); // Set enabled conditions in constant include tree $constantConditions = $this->handleToggledConstantConditions($constantIncludeTree, $moduleData, $parsedBody); $conditionEnforcerVisitor = GeneralUtility::makeInstance(IncludeTreeConditionEnforcerVisitor::class); @@ -192,8 +199,8 @@ final class ObjectBrowserController extends AbstractTemplateModuleController // Flatten constant AST. Needed for setup condition display and setup AST constant substitution. $flattenedConstants = $constantAst->flatten(); - // Build the setup include tree, note this uses the 'cached' variant from the constant run above to suppress db calls. - $setupIncludeTree = $this->treeBuilder->getTreeByRootline($rootLine, 'setup', true); + // Build the setup include tree + $setupIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $site); // Set enabled conditions in setup include tree and let it handle constant substitutions in setup conditions. $setupConditions = $this->handleToggledSetupConditions($setupIncludeTree, $moduleData, $parsedBody, $flattenedConstants); $conditionEnforcerVisitor = GeneralUtility::makeInstance(IncludeTreeConditionEnforcerVisitor::class); @@ -313,9 +320,13 @@ final class ObjectBrowserController extends AbstractTemplateModuleController } } + /** @var SiteInterface|null $site */ + $site = $request->getAttribute('site'); + $sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootLine, $site, $selectedTemplateUid); + // Get current value of to-edit object path // Build the constant include tree - $constantIncludeTree = $this->treeBuilder->getTreeByRootline($rootLine, 'constants', false, $selectedTemplateUid); + $constantIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $site); // Set enabled conditions in constant include tree $constantConditions = $this->handleToggledConstantConditions($constantIncludeTree, $moduleData, null); $conditionEnforcerVisitor = GeneralUtility::makeInstance(IncludeTreeConditionEnforcerVisitor::class); @@ -333,7 +344,7 @@ final class ObjectBrowserController extends AbstractTemplateModuleController $currentValue = $flattenedConstants[$currentObjectPath] ?? ''; } else { // Build the setup include tree - $setupIncludeTree = $this->treeBuilder->getTreeByRootline($rootLine, 'setup', false, $selectedTemplateUid); + $setupIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $site); // Set enabled conditions in setup include tree $setupConditions = $this->handleToggledSetupConditions($setupIncludeTree, $moduleData, null, $flattenedConstants); $conditionEnforcerVisitor = GeneralUtility::makeInstance(IncludeTreeConditionEnforcerVisitor::class); diff --git a/typo3/sysext/tstemplate/Classes/Controller/TemplateAnalyzerController.php b/typo3/sysext/tstemplate/Classes/Controller/TemplateAnalyzerController.php index 41817b12922c..34e9ecf758fc 100644 --- a/typo3/sysext/tstemplate/Classes/Controller/TemplateAnalyzerController.php +++ b/typo3/sysext/tstemplate/Classes/Controller/TemplateAnalyzerController.php @@ -25,7 +25,9 @@ use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\Http\RedirectResponse; use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction; use TYPO3\CMS\Core\Page\PageRenderer; +use TYPO3\CMS\Core\Site\Entity\SiteInterface; use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude; +use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository; use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser; use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser; use TYPO3\CMS\Core\TypoScript\IncludeTree\TreeBuilder; @@ -50,6 +52,7 @@ final class TemplateAnalyzerController extends AbstractTemplateModuleController public function __construct( private readonly PageRenderer $pageRenderer, private readonly ModuleTemplateFactory $moduleTemplateFactory, + private readonly SysTemplateRepository $sysTemplateRepository, private readonly IncludeTreeTraverser $treeTraverser, private readonly ConditionVerdictAwareIncludeTreeTraverser $treeTraverserConditionVerdictAware, private readonly TreeBuilder $treeBuilder, @@ -107,8 +110,12 @@ final class TemplateAnalyzerController extends AbstractTemplateModuleController $this->pageRenderer->loadJavaScriptModule('@typo3/t3editor/element/code-mirror-element.js'); } + /** @var SiteInterface|null $site */ + $site = $request->getAttribute('site'); + $sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootLine, $site, $selectedTemplateUid); + // Build the constant include tree - $constantIncludeTree = $this->treeBuilder->getTreeByRootline($rootLine, 'constants', false, $selectedTemplateUid); + $constantIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $site); // Set enabled conditions in constant include tree $constantConditions = $this->handleToggledConstantConditions($constantIncludeTree, $moduleData, $parsedBody); $conditionEnforcerVisitor = GeneralUtility::makeInstance(IncludeTreeConditionEnforcerVisitor::class); @@ -124,8 +131,8 @@ final class TemplateAnalyzerController extends AbstractTemplateModuleController $constantAst = $constantAstBuilderVisitor->getAst(); $flattenedConstants = $constantAst->flatten(); - // Build the setup include tree, note this uses the 'cached' variant from the constant run above to suppress db calls. - $setupIncludeTree = $this->treeBuilder->getTreeByRootline($rootLine, 'setup', true); + // Build the setup include tree + $setupIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $site); // Set enabled conditions in setup include tree and let it handle constant substitutions in setup conditions. $setupConditions = $this->handleToggledSetupConditions($setupIncludeTree, $moduleData, $parsedBody, $flattenedConstants); $conditionEnforcerVisitor = GeneralUtility::makeInstance(IncludeTreeConditionEnforcerVisitor::class); -- GitLab