From 3e57eebef788899a4b93eef7c7f9a1c5f39dfd21 Mon Sep 17 00:00:00 2001 From: Benni Mack <benni@typo3.org> Date: Mon, 13 Apr 2020 12:05:09 +0200 Subject: [PATCH] [FEATURE] Inject site settings into TypoScript constants and TSconfig YAML configuration set in a site configuration under the main key "settings:" are now available as TypoScript constants in templates and as constants in page TSconfig. This feature is a replacement for the former page TSconfig functionality "TSFE.constants", which has been removed. A former replacement approach had to be reverted in v10.1. Resolves: #91080 Resolves: #91081 Releases: master Change-Id: Ia5cd4e94049a359a23418c1fdb122a0d5f8c7311 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/64128 Tested-by: Markus Klein <markus.klein@typo3.org> Tested-by: TYPO3com <noreply@typo3.com> Tested-by: Oliver Hader <oliver.hader@typo3.org> Reviewed-by: Markus Klein <markus.klein@typo3.org> Reviewed-by: Oliver Hader <oliver.hader@typo3.org> --- .../Classes/Utility/BackendUtility.php | 8 +- .../Tests/Unit/Utility/BackendUtilityTest.php | 8 ++ .../Parser/PageTsConfigParser.php | 33 +++++++- .../Classes/TypoScript/TemplateService.php | 52 ++++++++++++- ...SiteSettingsAsTsConstantsAndInTsConfig.rst | 75 +++++++++++++++++++ .../Parser/PageTsConfigParserTest.php | 44 +++++++++++ .../Unit/TypoScript/TemplateServiceTest.php | 26 ++++++- .../TypoScriptFrontendController.php | 3 +- 8 files changed, 244 insertions(+), 5 deletions(-) create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-91080-SiteSettingsAsTsConstantsAndInTsConfig.rst diff --git a/typo3/sysext/backend/Classes/Utility/BackendUtility.php b/typo3/sysext/backend/Classes/Utility/BackendUtility.php index 554fc0863bd9..d1c1b53972b4 100644 --- a/typo3/sysext/backend/Classes/Utility/BackendUtility.php +++ b/typo3/sysext/backend/Classes/Utility/BackendUtility.php @@ -703,6 +703,12 @@ class BackendUtility // Order correctly ksort($rootLine); + try { + $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($id); + } catch (SiteNotFoundException $exception) { + $site = null; + } + // Load PageTS from all pages of the rootLine $pageTs = GeneralUtility::makeInstance(PageTsConfigLoader::class)->load($rootLine); @@ -713,7 +719,7 @@ class BackendUtility GeneralUtility::makeInstance(CacheManager::class)->getCache('hash') ); $matcher = GeneralUtility::makeInstance(ConditionMatcher::class, null, $id, $rootLine); - $tsConfig = $parser->parse($pageTs, $matcher); + $tsConfig = $parser->parse($pageTs, $matcher, $site); $cacheHash = md5(json_encode($tsConfig)); // Get User TSconfig overlay, if no backend user is logged-in, this needs to be checked as well diff --git a/typo3/sysext/backend/Tests/Unit/Utility/BackendUtilityTest.php b/typo3/sysext/backend/Tests/Unit/Utility/BackendUtilityTest.php index c777025e0387..c9f1116fecf5 100644 --- a/typo3/sysext/backend/Tests/Unit/Utility/BackendUtilityTest.php +++ b/typo3/sysext/backend/Tests/Unit/Utility/BackendUtilityTest.php @@ -37,6 +37,8 @@ use TYPO3\CMS\Core\Database\Query\QueryBuilder; use TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer; use TYPO3\CMS\Core\Database\RelationHandler; use TYPO3\CMS\Core\Localization\LanguageService; +use TYPO3\CMS\Core\Site\Entity\Site; +use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; @@ -1051,6 +1053,12 @@ class BackendUtilityTest extends UnitTestCase $matcherProphecy = $this->prophesize(ConditionMatcher::class); GeneralUtility::addInstance(ConditionMatcher::class, $matcherProphecy->reveal()); + $siteFinder = $this->prophesize(SiteFinder::class); + $siteFinder->getSiteByPageId($pageId)->willReturn( + new Site('dummy', $pageId, ['base' => 'https://example.com']) + ); + GeneralUtility::addInstance(SiteFinder::class, $siteFinder->reveal()); + $cacheManagerProphecy = $this->prophesize(CacheManager::class); $cacheProphecy = $this->prophesize(FrontendInterface::class); $cacheManagerProphecy->getCache('runtime')->willReturn($cacheProphecy->reveal()); diff --git a/typo3/sysext/core/Classes/Configuration/Parser/PageTsConfigParser.php b/typo3/sysext/core/Classes/Configuration/Parser/PageTsConfigParser.php index 7ce3b6c79da4..9b11f1207b03 100644 --- a/typo3/sysext/core/Classes/Configuration/Parser/PageTsConfigParser.php +++ b/typo3/sysext/core/Classes/Configuration/Parser/PageTsConfigParser.php @@ -19,7 +19,9 @@ namespace TYPO3\CMS\Core\Configuration\Parser; use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; use TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\ConditionMatcherInterface; +use TYPO3\CMS\Core\Site\Entity\Site; use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser; +use TYPO3\CMS\Core\Utility\ArrayUtility; /** * A TS-Config parsing class which performs condition evaluation. @@ -51,11 +53,14 @@ class PageTsConfigParser * - when an exact on the conditions are there * - when a parse is there, then matches are happening anyway, and it is checked if this can be cached as well. * + * If a site is provided the settings stored in the site's configuration is available as constants for the TSconfig. + * * @param string $content pageTSconfig, usually accumulated by the PageTsConfigLoader * @param ConditionMatcherInterface $matcher an instance to match strings + * @param Site|null $site The current site the page TSconfig is parsed for * @return array the */ - public function parse(string $content, ConditionMatcherInterface $matcher): array + public function parse(string $content, ConditionMatcherInterface $matcher, ?Site $site = null): array { $hashOfContent = md5('PAGES:' . $content); $cachedContent = $this->cache->get($hashOfContent); @@ -85,6 +90,32 @@ class PageTsConfigParser } return $result; } + + if ($site) { + $siteSettings = $site->getConfiguration()['settings'] ?? []; + if (!empty($siteSettings)) { + $siteSettings = ArrayUtility::flatten($siteSettings); + } + if (!empty($siteSettings)) { + // Recursive substitution of site settings (up to 10 nested levels) + // note: this code is more or less a duplicate of \TYPO3\CMS\Core\TypoScript\TemplateService::substituteConstants + for ($i = 0; $i < 10; $i++) { + $beforeSubstitution = $content; + $content = preg_replace_callback( + '/\\{\\$(.[^}]*)\\}/', + function (array $matches) use ($siteSettings): string { + return isset($siteSettings[$matches[1]]) && !is_array($siteSettings[$matches[1]]) + ? (string)$siteSettings[$matches[1]] : $matches[0]; + }, + $content + ); + if ($beforeSubstitution === $content) { + break; + } + } + } + } + // Nothing found in cache for this content string, let's do everything. $parsedAndMatchedData = $this->parseAndMatch($content, $matcher); // ALL parts, including the matching part is cached. diff --git a/typo3/sysext/core/Classes/TypoScript/TemplateService.php b/typo3/sysext/core/Classes/TypoScript/TemplateService.php index db8739575a9c..064b28e93e9d 100644 --- a/typo3/sysext/core/Classes/TypoScript/TemplateService.php +++ b/typo3/sysext/core/Classes/TypoScript/TemplateService.php @@ -25,7 +25,10 @@ use TYPO3\CMS\Core\Database\Query\Restriction\AbstractRestrictionContainer; use TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer; use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; use TYPO3\CMS\Core\Domain\Repository\PageRepository; +use TYPO3\CMS\Core\Exception\SiteNotFoundException; use TYPO3\CMS\Core\Package\PackageManager; +use TYPO3\CMS\Core\Site\Entity\Site; +use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\TimeTracker\TimeTracker; use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser; use TYPO3\CMS\Core\Utility\ArrayUtility; @@ -1205,6 +1208,53 @@ class TemplateService { // Add default TS for all code types, if not done already if (!$this->isDefaultTypoScriptAdded) { + $rootTemplateId = $this->hierarchyInfo[count($this->hierarchyInfo) - 1]['templateID'] ?? null; + + // adding constants from site settings + $siteConstants = ''; + if ($this->getTypoScriptFrontendController()) { + $site = $this->getTypoScriptFrontendController()->getSite(); + } else { + $currentPage = end($this->absoluteRootLine); + try { + $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId((int)$currentPage['uid'] ?? 0); + } catch (SiteNotFoundException $exception) { + $site = null; + } + } + if ($site instanceof Site) { + $siteSettings = $site->getConfiguration()['settings'] ?? []; + if (!empty($siteSettings)) { + $siteSettings = ArrayUtility::flatten($siteSettings); + foreach ($siteSettings as $k => $v) { + $siteConstants .= $k . ' = ' . $v . LF; + } + } + } + + if ($siteConstants !== '') { + // the count of elements in ->constants, ->config and ->templateIncludePaths have to be in sync + array_unshift($this->constants, $siteConstants); + array_unshift($this->config, ''); + array_unshift($this->templateIncludePaths, ''); + // prepare a proper entry to hierachyInfo (used by TemplateAnalyzer in BE) + $defaultTemplateInfo = [ + 'root' => '', + 'clConst' => '', + 'clConf' => '', + 'templateID' => '_siteConstants_', + 'templateParent' => $rootTemplateId, + 'title' => 'Site settings', + 'uid' => '_siteConstants_', + 'pid' => '', + 'configLines' => 0 + ]; + // push info to information arrays used in BE by TemplateTools (Analyzer) + array_unshift($this->clearList_const, $defaultTemplateInfo['uid']); + array_unshift($this->clearList_setup, $defaultTemplateInfo['uid']); + array_unshift($this->hierarchyInfo, $defaultTemplateInfo); + } + // adding default setup and constants // defaultTypoScript_setup is *very* unlikely to be empty // the count of elements in ->constants, ->config and ->templateIncludePaths have to be in sync @@ -1212,7 +1262,6 @@ class TemplateService array_unshift($this->config, (string)$GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_setup']); array_unshift($this->templateIncludePaths, ''); // prepare a proper entry to hierachyInfo (used by TemplateAnalyzer in BE) - $rootTemplateId = $this->hierarchyInfo[count($this->hierarchyInfo) - 1]['templateID'] ?? null; $defaultTemplateInfo = [ 'root' => '', 'clConst' => '', @@ -1228,6 +1277,7 @@ class TemplateService array_unshift($this->clearList_const, $defaultTemplateInfo['uid']); array_unshift($this->clearList_setup, $defaultTemplateInfo['uid']); array_unshift($this->hierarchyInfo, $defaultTemplateInfo); + $this->isDefaultTypoScriptAdded = true; } } diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-91080-SiteSettingsAsTsConstantsAndInTsConfig.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-91080-SiteSettingsAsTsConstantsAndInTsConfig.rst new file mode 100644 index 000000000000..c3542c189e43 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-91080-SiteSettingsAsTsConstantsAndInTsConfig.rst @@ -0,0 +1,75 @@ +.. include:: ../../Includes.txt + +======================================================================= +Feature: #91080 - Site settings as TypoScript constants and in TSconfig +======================================================================= + +See :issue:`91080` +See :issue:`91081` + +Description +=========== + +Prior to TYPO3 v10.0 it was possible to inject information from +page TSconfig into TypoScript constants with :ts:`TSFE.constants.const1 = a`. + +This could be used to centralize configuration of e.g. record storagePids, +which could then be used in Backend for modules or for IRRE and for Frontend plugins. + +This old feature has been removed, because it was recommended to add site settings. +The according new feature added with TYPO3 v10 was reverted in v10.1 though. + +This re-implementation now allows to define site settings via :file:`config/sites/<site-name>/config.yml` + +The newly introduced settings inside :file:`config.yml` are made available +as TypoScript constants and page TSconfig constants. + +An example configuration in the :file:`config/sites/<site-name>/config.yml`: + +.. code-block:: yaml + + settings: + categoryPid: 658 + Â styles: + Â Â Â content: + Â Â Â Â loginform: + Â Â Â Â Â Â Â pid: 23 + +This will make these constants available in the template and in page TSconfig: + +* :ts:`{$categoryPid}` +* :ts:`{$styles.content.loginform.pid}` + +The newly introduced constants for page TSconfig can be used just like constants +in TypoScript. + +In page TSconfig this can be used like this: + +.. code-block:: ts + + # store tx_ext_data records on the given storage page by default (e.g. through IRRE) + TCAdefaults.tx_ext_data.pid = {$categoryPid} + # load category selection for plugin from out dedicated storage page + TCEFORM.tt_content.pi_flexform.ext_pi1.sDEF.categories.PAGE_TSCONFIG_ID = {$categoryPid} + + +.. note:: + + The TypoScript constants are now evaluated in this order: + + #. Global :php:`'defaultTypoScript_constants'` + #. Site specific settings from the site configuration + #. Constants from sys_template database records + + +Impact +====== + +It is now possible again to have a central place for configuration relevant +for Backend and Frontend. + +For instance: It is now possible to define all page-uid related configuration centrally +with the site configuration and get templates and page TSconfig independent +of actual UIDs. + +.. index:: TypoScript, ext:core, ext:frontend, ext:backend diff --git a/typo3/sysext/core/Tests/Unit/Configuration/Parser/PageTsConfigParserTest.php b/typo3/sysext/core/Tests/Unit/Configuration/Parser/PageTsConfigParserTest.php index 4145246342ce..fb38f0527c3a 100644 --- a/typo3/sysext/core/Tests/Unit/Configuration/Parser/PageTsConfigParserTest.php +++ b/typo3/sysext/core/Tests/Unit/Configuration/Parser/PageTsConfigParserTest.php @@ -23,6 +23,7 @@ use TYPO3\CMS\Core\Cache\Frontend\NullFrontend; use TYPO3\CMS\Core\Cache\Frontend\VariableFrontend; use TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser; use TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\ConditionMatcherInterface; +use TYPO3\CMS\Core\Site\Entity\Site; use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; @@ -79,4 +80,47 @@ class PageTsConfigParserTest extends UnitTestCase $parsedTsConfig = $subject->parse($input, $matcherProphecy->reveal()); self::assertEquals($expectedParsedTsConfig, $parsedTsConfig); } + + /** + * @test + */ + public function parseReplacesSiteSettings(): void + { + $input = 'mod.web_layout = {$numberedThings.1}' . "\n" . + 'mod.no-replace = {$styles.content}' . "\n" . + 'mod.content = {$styles.content.loginform.pid}'; + $expectedParsedTsConfig = [ + 'mod.' => [ + 'web_layout' => 'foo', + 'no-replace' => '{$styles.content}', + 'content' => '123' + ] + ]; + + $matcherProphecy = $this->prophesize(ConditionMatcherInterface::class); + $cache = new NullFrontend('runtime'); + $site = new Site('dummy', 13, [ + 'base' => 'https://example.com', + 'settings' => [ + 'random' => 'value', + 'styles' => [ + 'content' => [ + 'loginform' => [ + 'pid' => 123 + ], + ], + ], + 'numberedThings' => [ + 1 => 'foo', + 99 => 'bar', + ] + ] + ]); + $subject = new PageTsConfigParser( + new TypoScriptParser(), + $cache + ); + $parsedTsConfig = $subject->parse($input, $matcherProphecy->reveal(), $site); + self::assertEquals($expectedParsedTsConfig, $parsedTsConfig); + } } diff --git a/typo3/sysext/core/Tests/Unit/TypoScript/TemplateServiceTest.php b/typo3/sysext/core/Tests/Unit/TypoScript/TemplateServiceTest.php index ee7f5a28b4bc..dd61e3022723 100644 --- a/typo3/sysext/core/Tests/Unit/TypoScript/TemplateServiceTest.php +++ b/typo3/sysext/core/Tests/Unit/TypoScript/TemplateServiceTest.php @@ -24,10 +24,12 @@ use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\QueryBuilder; use TYPO3\CMS\Core\Package\Package; use TYPO3\CMS\Core\Package\PackageManager; +use TYPO3\CMS\Core\Site\Entity\Site; use TYPO3\CMS\Core\Tests\Unit\Utility\AccessibleProxies\ExtensionManagementUtilityAccessibleProxy; use TYPO3\CMS\Core\TypoScript\TemplateService; use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; use TYPO3\TestingFramework\Core\AccessibleObjectInterface; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; @@ -65,7 +67,29 @@ class TemplateServiceTest extends UnitTestCase $GLOBALS['SIM_ACCESS_TIME'] = time(); $GLOBALS['ACCESS_TIME'] = time(); $this->packageManagerProphecy = $this->prophesize(PackageManager::class); - $this->templateService = new TemplateService(new Context(), $this->packageManagerProphecy->reveal()); + $frontendController = $this->prophesize(TypoScriptFrontendController::class); + $frontendController->getSite()->willReturn(new Site('dummy', 13, [ + 'base' => 'https://example.com', + 'settings' => [ + 'random' => 'value', + 'styles' => [ + 'content' => [ + 'loginform' => [ + 'pid' => 123 + ], + ], + ], + 'numberedThings' => [ + 1 => 'foo', + 99 => 'bar', + ] + ] + ])); + $this->templateService = new TemplateService( + new Context(), + $this->packageManagerProphecy->reveal(), + $frontendController->reveal() + ); $this->backupPackageManager = ExtensionManagementUtilityAccessibleProxy::getPackageManager(); } diff --git a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php index a063b2cd6c91..4f54b91b5900 100644 --- a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php +++ b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php @@ -3495,7 +3495,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface ); $this->pagesTSconfig = $parser->parse( $tsConfigString, - GeneralUtility::makeInstance(ConditionMatcher::class, $this->context, $this->id, $this->rootLine) + GeneralUtility::makeInstance(ConditionMatcher::class, $this->context, $this->id, $this->rootLine), + $this->site ); } return $this->pagesTSconfig; -- GitLab