From bb91ad794d2352ce7289c699486ea118a7e15026 Mon Sep 17 00:00:00 2001
From: Christian Kuhn <lolli@schwarzbu.ch>
Date: Fri, 11 Nov 2022 15:37:04 +0100
Subject: [PATCH] [TASK] Deprecate old TypoScriptParser
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The main goal of this patch is to avoid last usages
of old TypoScriptParser and to substitute it with
usages of the new parser approach.

A couple of side usages have to be adapted, but the
main work is to establish new factories for UserTsConfig
and PageTsConfig while deprecating the former approach
and adding functional test coverage to the factories.

BackendUtility::getPagesTSconfig() is still the main
API to retrieve PageTsConfig, for UserTsConfig it is
$backendUser->getTSConfig(). The new API is thus
considered @internal, at least for now.

Resolves: #99120
Related: #97816
Releases: main
Change-Id: I985929e1f2a90560059efe1e0f40f8c590ad1e4a
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/76565
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: core-ci <typo3@b13.com>
Reviewed-by: Benni Mack <benni@typo3.org>
---
 composer.json                                 |   3 +-
 .../Element/BackendLayoutWizardElement.php    |   7 +-
 .../Classes/Utility/BackendUtility.php        |  47 ++--
 .../Classes/View/BackendLayoutView.php        |   8 +-
 .../Classes/View/BackendViewFactory.php       |  10 +-
 .../backend/Configuration/Services.yaml       |   4 -
 .../BackendUserAuthentication.php             |  86 +++----
 .../Event/ModifyLoadedPageTsConfigEvent.php   |  29 +--
 .../Configuration/ExtensionConfiguration.php  |   3 +-
 .../Loader/PageTsConfigLoader.php             |   4 +
 .../Classes/Configuration/PageTsConfig.php    |   4 +
 .../Parser/PageTsConfigParser.php             |   4 +
 .../Package/AbstractServiceProvider.php       |   4 +
 typo3/sysext/core/Classes/ServiceProvider.php |  11 +-
 .../Event/ModifyLoadedPageTsConfigEvent.php   |  50 ++++
 .../IncludeNode/TsConfigInclude.php           |  27 +++
 .../IncludeTree/TsConfigTreeBuilder.php       | 190 +++++++++++++++
 .../core/Classes/TypoScript/PageTsConfig.php  |  46 ++++
 .../TypoScript/PageTsConfigFactory.php        | 132 +++++++++++
 .../TypoScript/Parser/TypoScriptParser.php    |   8 +-
 .../TypoScript/TypoScriptStringFactory.php    |  16 +-
 .../core/Classes/TypoScript/UserTsConfig.php  |  46 ++++
 .../TypoScript/UserTsConfigFactory.php        |  63 +++++
 .../Utility/ExtensionManagementUtility.php    |   8 +-
 typo3/sysext/core/Configuration/Services.yaml |  16 ++
 ...ion-99120-DeprecateOldTypoScriptParser.rst | 109 +++++++++
 .../LegacyModifyLoadedPageTsConfig.php        |  31 +++
 .../ModifyLoadedPageTsConfig.php              |  28 +++
 .../Configuration/Services.yaml               |  16 ++
 .../TsConfig/tsconfig-includes.tsconfig       |   1 +
 .../Configuration/page.tsconfig               |   1 +
 .../ext_emconf.php                            |  21 ++
 .../Fixtures/pageTsConfigTestFixture.csv      |   3 +
 .../Fixtures/userTsConfigTestFixture.csv      |  15 ++
 .../TypoScript/PageTsConfigFactoryTest.php    | 224 ++++++++++++++++++
 .../TypoScriptStringFactoryTest.php           |   8 +-
 .../TypoScript/UserTsConfigFactoryTest.php    | 104 ++++++++
 .../Parser/TypoScriptParserTest.php           |   2 +-
 .../Fixtures/ext_typoscript_setup.typoscript  |   1 -
 .../recursive_includes_setup.typoscript       |   1 -
 .../Loader/Fixtures/included.typoscript       |   0
 .../Loader/PageTsConfigLoaderTest.php         |  19 +-
 .../Parser/PageTsConfigParserTest.php         |   2 +-
 .../TypoScript/Fixtures/badfilename.php       |   0
 .../recursive_includes_setup.typoscript       |   1 +
 .../TypoScript/Fixtures/setup.typoscript      |   0
 .../Parser/TypoScriptParserTest.php           |  94 ++++----
 .../TypoScriptFrontendController.php          |   5 +
 .../Typolink/DatabaseRecordLinkBuilder.php    |  35 ++-
 .../DatabaseRecordLinkBuilderTest.php         |   5 +-
 .../InfoPageTyposcriptConfigController.php    |  37 +--
 .../Private/Language/InfoPageTsConfig.xlf     |   6 -
 .../ExtensionScanner/Php/ClassNameMatcher.php |  25 ++
 .../Php/MethodCallMatcher.php                 |   7 +
 .../Classes/Task/ValidatorTask.php            |  20 +-
 .../Resources/Private/Language/locallang.xlf  |   3 -
 56 files changed, 1418 insertions(+), 232 deletions(-)
 create mode 100644 typo3/sysext/core/Classes/TypoScript/IncludeTree/Event/ModifyLoadedPageTsConfigEvent.php
 create mode 100644 typo3/sysext/core/Classes/TypoScript/IncludeTree/IncludeNode/TsConfigInclude.php
 create mode 100644 typo3/sysext/core/Classes/TypoScript/IncludeTree/TsConfigTreeBuilder.php
 create mode 100644 typo3/sysext/core/Classes/TypoScript/PageTsConfig.php
 create mode 100644 typo3/sysext/core/Classes/TypoScript/PageTsConfigFactory.php
 create mode 100644 typo3/sysext/core/Classes/TypoScript/UserTsConfig.php
 create mode 100644 typo3/sysext/core/Classes/TypoScript/UserTsConfigFactory.php
 create mode 100644 typo3/sysext/core/Documentation/Changelog/12.2/Deprecation-99120-DeprecateOldTypoScriptParser.rst
 create mode 100644 typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Classes/EventListener/LegacyModifyLoadedPageTsConfig.php
 create mode 100644 typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Classes/EventListener/ModifyLoadedPageTsConfig.php
 create mode 100644 typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Configuration/Services.yaml
 create mode 100644 typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Configuration/TsConfig/tsconfig-includes.tsconfig
 create mode 100644 typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Configuration/page.tsconfig
 create mode 100644 typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/ext_emconf.php
 create mode 100644 typo3/sysext/core/Tests/Functional/TypoScript/Fixtures/pageTsConfigTestFixture.csv
 create mode 100644 typo3/sysext/core/Tests/Functional/TypoScript/Fixtures/userTsConfigTestFixture.csv
 create mode 100644 typo3/sysext/core/Tests/Functional/TypoScript/PageTsConfigFactoryTest.php
 create mode 100644 typo3/sysext/core/Tests/Functional/TypoScript/UserTsConfigFactoryTest.php
 rename typo3/sysext/core/Tests/{Functional => FunctionalDeprecated}/TypoScript/Parser/TypoScriptParserTest.php (96%)
 delete mode 100644 typo3/sysext/core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.typoscript
 delete mode 100644 typo3/sysext/core/Tests/Unit/TypoScript/Fixtures/recursive_includes_setup.typoscript
 rename typo3/sysext/core/Tests/{Unit => UnitDeprecated}/Configuration/Loader/Fixtures/included.typoscript (100%)
 rename typo3/sysext/core/Tests/{Unit => UnitDeprecated}/Configuration/Loader/PageTsConfigLoaderTest.php (77%)
 rename typo3/sysext/core/Tests/{Unit => UnitDeprecated}/Configuration/Parser/PageTsConfigParserTest.php (98%)
 rename typo3/sysext/core/Tests/{Unit => UnitDeprecated}/TypoScript/Fixtures/badfilename.php (100%)
 create mode 100644 typo3/sysext/core/Tests/UnitDeprecated/TypoScript/Fixtures/recursive_includes_setup.typoscript
 rename typo3/sysext/core/Tests/{Unit => UnitDeprecated}/TypoScript/Fixtures/setup.typoscript (100%)
 rename typo3/sysext/core/Tests/{Unit => UnitDeprecated}/TypoScript/Parser/TypoScriptParserTest.php (89%)

diff --git a/composer.json b/composer.json
index 721dc1968f13..b85a7141e9a6 100644
--- a/composer.json
+++ b/composer.json
@@ -298,7 +298,8 @@
 			"TYPO3Tests\\TestDataMapper\\": "typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/test_data_mapper/Classes/",
 			"TYPO3Tests\\TestFluidTemplate\\": "typo3/sysext/frontend/Tests/Functional/Fixtures/Extensions/test_fluid_template/Classes/",
 			"TYPO3Tests\\TestLogger\\": "typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_logger/Classes/",
