[TASK] Improved extension configuration API 34/55434/10
authorChristian Kuhn <lolli@schwarzbu.ch>
Tue, 23 Jan 2018 12:42:43 +0000 (13:42 +0100)
committerSusanne Moog <susanne.moog@typo3.org>
Mon, 29 Jan 2018 21:52:22 +0000 (22:52 +0100)
The patch implements feedback from the new
ExtensionConfiguration API.
get() now falls back to a parsing of ext_conf_template,
so the parser is moved from the install tool
to the ext:core class. This prevents a chicken-egg
issue in early setup phases.

The move of the synchronize* methods forces a raise
of the testing-framework:

composer require --dev typo3/testing-framework ^2.0.3

Change-Id: I9a6700fc66fe78d9df09038a89ea95f8dba81031
Resolves: #83666
Releases: master
Reviewed-on: https://review.typo3.org/55434
Reviewed-by: Helmut Hummel <typo3@helhum.io>
Tested-by: Helmut Hummel <typo3@helhum.io>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
composer.json
composer.lock
typo3/sysext/core/Classes/Configuration/ExtensionConfiguration.php
typo3/sysext/core/Tests/Unit/Configuration/ExtensionConfigurationTest.php
typo3/sysext/extensionmanager/Classes/Utility/InstallUtility.php
typo3/sysext/install/Classes/Controller/InstallerController.php
typo3/sysext/install/Classes/Controller/LayoutController.php
typo3/sysext/install/Classes/Service/ExtensionConfigurationService.php

index 4986828..8c8996f 100644 (file)
                "symfony/polyfill-intl-icu": "^1.6"
        },
        "require-dev": {
-               "typo3/testing-framework": "2.0.2",
                "codeception/codeception": "^2.3",
                "enm1989/chromedriver": "~2.30",
                "friendsofphp/php-cs-fixer": "^2.0",
                "fiunchinho/phpunit-randomizer": "~3.0.0",
-               "typo3/cms-styleguide": "~9.0.1"
+               "typo3/cms-styleguide": "~9.0.1",
+               "typo3/testing-framework": "^2.0.3"
        },
        "suggest": {
                "ext-gd": "GDlib/Freetype is required for building images with text (GIFBUILDER) and can also be used to scale images",
index f27e6e4..e1ec3e7 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "8100b1ed42d29e4858138859e0c78b4d",
+    "content-hash": "130e4cb12eccf704ca66950ae4c184fa",
     "packages": [
         {
             "name": "cogpowered/finediff",
         },
         {
             "name": "typo3/testing-framework",
-            "version": "2.0.2",
+            "version": "2.0.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/TYPO3/testing-framework.git",
-                "reference": "77c2906628d9c04cd6aa7b6c6ce6f753c3adb117"
+                "reference": "e271251d8190b4c8e40238cd2c61565a531fa6a4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/TYPO3/testing-framework/zipball/77c2906628d9c04cd6aa7b6c6ce6f753c3adb117",
-                "reference": "77c2906628d9c04cd6aa7b6c6ce6f753c3adb117",
+                "url": "https://api.github.com/repos/TYPO3/testing-framework/zipball/e271251d8190b4c8e40238cd2c61565a531fa6a4",
+                "reference": "e271251d8190b4c8e40238cd2c61565a531fa6a4",
                 "shasum": ""
             },
             "require": {
             },
             "suggest": {
                 "codeception/codeception": "^2.2",
-                "se/selenium-server-standalone": "~2.53",
                 "typo3/cms-saltedpasswords": "^8.5",
                 "typo3/cms-styleguide": "~8.0.8"
             },
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
-                "GPL-2.0+"
+                "GPL-2.0-or-later"
             ],
             "authors": [
                 {
                 "tests",
                 "typo3"
             ],
-            "time": "2018-01-04T12:33:55+00:00"
+            "time": "2018-01-24T08:50:16+00:00"
         },
         {
             "name": "webmozart/assert",
index 820716b..bed8c24 100644 (file)
@@ -17,11 +17,13 @@ namespace TYPO3\CMS\Core\Configuration;
 
 use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException;
 use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException;
+use TYPO3\CMS\Core\Core\Bootstrap;
+use TYPO3\CMS\Core\Package\PackageManager;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
- * API to get() and set() instance specific extension configuration options.
+ * API to get() instance specific extension configuration options.
  *
  * Extension authors are encouraged to use this API - it is currently a simple
  * wrapper to access TYPO3_CONF_VARS['EXTENSIONS'] but could later become something
@@ -33,10 +35,61 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  * ext_conf_template.txt files. The core (more specifically the install tool)
  * takes care default values and overridden values are properly prepared upon
  * loading or updating an extension.
+ *
+ * Note only ->get() is official API and other public methods are low level
+ * core internal API that is usually only used by extension manager and install tool.
  */
 class ExtensionConfiguration
 {
     /**
+     * TypoScript hierarchy being build.
+     * Used parsing ext_conf_template.txt
+     *
+     * @var array
+     */
+    protected $setup = [];
+
+    /**
+     * Raw data, the input string exploded by LF.
+     * Used parsing ext_conf_template.txt
+     *
+     * @var array
+     */
+    protected $raw;
+
+    /**
+     * Pointer to entry in raw data array.
+     * Used parsing ext_conf_template.txt
+     *
+     * @var int
+     */
+    protected $rawPointer = 0;
+
+    /**
+     * Holding the value of the last comment
+     * Used parsing ext_conf_template.txt
+     *
+     * @var string
+     */
+    protected $lastComment = '';
+
+    /**
+     * Internal flag to create a multi-line comment (one of those like /* ... * /)
+     * Used parsing ext_conf_template.txt
+     *
+     * @var bool
+     */
+    protected $commentSet = false;
+
+    /**
+     * Internally set, when in brace. Counter.
+     * Used parsing ext_conf_template.txt
+     *
+     * @var int
+     */
+    protected $inBrace = 0;
+
+    /**
      * Get a single configuration value, a sub array or the whole configuration.
      *
      * Examples:
@@ -52,6 +105,9 @@ class ExtensionConfiguration
      * ->get('myExtension', 'FE/forceSalted')
      *
      * Notes:
+     * - If a configuration or configuration path of an extension is not found, the
+     *   code tries to synchronize configuration with ext_conf_template.txt first, only
+     *   if still not found, it will throw exceptions.
      * - Return values are NOT type safe: A boolean false could be returned as string 0.
      *   Cast accordingly.
      * - This API throws exceptions if the path does not exist or the extension
@@ -72,22 +128,36 @@ class ExtensionConfiguration
      */
     public function get(string $extension, string $path = '')
     {
+        $hasBeenSynchronized = false;
         if (!isset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension]) || !is_array($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension])) {
-            throw new ExtensionConfigurationExtensionNotConfiguredException(
-                'No extension configuration for extension ' . $extension . ' found. Either this extension'
-                . ' has no extension configuration or the configuration is not up to date. Execute the'
-                . ' install tool to update configuration.',
-                1509654728
-            );
+            // This if() should not be hit at "casual" runtime, but only in early setup phases
+            $this->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions();
+            $hasBeenSynchronized = true;
+            if (!isset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension]) || !is_array($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension])) {
+                // If there is still no such entry, even after sync -> throw
+                throw new ExtensionConfigurationExtensionNotConfiguredException(
+                    'No extension configuration for extension ' . $extension . ' found. Either this extension'
+                    . ' has no extension configuration or the configuration is not up to date. Execute the'
+                    . ' install tool to update configuration.',
+                    1509654728
+                );
+            }
         }
         if (empty($path)) {
             return $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension];
         }
         if (!ArrayUtility::isValidPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path)) {
-            throw new ExtensionConfigurationPathDoesNotExistException(
-                'Path ' . $path . ' does not exist in extension configuration',
-                1509977699
-            );
+            // This if() should not be hit at "casual" runtime, but only in early setup phases
+            if (!$hasBeenSynchronized) {
+                $this->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions();
+            }
+            // If there is still no such entry, even after sync -> throw
+            if (!ArrayUtility::isValidPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path)) {
+                throw new ExtensionConfigurationPathDoesNotExistException(
+                    'Path ' . $path . ' does not exist in extension configuration',
+                    1509977699
+                );
+            }
         }
         return ArrayUtility::getValueByPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path);
     }
