[FEATURE] Unified PageTS resolving and parsing 49/62349/6
authorBenni Mack <benni@typo3.org>
Wed, 20 Nov 2019 23:29:44 +0000 (00:29 +0100)
committerAnja Leichsenring <aleichsenring@ab-softlab.de>
Thu, 21 Nov 2019 15:48:32 +0000 (16:48 +0100)
This change introduces two new API classes relevant for PageTSconfig:

- PageTsConfigLoader
- PageTsConfigParser

The loader class collects all PageTS found in a rootline, which was
previously available in two places - BackendUtility and TSFE, although
they were similar, they were not the same and error-prone in the past.

The previous "TsConfigParser" class had an unusal dependency to
the BackendConditionMatcher only, which did not even allow to send in
custom arguments.

The TSFE part is now also evaluating TSconfig conditions properly,
which was not the case in the past. This part is also now cached properly.

The TSconfig inclusion ("include from the list of TSconfig inclusions")
functionality is now built into the Info module.

In addition, the hard-coded "ConditionMatcher" is now seamlessly
injected into the parsing process, allowing
- Decoupling of Logic and Implementation of parsing in different contexts
- Making the ConditionMatcher extensible by having a new ConditionMatcherInterface

In the next steps:
- the UserTsConfig parsing can be applied separately and split from BE_USER
- ConditionMatcher Interface can be used properly
- TypoScriptParser can be split up
- BackendUtility can be cleaned up further.

The following functionality is deprecated:
- TYPO3\CMS\Core\Configuration\TsConfigParser
- TYPO3\CMS\Backend\Utility\BackendUtility::getRawPagesTSconfig()