-			"TYPO3Tests\\TestTyposcriptAstFunctionEvent\\": "typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_ast_function_event/Classes/"
+			"TYPO3Tests\\TestTyposcriptAstFunctionEvent\\": "typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_ast_function_event/Classes/",
+			"TYPO3Tests\\TestTyposcriptPagetsconfigfactory\\": "typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Classes/"
 		},
 		"classmap": [
 			"typo3/sysext/core/Tests/Unit/Core/Fixtures/test_extension/",
diff --git a/typo3/sysext/backend/Classes/Form/Element/BackendLayoutWizardElement.php b/typo3/sysext/backend/Classes/Form/Element/BackendLayoutWizardElement.php
index e6459ed57c76..2075aa8bc370 100644
--- a/typo3/sysext/backend/Classes/Form/Element/BackendLayoutWizardElement.php
+++ b/typo3/sysext/backend/Classes/Form/Element/BackendLayoutWizardElement.php
@@ -19,7 +19,6 @@ use TYPO3\CMS\Core\EventDispatcher\NoopEventDispatcher;
 use TYPO3\CMS\Core\Imaging\Icon;
 use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
 use TYPO3\CMS\Core\TypoScript\AST\AstBuilder;
-use TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer;
 use TYPO3\CMS\Core\TypoScript\TypoScriptStringFactory;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -198,11 +197,7 @@ class BackendLayoutWizardElement extends AbstractFormElement
         if (!empty($this->data['parameterArray']['itemFormElValue'])) {
             // Parse the TypoScript a-like syntax in case we already have a config (e.g. database value or default from TCA)
             $typoScriptStringFactory = GeneralUtility::makeInstance(TypoScriptStringFactory::class);
-            $typoScriptTree = $typoScriptStringFactory->parseFromString(
-                $this->data['parameterArray']['itemFormElValue'],
-                new LossyTokenizer(),
-                new AstBuilder(new NoopEventDispatcher())
-            );
+            $typoScriptTree = $typoScriptStringFactory->parseFromString($this->data['parameterArray']['itemFormElValue'], new AstBuilder(new NoopEventDispatcher()));
             $typoScriptArray = $typoScriptTree->toArray();
             if (is_array($typoScriptArray['backend_layout.'] ?? false)) {
                 // Only evaluate, in case the "backend_layout." array exists on root level
diff --git a/typo3/sysext/backend/Classes/Utility/BackendUtility.php b/typo3/sysext/backend/Classes/Utility/BackendUtility.php
index 2a65669b4ba9..8b8b4ecd328a 100644
--- a/typo3/sysext/backend/Classes/Utility/BackendUtility.php
+++ b/typo3/sysext/backend/Classes/Utility/BackendUtility.php
@@ -20,7 +20,7 @@ use Doctrine\DBAL\Types\Type;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Log\LoggerInterface;
 use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
-use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
+use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher as BackendConditionMatcher;
 use TYPO3\CMS\Backend\Domain\Model\Element\ImmediateActionElement;
 use TYPO3\CMS\Backend\Module\ModuleProvider;
 use TYPO3\CMS\Backend\Routing\Route;
@@ -28,7 +28,6 @@ use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
-use TYPO3\CMS\Core\Configuration\PageTsConfig;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Context\DateTimeAspect;
 use TYPO3\CMS\Core\Core\Environment;
@@ -55,8 +54,11 @@ use TYPO3\CMS\Core\Resource\ResourceFactory;
 use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
 use TYPO3\CMS\Core\Routing\RouterInterface;
 use TYPO3\CMS\Core\Routing\UnableToLinkToPageException;
+use TYPO3\CMS\Core\Site\Entity\NullSite;
 use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Type\Bitmask\Permission;
+use TYPO3\CMS\Core\TypoScript\PageTsConfig;
+use TYPO3\CMS\Core\TypoScript\PageTsConfigFactory;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -687,27 +689,40 @@ class BackendUtility
      * TypoScript related
      *
      *******************************************/
+
     /**
-     * Returns the Page TSconfig for page with id, $id
-     *
-     * @param int $id Page uid for which to create Page TSconfig
-     * @return array Page TSconfig
+     * Returns the PageTsConfig for page with uid $pageUid
      */
-    public static function getPagesTSconfig($id)
+    public static function getPagesTSconfig($pageUid): array
     {
-        $id = (int)$id;
-        $rootLine = self::BEgetRootLine($id, '', true);
+        $runtimeCache = static::getRuntimeCache();
+        $pageTsConfig = $runtimeCache->get('pageTsConfig-' . $pageUid);
+        if ($pageTsConfig instanceof PageTsConfig) {
+            return $pageTsConfig->getPageTsConfigArray();
+        }
+
+        $pageUid = (int)$pageUid;
+        $rootLine = self::BEgetRootLine($pageUid, '', true);
         // Order correctly
         ksort($rootLine);
 
         try {
-            $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($id);
-        } catch (SiteNotFoundException $exception) {
-            $site = null;
-        }
-        $matcher = GeneralUtility::makeInstance(ConditionMatcher::class, GeneralUtility::makeInstance(Context::class), $id, $rootLine);
-        $tsConfig = GeneralUtility::makeInstance(PageTsConfig::class);
-        return $tsConfig->getWithUserOverride($id, $rootLine, $site, $matcher, static::getBackendUserAuthentication());
+            $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($pageUid);
+        } catch (SiteNotFoundException) {
+            $site = new NullSite();
+        }
+
+        $conditionMatcher = GeneralUtility::makeInstance(BackendConditionMatcher::class, GeneralUtility::makeInstance(Context::class), $pageUid, $rootLine);
+        $pageTsConfigFactory = GeneralUtility::makeInstance(PageTsConfigFactory::class);
+        $pageTsConfig = $pageTsConfigFactory->create(
+            $rootLine,
+            $site,
+            $conditionMatcher,
+            static::getBackendUserAuthentication()?->getUserTsConfig()
+        );
+
+        $runtimeCache->set('pageTsConfig-' . $pageUid, $pageTsConfig);
+        return $pageTsConfig->getPageTsConfigArray();
     }
 
     /*******************************************
diff --git a/typo3/sysext/backend/Classes/View/BackendLayoutView.php b/typo3/sysext/backend/Classes/View/BackendLayoutView.php
index 1502fe29fea2..43cf692d903d 100644
--- a/typo3/sysext/backend/Classes/View/BackendLayoutView.php
+++ b/typo3/sysext/backend/Classes/View/BackendLayoutView.php
@@ -21,12 +21,10 @@ use TYPO3\CMS\Backend\View\BackendLayout\BackendLayout;
 use TYPO3\CMS\Backend\View\BackendLayout\DataProviderCollection;
 use TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext;
 use TYPO3\CMS\Backend\View\BackendLayout\DefaultDataProvider;
-use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\SingletonInterface;
-use TYPO3\CMS\Core\TypoScript\Tokenizer\TokenizerInterface;
 use TYPO3\CMS\Core\TypoScript\TypoScriptStringFactory;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -47,8 +45,6 @@ class BackendLayoutView implements SingletonInterface
         private readonly DataProviderCollection $dataProviderCollection,
         private readonly TypoScriptStringFactory $typoScriptStringFactory,
         private readonly BackendConditionMatcher $conditionMatcher,
-        private readonly TokenizerInterface $tokenizer,
-        private readonly PhpFrontend $typoScriptCache,
     ) {
         $this->dataProviderCollection->add('default', DefaultDataProvider::class);
         if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'])) {
@@ -346,9 +342,7 @@ class BackendLayoutView implements SingletonInterface
         $typoScriptTree = $this->typoScriptStringFactory->parseFromStringWithIncludesAndConditions(
             'backend-layout',
             $backendLayout->getConfiguration(),
-            $this->tokenizer,
-            $this->conditionMatcher,
-            $this->typoScriptCache
+            $this->conditionMatcher
         );
 
         $backendLayoutData = [];
diff --git a/typo3/sysext/backend/Classes/View/BackendViewFactory.php b/typo3/sysext/backend/Classes/View/BackendViewFactory.php
index bb0fe3e75697..e58f968264b4 100644
--- a/typo3/sysext/backend/Classes/View/BackendViewFactory.php
+++ b/typo3/sysext/backend/Classes/View/BackendViewFactory.php
@@ -22,6 +22,7 @@ use TYPO3\CMS\Backend\Routing\Route;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Package\PackageManager;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\View\FluidViewAdapter;
 use TYPO3\CMS\Core\View\ViewInterface as CoreViewInterface;
 use TYPO3\CMS\Fluid\Core\Rendering\RenderingContextFactory;
@@ -69,8 +70,15 @@ final class BackendViewFactory
 
         // @todo: This assumes the pageId is *always* given as 'id' in request.
         // @todo: It would be cool if a middleware adds final pageTS - already overlayed by userTS - as attribute to request, to use it here.
+        $pageTs = [];
         $pageId = $request->getParsedBody()['id'] ?? $request->getQueryParams()['id'] ?? 0;
-        $pageTs = BackendUtility::getPagesTSconfig($pageId);
+        if (MathUtility::canBeInterpretedAsInteger($pageId)) {
+            // Some BE controllers misuse the 'id' argument for something else than the page-uid (especially filelist module).
+            // We check if 'id' is an integer here to skip pageTsConfig calculation if that is the case.
+            // @todo: Mid-term, misuses should vanish, making 'id' a Backend convention. Affected is
+            //        at least ext:filelist, plus record linking modals that use 'pid'.
+            $pageTs = BackendUtility::getPagesTSconfig((int)$pageId);
+        }
 
         $templatePaths = [
             'templateRootPaths' => [],
diff --git a/typo3/sysext/backend/Configuration/Services.yaml b/typo3/sysext/backend/Configuration/Services.yaml
index c0d28a11d041..1d00f5d17156 100644
--- a/typo3/sysext/backend/Configuration/Services.yaml
+++ b/typo3/sysext/backend/Configuration/Services.yaml
@@ -70,10 +70,6 @@ services:
   TYPO3\CMS\Backend\View\AuthenticationStyleInformation:
     public: true
 
-  TYPO3\CMS\Backend\View\BackendLayoutView:
-    arguments:
-      $typoScriptCache: '@cache.typoscript'
-
   TYPO3\CMS\Backend\Search\LiveSearch\SearchProviderRegistry:
     arguments:
       - !tagged_iterator livesearch.provider
diff --git a/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php b/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php
index 87dd392c3d2d..497a60a8ce04 100644
--- a/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php
+++ b/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php
@@ -15,7 +15,6 @@
 
 namespace TYPO3\CMS\Core\Authentication;
 
-use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
 use TYPO3\CMS\Backend\Module\ModuleProvider;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Cache\CacheManager;
@@ -46,7 +45,8 @@ use TYPO3\CMS\Core\Type\Bitmask\BackendGroupMountOption;
 use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
 use TYPO3\CMS\Core\Type\Bitmask\Permission;
 use TYPO3\CMS\Core\Type\Exception\InvalidEnumerationValueException;
-use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
+use TYPO3\CMS\Core\TypoScript\UserTsConfig;
+use TYPO3\CMS\Core\TypoScript\UserTsConfigFactory;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
@@ -111,15 +111,13 @@ class BackendUserAuthentication extends AbstractUserAuthentication
      */
     public $workspaceRec = [];
 
-    /**
-     * @var array Parsed user TSconfig
-     */
-    protected $userTS = [];
+    protected ?UserTsConfig $userTsConfig = null;
 
     /**
-     * @var bool True if the user TSconfig was parsed and needs to be cached.
+     * True if the user TSconfig was parsed and needs to be cached.
+     * @todo: Should vanish, see todo below.
      */
-    protected $userTSUpdated = false;
+    protected bool $userTSUpdated = false;
 
     /**
      * Contains last error message
@@ -968,9 +966,19 @@ class BackendUserAuthentication extends AbstractUserAuthentication
      *
      * @return array Parsed and merged user TSconfig array
      */
-    public function getTSConfig()
+    public function getTSConfig(): array
+    {
+        return $this->getUserTsConfig()?->getUserTsConfigArray() ?? [];
+    }
+
+    /**
+     * Return the full UserTsConfig object instead of just the array as in getTSConfig()
+     *
+     * @internal for now until API stabilized
+     */
+    public function getUserTsConfig(): ?UserTsConfig
     {
-        return $this->userTS;
+        return $this->userTsConfig;
     }
 
     /**
@@ -1058,7 +1066,6 @@ class BackendUserAuthentication extends AbstractUserAuthentication
      * Generally this is required initialization of a backend user.
      *
      * @internal
-     * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
      */
     public function fetchGroupData()
     {
@@ -1133,8 +1140,8 @@ class BackendUserAuthentication extends AbstractUserAuthentication
                 }
             }
 
-            // Populating the $this->userGroupsUID -array with the groups in the order in which they were LAST included.!!
-            // Finally this is the list of group_uid's in the order they are parsed (including subgroups!)
+            // Populating the $this->userGroupsUID -array with the groups in the order in which they were LAST included.
+            // Finally, this is the list of group_uid's in the order they are parsed (including subgroups)
             // and without duplicates (duplicates are presented with their last entrance in the list,
             // which thus reflects the order of the TypoScript in TSconfig)
             $this->userGroupsUID = array_reverse(array_unique(array_reverse($this->userGroupsUID)));
@@ -1214,43 +1221,28 @@ class BackendUserAuthentication extends AbstractUserAuthentication
     }
 
     /**
-     * This method parses the UserTSconfig from the current user and all their groups.
-     * If the contents are the same, parsing is skipped. No matching is applied here currently.
+     * Parse userTsConfig from current user and its groups and set it as $this->userTS.
      */
     protected function prepareUserTsConfig(): void
     {
-        $collectedUserTSconfig = [
-            'default' => $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig'],
-        ];
-        // Default TSconfig for admin-users
-        if ($this->isAdmin()) {
-            $collectedUserTSconfig[] = 'admPanel.enable.all = 1';
-        }
-        // Setting defaults for sys_note author / email
-        $collectedUserTSconfig[] = '
-TCAdefaults.sys_note.author = ' . $this->user['realName'] . '
-TCAdefaults.sys_note.email = ' . $this->user['email'];
-
-        // Loop through all groups and add their 'TSconfig' fields
-        foreach ($this->userGroupsUID as $groupId) {
-            $collectedUserTSconfig['group_' . $groupId] = $this->userGroups[$groupId]['TSconfig'] ?? '';
-        }
-
-        $collectedUserTSconfig[] = $this->user['TSconfig'];
-        // Check external files
-        $collectedUserTSconfig = TypoScriptParser::checkIncludeLines_array($collectedUserTSconfig);
-        // Imploding with "[global]" will make sure that non-ended confinements with braces are ignored.
-        $userTS_text = implode("\n[GLOBAL]\n", $collectedUserTSconfig);
-        // Parsing the user TSconfig (or getting from cache)
-        $hash = md5('userTS:' . $userTS_text);
-        $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
-        if (!($this->userTS = $cache->get($hash))) {
-            $parseObj = GeneralUtility::makeInstance(TypoScriptParser::class);
-            $conditionMatcher = GeneralUtility::makeInstance(ConditionMatcher::class);
-            $parseObj->parse($userTS_text, $conditionMatcher);
-            $this->userTS = $parseObj->setup;
-            $cache->set($hash, $this->userTS, ['UserTSconfig'], 0);
-            // Ensure to update UC later
+        $tsConfigFactory = GeneralUtility::makeInstance(UserTsConfigFactory::class);
+        $this->userTsConfig = $tsConfigFactory->create($this);
+        if (!empty($this->getUserTsConfig()->getUserTsConfigArray()['setup.']['override.'])) {
+            // @todo: This logic is ugly. userTsConfig "setup.override." is used to force options
+            //        in the user settings module, along with "setup.fields." and "setup.default.".
+            //        See the docs about this.
+            //        The fun part is, this is merged into user UC. As such, whenever these setup
+            //        options are used, user UC has to be updated. The toggle below triggers this
+            //        and initiates an update query of this users UC.
+            //        Before v12, this was only triggered if userTsConfig could not be fetched from
+            //        cache, but this was flawed, too: When two users had the same userTsConfig, UC
+            //        of one user would be updated, but not UC of the other user if caches were not
+            //        cleared in between their two calls.
+            //        This toggle and UC overriding should vanish altogether: It would be better if
+            //        userTsConfig no longer overlays UC, instead the settings / setup module
+            //        controller should look at userTsConfig "setup." on the fly when rendering, and
+            //        consumers that access user setup / settings values should get values overloaded
+            //        on the fly as well using some helper or a late init logic or similar.
             $this->userTSUpdated = true;
         }
     }
diff --git a/typo3/sysext/core/Classes/Configuration/Event/ModifyLoadedPageTsConfigEvent.php b/typo3/sysext/core/Classes/Configuration/Event/ModifyLoadedPageTsConfigEvent.php
index fa7eafeb46cd..26f4c0926181 100644
--- a/typo3/sysext/core/Classes/Configuration/Event/ModifyLoadedPageTsConfigEvent.php
+++ b/typo3/sysext/core/Classes/Configuration/Event/ModifyLoadedPageTsConfigEvent.php
@@ -19,30 +19,11 @@ namespace TYPO3\CMS\Core\Configuration\Event;
 
 /**
  * Extensions can modify pageTSConfig entries that can be overridden or added, based on the root line
+ *
+ * @deprecated since v12, will be removed in v13. Switch to \TYPO3\CMS\Core\TypoScript\IncludeTree\Event\ModifyLoadedPageTsConfigEvent.
+ *             When removing, delete the class, adapt test_typoscript_pagetsconfigfactory test extension, related test and
+ *             set event \TYPO3\CMS\Core\TypoScript\IncludeTree\Event\ModifyLoadedPageTsConfigEvent final.
  */
-final class ModifyLoadedPageTsConfigEvent
+class ModifyLoadedPageTsConfigEvent extends \TYPO3\CMS\Core\TypoScript\IncludeTree\Event\ModifyLoadedPageTsConfigEvent
 {
-    public function __construct(private array $tsConfig, private readonly array $rootLine)
-    {
-    }
-
-    public function getTsConfig(): array
-    {
-        return $this->tsConfig;
-    }
-
-    public function addTsConfig(string $tsConfig): void
-    {
-        $this->tsConfig[] = $tsConfig;
-    }
-
-    public function setTsConfig(array $tsConfig): void
-    {
-        $this->tsConfig = $tsConfig;
-    }
-
-    public function getRootLine(): array
-    {
-        return $this->rootLine;
-    }
 }
diff --git a/typo3/sysext/core/Classes/Configuration/ExtensionConfiguration.php b/typo3/sysext/core/Classes/Configuration/ExtensionConfiguration.php
index 699c08b4bc89..3b37a479df78 100644
--- a/typo3/sysext/core/Classes/Configuration/ExtensionConfiguration.php
+++ b/typo3/sysext/core/Classes/Configuration/ExtensionConfiguration.php
@@ -22,7 +22,6 @@ use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExis
 use TYPO3\CMS\Core\EventDispatcher\NoopEventDispatcher;
 use TYPO3\CMS\Core\Package\PackageManager;
 use TYPO3\CMS\Core\TypoScript\AST\AstBuilder;
-use TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer;
 use TYPO3\CMS\Core\TypoScript\TypoScriptStringFactory;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -249,7 +248,7 @@ class ExtensionConfiguration
     {
         $rawConfigurationString = $this->getDefaultConfigurationRawString($extensionKey);
         $typoScriptStringFactory = GeneralUtility::makeInstance(TypoScriptStringFactory::class);
-        $typoScriptTree = $typoScriptStringFactory->parseFromString($rawConfigurationString, new LossyTokenizer(), new AstBuilder(new NoopEventDispatcher()));
+        $typoScriptTree = $typoScriptStringFactory->parseFromString($rawConfigurationString, new AstBuilder(new NoopEventDispatcher()));
         return GeneralUtility::removeDotsFromTS($typoScriptTree->toArray());
     }
 
diff --git a/typo3/sysext/core/Classes/Configuration/Loader/PageTsConfigLoader.php b/typo3/sysext/core/Classes/Configuration/Loader/PageTsConfigLoader.php
index d9d236762558..dc90d1713e67 100644
--- a/typo3/sysext/core/Classes/Configuration/Loader/PageTsConfigLoader.php
+++ b/typo3/sysext/core/Classes/Configuration/Loader/PageTsConfigLoader.php
@@ -34,6 +34,9 @@ use TYPO3\CMS\Core\Utility\PathUtility;
  *
  * Currently, this accumulated information of the pages is NOT cached, as it would need to be tagged with any
  * page, also including external files.
+ *
+ * @deprecated since TYPO3 v12, will be removed with v13. Use PageTsConfigFactory instead.
+ *             When removing, also remove entries in core ServiceProvider, AbstractServiceProvider and Services.yaml.
  */
 class PageTsConfigLoader
 {
@@ -42,6 +45,7 @@ class PageTsConfigLoader
 
     public function __construct(EventDispatcherInterface $eventDispatcher)
     {
+        trigger_error('Class ' . __CLASS__ . ' will be removed with TYPO3 v13.0. Use PageTsConfigFactory instead.', E_USER_DEPRECATED);
         $this->eventDispatcher = $eventDispatcher;
     }
 
diff --git a/typo3/sysext/core/Classes/Configuration/PageTsConfig.php b/typo3/sysext/core/Classes/Configuration/PageTsConfig.php
index 3413e38897b5..b60007cacef1 100644
--- a/typo3/sysext/core/Classes/Configuration/PageTsConfig.php
+++ b/typo3/sysext/core/Classes/Configuration/PageTsConfig.php
@@ -26,6 +26,9 @@ use TYPO3\CMS\Core\Site\Entity\Site;
 
 /**
  * Main entry point for fetching PageTsConfig for frontend and backend.
+ *
+ * @deprecated since TYPO3 v12, will be removed with v13. Use PageTsConfigFactory instead.
+ *             When removing, also remove entries in core Services.yaml and usage in TypoScriptFrontendController.
  */
 class PageTsConfig
 {
@@ -35,6 +38,7 @@ class PageTsConfig
 
     public function __construct(FrontendInterface $cache, PageTsConfigLoader $loader, PageTsConfigParser $parser)
     {
+        trigger_error('Class ' . __CLASS__ . ' will be removed with TYPO3 v13.0. Use PageTsConfigFactory instead.', E_USER_DEPRECATED);
         $this->cache = $cache;
         $this->loader = $loader;
         $this->parser = $parser;
diff --git a/typo3/sysext/core/Classes/Configuration/Parser/PageTsConfigParser.php b/typo3/sysext/core/Classes/Configuration/Parser/PageTsConfigParser.php
index f8d54e826872..3fa09b8d58c3 100644
--- a/typo3/sysext/core/Classes/Configuration/Parser/PageTsConfigParser.php
+++ b/typo3/sysext/core/Classes/Configuration/Parser/PageTsConfigParser.php
@@ -27,6 +27,9 @@ use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
  *
  * This class does parsing of a compiled TSconfig string, and applies matching() based on the
  * Context (FE or BE) in it, allowing to be fully agnostic to the outside world.
+ *
+ * @deprecated since TYPO3 v12, will be removed with v13. Use PageTsConfigFactory instead.
+ *             When removing, also remove entries in core Services.yaml.
  */
 class PageTsConfigParser
 {
@@ -35,6 +38,7 @@ class PageTsConfigParser
 
     public function __construct(TypoScriptParser $typoScriptParser, FrontendInterface $cache)
     {
+        trigger_error('Class ' . __CLASS__ . ' will be removed with TYPO3 v13.0. Use PageTsConfigFactory instead.', E_USER_DEPRECATED);
         $this->typoScriptParser = $typoScriptParser;
         $this->cache = $cache;
     }
diff --git a/typo3/sysext/core/Classes/Package/AbstractServiceProvider.php b/typo3/sysext/core/Classes/Package/AbstractServiceProvider.php
index d192ca447e0c..ca2effb8b5de 100644
--- a/typo3/sysext/core/Classes/Package/AbstractServiceProvider.php
+++ b/typo3/sysext/core/Classes/Package/AbstractServiceProvider.php
@@ -50,6 +50,7 @@ abstract class AbstractServiceProvider implements ServiceProviderInterface
         return [
             'middlewares' => [ static::class, 'configureMiddlewares' ],
             'backend.routes' => [ static::class, 'configureBackendRoutes' ],
+            // @deprecated since v12, will be removed with v13 together with class PageTsConfigLoader.
             'globalPageTsConfig' => [ static::class, 'configureGlobalPageTsConfig' ],
             'backend.modules' => [ static::class, 'configureBackendModules' ],
             'icons' => [ static::class, 'configureIcons' ],
@@ -110,6 +111,9 @@ abstract class AbstractServiceProvider implements ServiceProviderInterface
         return $routes;
     }
 
+    /**
+     * @deprecated since v12, will be removed with v13 together with class PageTsConfigLoader.
+     */
     public static function configureGlobalPageTsConfig(ContainerInterface $container, ArrayObject $tsConfigFiles, string $path = null): ArrayObject
     {
         $path = $path ?? static::getPackagePath();
diff --git a/typo3/sysext/core/Classes/ServiceProvider.php b/typo3/sysext/core/Classes/ServiceProvider.php
index 86f7cdbfa70a..c722683e03c9 100644
--- a/typo3/sysext/core/Classes/ServiceProvider.php
+++ b/typo3/sysext/core/Classes/ServiceProvider.php
@@ -30,6 +30,7 @@ use TYPO3\CMS\Core\DependencyInjection\ContainerBuilder;
 use TYPO3\CMS\Core\Imaging\IconRegistry;
 use TYPO3\CMS\Core\Package\AbstractServiceProvider;
 use TYPO3\CMS\Core\Package\PackageManager;
+use TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer;
 
 /**
  * @internal
@@ -99,6 +100,7 @@ class ServiceProvider extends AbstractServiceProvider
             TypoScript\AST\Traverser\AstTraverser::class => [ static::class, 'getAstTraverser' ],
             TypoScript\AST\CommentAwareAstBuilder::class => [ static::class, 'getCommentAwareAstBuilder' ],
             TypoScript\Tokenizer\LosslessTokenizer::class => [ static::class, 'getLosslessTokenizer'],
+            // @deprecated since v12, will be removed with v13 together with class PageTsConfigLoader.
             'globalPageTsConfig' => [ static::class, 'getGlobalPageTsConfig' ],
             'icons' => [ static::class, 'getIcons' ],
             'middlewares' => [ static::class, 'getMiddlewares' ],
@@ -110,6 +112,7 @@ class ServiceProvider extends AbstractServiceProvider
         return [
             Console\CommandRegistry::class => [ static::class, 'configureCommands' ],
             Imaging\IconRegistry::class => [ static::class, 'configureIconRegistry' ],
+            // @deprecated since v12, will be removed with v13 together with class PageTsConfigLoader.
             Configuration\Loader\PageTsConfigLoader::class => [ static::class, 'configurePageTsConfigLoader' ],
             EventDispatcherInterface::class => [ static::class, 'provideFallbackEventDispatcher' ],
             EventDispatcher\ListenerProvider::class => [ static::class, 'extendEventListenerProvider' ],
@@ -326,11 +329,17 @@ class ServiceProvider extends AbstractServiceProvider
         return self::new($container, Imaging\IconRegistry::class, [$container->get('cache.assets'), $container->get(Package\Cache\PackageDependentCacheIdentifier::class)->withPrefix('BackendIcons')->toString()]);
     }
 
+    /**
+     * @deprecated since v12, will be removed with v13 together with class PageTsConfigLoader.
+     */
     public static function getGlobalPageTsConfig(ContainerInterface $container): ArrayObject
     {
         return new ArrayObject();
     }
 
+    /**
+     * @deprecated since v12, will be removed with v13 together with class PageTsConfigLoader.
+     */
     public static function configurePageTsConfigLoader(ContainerInterface $container, PageTsConfigLoader $configLoader): PageTsConfigLoader
     {
         $cache = $container->get('cache.core');
@@ -480,7 +489,7 @@ class ServiceProvider extends AbstractServiceProvider
 
     public static function getTypoScriptStringFactory(ContainerInterface $container): TypoScript\TypoScriptStringFactory
     {
-        return new TypoScript\TypoScriptStringFactory($container);
+        return new TypoScript\TypoScriptStringFactory($container, new LossyTokenizer());
     }
 
     public static function getTypoScriptService(ContainerInterface $container): TypoScript\TypoScriptService
diff --git a/typo3/sysext/core/Classes/TypoScript/IncludeTree/Event/ModifyLoadedPageTsConfigEvent.php b/typo3/sysext/core/Classes/TypoScript/IncludeTree/Event/ModifyLoadedPageTsConfigEvent.php
new file mode 100644
index 000000000000..7a47d045e2c5
--- /dev/null
+++ b/typo3/sysext/core/Classes/TypoScript/IncludeTree/Event/ModifyLoadedPageTsConfigEvent.php
@@ -0,0 +1,50 @@
+<?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;
+
+/**
+ * Extensions can modify pageTsConfig entries that can be overridden or added, based on the root line
+ *
+ * @todo: Set final in v13 when class \TYPO3\CMS\Core\Configuration\Event\ModifyLoadedPageTsConfigEvent is gone.
+ */
+class ModifyLoadedPageTsConfigEvent
+{
+    public function __construct(private array $tsConfig, private readonly array $rootLine)
+    {
+    }
+
+    public function getTsConfig(): array
+    {
+        return $this->tsConfig;
+    }
+
+    public function addTsConfig(string $tsConfig): void
+    {
+        $this->tsConfig[] = $tsConfig;
+    }
+
+    public function setTsConfig(array $tsConfig): void
+    {
+        $this->tsConfig = $tsConfig;
+    }
+
+    public function getRootLine(): array
+    {
+        return $this->rootLine;
+    }
+}
diff --git a/typo3/sysext/core/Classes/TypoScript/IncludeTree/IncludeNode/TsConfigInclude.php b/typo3/sysext/core/Classes/TypoScript/IncludeTree/IncludeNode/TsConfigInclude.php
new file mode 100644
index 000000000000..3abbef82bee5
--- /dev/null
+++ b/typo3/sysext/core/Classes/TypoScript/IncludeTree/IncludeNode/TsConfigInclude.php
@@ -0,0 +1,27 @@
+<?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\IncludeNode;
+
+/**
+ * An include type used by user and pages TsConfig for single TsConfig snippets.
+ *
+ * @internal: Internal tree structure.
+ */
+final class TsConfigInclude extends AbstractInclude
+{
+}
diff --git a/typo3/sysext/core/Classes/TypoScript/IncludeTree/TsConfigTreeBuilder.php b/typo3/sysext/core/Classes/TypoScript/IncludeTree/TsConfigTreeBuilder.php
new file mode 100644
index 000000000000..0efa2734582a
--- /dev/null
+++ b/typo3/sysext/core/Classes/TypoScript/IncludeTree/TsConfigTreeBuilder.php
@@ -0,0 +1,190 @@
+<?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 TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
+use TYPO3\CMS\Core\Configuration\Event\ModifyLoadedPageTsConfigEvent as LegacyModifyLoadedPageTsConfigEvent;
+use TYPO3\CMS\Core\EventDispatcher\EventDispatcher;
+use TYPO3\CMS\Core\Package\PackageManager;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Event\ModifyLoadedPageTsConfigEvent;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\TsConfigInclude;
+use TYPO3\CMS\Core\TypoScript\Tokenizer\TokenizerInterface;
+use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\PathUtility;
+
+/**
+ * Build include tree for UserTsConfig and PageTsConfig. This is typically used only by
+ * UserTsConfigFactory and PageTsConfigFactory.
+ *
+ * @internal
+ */
+final class TsConfigTreeBuilder
+{
+    private TokenizerInterface $tokenizer;
+    private ?PhpFrontend $cache = null;
+
+    public function __construct(
+        private readonly TreeFromLineStreamBuilder $treeFromTokenStreamBuilder,
+        private readonly PackageManager $packageManager,
+        private readonly EventDispatcher $eventDispatcher,
+    ) {
+    }
+
+    public function getUserTsConfigTree(
+        BackendUserAuthentication $backendUser,
+        TokenizerInterface $tokenizer,
+        ?PhpFrontend $cache = null
+    ): RootInclude {
+        $this->tokenizer = $tokenizer;
+        $this->cache = $cache;
+        $includeTree = new RootInclude();
+        if (!empty($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig'] ?? '')) {
+            $includeTree->addChild($this->getTreeFromString('userTsConfig-globals', $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig']));
+        }
+        if ($backendUser->isAdmin()) {
+            // @todo: Could we maybe solve this differently somehow? Maybe in ext:adminpanel in FE directly?
+            $includeTree->addChild($this->getTreeFromString('userTsConfig-admpanel', 'admPanel.enable.all = 1'));
+        }
+        // @todo: Get rid of this, maybe by using a hook in DataHandler?
+        $includeTree->addChild($this->getTreeFromString(
+            'userTsConfig-sysnote',
+            'TCAdefaults.sys_note.author = ' . ($backendUser->user['realName'] ?? '') . chr(10) . 'TCAdefaults.sys_note.email = ' . ($backendUser->user['email'] ?? '')
+        ));
+        foreach ($backendUser->userGroupsUID as $groupId) {
+            // Loop through all groups and add their 'TSconfig' fields
+            if (!empty($backendUser->userGroups[$groupId]['TSconfig'] ?? '')) {
+                $includeTree->addChild($this->getTreeFromString('userTsConfig-group-' . $groupId, $backendUser->userGroups[$groupId]['TSconfig']));
+            }
+        }
+        if (!empty($backendUser->user['TSconfig'] ?? '')) {
+            $includeTree->addChild($this->getTreeFromString('userTsConfig-user', $backendUser->user['TSconfig']));
+        }
+        return $includeTree;
+    }
+
+    public function getPagesTsConfigTree(
+        array $rootLine,
+        TokenizerInterface $tokenizer,
+        ?PhpFrontend $cache = null
+    ): RootInclude {
+        $this->tokenizer = $tokenizer;
+        $this->cache = $cache;
+
+        $collectedPagesTsConfigArray = [];
+        $gotPackagesPagesTsConfigFromCache = false;
+        if ($cache) {
+            $collectedPagesTsConfigArrayFromCache = $cache->require('pagestsconfig-packages-strings');
+            if ($collectedPagesTsConfigArrayFromCache) {
+                $gotPackagesPagesTsConfigFromCache = true;
+                $collectedPagesTsConfigArray = $collectedPagesTsConfigArrayFromCache;
+            }
+        }
+        if (!$gotPackagesPagesTsConfigFromCache) {
+            foreach ($this->packageManager->getActivePackages() as $package) {
+                $packagePath = $package->getPackagePath();
+                $tsConfigFile = null;
+                if (file_exists($packagePath . 'Configuration/page.tsconfig')) {
+                    $tsConfigFile = $packagePath . 'Configuration/page.tsconfig';
+                } elseif (file_exists($packagePath . 'Configuration/Page.tsconfig')) {
+                    $tsConfigFile = $packagePath . 'Configuration/Page.tsconfig';
+                }
+                if ($tsConfigFile) {
+                    $typoScriptString = @file_get_contents($tsConfigFile);
+                    if (!empty($typoScriptString)) {
+                        $collectedPagesTsConfigArray['pagesTsConfig-package-' . $package->getPackageKey()] = $typoScriptString;
+                    }
+                }
+            }
+            $cache?->set('pagestsconfig-packages-strings', 'return unserialize(\'' . addcslashes(serialize($collectedPagesTsConfigArray), '\'\\') . '\');');
+        }
+
+        if (!empty($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '')) {
+            $collectedPagesTsConfigArray['pagesTsConfig-globals-defaultPageTSconfig'] = $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'];
+        }
+
+        foreach ($rootLine as $page) {
+            if (empty($page['uid'])) {
+                // Page 0 can happen when the rootline is given from BE context. It has not TSconfig. Skip this.
+                continue;
+            }
+            if (trim($page['tsconfig_includes'] ?? '')) {
+                $includeTsConfigFileList = GeneralUtility::trimExplode(',', $page['tsconfig_includes'], true);
+                foreach ($includeTsConfigFileList as $key => $includeTsConfigFile) {
+                    if (PathUtility::isExtensionPath($includeTsConfigFile)) {
+                        [$includeTsConfigFileExtensionKey, $includeTsConfigFilename] = explode('/', substr($includeTsConfigFile, 4), 2);
+                        if ($includeTsConfigFileExtensionKey !== ''
+                            && ExtensionManagementUtility::isLoaded($includeTsConfigFileExtensionKey)
+                            && $includeTsConfigFilename !== ''
+                        ) {
+                            $extensionPath = ExtensionManagementUtility::extPath($includeTsConfigFileExtensionKey);
+                            $includeTsConfigFileAndPath = PathUtility::getCanonicalPath($extensionPath . $includeTsConfigFilename);
+                            if (str_starts_with($includeTsConfigFileAndPath, $extensionPath) && file_exists($includeTsConfigFileAndPath)) {
+                                $typoScriptString = (string)file_get_contents($includeTsConfigFileAndPath);
+                                if (!empty($typoScriptString)) {
+                                    $collectedPagesTsConfigArray['pagesTsConfig-page-' . $page['uid'] . '-includes-' . $key] = (string)file_get_contents($includeTsConfigFileAndPath);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            if (!empty($page['TSconfig'])) {
+                $collectedPagesTsConfigArray['pagesTsConfig-page-' . $page['uid'] . '-tsConfig'] = $page['TSconfig'];
+            }
+        }
+
+        // @deprecated since TYPO3 v12, remove together with event class and set IncludeTree\Event\ModifyLoadedPageTsConfigEvent final in v13.
+        /** @var LegacyModifyLoadedPageTsConfigEvent $event */
+        $event = $this->eventDispatcher->dispatch(new LegacyModifyLoadedPageTsConfigEvent($collectedPagesTsConfigArray, $rootLine));
+        $collectedPagesTsConfigArray = $event->getTsConfig();
+
+        /** @var ModifyLoadedPageTsConfigEvent $event */
+        $event = $this->eventDispatcher->dispatch(new ModifyLoadedPageTsConfigEvent($collectedPagesTsConfigArray, $rootLine));
+        $collectedPagesTsConfigArray = $event->getTsConfig();
+
+        $includeTree = new RootInclude();
+        foreach ($collectedPagesTsConfigArray as $key => $typoScriptString) {
+            $includeTree->addChild($this->getTreeFromString((string)$key, $typoScriptString));
+        }
+        return $includeTree;
+    }
+
+    private function getTreeFromString(
+        string $name,
+        string $typoScriptString,
+    ): TsConfigInclude {
+        $lowercaseName = mb_strtolower($name);
+        $identifier = $lowercaseName . '-' . sha1($typoScriptString);
+        if ($this->cache) {
+            $includeNode = $this->cache->require($identifier);
+            if ($includeNode instanceof TsConfigInclude) {
+                return $includeNode;
+            }
+        }
+        $includeNode = new TsConfigInclude();
+        $includeNode->setName($name);
+        $includeNode->setIdentifier($lowercaseName);
+        $includeNode->setLineStream($this->tokenizer->tokenize($typoScriptString));
+        $this->treeFromTokenStreamBuilder->buildTree($includeNode, 'other', $this->tokenizer);
+        $this->cache?->set($identifier, 'return unserialize(\'' . addcslashes(serialize($includeNode), '\'\\') . '\');');
+        return $includeNode;
+    }
+}
diff --git a/typo3/sysext/core/Classes/TypoScript/PageTsConfig.php b/typo3/sysext/core/Classes/TypoScript/PageTsConfig.php
new file mode 100644
index 000000000000..d33886bb9939
--- /dev/null
+++ b/typo3/sysext/core/Classes/TypoScript/PageTsConfig.php
@@ -0,0 +1,46 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\TypoScript;
+
+use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
+
+/**
+ * A data object that carries the final PageTsConfig. This is created by PageTsConfigFactory.
+ *
+ * @internal Internal for now until API stabilized. Use BackendUtility::getPagesTSconfig().
+ */
+final class PageTsConfig
+{
+    private readonly array $pageTsConfigArray;
+
+    public function __construct(
+        private readonly RootNode $pageTsConfigTree
+    ) {
+        $this->pageTsConfigArray = $pageTsConfigTree->toArray();
+    }
+
+    public function getPageTsConfigTree(): RootNode
+    {
+        return $this->pageTsConfigTree;
+    }
+
+    public function getPageTsConfigArray(): array
+    {
+        return $this->pageTsConfigArray;
+    }
+}
diff --git a/typo3/sysext/core/Classes/TypoScript/PageTsConfigFactory.php b/typo3/sysext/core/Classes/TypoScript/PageTsConfigFactory.php
new file mode 100644
index 000000000000..a5089ecf0251
--- /dev/null
+++ b/typo3/sysext/core/Classes/TypoScript/PageTsConfigFactory.php
@@ -0,0 +1,132 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\TypoScript;
+
+use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
+use TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\ConditionMatcherInterface;
+use TYPO3\CMS\Core\Site\Entity\Site;
+use TYPO3\CMS\Core\Site\Entity\SiteInterface;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\SiteInclude;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\TsConfigInclude;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\TsConfigTreeBuilder;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeAstBuilderVisitor;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionMatcherVisitor;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeSetupConditionConstantSubstitutionVisitor;
+use TYPO3\CMS\Core\TypoScript\Tokenizer\TokenizerInterface;
+
+/**
+ * Calculate PageTsConfig. This does the heavy lifting additionally supported by
+ * TsConfigTreeBuilder: Load basic pageTsConfig tree, overload with userTsConfig, parse
+ * site settings ("constants"), then build the PageTsConfig AST and return PageTsConfig DTO.
+ *
+ * @internal Internal for now until API stabilized. Use BackendUtility::getPagesTSconfig().
+ */
+final class PageTsConfigFactory
+{
+    public function __construct(
+        private readonly ContainerInterface $container,
+        private readonly TokenizerInterface $tokenizer,
+        private readonly TsConfigTreeBuilder $tsConfigTreeBuilder,
+        private readonly PhpFrontend $cache,
+    ) {
+    }
+
+    public function create(
+        array $rootLine,
+        SiteInterface $site,
+        ConditionMatcherInterface $conditionMatcher,
+        ?UserTsConfig $userTsConfig = null
+    ): PageTsConfig {
+        $pagesTsConfigTree = $this->tsConfigTreeBuilder->getPagesTsConfigTree($rootLine, $this->tokenizer, $this->cache);
+
+        // Overloading with userTsConfig if hand over
+        if ($userTsConfig instanceof UserTsConfig) {
+            $userTsConfigAst = $userTsConfig->getUserTsConfigTree();
+            $userTsConfigPageOverrides = '';
+            $userTsConfigFlat = $userTsConfigAst->flatten();
+            foreach ($userTsConfigFlat as $userTsConfigIdentifier => $userTsConfigValue) {
+                if (str_starts_with($userTsConfigIdentifier, 'page.')) {
+                    $userTsConfigPageOverrides .= substr($userTsConfigIdentifier, 5) . ' = ' . $userTsConfigValue . chr(10);
+                }
+            }
+            if (!empty($userTsConfigPageOverrides)) {
+                $includeNode = new TsConfigInclude();
+                $includeNode->setName('pageTsConfig-overrides-by-userTsConfig');
+                $includeNode->setIdentifier('pagetsconfig-overrides-by-usertsconfig');
+                $includeNode->setLineStream($this->tokenizer->tokenize($userTsConfigPageOverrides));
+                $pagesTsConfigTree->addChild($includeNode);
+            }
+        }
+
+        // Prepare site constants to be substituted
+        $includeTreeTraverserConditionVerdictAware = new ConditionVerdictAwareIncludeTreeTraverser();
+        $siteSettingsFlat = [];
+        if ($site instanceof Site) {
+            $siteSettings = $site->getSettings();
+            if (!$siteSettings->isEmpty()) {
+                $siteSettingsCacheIdentifier = 'site-settings-flat-' . sha1(json_encode($siteSettings, JSON_THROW_ON_ERROR));
+                $gotSiteSettingsFromCache = false;
+                $siteSettingsNode = $this->cache->require($siteSettingsCacheIdentifier);
+                if ($siteSettingsNode) {
+                    $gotSiteSettingsFromCache = true;
+                }
+                if (!$gotSiteSettingsFromCache) {
+                    $siteConstants = '';
+                    $siteSettings = $siteSettings->getAllFlat();
+                    foreach ($siteSettings as $nodeIdentifier => $value) {
+                        $siteConstants .= $nodeIdentifier . ' = ' . $value . LF;
+                    }
+                    $siteSettingsNode = new SiteInclude();
+                    $siteSettingsNode->setIdentifier($siteSettingsCacheIdentifier);
+                    $siteSettingsNode->setName('Site constants settings of site ' . $site->getIdentifier());
+                    $siteSettingsNode->setLineStream($this->tokenizer->tokenize($siteConstants));
+                    $siteSettingsTreeRoot = new RootInclude();
+                    $siteSettingsTreeRoot->addChild($siteSettingsNode);
+                    /** @var IncludeTreeAstBuilderVisitor $astBuilderVisitor */
+                    $astBuilderVisitor = $this->container->get(IncludeTreeAstBuilderVisitor::class);
+                    $includeTreeTraverserConditionVerdictAware->resetVisitors();
+                    $includeTreeTraverserConditionVerdictAware->addVisitor($astBuilderVisitor);
+                    $includeTreeTraverserConditionVerdictAware->traverse($siteSettingsTreeRoot);
+                    $siteSettingsFlat = $astBuilderVisitor->getAst()->flatten();
+                    $this->cache->set($siteSettingsCacheIdentifier, 'return unserialize(\'' . addcslashes(serialize($siteSettingsFlat), '\'\\') . '\');');
+                }
+            }
+        }
+
+        // Create AST with constants from site and conditions
+        $includeTreeTraverserConditionVerdictAware->resetVisitors();
+        if (!empty($siteSettingsFlat)) {
+            $setupConditionConstantSubstitutionVisitor = new IncludeTreeSetupConditionConstantSubstitutionVisitor();
+            $setupConditionConstantSubstitutionVisitor->setFlattenedConstants($siteSettingsFlat);
+            $includeTreeTraverserConditionVerdictAware->addVisitor($setupConditionConstantSubstitutionVisitor);
+        }
+        $conditionMatcherVisitor = new IncludeTreeConditionMatcherVisitor();
+        $conditionMatcherVisitor->setConditionMatcher($conditionMatcher);
+        $includeTreeTraverserConditionVerdictAware->addVisitor($conditionMatcherVisitor);
+        /** @var IncludeTreeAstBuilderVisitor $astBuilderVisitor */
+        $astBuilderVisitor = $this->container->get(IncludeTreeAstBuilderVisitor::class);
+        $astBuilderVisitor->setFlatConstants($siteSettingsFlat);
+        $includeTreeTraverserConditionVerdictAware->addVisitor($astBuilderVisitor);
+        $includeTreeTraverserConditionVerdictAware->traverse($pagesTsConfigTree);
+
+        return new PageTsConfig($astBuilderVisitor->getAst());
+    }
+}
diff --git a/typo3/sysext/core/Classes/TypoScript/Parser/TypoScriptParser.php b/typo3/sysext/core/Classes/TypoScript/Parser/TypoScriptParser.php
index 46ff1c6227e8..c1d913f7c78e 100644
--- a/typo3/sysext/core/Classes/TypoScript/Parser/TypoScriptParser.php
+++ b/typo3/sysext/core/Classes/TypoScript/Parser/TypoScriptParser.php
@@ -32,8 +32,7 @@ use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatch
 /**
  * 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.
+ * @deprecated This class should not be used anymore. Switch to the new parser construct instead.
  */
 class TypoScriptParser
 {
@@ -150,6 +149,11 @@ class TypoScriptParser
      */
     public $lineNumberOffset = 0;
 
+    public function __construct()
+    {
+        trigger_error('Class ' . __CLASS__ . ' will be removed with TYPO3 v13.0. Use the new parser structure instead.', E_USER_DEPRECATED);
+    }
+
     /**
      * Start parsing the input TypoScript text piece. The result is stored in $this->setup
      *
diff --git a/typo3/sysext/core/Classes/TypoScript/TypoScriptStringFactory.php b/typo3/sysext/core/Classes/TypoScript/TypoScriptStringFactory.php
index 2d931643741c..230a0a96822b 100644
--- a/typo3/sysext/core/Classes/TypoScript/TypoScriptStringFactory.php
+++ b/typo3/sysext/core/Classes/TypoScript/TypoScriptStringFactory.php
@@ -18,6 +18,7 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\TypoScript;
 
 use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
 use TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\ConditionMatcherInterface;
 use TYPO3\CMS\Core\TypoScript\AST\AstBuilderInterface;
@@ -38,6 +39,7 @@ final class TypoScriptStringFactory
 {
     public function __construct(
         private readonly ContainerInterface $container,
+        private readonly TokenizerInterface $tokenizer,
     ) {
     }
 
@@ -46,11 +48,15 @@ final class TypoScriptStringFactory
      *
      * @param non-empty-string $name A name used as cache identifier, [a-z,A-Z,-] only
      */
-    public function parseFromStringWithIncludesAndConditions(string $name, string $typoScript, TokenizerInterface $tokenizer, ConditionMatcherInterface $conditionMatcher, ?PhpFrontend $cache = null): RootNode
+    public function parseFromStringWithIncludesAndConditions(string $name, string $typoScript, ConditionMatcherInterface $conditionMatcher): RootNode
     {
+        /** @var CacheManager $cacheManager */
+        $cacheManager = $this->container->get(CacheManager::class);
+        /** @var PhpFrontend $cache */
+        $cache = $cacheManager->getCache('typoscript');
         /** @var StringTreeBuilder $stringTreeBuilder */
         $stringTreeBuilder = $this->container->get(StringTreeBuilder::class);
-        $includeTree = $stringTreeBuilder->getTreeFromString($name, $typoScript, $tokenizer, $cache);
+        $includeTree = $stringTreeBuilder->getTreeFromString($name, $typoScript, $this->tokenizer, $cache);
         $conditionMatcherVisitor = new IncludeTreeConditionMatcherVisitor();
         $conditionMatcherVisitor->setConditionMatcher($conditionMatcher);
         $includeTreeTraverserConditionVerdictAware = new ConditionVerdictAwareIncludeTreeTraverser();
@@ -64,13 +70,13 @@ final class TypoScriptStringFactory
 
     /**
      * Parse a single string *not* supporting imports, conditions and caching.
-     * Used in install tool context only.
+     * Detail method used in install tool and in a couple of other special cases.
      *
      * @internal
      */
-    public function parseFromString(string $typoScript, TokenizerInterface $tokenizer, AstBuilderInterface $astBuilder): RootNode
+    public function parseFromString(string $typoScript, AstBuilderInterface $astBuilder): RootNode
     {
-        $lineStream = $tokenizer->tokenize($typoScript);
+        $lineStream = $this->tokenizer->tokenize($typoScript);
         return $astBuilder->build($lineStream, new RootNode());
     }
 }
diff --git a/typo3/sysext/core/Classes/TypoScript/UserTsConfig.php b/typo3/sysext/core/Classes/TypoScript/UserTsConfig.php
new file mode 100644
index 000000000000..3617a20642f7
--- /dev/null
+++ b/typo3/sysext/core/Classes/TypoScript/UserTsConfig.php
@@ -0,0 +1,46 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\TypoScript;
+
+use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
+
+/**
+ * A data object that carries the final UserTsConfig. This is created by UserTsConfigFactory.
+ *
+ * @internal Internal for now until API stabilized. Use backendUser->getTSConfig().
+ */
+final class UserTsConfig
+{
+    private readonly array $userTsConfigArray;
+
+    public function __construct(
+        private readonly RootNode $userTsConfigTree
+    ) {
+        $this->userTsConfigArray = $userTsConfigTree->toArray();
+    }
+
+    public function getUserTsConfigTree(): RootNode
+    {
+        return $this->userTsConfigTree;
+    }
+
+    public function getUserTsConfigArray(): array
+    {
+        return $this->userTsConfigArray;
+    }
+}
diff --git a/typo3/sysext/core/Classes/TypoScript/UserTsConfigFactory.php b/typo3/sysext/core/Classes/TypoScript/UserTsConfigFactory.php
new file mode 100644
index 000000000000..d97d75f3c2ab
--- /dev/null
+++ b/typo3/sysext/core/Classes/TypoScript/UserTsConfigFactory.php
@@ -0,0 +1,63 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\TypoScript;
+
+use Psr\Container\ContainerInterface;
+use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher as BackendConditionMatcher;
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\TsConfigTreeBuilder;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeAstBuilderVisitor;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionMatcherVisitor;
+use TYPO3\CMS\Core\TypoScript\Tokenizer\TokenizerInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Calculate UserTsConfig. This does the heavy lifting additionally supported by
+ * TsConfigTreeBuilder: Load basic userTsConfig tree, then build the UserTsConfig AST
+ * and return UserTsConfig DTO.
+ *
+ * @internal Internal for now until API stabilized. Use backendUser->getTSConfig().
+ */
+final class UserTsConfigFactory
+{
+    public function __construct(
+        private readonly ContainerInterface $container,
+        private readonly TokenizerInterface $tokenizer,
+        private readonly TsConfigTreeBuilder $tsConfigTreeBuilder,
+        private readonly PhpFrontend $cache,
+    ) {
+    }
+
+    public function create(BackendUserAuthentication $backendUser): UserTsConfig
+    {
+        $includeTreeTraverserConditionVerdictAware = new ConditionVerdictAwareIncludeTreeTraverser();
+        $userTsConfigTree = $this->tsConfigTreeBuilder->getUserTsConfigTree($backendUser, $this->tokenizer, $this->cache);
+        $conditionMatcherVisitor = new IncludeTreeConditionMatcherVisitor();
+        $backendConditionMatcher = GeneralUtility::makeInstance(BackendConditionMatcher::class, GeneralUtility::makeInstance(Context::class));
+        $conditionMatcherVisitor->setConditionMatcher($backendConditionMatcher);
+        $includeTreeTraverserConditionVerdictAware->addVisitor($conditionMatcherVisitor);
+        /** @var IncludeTreeAstBuilderVisitor $astBuilderVisitor */
+        $astBuilderVisitor = $this->container->get(IncludeTreeAstBuilderVisitor::class);
+        $includeTreeTraverserConditionVerdictAware->addVisitor($astBuilderVisitor);
+        $includeTreeTraverserConditionVerdictAware->traverse($userTsConfigTree);
+        return new UserTsConfig($astBuilderVisitor->getAst());
+    }
+}
diff --git a/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php b/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php
index 2bbe898daf3c..2015d683e892 100644
--- a/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php
+++ b/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php
@@ -732,9 +732,7 @@ class ExtensionManagementUtility
      */
     public static function addPageTSConfig(string $content): void
     {
-        $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] .= '
-[GLOBAL]
-' . $content;
+        $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] .= chr(10) . $content;
     }
 
     /**
@@ -746,9 +744,7 @@ class ExtensionManagementUtility
      */
     public static function addUserTSConfig(string $content): void
     {
-        $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig'] .= '
-[GLOBAL]
-' . $content;
+        $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig'] .= chr(10) . $content;
     }
 
     /**
diff --git a/typo3/sysext/core/Configuration/Services.yaml b/typo3/sysext/core/Configuration/Services.yaml
index e945bef45fe8..9fc1cd587901 100644
--- a/typo3/sysext/core/Configuration/Services.yaml
+++ b/typo3/sysext/core/Configuration/Services.yaml
@@ -57,14 +57,17 @@ services:
   TYPO3\CMS\Core\Http\MiddlewareDispatcher:
     autoconfigure: false
 
+  # @deprecated since v12, will be removed with v13 together with class PageTsConfigLoader.
   TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader:
     public: true
 
+  # @deprecated since v12, will be removed with v13 together with class PageTsConfigParser.
   TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser:
     public: true
     arguments:
       $cache: '@cache.hash'
 
+  # @deprecated since v12, will be removed with v13 together with class PageTsConfig.
   TYPO3\CMS\Core\Configuration\PageTsConfig:
     public: true
     arguments:
@@ -324,6 +327,9 @@ services:
   TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateTreeBuilder:
     public: true
 
+  TYPO3\CMS\Core\TypoScript\IncludeTree\TsConfigTreeBuilder:
+    public: true
+
   TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeAstBuilderVisitor:
     public: true
     # Ast builder visitor creates state and should not be re-used
@@ -337,6 +343,16 @@ services:
   TYPO3\CMS\Core\TypoScript\Tokenizer\TokenizerInterface:
     alias: TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer
 
+  TYPO3\CMS\Core\TypoScript\PageTsConfigFactory:
+    public: true
+    arguments:
+      $cache: '@cache.typoscript'
+
+  TYPO3\CMS\Core\TypoScript\UserTsConfigFactory:
+    public: true
+    arguments:
+      $cache: '@cache.typoscript'
+
   # Core caches, cache.core and cache.assets are injected as early
   # entries in TYPO3\CMS\Core\Core\Bootstrap and therefore omitted here
   cache.hash:
diff --git a/typo3/sysext/core/Documentation/Changelog/12.2/Deprecation-99120-DeprecateOldTypoScriptParser.rst b/typo3/sysext/core/Documentation/Changelog/12.2/Deprecation-99120-DeprecateOldTypoScriptParser.rst
new file mode 100644
index 000000000000..6514700a602c
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.2/Deprecation-99120-DeprecateOldTypoScriptParser.rst
@@ -0,0 +1,109 @@
+.. include:: /Includes.rst.txt
+
+.. _deprecation-99120-1670428555:
+
+====================================================
+Deprecation: #99120 - Deprecate old TypoScriptParser
+====================================================
+
+See :issue:`99120`
+
+Description
+===========
+
+To phase out usages of the old TypoScript parser by switching to the
+:ref:`new parser approach <breaking-97816-1664800747>`, a couple of classes
+and methods have been marked as deprecated in TYPO3 v12 that will be removed with v13:
+
+* Class :php:`\TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser`
+* Class :php:`\TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader`
+* Class :php:`\TYPO3\CMS\Core\Configuration\PageTsConfig`
+* Class :php:`\TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser`
+* Event :php:`\TYPO3\CMS\Core\Configuration\Event\ModifyLoadedPageTsConfigEvent`
+* Method :php:`\TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->getPagesTSconfig()`
+
+The existing main API to retrieve PageTsConfig using :php:`\TYPO3\CMS\Backend\Utility\BackendUtility::getPagesTSconfig()`
+and UserTsConfig using :php:`$backendUser->getTSConfig()` is kept.
+
+
+Impact
+======
+
+Using one of the above classes will raise a deprecation level log entry and
+will stop working with TYPO3 v13.
+
+
+Affected installations
+======================
+
+Instances with extensions that use one of the above classes are affected. The
+extension scanner will find usages with a mixture of weak and strong matches
+depending on the usage.
+
+Most deprecations are rather "internal" since extensions most likely use the
+existing outer API already. Some may be affected when using the
+:php:`\TYPO3\CMS\Core\Configuration\Event\ModifyLoadedPageTsConfigEvent` event
+or the frontend related method :php:`TypoScriptFrontendController->getPagesTSconfig()`,
+though.
+
+
+Migration
+=========
+
+:php:`\TYPO3\CMS\Core\Configuration\Event\ModifyLoadedPageTsConfigEvent`
+------------------------------------------------------------------------
+
+This event is consumed by some extensions to modify calculated PageTsConfig strings
+before parsing. The event moved its namespace from
+:php:`\TYPO3\CMS\Core\Configuration\Event\ModifyLoadedPageTsConfigEvent` to
+:php:`\TYPO3\CMS\Core\TypoScript\IncludeTree\Event\ModifyLoadedPageTsConfigEvent` in
+TYPO3 v12, but is kept as-is apart from that. The TYPO3 v12 core triggers *both* the old
+and the new event, and TYPO3 v13 will stop calling the old event.
+
+Extension that want to stay compatible with both TYPO3 v11 and v12 should continue to
+implement listen for the old event only. This will *not* raise a deprecation level log
+entry in v12, but it will stop working with TYPO3 v13.
+Extensions with compatibility for TYPO3 12 and above should switch to the new event.
+
+:php:`\TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->getPagesTSconfig()`
+--------------------------------------------------------------------------------------
+
+The TYPO3 Frontend should usually not need to retrieve Backend related PageTsConfig.
+Extensions using the method should avoid relying on bringing Backend related configuration
+into Frontend scope. However, the TYPO3 core comes with one place that does this: Class
+:php:`TYPO3\CMS\Frontend\Typolink\DatabaseRecordLinkBuilder` uses PageTsConfig related
+information in Frontend scope. Extensions with similar use cases could have a similar
+implementation as done with :php:`DatabaseRecordLinkBuilder->getPageTsConfig()`. Note
+any implementation of this will have to rely on :php:`@internal` usages of the new
+TypoScript parser approach, and using this low level API may thus break without
+further notice. Extensions are encouraged to cover usages with functional tests to find
+issues quickly in case the TYPO3 core still changes used classes.
+
+:php:`\TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser`
+---------------------------------------------------------
+
+In general, extensions probably don't need to use the old :php:`TypoScriptParser`
+often: Frontend TypoScript is :ref:`available as request attribute <deprecation-99020-1667911024>`,
+pageTsConfig should be retrieved using :php:`BackendUtility::getPagesTSconfig()` and
+userTsConfig should be retrieved using :php:`$backendUser->getTSConfig()`.
+
+In case extensions want to parse any other strings that follow a TypoScript-a-like syntax,
+they can use :php:`\TYPO3\CMS\Core\TypoScript\TypoScriptStringFactory`, or could set up
+their own factory using the new parser classes for more complex scenarios. Note the new parser
+approach is still marked :php:`@internal`, using this low level API may thus break without
+further notice. Extensions are encouraged to cover usages with functional tests to find
+issues quickly in case the TYPO3 core still changes used classes.
+
+:php:`\TYPO3\CMS\Core\Configuration\PageTsConfig`
+-------------------------------------------------
+
+There is little need to use :php:`\TYPO3\CMS\Core\Configuration\PageTsConfig` and their helper
+classes :php:`\TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader` and
+:php:`\TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser` directly: The main API
+in Backend context is :php:`\TYPO3\CMS\Backend\Utility\BackendUtility::getPagesTSconfig()`.
+
+See the hint on :php:`\TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->getPagesTSconfig()`
+above for notes on how to migrate usages in Frontend context.
+
+
+.. index:: PHP-API, TSConfig, TypoScript, FullyScanned, ext:core
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Classes/EventListener/LegacyModifyLoadedPageTsConfig.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Classes/EventListener/LegacyModifyLoadedPageTsConfig.php
new file mode 100644
index 000000000000..8ae065ec3def
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Classes/EventListener/LegacyModifyLoadedPageTsConfig.php
@@ -0,0 +1,31 @@
+<?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 TYPO3Tests\TestTyposcriptPagetsconfigfactory\EventListener;
+
+use TYPO3\CMS\Core\Configuration\Event\ModifyLoadedPageTsConfigEvent;
+
+/**
+ * @deprecated since v12, remove along with 'legacy' event class in v13.
+ */
+final class LegacyModifyLoadedPageTsConfig
+{
+    public function __invoke(ModifyLoadedPageTsConfigEvent $event): void
+    {
+        $event->addTsConfig('loadedFromLegacyEvent = loadedFromLegacyEvent');
+    }
+}
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Classes/EventListener/ModifyLoadedPageTsConfig.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Classes/EventListener/ModifyLoadedPageTsConfig.php
new file mode 100644
index 000000000000..6bf69048f266
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Classes/EventListener/ModifyLoadedPageTsConfig.php
@@ -0,0 +1,28 @@
+<?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 TYPO3Tests\TestTyposcriptPagetsconfigfactory\EventListener;
+
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Event\ModifyLoadedPageTsConfigEvent;
+
+final class ModifyLoadedPageTsConfig
+{
+    public function __invoke(ModifyLoadedPageTsConfigEvent $event): void
+    {
+        $event->addTsConfig('loadedFromEvent = loadedFromEvent');
+    }
+}
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Configuration/Services.yaml b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Configuration/Services.yaml
new file mode 100644
index 000000000000..487c40bc4127
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Configuration/Services.yaml
@@ -0,0 +1,16 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3Tests\TestTyposcriptPagetsconfigfactory\EventListener\ModifyLoadedPageTsConfig:
+    tags:
+      - name: event.listener
+        identifier: 'typo3tests-test-typoscript-pagetsconfigfactory-event/modify-loaded-page-tsconfig'
+
+  # @deprecated since v12, remove along with 'legacy' event class in v13.
+  TYPO3Tests\TestTyposcriptPagetsconfigfactory\EventListener\LegacyModifyLoadedPageTsConfig:
+    tags:
+      - name: event.listener
+        identifier: 'typo3tests-test-typoscript-pagetsconfigfactory-event/legacy-modify-loaded-page-tsconfig'
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Configuration/TsConfig/tsconfig-includes.tsconfig b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Configuration/TsConfig/tsconfig-includes.tsconfig
new file mode 100644
index 000000000000..c9aeea6c6b17
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Configuration/TsConfig/tsconfig-includes.tsconfig
@@ -0,0 +1 @@
+loadedFromTsconfigIncludes = loadedFromTsconfigIncludes
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Configuration/page.tsconfig b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Configuration/page.tsconfig
new file mode 100644
index 000000000000..697c87165318
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Configuration/page.tsconfig
@@ -0,0 +1 @@
+loadedFromTestExtensionConfigurationPageTsConfig = loadedFromTestExtensionConfigurationPageTsConfig
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/ext_emconf.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/ext_emconf.php
new file mode 100644
index 000000000000..b37805b84e4d
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/ext_emconf.php
@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+
+$EM_CONF[$_EXTKEY] = [
+    'title' => 'TypoScript PageTsConfigFactory extension test',
+    'description' => 'TypoScript PageTsConfigFactory extension test',
+    'category' => 'example',
+    'version' => '12.2.0',
+    'state' => 'beta',
+    'author' => 'Christian Kuhn',
+    'author_email' => 'lolli@schwarzbu.ch',
+    'author_company' => '',
+    'constraints' => [
+        'depends' => [
+            'typo3' => '12.2.0',
+        ],
+        'conflicts' => [],
+        'suggests' => [],
+    ],
+];
diff --git a/typo3/sysext/core/Tests/Functional/TypoScript/Fixtures/pageTsConfigTestFixture.csv b/typo3/sysext/core/Tests/Functional/TypoScript/Fixtures/pageTsConfigTestFixture.csv
new file mode 100644
index 000000000000..925850b570b8
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/TypoScript/Fixtures/pageTsConfigTestFixture.csv
@@ -0,0 +1,3 @@
+"be_users"
+,"uid","pid","username","admin","usergroup","TSconfig"
+,1,0,"user1",0,,"page.valueOverriddenByUserTsConfig = overridden"
diff --git a/typo3/sysext/core/Tests/Functional/TypoScript/Fixtures/userTsConfigTestFixture.csv b/typo3/sysext/core/Tests/Functional/TypoScript/Fixtures/userTsConfigTestFixture.csv
new file mode 100644
index 000000000000..65c652159cca
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/TypoScript/Fixtures/userTsConfigTestFixture.csv
@@ -0,0 +1,15 @@
+"be_users"
+,"uid","pid","username","admin","usergroup","TSconfig"
+,1,0,"user1",0,,
+,2,0,"user2",0,,"loadedFromUser = loadedFromUser"
+,3,0,"user3",0,"1",
+,4,0,"user4",0,"1,2",
+,5,0,"user5",1,,"isHttps = off
+[request.getNormalizedParams().isHttps()]
+  isHttps = on
+[end]
+"
+"be_groups"
+,"uid","pid","title","TSconfig"
+,1,0,"group1","loadedFromUserGroup = loadedFromUserGroup"
+,2,0,"group2","loadedFromUserGroup = loadedFromUserGroupOverride"
diff --git a/typo3/sysext/core/Tests/Functional/TypoScript/PageTsConfigFactoryTest.php b/typo3/sysext/core/Tests/Functional/TypoScript/PageTsConfigFactoryTest.php
new file mode 100644
index 000000000000..b81ecdcd4dd8
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/TypoScript/PageTsConfigFactoryTest.php
@@ -0,0 +1,224 @@
+<?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;
+
+use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
+use TYPO3\CMS\Core\Http\NormalizedParams;
+use TYPO3\CMS\Core\Http\ServerRequest;
+use TYPO3\CMS\Core\Site\Entity\NullSite;
+use TYPO3\CMS\Core\Site\Entity\Site;
+use TYPO3\CMS\Core\Site\Entity\SiteSettings;
+use TYPO3\CMS\Core\TypoScript\PageTsConfigFactory;
+use TYPO3\CMS\Core\TypoScript\UserTsConfigFactory;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+/**
+ * Tests PageTsConfigFactory and indirectly IncludeTree/TsConfigTreeBuilder
+ */
+class PageTsConfigFactoryTest extends FunctionalTestCase
+{
+    protected array $testExtensionsToLoad = [
+        'typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory',
+    ];
+
+    /**
+     * @test
+     */
+    public function pageTsConfigLoadsDefaultsFromGlobals(): void
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] = 'loadedFromGlobals = loadedFromGlobals';
+        /** @var PageTsConfigFactory $subject */
+        $subject = $this->get(PageTsConfigFactory::class);
+        $pageTsConfig = $subject->create([], new NullSite(), new ConditionMatcher());
+        self::assertSame('loadedFromGlobals', $pageTsConfig->getPageTsConfigArray()['loadedFromGlobals']);
+    }
+
+    /**
+     * @test
+     */
+    public function pageTsConfigLoadsFromTestExtensionConfigurationFile(): void
+    {
+        /** @var PageTsConfigFactory $subject */
+        $subject = $this->get(PageTsConfigFactory::class);
+        $pageTsConfig = $subject->create([], new NullSite(), new ConditionMatcher());
+        self::assertSame('loadedFromTestExtensionConfigurationPageTsConfig', $pageTsConfig->getPageTsConfigArray()['loadedFromTestExtensionConfigurationPageTsConfig']);
+    }
+
+    /**
+     * @test
+     */
+    public function pageTsConfigLoadsFromPagesTestExtensionConfigurationFile(): void
+    {
+        /** @var PageTsConfigFactory $subject */
+        $subject = $this->get(PageTsConfigFactory::class);
+        $pageTsConfig = $subject->create([], new NullSite(), new ConditionMatcher());
+        self::assertSame('loadedFromTestExtensionConfigurationPageTsConfig', $pageTsConfig->getPageTsConfigArray()['loadedFromTestExtensionConfigurationPageTsConfig']);
+    }
+
+    /**
+     * @test
+     */
+    public function pageTsConfigLoadsFromPageRecordTsconfigField(): void
+    {
+        $rootLine = [
+            [
+                'uid' => 1,
+                'TSconfig' => 'loadedFromTsConfigField = loadedFromTsConfigField',
+            ],
+        ];
+        /** @var PageTsConfigFactory $subject */
+        $subject = $this->get(PageTsConfigFactory::class);
+        $pageTsConfig = $subject->create($rootLine, new NullSite(), new ConditionMatcher());
+        self::assertSame('loadedFromTsConfigField', $pageTsConfig->getPageTsConfigArray()['loadedFromTsConfigField']);
+    }
+
+    /**
+     * @test
+     */
+    public function pageTsConfigLoadsFromPageRecordTsconfigFieldOverridesByLowerLevel(): void
+    {
+        $rootLine = [
+            [
+                'uid' => 1,
+                'TSconfig' => 'loadedFromTsConfigField1 = loadedFromTsConfigField1'
+                    . chr(10) . 'loadedFromTsConfigField2 = loadedFromTsConfigField2',
+            ],
+            [
+                'uid' => 2,
+                'TSconfig' => 'loadedFromTsConfigField1 = loadedFromTsConfigField1'
+                    . chr(10) . 'loadedFromTsConfigField2 = loadedFromTsConfigField2Override',
+            ],
+        ];
+        /** @var PageTsConfigFactory $subject */
+        $subject = $this->get(PageTsConfigFactory::class);
+        $pageTsConfig = $subject->create($rootLine, new NullSite(), new ConditionMatcher());
+        self::assertSame('loadedFromTsConfigField1', $pageTsConfig->getPageTsConfigArray()['loadedFromTsConfigField1']);
+        self::assertSame('loadedFromTsConfigField2Override', $pageTsConfig->getPageTsConfigArray()['loadedFromTsConfigField2']);
+    }
+
+    /**
+     * @test
+     */
+    public function pageTsConfigSubstitutesSettingsFromSite(): void
+    {
+        $rootLine = [
+            [
+                'uid' => 1,
+                'TSconfig' => 'siteSetting = {$aSiteSetting}',
+            ],
+        ];
+        /** @var PageTsConfigFactory $subject */
+        $subject = $this->get(PageTsConfigFactory::class);
+        $siteSettings = new SiteSettings(['aSiteSetting' => 'aSiteSettingValue']);
+        $site = new Site('siteIdentifier', 1, [], $siteSettings);
+        $pageTsConfig = $subject->create($rootLine, $site, new ConditionMatcher());
+        self::assertSame('aSiteSettingValue', $pageTsConfig->getPageTsConfigArray()['siteSetting']);
+    }
+
+    /**
+     * @test
+     */
+    public function pageTsConfigMatchesRequestHttpsCondition(): void
+    {
+        $request = (new ServerRequest('https://www.example.com/', null, 'php://input', [], ['HTTPS' => 'ON']));
+        $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request));
+        $GLOBALS['TYPO3_REQUEST'] = $request;
+        $rootLine = [
+            [
+                'uid' => 1,
+                'TSconfig' => 'isHttps = off'
+                    . chr(10) . '[request.getNormalizedParams().isHttps()]'
+                    . chr(10) . '  isHttps = on'
+                    . chr(10) . '[end]',
+            ],
+        ];
+        /** @var PageTsConfigFactory $subject */
+        $subject = $this->get(PageTsConfigFactory::class);
+        $pageTsConfig = $subject->create($rootLine, new NullSite(), new ConditionMatcher());
+        self::assertSame('on', $pageTsConfig->getPageTsConfigArray()['isHttps']);
+    }
+
+    /**
+     * @test
+     */
+    public function pageTsConfigMatchesRequestHttpsConditionUsingSiteConstant(): void
+    {
+        $request = (new ServerRequest('https://www.example.com/', null, 'php://input', [], ['HTTPS' => 'ON']));
+        $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request));
+        $GLOBALS['TYPO3_REQUEST'] = $request;
+        $rootLine = [
+            [
+                'uid' => 1,
+                'TSconfig' => 'isHttps = off'
+                    . chr(10) . '[{$aSiteSetting}]'
+                    . chr(10) . '  isHttps = on'
+                    . chr(10) . '[end]',
+            ],
+        ];
+        /** @var PageTsConfigFactory $subject */
+        $subject = $this->get(PageTsConfigFactory::class);
+        $siteSettings = new SiteSettings(['aSiteSetting' => 'request.getNormalizedParams().isHttps()']);
+        $site = new Site('siteIdentifier', 1, [], $siteSettings);
+        $pageTsConfig = $subject->create($rootLine, $site, new ConditionMatcher());
+        self::assertSame('on', $pageTsConfig->getPageTsConfigArray()['isHttps']);
+    }
+
+    /**
+     * @test
+     */
+    public function pageTsConfigLoadsFromEvent(): void
+    {
+        /** @var PageTsConfigFactory $subject */
+        $subject = $this->get(PageTsConfigFactory::class);
+        $pageTsConfig = $subject->create([], new NullSite(), new ConditionMatcher());
+        self::assertSame('loadedFromEvent', $pageTsConfig->getPageTsConfigArray()['loadedFromEvent']);
+    }
+
+    /**
+     * @test
+     */
+    public function pageTsConfigLoadsFromLegacyEvent(): void
+    {
+        /** @var PageTsConfigFactory $subject */
+        $subject = $this->get(PageTsConfigFactory::class);
+        $pageTsConfig = $subject->create([], new NullSite(), new ConditionMatcher());
+        self::assertSame('loadedFromLegacyEvent', $pageTsConfig->getPageTsConfigArray()['loadedFromLegacyEvent']);
+    }
+
+    /**
+     * @test
+     */
+    public function pageTsConfigCanBeOverloadedWithUserTsConfig(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/pageTsConfigTestFixture.csv');
+        $backendUser = $this->setUpBackendUser(1);
+        /** @var UserTsConfigFactory $userTsConfigFactory */
+        $userTsConfigFactory = $this->get(UserTsConfigFactory::class);
+        $userTsConfig = $userTsConfigFactory->create($backendUser);
+        $rootLine = [
+            [
+                'uid' => 1,
+                'TSconfig' => 'valueOverriddenByUserTsConfig = base',
+            ],
+        ];
+        /** @var PageTsConfigFactory $subject */
+        $subject = $this->get(PageTsConfigFactory::class);
+        $pageTsConfig = $subject->create($rootLine, new NullSite(), new ConditionMatcher(), $userTsConfig);
+        self::assertSame('overridden', $pageTsConfig->getPageTsConfigArray()['valueOverriddenByUserTsConfig']);
+    }
+}
diff --git a/typo3/sysext/core/Tests/Functional/TypoScript/TypoScriptStringFactoryTest.php b/typo3/sysext/core/Tests/Functional/TypoScript/TypoScriptStringFactoryTest.php
index 3c7fb9e14b13..92febd8aa637 100644
--- a/typo3/sysext/core/Tests/Functional/TypoScript/TypoScriptStringFactoryTest.php
+++ b/typo3/sysext/core/Tests/Functional/TypoScript/TypoScriptStringFactoryTest.php
@@ -22,7 +22,6 @@ use TYPO3\CMS\Core\EventDispatcher\NoopEventDispatcher;
 use TYPO3\CMS\Core\TypoScript\AST\AstBuilder;
 use TYPO3\CMS\Core\TypoScript\AST\Node\ChildNode;
 use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
-use TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer;
 use TYPO3\CMS\Core\TypoScript\TypoScriptStringFactory;
 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
 
@@ -47,7 +46,6 @@ class TypoScriptStringFactoryTest extends FunctionalTestCase
         $result = $subject->parseFromStringWithIncludesAndConditions(
             'testing',
             '@import \'EXT:core/Tests/Functional/TypoScript/Fixtures/SimpleCondition.typoscript\'',
-            new LossyTokenizer(),
             new BackendConditionMatcher()
         );
         self::assertEquals($expected, $result);
@@ -64,11 +62,7 @@ class TypoScriptStringFactoryTest extends FunctionalTestCase
         $expected->addChild($fooNode);
         /** @var TypoScriptStringFactory $subject */
         $subject = $this->get(TypoScriptStringFactory::class);
-        $result = $subject->parseFromString(
-            'foo = bar',
-            new LossyTokenizer(),
-            new AstBuilder(new NoopEventDispatcher())
-        );
+        $result = $subject->parseFromString('foo = bar', new AstBuilder(new NoopEventDispatcher()));
         self::assertEquals($expected, $result);
     }
 }
diff --git a/typo3/sysext/core/Tests/Functional/TypoScript/UserTsConfigFactoryTest.php b/typo3/sysext/core/Tests/Functional/TypoScript/UserTsConfigFactoryTest.php
new file mode 100644
index 000000000000..f00bd62405c4
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/TypoScript/UserTsConfigFactoryTest.php
@@ -0,0 +1,104 @@
+<?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;
+
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Http\NormalizedParams;
+use TYPO3\CMS\Core\Http\ServerRequest;
+use TYPO3\CMS\Core\TypoScript\UserTsConfigFactory;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+/**
+ * Tests UserTsConfigFactory and indirectly IncludeTree/TsConfigTreeBuilder
+ */
+class UserTsConfigFactoryTest extends FunctionalTestCase
+{
+    /**
+     * @test
+     */
+    public function userTsConfigLoadsDefaultFromGlobals(): void
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig'] = 'loadedFromGlobals = loadedFromGlobals';
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/userTsConfigTestFixture.csv');
+        $backendUser = $this->setUpBackendUser(1);
+        /** @var UserTsConfigFactory $subject */
+        $subject = $this->get(UserTsConfigFactory::class);
+        $userTsConfig = $subject->create($backendUser);
+        self::assertSame('loadedFromGlobals', $userTsConfig->getUserTsConfigArray()['loadedFromGlobals']);
+    }
+
+    /**
+     * @test
+     */
+    public function userTsConfigLoadsDefaultFromBackendUserTsConfigField(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/userTsConfigTestFixture.csv');
+        $backendUser = $this->setUpBackendUser(2);
+        /** @var UserTsConfigFactory $subject */
+        $subject = $this->get(UserTsConfigFactory::class);
+        $userTsConfig = $subject->create($backendUser);
+        self::assertSame('loadedFromUser', $userTsConfig->getUserTsConfigArray()['loadedFromUser']);
+    }
+
+    /**
+     * @test
+     */
+    public function userTsConfigLoadsDefaultFromBackendUserGroupTsConfigField(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/userTsConfigTestFixture.csv');
+        $backendUser = $this->setUpBackendUser(3);
+        /** @var UserTsConfigFactory $subject */
+        $subject = $this->get(UserTsConfigFactory::class);
+        $userTsConfig = $subject->create($backendUser);
+        self::assertSame('loadedFromUserGroup', $userTsConfig->getUserTsConfigArray()['loadedFromUserGroup']);
+    }
+
+    /**
+     * @test
+     */
+    public function userTsConfigLoadsDefaultFromBackendUserGroupTsConfigFieldAndGroupOverride(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/userTsConfigTestFixture.csv');
+        $backendUser = $this->setUpBackendUser(4);
+        /** @var UserTsConfigFactory $subject */
+        $subject = $this->get(UserTsConfigFactory::class);
+        $userTsConfig = $subject->create($backendUser);
+        self::assertSame('loadedFromUserGroupOverride', $userTsConfig->getUserTsConfigArray()['loadedFromUserGroup']);
+    }
+
+    /**
+     * @test
+     */
+    public function userTsConfigMatchesRequestHttpsCondition(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/userTsConfigTestFixture.csv');
+        $userRow = $this->getBackendUserRecordFromDatabase(5);
+        $backendUser = GeneralUtility::makeInstance(BackendUserAuthentication::class);
+        $request = (new ServerRequest('https://www.example.com/', null, 'php://input', [], ['HTTPS' => 'ON']));
+        $session = $backendUser->createUserSession($userRow);
+        $request = $request->withCookieParams(['be_typo_user' => $session->getJwt()]);
+        $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request));
+        $GLOBALS['TYPO3_REQUEST'] = $request;
+        $backendUser = $this->authenticateBackendUser($backendUser, $request);
+        /** @var UserTsConfigFactory $subject */
+        $subject = $this->get(UserTsConfigFactory::class);
+        $userTsConfig = $subject->create($backendUser);
+        self::assertSame('on', $userTsConfig->getUserTsConfigArray()['isHttps']);
+    }
+}
diff --git a/typo3/sysext/core/Tests/Functional/TypoScript/Parser/TypoScriptParserTest.php b/typo3/sysext/core/Tests/FunctionalDeprecated/TypoScript/Parser/TypoScriptParserTest.php
similarity index 96%
rename from typo3/sysext/core/Tests/Functional/TypoScript/Parser/TypoScriptParserTest.php
rename to typo3/sysext/core/Tests/FunctionalDeprecated/TypoScript/Parser/TypoScriptParserTest.php
index bea10144744a..b82e7752b31b 100644
--- a/typo3/sysext/core/Tests/Functional/TypoScript/Parser/TypoScriptParserTest.php
+++ b/typo3/sysext/core/Tests/FunctionalDeprecated/TypoScript/Parser/TypoScriptParserTest.php
@@ -15,7 +15,7 @@ declare(strict_types=1);
  * The TYPO3 project - inspiring people to share!
  */
 
-namespace TYPO3\CMS\Core\Tests\Functional\TypoScript\Parser;
+namespace TYPO3\CMS\Core\Tests\FunctionalDeprecated\TypoScript\Parser;
 
 use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
diff --git a/typo3/sysext/core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.typoscript b/typo3/sysext/core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.typoscript
deleted file mode 100644
index b843d96cb232..000000000000
--- a/typo3/sysext/core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.typoscript
+++ /dev/null
@@ -1 +0,0 @@
-test.Core.TypoScript = 1
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Unit/TypoScript/Fixtures/recursive_includes_setup.typoscript b/typo3/sysext/core/Tests/Unit/TypoScript/Fixtures/recursive_includes_setup.typoscript
deleted file mode 100644
index 4c5a49bd4f2d..000000000000
--- a/typo3/sysext/core/Tests/Unit/TypoScript/Fixtures/recursive_includes_setup.typoscript
+++ /dev/null
@@ -1 +0,0 @@
-@import 'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript'
diff --git a/typo3/sysext/core/Tests/Unit/Configuration/Loader/Fixtures/included.typoscript b/typo3/sysext/core/Tests/UnitDeprecated/Configuration/Loader/Fixtures/included.typoscript
similarity index 100%
rename from typo3/sysext/core/Tests/Unit/Configuration/Loader/Fixtures/included.typoscript
rename to typo3/sysext/core/Tests/UnitDeprecated/Configuration/Loader/Fixtures/included.typoscript
diff --git a/typo3/sysext/core/Tests/Unit/Configuration/Loader/PageTsConfigLoaderTest.php b/typo3/sysext/core/Tests/UnitDeprecated/Configuration/Loader/PageTsConfigLoaderTest.php
similarity index 77%
rename from typo3/sysext/core/Tests/Unit/Configuration/Loader/PageTsConfigLoaderTest.php
rename to typo3/sysext/core/Tests/UnitDeprecated/Configuration/Loader/PageTsConfigLoaderTest.php
index 8dce931a326e..39fc0f64fb92 100644
--- a/typo3/sysext/core/Tests/Unit/Configuration/Loader/PageTsConfigLoaderTest.php
+++ b/typo3/sysext/core/Tests/UnitDeprecated/Configuration/Loader/PageTsConfigLoaderTest.php
@@ -15,11 +15,12 @@ declare(strict_types=1);
  * The TYPO3 project - inspiring people to share!
  */
 
-namespace TYPO3\CMS\Core\Tests\Unit\Configuration\Loader;
+namespace TYPO3\CMS\Core\Tests\UnitDeprecated\Configuration\Loader;
 
 use Psr\EventDispatcher\EventDispatcherInterface;
 use TYPO3\CMS\Core\Configuration\Event\ModifyLoadedPageTsConfigEvent;
 use TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader;
+use TYPO3\CMS\Core\EventDispatcher\NoopEventDispatcher;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 class PageTsConfigLoaderTest extends UnitTestCase
@@ -69,17 +70,15 @@ class PageTsConfigLoaderTest extends UnitTestCase
     public function loadExternalInclusionsCorrectlyAndKeepLoadingOrder(): void
     {
         $expected = [
+            'global' => '',
             'default' => $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'],
             'page_13_includes_0' => 'Show_me = more
 ',
             'page_13' => 'waiting for = love',
             'page_27' => '',
         ];
-        $rootLine = [['uid' => 13, 'TSconfig' => 'waiting for = love', 'tsconfig_includes' => 'EXT:core/Tests/Unit/Configuration/Loader/Fixtures/included.typoscript'], ['uid' => 27, 'TSconfig' => '']];
-        $eventDispatcherMock = $this->createMock(EventDispatcherInterface::class);
-        $event = new ModifyLoadedPageTsConfigEvent($expected, $rootLine);
-        $eventDispatcherMock->method('dispatch')->with(self::isInstanceOf(ModifyLoadedPageTsConfigEvent::class))->willReturn($event);
-        $subject = new PageTsConfigLoader($eventDispatcherMock);
+        $rootLine = [['uid' => 13, 'TSconfig' => 'waiting for = love', 'tsconfig_includes' => 'EXT:core/Tests/UnitDeprecated/Configuration/Loader/Fixtures/included.typoscript'], ['uid' => 27, 'TSconfig' => '']];
+        $subject = new PageTsConfigLoader(new NoopEventDispatcher());
         $result = $subject->collect($rootLine);
         self::assertSame($expected, $result);
     }
@@ -90,16 +89,14 @@ class PageTsConfigLoaderTest extends UnitTestCase
     public function invalidExternalFileIsNotLoaded(): void
     {
         $expected = [
+            'global' => '',
             'default' => $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'],
             'page_13' => 'waiting for = love',
             'page_27' => '',
         ];
         $expectedString = implode("\n[GLOBAL]\n", $expected);
-        $rootLine = [['uid' => 13, 'TSconfig' => 'waiting for = love', 'tsconfig_includes' => 'EXT:core/Tests/Unit/Configuration/Loader/Fixtures/me_does_not_exist.typoscript'], ['uid' => 27, 'TSconfig' => '']];
-        $eventDispatcherMock = $this->createMock(EventDispatcherInterface::class);
-        $event = new ModifyLoadedPageTsConfigEvent($expected, $rootLine);
-        $eventDispatcherMock->method('dispatch')->with(self::isInstanceOf(ModifyLoadedPageTsConfigEvent::class))->willReturn($event);
-        $subject = new PageTsConfigLoader($eventDispatcherMock);
+        $rootLine = [['uid' => 13, 'TSconfig' => 'waiting for = love', 'tsconfig_includes' => 'EXT:core/Tests/UnitDeprecated/Configuration/Loader/Fixtures/me_does_not_exist.typoscript'], ['uid' => 27, 'TSconfig' => '']];
+        $subject = new PageTsConfigLoader(new NoopEventDispatcher());
         $result = $subject->collect($rootLine);
         self::assertSame($expected, $result);
 
diff --git a/typo3/sysext/core/Tests/Unit/Configuration/Parser/PageTsConfigParserTest.php b/typo3/sysext/core/Tests/UnitDeprecated/Configuration/Parser/PageTsConfigParserTest.php
similarity index 98%
rename from typo3/sysext/core/Tests/Unit/Configuration/Parser/PageTsConfigParserTest.php
rename to typo3/sysext/core/Tests/UnitDeprecated/Configuration/Parser/PageTsConfigParserTest.php
index 2311135bf2dd..c97269b9f2bf 100644
--- a/typo3/sysext/core/Tests/Unit/Configuration/Parser/PageTsConfigParserTest.php
+++ b/typo3/sysext/core/Tests/UnitDeprecated/Configuration/Parser/PageTsConfigParserTest.php
@@ -15,7 +15,7 @@ declare(strict_types=1);
  * The TYPO3 project - inspiring people to share!
  */
 
-namespace TYPO3\CMS\Core\Tests\Unit\Configuration\Parser;
+namespace TYPO3\CMS\Core\Tests\UnitDeprecated\Configuration\Parser;
 
 use Psr\Log\NullLogger;
 use TYPO3\CMS\Core\Cache\Backend\TransientMemoryBackend;
diff --git a/typo3/sysext/core/Tests/Unit/TypoScript/Fixtures/badfilename.php b/typo3/sysext/core/Tests/UnitDeprecated/TypoScript/Fixtures/badfilename.php
similarity index 100%
rename from typo3/sysext/core/Tests/Unit/TypoScript/Fixtures/badfilename.php
rename to typo3/sysext/core/Tests/UnitDeprecated/TypoScript/Fixtures/badfilename.php
diff --git a/typo3/sysext/core/Tests/UnitDeprecated/TypoScript/Fixtures/recursive_includes_setup.typoscript b/typo3/sysext/core/Tests/UnitDeprecated/TypoScript/Fixtures/recursive_includes_setup.typoscript
new file mode 100644
index 000000000000..6a355fc02c88
--- /dev/null
+++ b/typo3/sysext/core/Tests/UnitDeprecated/TypoScript/Fixtures/recursive_includes_setup.typoscript
@@ -0,0 +1 @@
+@import 'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript'
diff --git a/typo3/sysext/core/Tests/Unit/TypoScript/Fixtures/setup.typoscript b/typo3/sysext/core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript
similarity index 100%
rename from typo3/sysext/core/Tests/Unit/TypoScript/Fixtures/setup.typoscript
rename to typo3/sysext/core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript
diff --git a/typo3/sysext/core/Tests/Unit/TypoScript/Parser/TypoScriptParserTest.php b/typo3/sysext/core/Tests/UnitDeprecated/TypoScript/Parser/TypoScriptParserTest.php
similarity index 89%
rename from typo3/sysext/core/Tests/Unit/TypoScript/Parser/TypoScriptParserTest.php
rename to typo3/sysext/core/Tests/UnitDeprecated/TypoScript/Parser/TypoScriptParserTest.php
index 66c2ad83efb6..7d1f1e3424c4 100644
--- a/typo3/sysext/core/Tests/Unit/TypoScript/Parser/TypoScriptParserTest.php
+++ b/typo3/sysext/core/Tests/UnitDeprecated/TypoScript/Parser/TypoScriptParserTest.php
@@ -15,7 +15,7 @@ declare(strict_types=1);
  * The TYPO3 project - inspiring people to share!
  */
 
-namespace TYPO3\CMS\Core\Tests\Unit\TypoScript\Parser;
+namespace TYPO3\CMS\Core\Tests\UnitDeprecated\TypoScript\Parser;
 
 use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
 use TYPO3\CMS\Core\Cache\CacheManager;
@@ -466,34 +466,34 @@ class TypoScriptParserTest extends UnitTestCase
         return [
             'Found include file as single file is imported' => [
                 // Input TypoScript
-                '@import "EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.typoscript"'
+                '@import "EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/ext_typoscript_setup.typoscript"'
                 ,
                 // Expected
                 '
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' begin ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' begin ###
 test.Core.TypoScript = 1
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' end ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' end ###
 ',
             ],
             'Found include file is imported' => [
                 // Input TypoScript
                 'bennilove = before
-@import "EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.typoscript"
+@import "EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/ext_typoscript_setup.typoscript"
 '
                 ,
                 // Expected
                 '
 bennilove = before
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' begin ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' begin ###
 test.Core.TypoScript = 1
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' end ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' end ###
 ',
             ],
             'Not found file is not imported' => [
                 // Input TypoScript
                 'bennilove = before
-@import "EXT:core/Tests/Unit/TypoScript/Fixtures/notfoundfile.typoscript"
+@import "EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/notfoundfile.typoscript"
 '
                 ,
                 // Expected
@@ -501,138 +501,138 @@ test.Core.TypoScript = 1
 bennilove = before
 
 ###
-### ERROR: No file or folder found for importing TypoScript on "EXT:core/Tests/Unit/TypoScript/Fixtures/notfoundfile.typoscript".
+### ERROR: No file or folder found for importing TypoScript on "EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/notfoundfile.typoscript".
 ###
 ',
             ],
             'All files with glob are imported' => [
                 // Input TypoScript
                 'bennilove = before
-@import "EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript*"
+@import "EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/ext_typoscript*"
 '
                 ,
                 // Expected
                 '
 bennilove = before
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' begin ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' begin ###
 test.Core.TypoScript = 1
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' end ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' end ###
 ',
             ],
             'Specific file with typoscript ending is imported' => [
                 // Input TypoScript
                 'bennilove = before
-@import "EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript"
+@import "EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript"
 '
                 ,
                 // Expected
                 '
 bennilove = before
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' begin ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript\' begin ###
 test.TYPO3Forever.TypoScript = 1
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' end ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript\' end ###
 ',
             ],
             'All files in folder are imported, sorted by name' => [
                 // Input TypoScript
                 'bennilove = before
-@import "EXT:core/Tests/Unit/TypoScript/Fixtures/"
+@import "EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/"
 '
                 ,
                 // Expected
                 '
 bennilove = before
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' begin ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' begin ###
 test.Core.TypoScript = 1
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' end ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' end ###
 
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/recursive_includes_setup.typoscript\' begin ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/recursive_includes_setup.typoscript\' begin ###
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' begin ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript\' begin ###
 test.TYPO3Forever.TypoScript = 1
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' end ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript\' end ###
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/recursive_includes_setup.typoscript\' end ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/recursive_includes_setup.typoscript\' end ###
 
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' begin ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript\' begin ###
 test.TYPO3Forever.TypoScript = 1
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' end ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript\' end ###
 ',
             ],
             'All files ending with typoscript in folder are imported' => [
                 // Input TypoScript
                 'bennilove = before
-@import "EXT:core/Tests/Unit/TypoScript/Fixtures/*typoscript"
+@import "EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/*typoscript"
 '
                 ,
                 // Expected
                 '
 bennilove = before
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' begin ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' begin ###
 test.Core.TypoScript = 1
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' end ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' end ###
 
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/recursive_includes_setup.typoscript\' begin ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/recursive_includes_setup.typoscript\' begin ###
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' begin ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript\' begin ###
 test.TYPO3Forever.TypoScript = 1
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' end ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript\' end ###
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/recursive_includes_setup.typoscript\' end ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/recursive_includes_setup.typoscript\' end ###
 
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' begin ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript\' begin ###
 test.TYPO3Forever.TypoScript = 1
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' end ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript\' end ###
 ',
             ],
             'All typoscript files in folder are imported' => [
                 // Input TypoScript
                 'bennilove = before
-@import "EXT:core/Tests/Unit/TypoScript/Fixtures/*.typoscript"
+@import "EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/*.typoscript"
 '
                 ,
                 // Expected
                 '
 bennilove = before
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' begin ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' begin ###
 test.Core.TypoScript = 1
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' end ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/ext_typoscript_setup.typoscript\' end ###
 
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/recursive_includes_setup.typoscript\' begin ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/recursive_includes_setup.typoscript\' begin ###
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' begin ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript\' begin ###
 test.TYPO3Forever.TypoScript = 1
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' end ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript\' end ###
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/recursive_includes_setup.typoscript\' end ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/recursive_includes_setup.typoscript\' end ###
 
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' begin ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript\' begin ###
 test.TYPO3Forever.TypoScript = 1
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' end ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript\' end ###
 ',
             ],
             'All typoscript files in folder with glob are not imported due to recursion level=0' => [
                 // Input TypoScript
                 'bennilove = before
-@import "EXT:core/Tests/Unit/**/*.typoscript"
+@import "EXT:core/Tests/UnitDeprecated/**/*.typoscript"
 '
                 ,
                 // Expected
@@ -640,24 +640,24 @@ test.TYPO3Forever.TypoScript = 1
 bennilove = before
 
 ###
-### ERROR: No file or folder found for importing TypoScript on "EXT:core/Tests/Unit/**/*.typoscript".
+### ERROR: No file or folder found for importing TypoScript on "EXT:core/Tests/UnitDeprecated/**/*.typoscript".
 ###
 ',
             ],
             'TypoScript file ending is automatically added' => [
                 // Input TypoScript
                 'bennilove = before
-@import "EXT:core/Tests/Unit/TypoScript/Fixtures/setup"
+@import "EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/setup"
 '
                 ,
                 // Expected
                 '
 bennilove = before
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' begin ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript\' begin ###
 test.TYPO3Forever.TypoScript = 1
 
-### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' end ###
+### @import \'EXT:core/Tests/UnitDeprecated/TypoScript/Fixtures/setup.typoscript\' end ###
 ',
             ],
         ];
diff --git a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
index f168a2e72f10..4eaf76043bd9 100644
--- a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
+++ b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
@@ -2543,9 +2543,14 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      *******************************************/
     /**
      * Returns the pages TSconfig array based on the current ->rootLine
+     *
+     * @deprecated since TYPO3 v12, will be removed in v13. Frontend should typically not depend on Backend TsConfig.
+     *             If really needed, use PageTsConfigFactory, see usage in DatabaseRecordLinkBuilder.
+     *             Remove together with class PageTsConfig.
      */
     public function getPagesTSconfig(): array
     {
+        trigger_error('Method getPagesTSconfig() is deprecated since TYPO3 v12 and will be removed with TYPO3 v13.0.', E_USER_DEPRECATED);
         if (!is_array($this->pagesTSconfig)) {
             $matcher = GeneralUtility::makeInstance(FrontendConditionMatcher::class, $this->context, $this->id, $this->rootLine);
             $this->pagesTSconfig = GeneralUtility::makeInstance(PageTsConfig::class)
diff --git a/typo3/sysext/frontend/Classes/Typolink/DatabaseRecordLinkBuilder.php b/typo3/sysext/frontend/Classes/Typolink/DatabaseRecordLinkBuilder.php
index 96f0e202de1d..7130b84d2939 100644
--- a/typo3/sysext/frontend/Classes/Typolink/DatabaseRecordLinkBuilder.php
+++ b/typo3/sysext/frontend/Classes/Typolink/DatabaseRecordLinkBuilder.php
@@ -17,9 +17,17 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Frontend\Typolink;
 
+use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Core\Cache\CacheManager;
+use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\LinkHandling\TypoLinkCodecService;
+use TYPO3\CMS\Core\Site\Entity\NullSite;
+use TYPO3\CMS\Core\TypoScript\PageTsConfig;
+use TYPO3\CMS\Core\TypoScript\PageTsConfigFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher as FrontendConditionMatcher;
 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
+use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 
 /**
  * Builds a TypoLink to a database record
@@ -29,9 +37,9 @@ class DatabaseRecordLinkBuilder extends AbstractTypolinkBuilder
     public function build(array &$linkDetails, string $linkText, string $target, array $conf): LinkResultInterface
     {
         $tsfe = $this->getTypoScriptFrontendController();
-        $pageTsConfig = $tsfe->getPagesTSconfig();
-        $configurationKey = $linkDetails['identifier'] . '.';
         $request = $this->contentObjectRenderer->getRequest();
+        $pageTsConfig = $this->getPageTsConfig($tsfe, $request);
+        $configurationKey = $linkDetails['identifier'] . '.';
         $typoScriptArray = $request->getAttribute('frontend.typoscript')->getSetupArray();
         $configuration = $typoScriptArray['config.']['recordLinks.'] ?? [];
         $linkHandlerConfiguration = $pageTsConfig['TCEMAIN.']['linkHandler.'] ?? [];
@@ -98,4 +106,27 @@ class DatabaseRecordLinkBuilder extends AbstractTypolinkBuilder
         $localContentObjectRenderer->parameters = $this->contentObjectRenderer->parameters;
         return $localContentObjectRenderer->createLink($linkText, $typoScriptConfiguration);
     }
+
+    /**
+     * Helper method to calculate pageTsConfig in frontend scope, we can't use BackendUtility::getPagesTSconfig() here.
+     */
+    protected function getPageTsConfig(TypoScriptFrontendController $tsfe, ServerRequestInterface $request): array
+    {
+        $id = $tsfe->id;
+        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
+        $pageTsConfig = $runtimeCache->get('pageTsConfig-' . $id);
+        if ($pageTsConfig instanceof PageTsConfig) {
+            return $pageTsConfig->getPageTsConfigArray();
+        }
+        $conditionMatcher = GeneralUtility::makeInstance(FrontendConditionMatcher::class, GeneralUtility::makeInstance(Context::class), $tsfe->id, $tsfe->rootLine);
+        $site = $request->getAttribute('site') ?? new NullSite();
+        $pageTsConfigFactory = GeneralUtility::makeInstance(PageTsConfigFactory::class);
+        $pageTsConfig = $pageTsConfigFactory->create(
+            $tsfe->rootLine,
+            $site,
+            $conditionMatcher
+        );
+        $runtimeCache->set('pageTsConfig-' . $id, $pageTsConfig);
+        return $pageTsConfig->getPageTsConfigArray();
+    }
 }
diff --git a/typo3/sysext/frontend/Tests/Unit/Typolink/DatabaseRecordLinkBuilderTest.php b/typo3/sysext/frontend/Tests/Unit/Typolink/DatabaseRecordLinkBuilderTest.php
index 12d0ef785ffe..c1806bea8c56 100644
--- a/typo3/sysext/frontend/Tests/Unit/Typolink/DatabaseRecordLinkBuilderTest.php
+++ b/typo3/sysext/frontend/Tests/Unit/Typolink/DatabaseRecordLinkBuilderTest.php
@@ -176,10 +176,9 @@ class DatabaseRecordLinkBuilderTest extends UnitTestCase
         $contentObjectRendererMock->expects(self::once())->method('start');
         $contentObjectRendererMock->expects(self::once())->method('createLink');
 
-        $frontendControllerMock->method('getPagesTSconfig')->willReturn($pageTsConfig);
-
         // Act
-        $databaseRecordLinkBuilder = new DatabaseRecordLinkBuilder($contentObjectRendererMock, $frontendControllerMock);
+        $databaseRecordLinkBuilder = $this->getAccessibleMock(DatabaseRecordLinkBuilder::class, ['getPageTsConfig'], [$contentObjectRendererMock, $frontendControllerMock]);
+        $databaseRecordLinkBuilder->method('getPageTsConfig')->willReturn($pageTsConfig);
         try {
             $databaseRecordLinkBuilder->build($extractedLinkDetails, $linkText, $target, $confFromDb);
         } catch (UnableToLinkException) {
diff --git a/typo3/sysext/info/Classes/Controller/InfoPageTyposcriptConfigController.php b/typo3/sysext/info/Classes/Controller/InfoPageTyposcriptConfigController.php
index a471eef824d7..0acc506c627b 100644
--- a/typo3/sysext/info/Classes/Controller/InfoPageTyposcriptConfigController.php
+++ b/typo3/sysext/info/Classes/Controller/InfoPageTyposcriptConfigController.php
@@ -20,12 +20,14 @@ namespace TYPO3\CMS\Info\Controller;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
-use TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
 use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
 use TYPO3\CMS\Core\Imaging\Icon;
 use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\TsConfigTreeBuilder;
+use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\LineStream;
+use TYPO3\CMS\Core\TypoScript\Tokenizer\LosslessTokenizer;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
@@ -59,23 +61,30 @@ class InfoPageTyposcriptConfigController extends InfoModuleController
 
         if ((int)$moduleData->get('tsconf_parts') === 99) {
             $rootLine = BackendUtility::BEgetRootLine($this->id, '', true);
-            /** @var array<string, string> $TSparts */
-            $TSparts = GeneralUtility::makeInstance(PageTsConfigLoader::class)->collect($rootLine);
+
+            $tsConfigTreeBuilder = GeneralUtility::makeInstance(TsConfigTreeBuilder::class);
+            $pageTsConfigTree = $tsConfigTreeBuilder->getPagesTsConfigTree($rootLine, new LosslessTokenizer());
+            // @todo: This is a bit dusty. It would be better to render the full tree similar to
+            //        tstemplate Template Analyzer. For now, we simply create an array with the
+            //        to-string'ed content and render this.
+            $TSparts = [];
+            foreach ($pageTsConfigTree->getNextChild() as $child) {
+                $lineStream = $child->getLineStream();
+                if ($lineStream instanceof LineStream) {
+                    $TSparts[$child->getName()] = (string)$lineStream;
+                }
+            }
+
             $lines = [];
             $pUids = [];
 
             foreach ($TSparts as $key => $value) {
-                $line = [];
-                if ($key === 'global') {
-                    $title = $this->getLanguageService()->sL('LLL:EXT:info/Resources/Private/Language/InfoPageTsConfig.xlf:editTSconfig_global');
-                    $line['title'] = $title;
-                } elseif ($key === 'default') {
-                    $title = $this->getLanguageService()->sL('LLL:EXT:info/Resources/Private/Language/InfoPageTsConfig.xlf:editTSconfig_default');
-                    $line['title'] = $title;
-                } else {
-                    // Remove the "page_" prefix
-                    [, $pageId] = explode('_', $key, 3);
-                    $pageId = (int)$pageId;
+                $title = $key;
+                $line = [
+                    'title' => $key,
+                ];
+                if (str_starts_with($key, 'pagesTsConfig-page-')) {
+                    $pageId = (int)explode('-', $key)[2];
                     $pUids[] = $pageId;
                     $row = BackendUtility::getRecordWSOL('pages', $pageId);
                     $icon = $this->iconFactory->getIconForRecord('pages', $row, Icon::SIZE_SMALL);
diff --git a/typo3/sysext/info/Resources/Private/Language/InfoPageTsConfig.xlf b/typo3/sysext/info/Resources/Private/Language/InfoPageTsConfig.xlf
index 062036cda980..cc503d491cd9 100644
--- a/typo3/sysext/info/Resources/Private/Language/InfoPageTsConfig.xlf
+++ b/typo3/sysext/info/Resources/Private/Language/InfoPageTsConfig.xlf
@@ -36,12 +36,6 @@
 			<trans-unit id="editTSconfig_all" resname="editTSconfig_all">
 				<source>Edit TSconfig for all shown pages</source>
 			</trans-unit>
-			<trans-unit id="editTSconfig_global" resname="editTSconfig_global">
-				<source>Global Configuration</source>
-			</trans-unit>
-			<trans-unit id="editTSconfig_default" resname="editTSconfig_default">
-				<source>Default Configuration from TYPO3_CONF_VARS</source>
-			</trans-unit>
 			<trans-unit id="mod_pagetsconfig" resname="mod_pagetsconfig">
 				<source>Page TSconfig</source>
 			</trans-unit>
diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php
index 64d85dc2d751..d00c427452fb 100644
--- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php
+++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php
@@ -2208,4 +2208,29 @@ return [
             'Breaking-97816-NewTypoScriptParserInFrontend.rst',
         ],
     ],
+    'TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser' => [
+        'restFiles' => [
+            'Deprecation-99120-DeprecateOldTypoScriptParser.rst',
+        ],
+    ],
+    'TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader' => [
+        'restFiles' => [
+            'Deprecation-99120-DeprecateOldTypoScriptParser.rst',
+        ],
+    ],
+    'TYPO3\CMS\Core\Configuration\PageTsConfig' => [
+        'restFiles' => [
+            'Deprecation-99120-DeprecateOldTypoScriptParser.rst',
+        ],
+    ],
+    'TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser' => [
+        'restFiles' => [
+            'Deprecation-99120-DeprecateOldTypoScriptParser.rst',
+        ],
+    ],
+    'TYPO3\CMS\Core\Configuration\Event\ModifyLoadedPageTsConfigEvent' => [
+        'restFiles' => [
+            'Deprecation-99120-DeprecateOldTypoScriptParser.rst',
+        ],
+    ],
 ];
diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
index ae53a8536548..d3ca927f8103 100644
--- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
+++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
@@ -5407,4 +5407,11 @@ return [
             'Deprecation-99170-ConfigbaseURLAndBaseTagFunctionality.rst',
         ],
     ],
+    'TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->getPagesTSconfig' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-99120-DeprecateOldTypoScriptParser.rst',
+        ],
+    ],
 ];
diff --git a/typo3/sysext/linkvalidator/Classes/Task/ValidatorTask.php b/typo3/sysext/linkvalidator/Classes/Task/ValidatorTask.php
index 65457d9e691e..041ca74de2cd 100644
--- a/typo3/sysext/linkvalidator/Classes/Task/ValidatorTask.php
+++ b/typo3/sysext/linkvalidator/Classes/Task/ValidatorTask.php
@@ -23,7 +23,8 @@ use Symfony\Component\Mime\Address;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Mail\FluidEmail;
 use TYPO3\CMS\Core\Mail\MailerInterface;
-use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
+use TYPO3\CMS\Core\TypoScript\AST\AstBuilder;
+use TYPO3\CMS\Core\TypoScript\TypoScriptStringFactory;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MailUtility;
@@ -326,19 +327,12 @@ class ValidatorTask extends AbstractTask
      */
     protected function loadModTSconfig(): void
     {
-        $parseObj = GeneralUtility::makeInstance(TypoScriptParser::class);
-        $parseObj->parse($this->configuration);
-        if (!empty($parseObj->errors)) {
-            $parseErrorMessage = $this->getLanguageService()->sL($this->languageFile . ':tasks.error.invalidTSconfig') . '<br />';
-            foreach ($parseObj->errors as $errorInfo) {
-                $parseErrorMessage .= $errorInfo[0] . '<br />';
-            }
-            throw new \Exception($parseErrorMessage, 1295476989);
-        }
         $modTs = BackendUtility::getPagesTSconfig($this->page)['mod.']['linkvalidator.'] ?? [];
-        $overrideTs = $parseObj->setup['mod.']['linkvalidator.'] ?? [];
-        if (is_array($overrideTs) && $overrideTs !== []) {
-            ArrayUtility::mergeRecursiveWithOverrule($modTs, $overrideTs);
+
+        if (!empty($this->configuration)) {
+            $typoScriptStringFactory = GeneralUtility::makeInstance(TypoScriptStringFactory::class);
+            $overrideTs = $typoScriptStringFactory->parseFromString($this->configuration, GeneralUtility::makeInstance(AstBuilder::class));
+            ArrayUtility::mergeRecursiveWithOverrule($modTs, $overrideTs->toArray()['mod.']['linkvalidator.'] ?? []);
         }
 
         if (empty($modTs['mail.']['fromemail'])) {
diff --git a/typo3/sysext/linkvalidator/Resources/Private/Language/locallang.xlf b/typo3/sysext/linkvalidator/Resources/Private/Language/locallang.xlf
index f71788aaeed2..7e90ccd73f0d 100644
--- a/typo3/sysext/linkvalidator/Resources/Private/Language/locallang.xlf
+++ b/typo3/sysext/linkvalidator/Resources/Private/Language/locallang.xlf
@@ -58,9 +58,6 @@
 			<trans-unit id="tasks.error.invalidFromEmail" resname="tasks.error.invalidFromEmail">
 				<source>Invalid format of the email address in the from header</source>
 			</trans-unit>
-			<trans-unit id="tasks.error.invalidTSconfig" resname="tasks.error.invalidTSconfig">
-				<source>Invalid TSconfig in the task configuration!</source>
-			</trans-unit>
 			<trans-unit id="tasks.error.invalidPageUid" resname="tasks.error.invalidPageUid">
 				<source>The given page id %d is invalid!</source>
 			</trans-unit>
-- 
GitLab