@@ -116,26 +186,25 @@ class ExtensionConfiguration
      * ->set('myExtension', 'myFeature')
      *
      * Notes:
+     * - Do NOT call this at arbitrary places during runtime (eg. NOT in ext_localconf.php or
+     *   similar). ->set() is not supposed to be called each request since it writes LocalConfiguration
+     *   each time. This API is however OK to be called from extension manager hooks.
      * - $path is NOT validated. It is up to an ext author to also define them in
-     *   ext_conf_template.txt to have an interface in install tool reflecting these settings
+     *   ext_conf_template.txt to have an interface in install tool reflecting these settings.
      * - If $path is currently an array, $value overrides the whole thing. Merging existing values
      *   is up to the extension author
      * - Values are not type safe, if the install tool wrote them,
      *   boolean true could become string 1 on ->get()
      * - It is not possible to store 'null' as value, giving $value=null
      *   or no value at all will unset the path
-     * - Setting a value and calling ->get() afterwards will still return the old (!) value, the
-     *   new value is only available in ->get() with next request. This is to have consistent
-     *   values if the setting is possibly overwritten in AdditionalConfiguration again, which
-     *   this API does not know and is only evaluated early during bootstrap.
+     * - Setting a value and calling ->get() afterwards will still return the new value.
      * - Warning on AdditionalConfiguration.php: If this file overwrites settings, it spoils the
-     *   ->set() call and values may not finally end up as expected. Avoid using AdditionalConfiguration.php
-     *   in general ...
+     *   ->set() call and values may not end up as expected.
      *
      * @param string $extension Extension name
      * @param string $path Configuration path to set - eg. "featureCategory/coolThingIsEnabled"
      * @param null $value The value. If null, unset the path
-     * @api
+     * @internal
      */
     public function set(string $extension, string $path = '', $value = null)
     {
@@ -164,6 +233,90 @@ class ExtensionConfiguration
             $extensionConfig = $this->addDotsToArrayKeysRecursiveForLegacyExtConf($extensionConfig);
             $configurationManager->setLocalConfigurationValueByPath('EXT/extConf/' . $extensionName, serialize($extensionConfig));
         }