Resolves: #89718
Releases: master
Change-Id: Ibd0a2d086d7e5166f16213fa4aadffd41ecb645c
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/62349
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
19 files changed:
typo3/sysext/backend/Classes/Configuration/TsConfigParser.php
typo3/sysext/backend/Classes/Configuration/TypoScript/ConditionMatching/ConditionMatcher.php
typo3/sysext/backend/Classes/Utility/BackendUtility.php
typo3/sysext/backend/Tests/Functional/Configuration/TypoScript/ConditionMatching/ConditionMatcherTest.php
typo3/sysext/backend/Tests/Unit/Utility/BackendUtilityTest.php
typo3/sysext/core/Classes/Configuration/Loader/PageTsConfigLoader.php [new file with mode: 0644]
typo3/sysext/core/Classes/Configuration/Parser/PageTsConfigParser.php [new file with mode: 0644]
typo3/sysext/core/Classes/Configuration/TypoScript/ConditionMatching/AbstractConditionMatcher.php
typo3/sysext/core/Classes/Configuration/TypoScript/ConditionMatching/ConditionMatcherInterface.php [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Deprecation-89718-LegacyPageTSconfigParsingLowlevelAPI.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-89718-UnifiedPHPAPIForLoadingPageTSconfig.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Configuration/Loader/Fixtures/included.typoscript [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Configuration/Loader/PageTsConfigLoaderTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Configuration/Parser/PageTsConfigParserTest.php [new file with mode: 0644]
typo3/sysext/frontend/Classes/Configuration/TypoScript/ConditionMatching/ConditionMatcher.php
typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
typo3/sysext/info/Classes/Controller/InfoPageTyposcriptConfigController.php
typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php
typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php

index 1610385..1f86d43 100644 (file)
@@ -19,6 +19,8 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
  * A TS-Config parsing class which performs condition evaluation
+ *
+ * @deprecated Should not be used anymore. See the PageTsConfigParser class, which has a more straightforward API. This class will be removed in TYPO3 v11.0.
  */
 class TsConfigParser extends \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
 {
@@ -39,6 +41,11 @@ class TsConfigParser extends \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
      */
     public $type;
 
+    public function __construct()
+    {
+        trigger_error(__CLASS__ . ' has been superseded by PageTsConfigParser. This class will be removed in TYPO3 v11.0', E_USER_DEPRECATED);
+    }
+
     /**
      * Parses the passed TS-Config using conditions and caching
      *
index 5a56c64..4647cfe 100644 (file)
@@ -32,11 +32,11 @@ class ConditionMatcher extends AbstractConditionMatcher
      */
     protected $context;
 
-    public function __construct(Context $context = null)
+    public function __construct(Context $context = null, int $pageId = null, array $rootLine = null)
     {
-        $pageId = $this->pageId ?? $this->determinePageId();
         $this->context = $context ?? GeneralUtility::makeInstance(Context::class);
-        $this->rootline = BackendUtility::BEgetRootLine($pageId, '', true);
+        $this->pageId = $pageId ?? $this->determinePageId();
+        $this->rootline = $rootLine ?? BackendUtility::BEgetRootLine($pageId, '', true);
         $this->initializeExpressionLanguageResolver();
     }
 
index 828f447..836cfeb 100644 (file)
@@ -15,11 +15,13 @@ namespace TYPO3\CMS\Backend\Utility;
  */
 
 use Psr\Log\LoggerInterface;
-use TYPO3\CMS\Backend\Configuration\TsConfigParser;
+use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
 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\Loader\PageTsConfigLoader;
+use TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
@@ -674,18 +676,22 @@ class BackendUtility
             return $cache->get('pagesTsConfigHashToContent' . $cache->get('pagesTsConfigIdToHash' . $id));
         }
 
-        $tsConfig = [];
         $rootLine = self::BEgetRootLine($id, '', true);
-        $TSdataArray = static::getRawPagesTSconfig($id, $rootLine);
+        // Order correctly
+        ksort($rootLine);
 
-        // Parsing the page TS-Config
-        $pageTs = implode(LF . '[GLOBAL]' . LF, $TSdataArray);
-        $parseObj = GeneralUtility::makeInstance(TsConfigParser::class);
-        $res = $parseObj->parseTSconfig($pageTs, 'PAGES', $id, $rootLine);
-        if ($res) {
-            $tsConfig = $res['TSconfig'];
-        }
-        $cacheHash = $res['hash'];
+        // Load PageTS from all pages of the rootLine
+        $pageTs = GeneralUtility::makeInstance(PageTsConfigLoader::class)->load($rootLine);
+
+        // Parse the PageTS into an array, also applying conditions
+        $parser = GeneralUtility::makeInstance(
+            PageTsConfigParser::class,
+            GeneralUtility::makeInstance(TypoScriptParser::class),
+            GeneralUtility::makeInstance(CacheManager::class)->getCache('hash')
+        );
+        $matcher = GeneralUtility::makeInstance(ConditionMatcher::class, null, $id, $rootLine);
+        $tsConfig = $parser->parse($pageTs, $matcher);
+        $cacheHash = md5(json_encode($tsConfig));
 
         // Get User TSconfig overlay, if no backend user is logged-in, this needs to be checked as well
         if (static::getBackendUserAuthentication()) {
@@ -718,6 +724,7 @@ class BackendUtility
      */
     public static function getRawPagesTSconfig($id, array $rootLine = null)
     {
+        trigger_error('BackendUtility::getRawPagesTSconfig will be removed in TYPO3 v11.0. Use PageTsConfigLoader instead.', E_USER_DEPRECATED);
         if (!is_array($rootLine)) {
             $rootLine = self::BEgetRootLine($id, '', true);
         }
index 9a57e5e..58ef2f1 100644 (file)
@@ -139,8 +139,7 @@ class ConditionMatcherTest extends FunctionalTestCase
      */
     public function treeLevelConditionMatchesSingleValue(): void
     {
-        $this->subject->setPageId(2);
-        $this->subject->__construct();
+        $this->subject->__construct(null, 2);
         self::assertTrue($this->subject->match('[tree.level == 2]'));
     }
 
@@ -151,8 +150,7 @@ class ConditionMatcherTest extends FunctionalTestCase
      */
     public function treeLevelConditionMatchesMultipleValues(): void
     {
-        $this->subject->setPageId(2);
-        $this->subject->__construct();
+        $this->subject->__construct(null, 2);
         self::assertTrue($this->subject->match('[tree.level in [999,998,2]]'));
     }
 
@@ -173,8 +171,7 @@ class ConditionMatcherTest extends FunctionalTestCase
      */
     public function PIDupinRootlineConditionMatchesSinglePageIdInRootline(): void
     {
-        $this->subject->setPageId(3);
-        $this->subject->__construct();
+        $this->subject->__construct(null, 3);
         self::assertTrue($this->subject->match('[2 in tree.rootLineIds]'));
         self::assertTrue($this->subject->match('["2" in tree.rootLineIds]'));
         self::assertTrue($this->subject->match('[\'2\' in tree.rootLineIds]'));
@@ -198,8 +195,7 @@ class ConditionMatcherTest extends FunctionalTestCase
      */
     public function PIDinRootlineConditionMatchesSinglePageIdInRootline(): void
     {
-        $this->subject->setPageId(3);
-        $this->subject->__construct();
+        $this->subject->__construct(null, 3);
         self::assertTrue($this->subject->match('[2 in tree.rootLineIds]'));
     }
 
@@ -210,8 +206,7 @@ class ConditionMatcherTest extends FunctionalTestCase
      */
     public function PIDinRootlineConditionMatchesLastPageIdInRootline(): void
     {
-        $this->subject->setPageId(3);
-        $this->subject->__construct();
+        $this->subject->__construct(null, 3);
         self::assertTrue($this->subject->match('[3 in tree.rootLineIds]'));
     }
 
index c10e73f..c11d1e2 100644 (file)
@@ -16,7 +16,7 @@ namespace TYPO3\CMS\Backend\Tests\Unit\Utility;
 
 use Prophecy\Argument;
 use Prophecy\Prophecy\ObjectProphecy;
-use TYPO3\CMS\Backend\Configuration\TsConfigParser;
+use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
 use TYPO3\CMS\Backend\Tests\Unit\Utility\Fixtures\LabelFromItemListMergedReturnsCorrectFieldsFixture;
 use TYPO3\CMS\Backend\Tests\Unit\Utility\Fixtures\ProcessedValueForGroupWithMultipleAllowedTablesFixture;
 use TYPO3\CMS\Backend\Tests\Unit\Utility\Fixtures\ProcessedValueForGroupWithOneAllowedTableFixture;
@@ -24,6 +24,7 @@ use TYPO3\CMS\Backend\Tests\Unit\Utility\Fixtures\ProcessedValueForSelectWithMMR
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
+use TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
@@ -32,7 +33,6 @@ use TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer;
 use TYPO3\CMS\Core\Database\RelationHandler;
 use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Extbase\SignalSlot\Dispatcher as SignalSlotDispatcher;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 /**
@@ -1035,24 +1035,23 @@ class BackendUtilityTest extends UnitTestCase
     {
         $expected = ['called.' => ['config']];
         $pageId = 13;
-        $parserProphecy = $this->prophesize(TsConfigParser::class);
-        $parserProphecy->parseTSconfig(Argument::cetera())->willReturn(['hash' => $pageId, 'TSconfig' => $expected]);
-        GeneralUtility::addInstance(TsConfigParser::class, $parserProphecy->reveal());
+        $parserProphecy = $this->prophesize(PageTsConfigParser::class);
+        $parserProphecy->parse(Argument::cetera())->willReturn($expected);
+        GeneralUtility::addInstance(PageTsConfigParser::class, $parserProphecy->reveal());
+
+        $matcherProphecy = $this->prophesize(ConditionMatcher::class);
+        GeneralUtility::addInstance(ConditionMatcher::class, $matcherProphecy->reveal());
 
         $cacheManagerProphecy = $this->prophesize(CacheManager::class);
         $cacheProphecy = $this->prophesize(FrontendInterface::class);
         $cacheManagerProphecy->getCache('runtime')->willReturn($cacheProphecy->reveal());
         $cacheHashProphecy = $this->prophesize(FrontendInterface::class);
-        $cacheManagerProphecy->hasCache('extbase')->willReturn(false);
         $cacheManagerProphecy->getCache('hash')->willReturn($cacheHashProphecy->reveal());
         $cacheProphecy->has(Argument::cetera())->willReturn(false);
         $cacheProphecy->get(Argument::cetera())->willReturn(false);
         $cacheProphecy->set(Argument::cetera())->willReturn(false);
         $cacheProphecy->get('backendUtilityBeGetRootLine')->willReturn(['13--1' => []]);
         GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerProphecy->reveal());
-        $signalSlotDispatcherProphecy = $this->prophesize(SignalSlotDispatcher::class);
-        $signalSlotDispatcherProphecy->dispatch(Argument::any(), Argument::any(), Argument::type('array'))->willReturnArgument(2);
-        GeneralUtility::setSingletonInstance(SignalSlotDispatcher::class, $signalSlotDispatcherProphecy->reveal());
 
         $result = BackendUtility::getPagesTSconfig($pageId);
         self::assertEquals($expected, $result);
diff --git a/typo3/sysext/core/Classes/Configuration/Loader/PageTsConfigLoader.php b/typo3/sysext/core/Classes/Configuration/Loader/PageTsConfigLoader.php
new file mode 100644 (file)
index 0000000..ee5c631
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Configuration\Loader;
+
+/*
+ * 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!
+ */
+
+use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
+use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\PathUtility;
+
+/**
+ * Traverses a root line of a pagetree up and includes all available TSconfig settings, including default
+ * setup. Then include lines are checked, and merged together into one string, ready to be parsed.
+ *
+ * Can be used in Frontend or Backend.
+ *
+ * Have a look at the PageTsConfigParser which can then parse (and cache) this information based on the.
+ *
+ * Currently, this accumulated information of the pages is NOT cached, as it would need to be tagged with any
+ * page, also including external files.
+ */
+class PageTsConfigLoader
+{
+    /**
+     * Main method to get all PageTSconfig from the rootline including the defaultTSconfig settings.
+     * @param array $rootLine
+     * @return string
+     */
+    public function load(array $rootLine): string
+    {
+        // Verifying includes, and melt the inclusions together into one string
+        $tsData = $this->collect($rootLine);
+        return implode("\n[GLOBAL]\n", $tsData);
+    }
+
+    /**
+     * Same as "load()" but returns an array of all parts. Only useful in the TYPO3 Backend for inspection purposes.
+     *
+     * @param array $rootLine
+     * @return array
+     * @internal
+     */
+    public function collect(array $rootLine): array
+    {
+        $tsData = [
+            'default' => $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? []
+        ];
+        foreach ($rootLine as $page) {
+            // Can happen when the rootline is given from BE context, we skip this
+            if ((int)$page['uid'] === 0) {
+                continue;
+            }
+            if (trim($page['tsconfig_includes'] ?? '')) {
+                $includeTsConfigFileList = GeneralUtility::trimExplode(',', $page['tsconfig_includes'], true);
+                // Traversing list
+                foreach ($includeTsConfigFileList as $key => $includeTsConfigFile) {
+                    if (strpos($includeTsConfigFile, 'EXT:') === 0) {
+                        [$includeTsConfigFileExtensionKey, $includeTsConfigFilename] = explode(
+                            '/',
+                            substr($includeTsConfigFile, 4),
+                            2
+                        );
+                        if ((string)$includeTsConfigFileExtensionKey !== ''
+                            && ExtensionManagementUtility::isLoaded($includeTsConfigFileExtensionKey)
+                            && (string)$includeTsConfigFilename !== ''
+                        ) {
+                            $extensionPath = ExtensionManagementUtility::extPath($includeTsConfigFileExtensionKey);
+                            $includeTsConfigFileAndPath = PathUtility::getCanonicalPath($extensionPath . $includeTsConfigFilename);
+                            if (strpos($includeTsConfigFileAndPath, $extensionPath) === 0 && file_exists($includeTsConfigFileAndPath)) {
+                                $tsData['page_' . $page['uid'] . '_includes_' . $key] = file_get_contents($includeTsConfigFileAndPath);
+                            }
+                        }
+                    }
+                }
+            }
+            $tsData['page_' . $page['uid']] = $page['TSconfig'] ?? '';
+        }
+        // Apply includes
+        return TypoScriptParser::checkIncludeLines_array($tsData);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Configuration/Parser/PageTsConfigParser.php b/typo3/sysext/core/Classes/Configuration/Parser/PageTsConfigParser.php
new file mode 100644 (file)
index 0000000..4f6d71d
--- /dev/null
@@ -0,0 +1,128 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Configuration\Parser;
+
+/*
+ * 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!
+ */
+
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
+use TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\ConditionMatcherInterface;
+use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
+
+/**
+ * A TS-Config parsing class which performs condition evaluation.
+ *
+ * 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.
+ */
+class PageTsConfigParser
+{
+    /**
+     * @var TypoScriptParser
+     */
+    protected $typoScriptParser;
+
+    /**
+     * @var FrontendInterface
+     */
+    protected $cache;
+
+    public function __construct(TypoScriptParser $typoScriptParser, FrontendInterface $cache)
+    {
+        $this->typoScriptParser = $typoScriptParser;
+        $this->cache = $cache;
+    }
+
+    /**
+     * Parses and matches a given string
+     * Adds entries to the cache:
+     * - when an exact on the conditions are there
+     * - when a parse is there, then matches are happening anyway, and it is checked if this can be cached as well.
+     *
+     * @param string $content pageTSconfig, usually accumulated by the PageTsConfigLoader
+     * @param ConditionMatcherInterface $matcher an instance to match strings
+     * @return array the
+     */
+    public function parse(string $content, ConditionMatcherInterface $matcher): array
+    {
+        $hashOfContent = md5('PAGES:' . $content);
+        $cachedContent = $this->cache->get($hashOfContent);
+        // Something about this content has been cached before, lets verify the matchings, if they also apply
+        if (is_array($cachedContent) && is_array($cachedContent[0])) {
+            // Cache entry found, see if the "matching" section matches with the matcher
+            $storedData = $cachedContent[0];
+            $storedMD5 = $cachedContent[1];
+            $storedData['match'] = $this->matching($storedData['sections'] ?? [], $matcher);
+            $hashOfDataWithMatches = md5(json_encode($storedData));
+            // The matches are the same, so nothing to do here
+            if ($hashOfDataWithMatches === $storedMD5) {
+                $result = $storedData['TSconfig'];
+            } else {
+                // Create a hash out of the content-hash PLUS the matching information and try again
+                $shash = md5($hashOfDataWithMatches . $hashOfContent);
+                $storedData = $this->cache->get($shash);
+                if (is_array($storedData)) {
+                    $result = $storedData['TSconfig'];
+                } else {
+                    // Create a new content with the matcher, and cache it as a new entry
+                    $parsedAndMatchedData = $this->parseAndMatch($content, $matcher);
+                    // Now store the full data from the parser (with matches)
+                    $this->cache->set($shash, $parsedAndMatchedData, ['pageTSconfig'], 0);
+                    $result = $parsedAndMatchedData['TSconfig'];
+                }
+            }
+            return $result;
+        }
+        // Nothing found in cache for this content string, let's do everything.
+        $parsedAndMatchedData = $this->parseAndMatch($content, $matcher);
+        // ALL parts, including the matching part is cached.
+        $md5 = md5(json_encode($parsedAndMatchedData));
+        $this->cache->set($hashOfContent, [$parsedAndMatchedData, $md5], ['pageTSconfig'], 0);
+        return $parsedAndMatchedData['TSconfig'];
+    }
+
+    /**
+     * Does the actual parsing using the TypoScriptParser "parse" method by applying a condition matcher.
+     *
+     * @param string $content The TSConfig being parsed
+     * @param ConditionMatcherInterface $matcher
+     * @return array Array containing the parsed TSConfig, the encountered sections, the matched sections. This is stored in cache.
+     */
+    protected function parseAndMatch(string $content, ConditionMatcherInterface $matcher): array
+    {
+        $this->typoScriptParser->parse($content, $matcher);
+        return [
+            'TSconfig' => $this->typoScriptParser->setup,
+            'sections' => $this->typoScriptParser->sections,
+            'match' => $this->typoScriptParser->sectionsMatch
+        ];
+    }
+
+    /**
+     * Is just going through an array of conditions to determine which are matching (for getting correct cache entry)
+     *
+     * @param array $sectionsToMatch An array containing the sections to match
+     * @param ConditionMatcherInterface $matcher
+     * @return array The input array with matching sections to be filled into the "match" key
+     */
+    protected function matching(array $sectionsToMatch, ConditionMatcherInterface $matcher): array
+    {
+        $matches = [];
+        foreach ($sectionsToMatch ?? [] as $key => $pre) {
+            if ($matcher->match($pre)) {
+                $matches[$key] = $pre;
+            }
+        }
+        return $matches;
+    }
+}
index b25b8b0..d8a239f 100644 (file)
@@ -20,7 +20,6 @@ use Symfony\Component\ExpressionLanguage\SyntaxError;
 use TYPO3\CMS\Core\Configuration\TypoScript\Exception\InvalidTypoScriptConditionException;
 use TYPO3\CMS\Core\Error\Exception;
 use TYPO3\CMS\Core\ExpressionLanguage\Resolver;
-use TYPO3\CMS\Core\Log\LogLevel;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
@@ -29,7 +28,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  * Used with the TypoScript parser.
  * Matches IP numbers etc. for use with templates
  */
-abstract class AbstractConditionMatcher implements LoggerAwareInterface
+abstract class AbstractConditionMatcher implements LoggerAwareInterface, ConditionMatcherInterface
 {
     use LoggerAwareTrait;
 
@@ -195,7 +194,7 @@ abstract class AbstractConditionMatcher implements LoggerAwareInterface
             }
         } catch (SyntaxError $exception) {
             $message = 'Expression could not be parsed.';
-            $this->logger->log(LogLevel::ERROR, $message, ['expression' => $expression]);
+            $this->logger->error($message, ['expression' => $expression]);
         } catch (\Throwable $exception) {
             // The following error handling is required to mitigate a missing type check
             // in the Symfony Expression Language handling. In case a condition
diff --git a/typo3/sysext/core/Classes/Configuration/TypoScript/ConditionMatching/ConditionMatcherInterface.php b/typo3/sysext/core/Classes/Configuration/TypoScript/ConditionMatching/ConditionMatcherInterface.php
new file mode 100644 (file)
index 0000000..2c3448f
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching;
+
+/*
+ * 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!
+ */
+
+/**
+ * Used for TypoScript Conditions to be evaluated.
+ */
+interface ConditionMatcherInterface
+{
+    /**
+     * Matches a TypoScript condition expression.
+     *
+     * @param string $expression The expression to match
+     * @return bool Whether the expression matched
+     */
+    public function match($expression): bool;
+}
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-89718-LegacyPageTSconfigParsingLowlevelAPI.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-89718-LegacyPageTSconfigParsingLowlevelAPI.rst
new file mode 100644 (file)
index 0000000..77e74a9
--- /dev/null
@@ -0,0 +1,70 @@
+.. include:: ../../Includes.txt
+
+==============================================================
+Deprecation: #89718 - Legacy PageTSconfig parsing lowlevel API
+==============================================================
+
+See :issue:`89718`
+
+Description
+===========
+
+Two new PHP API classes for retrieving and parsing TsConfig are
+introduced:
+
+- :php:`TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader`
+- :php:`TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser`
+
+As this API is more consistent, and flexible, as well as agnostic
+of the current Context of backend or frontend, the following
+functionality has been marked as deprecated:
+
+- :php:`TYPO3\CMS\Core\Configuration\TsConfigParser`
+- :php:`TYPO3\CMS\Backend\Utility\BackendUtility::getRawPagesTSconfig()`
+
+
+Impact
+======
+
+Instantiating the PHP class or the mentioned PHP method will trigger
+a deprecation message.
+
+
+Affected Installations
+======================
+
+TYPO3 Installations with extensions using the lowlevel API for handling PageTSconfig.
+
+
+Migration
+=========
+
+Loading and parsing PageTSconfig on a low-level should be done via the new PHP classes:
+
+- :php:`TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader`
+- :php:`TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser`
+
+Usages for fetching all available PageTS of a page/rootline in one large string:
+
+:php:
+    $loader = GeneralUtility::makeInstance(PageTsConfigLoader::class);
+    $tsConfigString = $loader->load($rootLine);
+
+
+The string is parsed (and conditions are applied) with the Parser:
+
+:php:
+            $parser = GeneralUtility::makeInstance(
+                PageTsConfigParser::class,
+                $typoScriptParser,
+                $hashCache
+            );
+            $pagesTSconfig = $parser->parse(
+                $tsConfigString,
+                $conditionMatcher
+            );
+
+Extension developers should rely on this syntax rather than
+on :php:`$GLOBALS['TSFE']->getPagesTSconfig()` or :php:`BackendUtility::getPagesTsConfig()`, or the deprecated method / class.
+
+.. index:: PHP-API, TSConfig, FullyScanned, ext:core
\ No newline at end of file
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-89718-UnifiedPHPAPIForLoadingPageTSconfig.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-89718-UnifiedPHPAPIForLoadingPageTSconfig.rst
new file mode 100644 (file)
index 0000000..215a650
--- /dev/null
@@ -0,0 +1,59 @@
+.. include:: ../../Includes.txt
+
+==========================================================
+Feature: #89718 - Unified PHP API for loading PageTSconfig
+==========================================================
+
+See :issue:`89718`
+
+Description
+===========
+
+Most parts of TYPO3 Core share duplicate or similar functionality in
+Frontend or Backend context. One of that is the loading and parsing
+of PageTSconfig, the configuration syntax for various places in
+TYPO3 Backend, which can also be used to define Backend Layouts.
+
+In order to streamline this functionality, the loading process of
+gathering all data from a rootline of a page is now simplified in
+a new PageTsLoader PHP class.
+
+Additionally, parsing, and additional matching against conditions,
+which was added later-on in 2009 and put on top, is now separated
+properly, building a truly separation of concerns for compiling
+and parsing TSconfig. This is put in the PageTsConfigParser PHP class.
+
+
+Impact
+======
+
+When there is the necessity for fetching and loading PageTSconfig,
+it is recommended for extension developers to make use of both new
+PHP classes:
+- :php:`TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader`
+- :php:`TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser`
+
+Usages for fetching all available PageTS in one large string (not parsed yet):
+
+:php:
+    $loader = GeneralUtility::makeInstance(PageTsConfigLoader::class);
+    $tsConfigString = $loader->load($rootLine);
+
+
+The string can then be put in proper TSconfig array syntax:
+
+:php:
+            $parser = GeneralUtility::makeInstance(
+                PageTsConfigParser::class,
+                $typoScriptParser,
+                $hashCache
+            );
+            $pagesTSconfig = $parser->parse(
+                $tsConfigString,
+                $conditionMatcher
+            );
+
+Extension developers should rely on this syntax rather than
+on :php:`$GLOBALS['TSFE']->getPagesTSconfig()` or :php:`BackendUtility::getPagesTsConfig()`.
+
+.. index:: PHP-API, TSConfig, ext:core
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Unit/Configuration/Loader/Fixtures/included.typoscript b/typo3/sysext/core/Tests/Unit/Configuration/Loader/Fixtures/included.typoscript
new file mode 100644 (file)
index 0000000..9d8e541
--- /dev/null
@@ -0,0 +1 @@
+Show_me = more
diff --git a/typo3/sysext/core/Tests/Unit/Configuration/Loader/PageTsConfigLoaderTest.php b/typo3/sysext/core/Tests/Unit/Configuration/Loader/PageTsConfigLoaderTest.php
new file mode 100644 (file)
index 0000000..75e0391
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\Configuration\Loader;
+
+/*
+ * 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!
+ */
+
+use TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class PageTsConfigLoaderTest extends UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function alwaysLoadDefaultSettings(): void
+    {
+        $expected = [
+            'default' => $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig']
+        ];
+        $expectedString = implode('"\n[GLOBAL]\n"', $expected);
+        $subject = new PageTsConfigLoader();
+        $result = $subject->collect([]);
+        self::assertSame($expected, $result);
+
+        $result = $subject->load([]);
+        self::assertSame($expectedString, $result);
+    }
+
+    /**
+     * @test
+     */
+    public function loadDefaultSettingsAtTheBeginningAndKeepEmptyEntriesExpectUidZero(): void
+    {
+        $expected = [
+            'default' => $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'],
+            'page_13' => 'waiting for = love',
+            'page_27' => '',
+        ];
+        $rootLine = [['uid' => 0, 'pid' => 0], ['uid' => 13, 'TSconfig' => 'waiting for = love'], ['uid' => 27, 'TSconfig' => '']];
+        $subject = new PageTsConfigLoader();
+        $result = $subject->collect($rootLine);
+        self::assertSame($expected, $result);
+    }
+
+    /**
+     * @test
+     */
+    public function loadExternalInclusionsCorrectlyAndKeepLoadingOrder(): void
+    {
+        $expected = [
+            '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' => '']];
+        $subject = new PageTsConfigLoader();
+        $result = $subject->collect($rootLine);
+        self::assertSame($expected, $result);
+    }
+
+    /**
+     * @test
+     */
+    public function invalidExternalFileIsNotLoaded(): void
+    {
+        $expected = [
+            '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' => '']];
+        $subject = new PageTsConfigLoader();
+        $result = $subject->collect($rootLine);
+        self::assertSame($expected, $result);
+
+        $result = $subject->load($rootLine);
+        self::assertSame($expectedString, $result);
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Configuration/Parser/PageTsConfigParserTest.php b/typo3/sysext/core/Tests/Unit/Configuration/Parser/PageTsConfigParserTest.php
new file mode 100644 (file)
index 0000000..e650881
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\Configuration\Loader;
+
+/*
+ * 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!
+ */
+
+use Psr\Log\NullLogger;
+use TYPO3\CMS\Core\Cache\Backend\TransientMemoryBackend;
+use TYPO3\CMS\Core\Cache\Frontend\NullFrontend;
+use TYPO3\CMS\Core\Cache\Frontend\VariableFrontend;
+use TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser;
+use TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\ConditionMatcherInterface;
+use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class PageTsConfigParserTest extends UnitTestCase
+{
+
+    /**
+     * @test
+     */
+    public function invalidCacheAlwaysExecutesMatcher(): void
+    {
+        $input = 'mod.web_layout = disabled';
+        $expectedParsedTsConfig = ['mod' => ['web_layout' => 'disabled']];
+        $matcherProphecy = $this->prophesize(ConditionMatcherInterface::class);
+        $typoScriptParserProphecy = $this->prophesize(TypoScriptParser::class);
+        $typoScriptParserProphecy->parse($input, $matcherProphecy)->shouldBeCalled()->will(function () use ($expectedParsedTsConfig) {
+            $this->setup = $expectedParsedTsConfig;
+        });
+        $cache = new NullFrontend('runtime');
+        $subject = new PageTsConfigParser(
+            $typoScriptParserProphecy->reveal(),
+            $cache
+        );
+        $parsedTsConfig = $subject->parse($input, $matcherProphecy->reveal());
+        self::assertEquals($expectedParsedTsConfig, $parsedTsConfig);
+    }
+
+    /**
+     * @test
+     */
+    public function cachedHitOnlyExecutesMatcher(): void
+    {
+        $cachedSection = 'mod.web_layout = disabled';
+        $input = 'mod.web_layout = disabled';
+        $expectedParsedTsConfig = ['mod' => ['web_layout' => 'disabled']];
+        $matcherProphecy = $this->prophesize(ConditionMatcherInterface::class);
+        $matcherProphecy->match($cachedSection)->shouldBeCalled()->willReturn('matched');
+        $typoScriptParserProphecy = $this->prophesize(TypoScriptParser::class);
+        $typoScriptParserProphecy->parse($input, $matcherProphecy)->shouldNotBecalled();
+        $cache = new VariableFrontend('runtime', new TransientMemoryBackend('nothing', ['logger' => new NullLogger()]));
+        $cache->set(
+            '1d0a3029a36cc56a82bfdb0642fcd912',
+            [
+            0 => [
+                'sections' => [$cachedSection],
+                'TSconfig' => ['mod' => ['web_layout' => 'disabled']]
+            ],
+            1 => 'fb3c41ea55f42a993fc143a54e09bbdd']
+        );
+        $subject = new PageTsConfigParser(
+            $typoScriptParserProphecy->reveal(),
+            $cache
+        );
+        $parsedTsConfig = $subject->parse($input, $matcherProphecy->reveal());
+        self::assertEquals($expectedParsedTsConfig, $parsedTsConfig);
+    }
+}
index 92cd95b..5255870 100644 (file)
@@ -34,11 +34,14 @@ class ConditionMatcher extends AbstractConditionMatcher
 
     /**
      * @param Context $context optional context to fetch data from
+     * @param int|null $pageId
+     * @param array|null $rootLine
      */
-    public function __construct(Context $context = null)
+    public function __construct(Context $context = null, int $pageId = null, array $rootLine = null)
     {
         $this->context = $context ?? GeneralUtility::makeInstance(Context::class);
-        $this->rootline = (array)$GLOBALS['TSFE']->tmpl->rootLine;
+        $this->pageId = $pageId;
+        $this->rootline = $rootLine ?? (array)$GLOBALS['TSFE']->tmpl->rootLine;
         $this->initializeExpressionLanguageResolver();
     }
 
index efd8180..15190f0 100644 (file)
@@ -22,6 +22,8 @@ use TYPO3\CMS\Backend\FrontendBackendUserAuthentication;
 use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Charset\CharsetConverter;
 use TYPO3\CMS\Core\Charset\UnknownCharsetException;
+use TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader;
+use TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Context\DateTimeAspect;
 use TYPO3\CMS\Core\Context\LanguageAspect;
@@ -60,7 +62,6 @@ use TYPO3\CMS\Core\Type\Bitmask\Permission;
 use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
 use TYPO3\CMS\Core\TypoScript\TemplateService;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
-use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
@@ -68,6 +69,7 @@ use TYPO3\CMS\Core\Utility\PathUtility;
 use TYPO3\CMS\Core\Utility\RootlineUtility;
 use TYPO3\CMS\Frontend\Aspect\PreviewAspect;
 use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
+use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
 use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
 use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
@@ -3424,52 +3426,18 @@ class TypoScriptFrontendController implements LoggerAwareInterface
     public function getPagesTSconfig()
     {
         if (!is_array($this->pagesTSconfig)) {
-            $TSdataArray = [];
-            foreach ($this->rootLine as $k => $v) {
-                // add TSconfig first, as $TSdataArray is reversed below and it shall be included last
-                $TSdataArray[] = $v['TSconfig'];
-                if (trim($v['tsconfig_includes'])) {
-                    $includeTsConfigFileList = GeneralUtility::trimExplode(',', $v['tsconfig_includes'], true);
-                    // reverse the includes first to make sure their order is preserved when $TSdataArray is reversed
-                    $includeTsConfigFileList = array_reverse($includeTsConfigFileList);
-                    // Traversing list
-                    foreach ($includeTsConfigFileList as $includeTsConfigFile) {
-                        if (strpos($includeTsConfigFile, 'EXT:') === 0) {
-                            list($includeTsConfigFileExtensionKey, $includeTsConfigFilename) = explode(
-                                '/',
-                                substr($includeTsConfigFile, 4),
-                                2
-                            );
-                            if ((string)$includeTsConfigFileExtensionKey !== ''
-                                && (string)$includeTsConfigFilename !== ''
-                                && ExtensionManagementUtility::isLoaded($includeTsConfigFileExtensionKey)
-                            ) {
-                                $extensionPath = ExtensionManagementUtility::extPath($includeTsConfigFileExtensionKey);
-                                $includeTsConfigFileAndPath = PathUtility::getCanonicalPath($extensionPath . $includeTsConfigFilename);
-                                if (strpos($includeTsConfigFileAndPath, $extensionPath) === 0 && file_exists($includeTsConfigFileAndPath)) {
-                                    $TSdataArray[] = file_get_contents($includeTsConfigFileAndPath);
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-            // Adding the default configuration:
-            $TSdataArray[] = $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'];
-            // Bring everything in the right order. Default first, then the Rootline down to the current page
-            $TSdataArray = array_reverse($TSdataArray);
-            // Parsing the user TS (or getting from cache)
-            $TSdataArray = TypoScriptParser::checkIncludeLines_array($TSdataArray);
-            $userTS = implode(LF . '[GLOBAL]' . LF, $TSdataArray);
-            $identifier = md5('pageTS:' . $userTS);
             $contentHashCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
-            $this->pagesTSconfig = $contentHashCache->get($identifier);
-            if (!is_array($this->pagesTSconfig)) {
-                $parseObj = GeneralUtility::makeInstance(TypoScriptParser::class);
-                $parseObj->parse($userTS);
-                $this->pagesTSconfig = $parseObj->setup;
-                $contentHashCache->set($identifier, $this->pagesTSconfig, ['PAGES_TSconfig'], 0);
-            }
+            $loader = GeneralUtility::makeInstance(PageTsConfigLoader::class);
+            $tsConfigString = $loader->load(array_reverse($this->rootLine));
+            $parser = GeneralUtility::makeInstance(
+                PageTsConfigParser::class,
+                GeneralUtility::makeInstance(TypoScriptParser::class),
+                $contentHashCache
+            );
+            $this->pagesTSconfig = $parser->parse(
+                $tsConfigString,
+                GeneralUtility::makeInstance(ConditionMatcher::class, $this->context, $this->id, $this->rootLine)
+            );
         }
         return $this->pagesTSconfig;
     }
index 67bec03..9ffdc2d 100644 (file)
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Info\Controller;
 use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
@@ -90,40 +91,41 @@ class InfoPageTyposcriptConfigController
             }
         } else {
             if ($this->pObj->MOD_SETTINGS['tsconf_parts'] == 99) {
-                $TSparts = BackendUtility::getRawPagesTSconfig($this->id);
+                $rootLine = BackendUtility::BEgetRootLine($this->id, '', true);
+                $TSparts = GeneralUtility::makeInstance(PageTsConfigLoader::class)->collect($rootLine);
                 $lines = [];
                 $pUids = [];
 
                 foreach ($TSparts as $k => $v) {
-                    if ($k !== 'uid_0') {
-                        $line = [];
-                        if ($k === 'defaultPageTSconfig') {
-                            $line['defaultPageTSconfig'] = 1;
-                        } else {
-                            $editIdList = substr($k, 4);
-                            $pUids[] = $editIdList;
-                            $row = BackendUtility::getRecordWSOL('pages', $editIdList);
+                    $line = [];
+                    if ($k === 'default') {
+                        $line['defaultPageTSconfig'] = 1;
+                    } else {
+                        // Remove the "page_" prefix
+                        [, $pageId] = explode('_', $k, 3);
+                        $pageId = (int)$pageId;
+                        $pUids[] = $pageId;
+                        $row = BackendUtility::getRecordWSOL('pages', $pageId);
 
-                            $icon = $this->iconFactory->getIconForRecord('pages', $row, Icon::SIZE_SMALL);
-                            $urlParameters = [
-                                'edit' => [
-                                    'pages' => [
-                                        $editIdList => 'edit',
-                                    ]
-                                ],
-                                'columnsOnly' => 'TSconfig',
-                                'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
-                            ];
-                            $line['editIcon'] = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
-                            $line['editTitle'] = 'editTSconfig';
-                            $line['title'] = BackendUtility::wrapClickMenuOnIcon($icon, 'pages', $row['uid'])
-                                . ' ' . htmlspecialchars(BackendUtility::getRecordTitle('pages', $row));
-                        }
-                        $tsparser = GeneralUtility::makeInstance(TypoScriptParser::class);
-                        $tsparser->lineNumberOffset = 0;
-                        $line['content'] = $tsparser->doSyntaxHighlight(trim($v) . LF);
-                        $lines[] = $line;
+                        $icon = $this->iconFactory->getIconForRecord('pages', $row, Icon::SIZE_SMALL);
+                        $urlParameters = [
+                            'edit' => [
+                                'pages' => [
+                                    $pageId => 'edit',
+                                ]
+                            ],
+                            'columnsOnly' => 'TSconfig,tsconfig_includes',
+                            'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
+                        ];
+                        $line['editIcon'] = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
+                        $line['editTitle'] = 'editTSconfig';
+                        $line['title'] = BackendUtility::wrapClickMenuOnIcon($icon, 'pages', $row['uid'])
+                            . ' ' . htmlspecialchars(BackendUtility::getRecordTitle('pages', $row));
                     }
+                    $tsparser = GeneralUtility::makeInstance(TypoScriptParser::class);
+                    $tsparser->lineNumberOffset = 0;
+                    $line['content'] = $tsparser->doSyntaxHighlight(trim($v) . LF);
+                    $lines[] = $line;
                 }
 
                 if (!empty($pUids)) {
@@ -133,7 +135,7 @@ class InfoPageTyposcriptConfigController
                                 implode(',', $pUids) => 'edit',
                             ]
                         ],
-                        'columnsOnly' => 'TSconfig',
+                        'columnsOnly' => 'TSconfig,tsconfig_includes',
                         'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
                     ];
                     $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
index 7743e31..b58dafe 100644 (file)
@@ -1341,5 +1341,10 @@ return [
         'restFiles' => [
             'Deprecation-89577-FALSignalSlotHandlingMigratedToPSR-14Events.rst',
         ],
+    ],
+    'TYPO3\CMS\Core\Configuration\TsConfigParser' => [
+        'restFiles' => [
+            'Deprecation-89718-LegacyPageTSconfigParsingLowlevelAPI.rst',
+        ],
     ]
 ];
index 5a1ae70..12e71ea 100644 (file)
@@ -945,4 +945,11 @@ return [
             'Deprecation-89631-UseEnvironmentAPIToFetchApplicationContext.rst'
         ],
     ],
+    'TYPO3\CMS\Backend\Utility\BackendUtility::getRawPagesTSconfig' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-89718-LegacyPageTSconfigParsingLowlevelAPI.rst',
+        ],
+    ],
 ];