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