+
+        // Reload TYPO3_CONF_VARS from LocalConfiguration & AdditionalConfiguration
+        Bootstrap::getInstance()->populateLocalConfiguration();
+    }
+
+    /**
+     * Set new configuration of all extensions and reload TYPO3_CONF_VARS.
+     * This is a "do all" variant of set() for all extensions that prevents
+     * writing and loading LocalConfiguration many times.
+     *
+     * @param array $configuration Configuration of all extensions
+     * @internal
+     */
+    public function setAll(array $configuration)
+    {
+        $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
+        $configurationManager->setLocalConfigurationValueByPath('EXTENSIONS', $configuration);
+
+        // After TYPO3_CONF_VARS['EXTENSIONS'] has been written, update legacy layer TYPO3_CONF_VARS['EXTENSIONS']['extConf']
+        // @deprecated since TYPO3 v9, will be removed in v10 with removal of old serialized 'extConf' layer
+        $extConfArray = [];
+        foreach ($configuration as $extensionName => $extensionConfig) {
+            $extConfArray[$extensionName] = serialize($this->addDotsToArrayKeysRecursiveForLegacyExtConf($extensionConfig));
+        }
+        $configurationManager->setLocalConfigurationValueByPath('EXT/extConf', $extConfArray);
+
+        // Reload TYPO3_CONF_VARS from LocalConfiguration & AdditionalConfiguration
+        Bootstrap::getInstance()->populateLocalConfiguration();
+    }
+
+    /**
+     * If there are new config settings in ext_conf_template of an extension,
+     * they are found here and synchronized to LocalConfiguration['EXTENSIONS'].
+     *
+     * Used when entering the install tool, during installation and if calling ->get()
+     * with an extension or path that is not yet found in LocalConfiguration
+     *
+     * @internal
+     */
+    public function synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions()
+    {
+        $activePackages = GeneralUtility::makeInstance(PackageManager::class)->getActivePackages();
+        $fullConfiguration = [];
+        $currentLocalConfiguration = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'] ?? [];
+        foreach ($activePackages as $package) {
+            if (!@is_file($package->getPackagePath() . 'ext_conf_template.txt')) {
+                continue;
+            }
+            $extensionKey = $package->getPackageKey();
+            $currentExtensionConfig = $currentLocalConfiguration[$extensionKey] ?? [];
+            $extConfTemplateConfiguration = $this->getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots($extensionKey);
+            ArrayUtility::mergeRecursiveWithOverrule($extConfTemplateConfiguration, $currentExtensionConfig);
+            if (!empty($extConfTemplateConfiguration)) {
+                $fullConfiguration[$extensionKey] = $extConfTemplateConfiguration;
+            }
+        }
+        // Write new config if changed. Loose array comparison to not write if only array key order is different
+        if ($fullConfiguration != $currentLocalConfiguration) {
+            $this->setAll($fullConfiguration);
+        }
+    }
+
+    /**
+     * Read values from ext_conf_template, verify if they are in LocalConfiguration.php
+     * already and if not, add them.
+     *
+     * Used public by extension manager when updating extension
+     *
+     * @param string $extensionKey The extension to sync
+     * @internal
+     */
+    public function synchronizeExtConfTemplateWithLocalConfiguration(string $extensionKey)
+    {
+        $package = GeneralUtility::makeInstance(PackageManager::class)->getPackage($extensionKey);
+        if (!@is_file($package->getPackagePath() . 'ext_conf_template.txt')) {
+            return;
+        }
+        $currentLocalConfiguration = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extensionKey] ?? [];
+        $extConfTemplateConfiguration = $this->getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots($extensionKey);
+        ArrayUtility::mergeRecursiveWithOverrule($extConfTemplateConfiguration, $currentLocalConfiguration);
+        // Write new config if changed. Loose array comparison to not write if only array key order is different
+        if ($extConfTemplateConfiguration != $currentLocalConfiguration) {
+            $this->set($extensionKey, '', $extConfTemplateConfiguration);
+        }
     }
 
     /**
@@ -193,7 +346,7 @@ class ExtensionConfiguration
      * @return array
      * @deprecated since TYPO3 v9, will be removed in v10 with removal of old serialized 'extConf' layer
      */
-    private function addDotsToArrayKeysRecursiveForLegacyExtConf(array $extensionConfig)
+    private function addDotsToArrayKeysRecursiveForLegacyExtConf(array $extensionConfig): array
     {
         $newArray = [];
         foreach ($extensionConfig as $key => $value) {
@@ -205,4 +358,322 @@ class ExtensionConfiguration
         }
         return $newArray;
     }
+
+    /**
+     * Helper method of ext_conf_template.txt parsing.
+     *
+     * Poor man version of getDefaultConfigurationFromExtConfTemplateAsValuedArray() which ignores
+     * comments and returns ext_conf_template as array where nested keys have no dots.
+     *
+     * @param string $extensionKey
+     * @return array
+     */
+    protected function getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots(string $extensionKey): array
+    {
+        $configuration = $this->getParsedExtConfTemplate($extensionKey);
+        return $this->removeCommentsAndDotsRecursive($configuration);
+    }
+
+    /**
+     * Trigger main ext_conf_template.txt parsing logic.
+     * Needs to be public as it is used by install tool ExtensionConfigurationService
+     * which adds the comment parsing on top for display of options in install tool.
+     *
+     * @param string $extensionKey
+     * @return array
+     * @internal
+     */
+    public function getParsedExtConfTemplate(string $extensionKey): array
+    {
+        $rawConfigurationString = $this->getDefaultConfigurationRawString($extensionKey);
+        $configuration = [];
+        if ((string)$rawConfigurationString !== '') {
+            $this->raw = explode(LF, $rawConfigurationString);
+            $this->rawPointer = 0;
+            $this->setup = [];
+            $this->parseSub($this->setup);
+            if ($this->inBrace) {
+                throw new \RuntimeException(
+                    'Line ' . ($this->rawPointer - 1) . ': The script is short of ' . $this->inBrace . ' end brace(s)',
+                    1507645349
+                );
+            }
+            $configuration = $this->setup;
+        }
+        return $configuration;
+    }
+
+    /**
+     * Helper method of ext_conf_template.txt parsing.
+     *
+     * Return content of an extensions ext_conf_template.txt file if
+     * the file exists, empty string if file does not exist.
+     *
+     * @param string $extensionKey Extension key
+     * @return string
+     */
+    protected function getDefaultConfigurationRawString(string $extensionKey): string
+    {
+        $rawString = '';
+        $extConfTemplateFileLocation = GeneralUtility::getFileAbsFileName(
+            'EXT:' . $extensionKey . '/ext_conf_template.txt'
+        );
+        if (file_exists($extConfTemplateFileLocation)) {
+            $rawString = file_get_contents($extConfTemplateFileLocation);
+        }
+        return $rawString;
+    }
+
+    /**
+     * Helper method of ext_conf_template.txt parsing.
+     *
+     * "Comments" from the "TypoScript" parser below are identified by two (!) dots at the end of array keys
+     * and all array keys have a single dot at the end, if they have sub arrays. This is cleaned here.
+     *
+     * Incoming array:
+     * [
+     *  'automaticInstallation' => '1',
+     *  'automaticInstallation..' => '# cat=basic/enabled; ...'
+     *  'FE.' => [
+     *      'enabled' = '1',
+     *      'enabled..' => '# cat=basic/enabled; ...'
+     *  ]
+     * ]
+     *
+     * Output array:
+     * [
+     *  'automaticInstallation' => '1',
+     *  'FE' => [
+     *      'enabled' => '1',
+     * ]
+     *
+     * @param array $config Incoming configuration
+     * @return array
+     */
+    protected function removeCommentsAndDotsRecursive(array $config): array
+    {
+        $cleanedConfig = [];
+        foreach ($config as $key => $value) {
+            if (substr($key, -2) === '..') {
+                continue;
+            }
+            if (substr($key, -1) === '.') {
+                $cleanedConfig[rtrim($key, '.')] = $this->removeCommentsAndDotsRecursive($value);
+            } else {
+                $cleanedConfig[$key] = $value;
+            }
+        }
+        return $cleanedConfig;
+    }
+
+    /**
+     * Helper method of ext_conf_template.txt parsing.
+     *
+     * Parsing the $this->raw TypoScript lines from pointer, $this->rawP
+     *
+     * @param array $setup Reference to the setup array in which to accumulate the values.
+     */
+    protected function parseSub(array &$setup)
+    {
+        while (isset($this->raw[$this->rawPointer])) {
+            $line = ltrim($this->raw[$this->rawPointer]);
+            $this->rawPointer++;
+            // Set comment flag?
+            if (strpos($line, '/*') === 0) {
+                $this->commentSet = 1;
+            }
+            if (!$this->commentSet && ($line)) {
+                if ($line[0] !== '}' && $line[0] !== '#' && $line[0] !== '/') {
+                    // If not brace-end or comment
+                    // Find object name string until we meet an operator
+                    $varL = strcspn($line, TAB . ' {=<>(');
+                    // check for special ":=" operator
+                    if ($varL > 0 && substr($line, $varL - 1, 2) === ':=') {
+                        --$varL;
+                    }
+                    // also remove tabs after the object string name
+                    $objStrName = substr($line, 0, $varL);
+                    if ($objStrName !== '') {
+                        $r = [];
+                        if (preg_match('/[^[:alnum:]_\\\\\\.:-]/i', $objStrName, $r)) {
+                            throw new \RuntimeException(
+                                'Line ' . ($this->rawPointer - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" contains invalid character "' . $r[0] . '". Must be alphanumeric or one of: "_:-\\."',
+                                1507645381
+                            );
+                        }
+                        $line = ltrim(substr($line, $varL));
+                        if ($line === '') {
+                            throw new \RuntimeException(
+                                'Line ' . ($this->rawPointer - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({',
+                                1507645417
+                            );
+                        }
+                        switch ($line[0]) {
+                            case '=':
+                                if (strpos($objStrName, '.') !== false) {
+                                    $value = [];
+                                    $value[0] = trim(substr($line, 1));
+                                    $this->setVal($objStrName, $setup, $value);
+                                } else {
+                                    $setup[$objStrName] = trim(substr($line, 1));
+                                    if ($this->lastComment) {
+                                        // Setting comment..
+                                        $setup[$objStrName . '..'] .= $this->lastComment;
+                                    }
+                                }
+                                break;
+                            case '{':
+                                $this->inBrace++;
+                                if (strpos($objStrName, '.') !== false) {
+                                    $this->rollParseSub($objStrName, $setup);
+                                } else {
+                                    if (!isset($setup[$objStrName . '.'])) {
+                                        $setup[$objStrName . '.'] = [];
+                                    }
+                                    $this->parseSub($setup[$objStrName . '.']);
+                                }
+                                break;
+                            default:
+                                throw new \RuntimeException(
+                                    'Line ' . ($this->rawPointer - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({',
+                                    1507645445
+                                );
+                        }
+
+                        $this->lastComment = '';
+                    }
+                } elseif ($line[0] === '}') {
+                    $this->inBrace--;
+                    $this->lastComment = '';
+                    if ($this->inBrace < 0) {
+                        throw new \RuntimeException(
+                            'Line ' . ($this->rawPointer - 1) . ': An end brace is in excess.',
+                            1507645489
+                        );
+                    }
+                    break;
+                } else {
+                    $this->lastComment .= rtrim($line) . LF;
+                }
+            }
+            // Unset comment
+            if ($this->commentSet) {
+                if (strpos($line, '*/') === 0) {
+                    $this->commentSet = 0;
+                }
+            }
+        }
+    }
+
+    /**
+     * Helper method of ext_conf_template.txt parsing.
+     *
+     * Parsing of TypoScript keys inside a curly brace where the key is composite of at least two keys,
+     * thus having to recursively call itself to get the value.
+     *
+     * @param string $string The object sub-path, eg "thisprop.another_prot"
+     * @param array $setup The local setup array from the function calling this function
+     */
+    protected function rollParseSub($string, array &$setup)
+    {
+        if ((string)$string === '') {
+            return;
+        }
+        list($key, $remainingKey) = $this->parseNextKeySegment($string);
+        $key .= '.';
+        if (!isset($setup[$key])) {
+            $setup[$key] = [];
+        }
+        $remainingKey === ''
+            ? $this->parseSub($setup[$key])
+            : $this->rollParseSub($remainingKey, $setup[$key]);
+    }
+
+    /**
+     * Helper method of ext_conf_template.txt parsing.
+     *
+     * Setting a value/property of an object string in the setup array.
+     *
+     * @param string $string The object sub-path, eg "thisprop.another_prot
+     * @param array $setup The local setup array from the function calling this function.
+     * @param void
+     */
+    protected function setVal($string, array &$setup, $value)
+    {
+        if ((string)$string === '') {
+            return;
+        }
+
+        list($key, $remainingKey) = $this->parseNextKeySegment($string);
+        $subKey = $key . '.';
+        if ($remainingKey === '') {
+            if (isset($value[0])) {
+                $setup[$key] = $value[0];
+            }
+            if (isset($value[1])) {
+                $setup[$subKey] = $value[1];
+            }
+            if ($this->lastComment) {
+                $setup[$key . '..'] .= $this->lastComment;
+            }
+        } else {
+            if (!isset($setup[$subKey])) {
+                $setup[$subKey] = [];
+            }
+            $this->setVal($remainingKey, $setup[$subKey], $value);
+        }
+    }
+
+    /**
+     * Helper method of ext_conf_template.txt parsing.
+     *
+     * Determines the first key segment of a TypoScript key by searching for the first
+     * unescaped dot in the given key string.
+     *
+     * Since the escape characters are only needed to correctly determine the key
+     * segment any escape characters before the first unescaped dot are
+     * stripped from the key.
+     *
+     * @param string $key The key, possibly consisting of multiple key segments separated by unescaped dots
+     * @return array Array with key segment and remaining part of $key
+     */
+    protected function parseNextKeySegment($key): array
+    {
+        // if no dot is in the key, nothing to do
+        $dotPosition = strpos($key, '.');
+        if ($dotPosition === false) {
+            return [$key, ''];
+        }
+
+        if (strpos($key, '\\') !== false) {
+            // backslashes are in the key, so we do further parsing
+            while ($dotPosition !== false) {
+                if ($dotPosition > 0 && $key[$dotPosition - 1] !== '\\' || $dotPosition > 1 && $key[$dotPosition - 2] === '\\') {
+                    break;
+                }
+                // escaped dot found, continue
+                $dotPosition = strpos($key, '.', $dotPosition + 1);
+            }
+
+            if ($dotPosition === false) {
+                // no regular dot found
+                $keySegment = $key;
+                $remainingKey = '';
+            } else {
+                if ($dotPosition > 1 && $key[$dotPosition - 2] === '\\' && $key[$dotPosition - 1] === '\\') {
+                    $keySegment = substr($key, 0, $dotPosition - 1);
+                } else {
+                    $keySegment = substr($key, 0, $dotPosition);
+                }
+                $remainingKey = substr($key, $dotPosition + 1);
+            }
+
+            // fix key segment by removing escape sequences
+            $keySegment = str_replace('\\.', '.', $keySegment);
+        } else {
+            // no backslash in the key, we're fine off
+            list($keySegment, $remainingKey) = explode('.', $key, 2);
+        }
+        return [$keySegment, $remainingKey];
+    }
 }
index b69ef50..08c3ac8 100644 (file)
@@ -17,7 +17,6 @@ namespace TYPO3\CMS\Core\Tests\Unit\Configuration;
 
 use Prophecy\Argument;
 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
-use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException;
 use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
@@ -30,17 +29,6 @@ class ExtensionConfigurationTest extends UnitTestCase
     /**
      * @test
      */
-    public function getThrowExceptionIfExtensionConfigurationDoesNotExist()
-    {
-        $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['someOtherExtension']['someKey'] = 'someValue';
-        $this->expectException(ExtensionConfigurationExtensionNotConfiguredException::class);
-        $this->expectExceptionCode(1509654728);
-        (new ExtensionConfiguration())->get('someExtension');
-    }
-
-    /**
-     * @test
-     */
     public function getWithEmptyPathReturnsEntireExtensionConfiguration()
     {
         $extConf = [
index 2e173f4..e032444 100644 (file)
@@ -14,7 +14,7 @@ namespace TYPO3\CMS\Extensionmanager\Utility;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Core\Core\Bootstrap;
+use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
 use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
 use TYPO3\CMS\Core\Database\Schema\SqlReader;
 use TYPO3\CMS\Core\Service\OpcodeCacheService;
@@ -22,7 +22,6 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extensionmanager\Domain\Model\Extension;
 use TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException;
 use TYPO3\CMS\Impexp\Utility\ImportExportUtility;
-use TYPO3\CMS\Install\Service\ExtensionConfigurationService;
 
 /**
  * Extension Manager Install Utility
@@ -162,8 +161,9 @@ class InstallUtility implements \TYPO3\CMS\Core\SingletonInterface
         } else {
             $this->cacheManager->flushCachesInGroup('system');
         }
+        // TODO: Should be possible to move this call to processExtensionSetup.
+        // TODO: We need to check why our acceptance test on postgress fails then
         $this->saveDefaultConfiguration($extensionKey);
-        Bootstrap::getInstance()->populateLocalConfiguration();
         $this->reloadCaches();
         $this->processExtensionSetup($extensionKey);
 
@@ -441,8 +441,8 @@ class InstallUtility implements \TYPO3\CMS\Core\SingletonInterface
      */
     protected function saveDefaultConfiguration($extensionKey)
     {
-        $configUtility = $this->objectManager->get(ExtensionConfigurationService::class);
-        $configUtility->synchronizeExtConfTemplateWithLocalConfiguration($extensionKey);
+        $extensionConfiguration = $this->objectManager->get(ExtensionConfiguration::class);
+        $extensionConfiguration->synchronizeExtConfTemplateWithLocalConfiguration($extensionKey);
     }
 
     /**
index bc8272a..b4a38d3 100644 (file)
@@ -20,6 +20,7 @@ use Doctrine\DBAL\DriverManager;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
+use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
 use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
@@ -41,7 +42,6 @@ use TYPO3\CMS\Install\Configuration\FeatureManager;
 use TYPO3\CMS\Install\FolderStructure\DefaultFactory;
 use TYPO3\CMS\Install\Service\EnableFileService;
 use TYPO3\CMS\Install\Service\Exception\ConfigurationChangedException;
-use TYPO3\CMS\Install\Service\ExtensionConfigurationService;
 use TYPO3\CMS\Install\Service\SilentConfigurationUpgradeService;
 use TYPO3\CMS\Install\SystemEnvironment\Check;
 use TYPO3\CMS\Install\SystemEnvironment\SetupCheck;
@@ -168,8 +168,8 @@ class InstallerController
                 }
                 $packageManager->forceSortAndSavePackageStates();
             }
-            $extensionConfigurationService = new ExtensionConfigurationService();
-            $extensionConfigurationService->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions();
+            $extensionConfiguration = new ExtensionConfiguration();
+            $extensionConfiguration->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions();
             Bootstrap::getInstance()->populateLocalConfiguration();
 
             return new JsonResponse([
index 3ffe732..a07a18b 100644 (file)
@@ -18,11 +18,11 @@ namespace TYPO3\CMS\Install\Controller;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
+use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
 use TYPO3\CMS\Core\Http\HtmlResponse;
 use TYPO3\CMS\Core\Http\JsonResponse;
 use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
 use TYPO3\CMS\Install\Service\Exception\ConfigurationChangedException;
-use TYPO3\CMS\Install\Service\ExtensionConfigurationService;
 use TYPO3\CMS\Install\Service\SilentConfigurationUpgradeService;
 
 /**
@@ -138,8 +138,8 @@ class LayoutController extends AbstractController
      */
     public function executeSilentExtensionConfigurationSynchronizationAction(): ResponseInterface
     {
-        $extensionConfigurationService = new ExtensionConfigurationService();
-        $extensionConfigurationService->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions();
+        $extensionConfiguration = new ExtensionConfiguration();
+        $extensionConfiguration->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions();
         return new JsonResponse([
             'success' => true,
         ]);
index 0f81409..8721f3d 100644 (file)
@@ -15,7 +15,6 @@ namespace TYPO3\CMS\Install\Service;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException;
 use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException;
 use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
 use TYPO3\CMS\Core\Localization\LanguageService;
@@ -24,66 +23,18 @@ use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
- * Utility for dealing with ext_conf_template settings and their instance
- * specific LocalConfiguration settings. This class is @internal and only
- * used by extension manager and install tool itself.
+ * Service to prepare extension configuration settings from ext_conf_template.txt
+ * to be viewed in the install tool. The class basically adds display related
+ * stuff on top of ext:core ExtensionConfiguration.
  *
  * Extension authors should use TYPO3\CMS\Core\Configuration\ExtensionConfiguration
- * class to get() and set() extension configuration settings.
+ * class to get() extension configuration settings.
  *
  * @internal
  */
 class ExtensionConfigurationService
 {
     /**
-     * TypoScript hierarchy being build.
-     * Used parsing ext_conf_template.txt
-     *
-     * @var array
-     */
-    protected $setup = [];
-
-    /**
-     * Raw data, the input string exploded by LF.
-     * Used parsing ext_conf_template.txt
-     *
-     * @var array
-     */
-    protected $raw;
-
-    /**
-     * Pointer to entry in raw data array.
-     * Used parsing ext_conf_template.txt
-     *
-     * @var int
-     */
-    protected $rawPointer = 0;
-
-    /**
-     * Holding the value of the last comment
-     * Used parsing ext_conf_template.txt
-     *
-     * @var string
-     */
-    protected $lastComment = '';
-
-    /**
-     * Internal flag to create a multi-line comment (one of those like /* ... * /)
-     * Used parsing ext_conf_template.txt
-     *
-     * @var bool
-     */
-    protected $commentSet = false;
-
-    /**
-     * Internally set, when in brace. Counter.
-     * Used parsing ext_conf_template.txt
-     *
-     * @var int
-     */
-    protected $inBrace = 0;
-
-    /**
      * This will be filled with the available categories of the current template.
      * Used parsing ext_conf_template.txt
      *
@@ -119,48 +70,6 @@ class ExtensionConfigurationService
     ];
 
     /**
-     * If there are new config settings in ext_conf_template of an extenison,
-     * they are found here and synchronized to LocalConfiguration['EXTENSIONS'].
-     *
-     * Used when entering the install tool and during installation.
-     */
-    public function synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions()
-    {
-        $activePackages = GeneralUtility::makeInstance(PackageManager::class)->getActivePackages();
-        foreach ($activePackages as $package) {
-            $this->synchronizeExtConfTemplateWithLocalConfiguration($package->getPackageKey());
-        }
-    }
-
-    /**
-     * Read values from ext_conf_template, verify if they are in LocalConfiguration.php
-     * already and if not, add them.
-     *
-     * Used public by extension manager when updating extension
-     *
-     * @param string $extensionKey The extension to sync
-     */
-    public function synchronizeExtConfTemplateWithLocalConfiguration(string $extensionKey)
-    {
-        $package = GeneralUtility::makeInstance(PackageManager::class)->getPackage($extensionKey);
-        if (!@is_file($package->getPackagePath() . 'ext_conf_template.txt')) {
-            return;
-        }
-        $extensionConfiguration = new ExtensionConfiguration();
-        try {
-            $currentLocalConfiguration = $extensionConfiguration->get($extensionKey);
-        } catch (ExtensionConfigurationExtensionNotConfiguredException $e) {
-            $currentLocalConfiguration = [];
-        }
-        $extConfTemplateConfiguration = $this->getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots($extensionKey);
-        ArrayUtility::mergeRecursiveWithOverrule($extConfTemplateConfiguration, $currentLocalConfiguration);
-        // Write new config if changed. Loose array comparison to not write if only array key order is different
-        if ($extConfTemplateConfiguration != $currentLocalConfiguration) {
-            $extensionConfiguration->set($extensionKey, '', $extConfTemplateConfiguration);
-        }
-    }
-
-    /**
      * Compiles ext_conf_template file and merges it with values from LocalConfiguration['EXTENSIONS'].
      * Returns a funny array used to display the configuration form in the install tool.
      *
@@ -212,30 +121,6 @@ class ExtensionConfigurationService
     }
 
     /**
-     * Poor man version of getDefaultConfigurationFromExtConfTemplateAsValuedArray() which ignores
-     * comments and returns ext_conf_template as array where nested keys have no dots.
-     */
-    protected function getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots(string $extensionKey)
-    {
-        $rawConfigurationString = $this->getDefaultConfigurationRawString($extensionKey);
-        $configuration = [];
-        if ((string)$rawConfigurationString !== '') {
-            $this->raw = explode(LF, $rawConfigurationString);
-            $this->rawPointer = 0;
-            $this->setup = [];
-            $this->parseSub($this->setup);
-            if ($this->inBrace) {
-                throw new \RuntimeException(
-                    'Line ' . ($this->rawPointer - 1) . ': The script is short of ' . $this->inBrace . ' end brace(s)',
-                    1507645349
-                );
-            }
-            $configuration = $this->removeCommentsAndDotsRecursive($this->setup);
-        }
-        return $configuration;
-    }
-
-    /**
      * Builds a configuration array from each line (option) of the config file.
      * Helper method for getConfigurationPreparedForView()
      *
@@ -327,58 +212,25 @@ class ExtensionConfigurationService
      */
     protected function getDefaultConfigurationFromExtConfTemplateAsValuedArray(string $extensionKey): array
     {
-        $rawConfigurationString = $this->getDefaultConfigurationRawString($extensionKey);
-        $theConstants = [];
-        if ((string)$rawConfigurationString !== '') {
-            $this->raw = explode(LF, $rawConfigurationString);
-            $this->rawPointer = 0;
-            $this->setup = [];
-            $this->parseSub($this->setup);
-            if ($this->inBrace) {
-                throw new \RuntimeException(
-                    'Line ' . ($this->rawPointer - 1) . ': The script is short of ' . $this->inBrace . ' end brace(s)',
-                    1507645348
-                );
-            }
-            $parsedConstants = $this->setup;
-            $flatSetup = $this->flattenSetup($parsedConstants);
-            $theConstants = $this->parseComments($flatSetup);
-
-            // Loop through configuration items, see if it is assigned to a sub category
-            // and add the sub category label to the item property if so.
-            foreach ($theConstants as $configurationOptionName => $configurationOption) {
-                if (
-                    array_key_exists('subcat_name', $configurationOption)
-                    && isset($this->subCategories[$configurationOption['subcat_name']])
-                    && isset($this->subCategories[$configurationOption['subcat_name']][0])
-                ) {
-                    $theConstants[$configurationOptionName]['subcat_label'] = $this->subCategories[$configurationOption['subcat_name']][0];
-                }
+        $parsedConstants = (new ExtensionConfiguration())->getParsedExtConfTemplate($extensionKey);
+        $flatSetup = $this->flattenSetup($parsedConstants);
+        $theConstants = $this->parseComments($flatSetup);
+
+        // Loop through configuration items, see if it is assigned to a sub category
+        // and add the sub category label to the item property if so.
+        foreach ($theConstants as $configurationOptionName => $configurationOption) {
+            if (
+                array_key_exists('subcat_name', $configurationOption)
+                && isset($this->subCategories[$configurationOption['subcat_name']])
+                && isset($this->subCategories[$configurationOption['subcat_name']][0])
+            ) {
+                $theConstants[$configurationOptionName]['subcat_label'] = $this->subCategories[$configurationOption['subcat_name']][0];
             }
         }
         return $theConstants;
     }
 
     /**
-     * Return content of an extensions ext_conf_template.txt file if
-     * the file exists, empty string if file does not exist.
-     *
-     * @param string $extensionKey Extension key
-     * @return string
-     */
-    protected function getDefaultConfigurationRawString(string $extensionKey): string
-    {
-        $rawString = '';
-        $extConfTemplateFileLocation = GeneralUtility::getFileAbsFileName(
-            'EXT:' . $extensionKey . '/ext_conf_template.txt'
-        );
-        if (file_exists($extConfTemplateFileLocation)) {
-            $rawString = file_get_contents($extConfTemplateFileLocation);
-        }
-        return $rawString;
-    }
-
-    /**
      * This function compares the flattened constants (default and all).
      * Returns an array with the constants from the whole template which may be edited by the module.
      *
@@ -467,246 +319,6 @@ class ExtensionConfigurationService
     }
 
     /**
-     * Parsing the $this->raw TypoScript lines from pointer, $this->rawP
-     *
-     * @param array $setup Reference to the setup array in which to accumulate the values.
-     */
-    protected function parseSub(array &$setup)
-    {
-        while (isset($this->raw[$this->rawPointer])) {
-            $line = ltrim($this->raw[$this->rawPointer]);
-            $this->rawPointer++;
-            // Set comment flag?
-            if (strpos($line, '/*') === 0) {
-                $this->commentSet = 1;
-            }
-            if (!$this->commentSet && ($line)) {
-                if ($line[0] !== '}' && $line[0] !== '#' && $line[0] !== '/') {
-                    // If not brace-end or comment
-                    // Find object name string until we meet an operator
-                    $varL = strcspn($line, TAB . ' {=<>(');
-                    // check for special ":=" operator
-                    if ($varL > 0 && substr($line, $varL - 1, 2) === ':=') {
-                        --$varL;
-                    }
-                    // also remove tabs after the object string name
-                    $objStrName = substr($line, 0, $varL);
-                    if ($objStrName !== '') {
-                        $r = [];
-                        if (preg_match('/[^[:alnum:]_\\\\\\.:-]/i', $objStrName, $r)) {
-                            throw new \RuntimeException(
-                                'Line ' . ($this->rawPointer - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" contains invalid character "' . $r[0] . '". Must be alphanumeric or one of: "_:-\\."',
-                                1507645381
-                            );
-                        }
-                        $line = ltrim(substr($line, $varL));
-                        if ($line === '') {
-                            throw new \RuntimeException(
-                                    'Line ' . ($this->rawPointer - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({',
-                                    1507645417
-                                );
-                        }
-                        switch ($line[0]) {
-                                    case '=':
-                                        if (strpos($objStrName, '.') !== false) {
-                                            $value = [];
-                                            $value[0] = trim(substr($line, 1));
-                                            $this->setVal($objStrName, $setup, $value);
-                                        } else {
-                                            $setup[$objStrName] = trim(substr($line, 1));
-                                            if ($this->lastComment) {
-                                                // Setting comment..
-                                                $setup[$objStrName . '..'] .= $this->lastComment;
-                                            }
-                                        }
-                                        break;
-                                    case '{':
-                                        $this->inBrace++;
-                                        if (strpos($objStrName, '.') !== false) {
-                                            $this->rollParseSub($objStrName, $setup);
-                                        } else {
-                                            if (!isset($setup[$objStrName . '.'])) {
-                                                $setup[$objStrName . '.'] = [];
-                                            }
-                                            $this->parseSub($setup[$objStrName . '.']);
-                                        }
-                                        break;
-                                    default:
-                                        throw new \RuntimeException(
-                                            'Line ' . ($this->rawPointer - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({',
-                                            1507645445
-                                        );
-                                }
-
-                        $this->lastComment = '';
-                    }
-                } elseif ($line[0] === '}') {
-                    $this->inBrace--;
-                    $this->lastComment = '';
-                    if ($this->inBrace < 0) {
-                        throw new \RuntimeException(
-                            'Line ' . ($this->rawPointer - 1) . ': An end brace is in excess.',
-                            1507645489
-                        );
-                    }
-                    break;
-                } else {
-                    $this->lastComment .= rtrim($line) . LF;
-                }
-            }
-            // Unset comment
-            if ($this->commentSet) {
-                if (strpos($line, '*/') === 0) {
-                    $this->commentSet = 0;
-                }
-            }
-        }
-    }
-
-    /**
-     * Parsing of TypoScript keys inside a curly brace where the key is composite of at least two keys,
-     * thus having to recursively call itself to get the value
-     *
-     * @param string $string The object sub-path, eg "thisprop.another_prot
-     * @param array $setup The local setup array from the function calling this function
-     */
-    protected function rollParseSub($string, array &$setup)
-    {
-        if ((string)$string === '') {
-            return;
-        }
-        list($key, $remainingKey) = $this->parseNextKeySegment($string);
-        $key .= '.';
-        if (!isset($setup[$key])) {
-            $setup[$key] = [];
-        }
-        $remainingKey === ''
-            ? $this->parseSub($setup[$key])
-            : $this->rollParseSub($remainingKey, $setup[$key]);
-    }
-
-    /**
-     * Setting a value/property of an object string in the setup array.
-     *
-     * @param string $string The object sub-path, eg "thisprop.another_prot
-     * @param array $setup The local setup array from the function calling this function.
-     * @param void
-     */
-    protected function setVal($string, array &$setup, $value)
-    {
-        if ((string)$string === '') {
-            return;
-        }
-
-        list($key, $remainingKey) = $this->parseNextKeySegment($string);
-        $subKey = $key . '.';
-        if ($remainingKey === '') {
-            if (isset($value[0])) {
-                $setup[$key] = $value[0];
-            }
-            if (isset($value[1])) {
-                $setup[$subKey] = $value[1];
-            }
-            if ($this->lastComment) {
-                $setup[$key . '..'] .= $this->lastComment;
-            }
-        } else {
-            if (!isset($setup[$subKey])) {
-                $setup[$subKey] = [];
-            }
-            $this->setVal($remainingKey, $setup[$subKey], $value);
-        }
-    }
-
-    /**
-     * Determines the first key segment of a TypoScript key by searching for the first
-     * unescaped dot in the given key string.
-     *
-     * Since the escape characters are only needed to correctly determine the key
-     * segment any escape characters before the first unescaped dot are
-     * stripped from the key.
-     *
-     * @param string $key The key, possibly consisting of multiple key segments separated by unescaped dots
-     * @return array Array with key segment and remaining part of $key
-     */
-    protected function parseNextKeySegment($key)
-    {
-        // if no dot is in the key, nothing to do
-        $dotPosition = strpos($key, '.');
-        if ($dotPosition === false) {
-            return [$key, ''];
-        }
-
-        if (strpos($key, '\\') !== false) {
-            // backslashes are in the key, so we do further parsing
-            while ($dotPosition !== false) {
-                if ($dotPosition > 0 && $key[$dotPosition - 1] !== '\\' || $dotPosition > 1 && $key[$dotPosition - 2] === '\\') {
-                    break;
-                }
-                // escaped dot found, continue
-                $dotPosition = strpos($key, '.', $dotPosition + 1);
-            }
-
-            if ($dotPosition === false) {
-                // no regular dot found
-                $keySegment = $key;
-                $remainingKey = '';
-            } else {
-                if ($dotPosition > 1 && $key[$dotPosition - 2] === '\\' && $key[$dotPosition - 1] === '\\') {
-                    $keySegment = substr($key, 0, $dotPosition - 1);
-                } else {
-                    $keySegment = substr($key, 0, $dotPosition);
-                }
-                $remainingKey = substr($key, $dotPosition + 1);
-            }
-
-            // fix key segment by removing escape sequences
-            $keySegment = str_replace('\\.', '.', $keySegment);
-        } else {
-            // no backslash in the key, we're fine off
-            list($keySegment, $remainingKey) = explode('.', $key, 2);
-        }
-        return [$keySegment, $remainingKey];
-    }
-
-    /**
-     * "Comments" from the "TypoScript" parser below are identified by two (!) dots at the end of array keys
-     * and all array keys have a single dot at the end, if they have sub arrays. This is cleaned here.
-     *
-     * Incoming array:
-     * [
-     *  'automaticInstallation' => '1',
-     *  'automaticInstallation..' => '# cat=basic/enabled; ...'
-     *  'FE.' => [
-     *      'enabled' = '1',
-     *      'enabled..' => '# cat=basic/enabled; ...'
-     *  ]
-     * ]
-     *
-     * Output array:
-     * [
-     *  'automaticInstallation' => '1',
-     *  'FE' => [
-     *      'enabled' => '1',
-     * ]
-     */
-    protected function removeCommentsAndDotsRecursive(array $config): array
-    {
-        $cleanedConfig = [];
-        foreach ($config as $key => $value) {
-            if (substr($key, -2) === '..') {
-                continue;
-            }
-            if (substr($key, -1) === '.') {
-                $cleanedConfig[rtrim($key, '.')] = $this->removeCommentsAndDotsRecursive($value);
-            } else {
-                $cleanedConfig[$key] = $value;
-            }
-        }
-        return $cleanedConfig;
-    }
-
-    /**
      * This flattens a hierarchical TypoScript array to a dotted notation
      *
      * @param array $setupArray TypoScript array