[TASK] Drop salted passwords configuration options
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Configuration / ExtensionConfiguration.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Core\Configuration;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException;
19 use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException;
20 use TYPO3\CMS\Core\Package\PackageManager;
21 use TYPO3\CMS\Core\Utility\ArrayUtility;
22 use TYPO3\CMS\Core\Utility\GeneralUtility;
23
24 /**
25 * API to get() instance specific extension configuration options.
26 *
27 * Extension authors are encouraged to use this API - it is currently a simple
28 * wrapper to access TYPO3_CONF_VARS['EXTENSIONS'] but could later become something
29 * different in case core decides to store extension configuration elsewhere.
30 *
31 * Extension authors must not access TYPO3_CONF_VARS['EXTENSIONS'] on their own.
32 *
33 * Extension configurations are often 'feature flags' currently defined by
34 * ext_conf_template.txt files. The core (more specifically the install tool)
35 * takes care default values and overridden values are properly prepared upon
36 * loading or updating an extension.
37 *
38 * Note only ->get() is official API and other public methods are low level
39 * core internal API that is usually only used by extension manager and install tool.
40 */
41 class ExtensionConfiguration
42 {
43 /**
44 * TypoScript hierarchy being build.
45 * Used parsing ext_conf_template.txt
46 *
47 * @var array
48 */
49 protected $setup = [];
50
51 /**
52 * Raw data, the input string exploded by LF.
53 * Used parsing ext_conf_template.txt
54 *
55 * @var array
56 */
57 protected $raw;
58
59 /**
60 * Pointer to entry in raw data array.
61 * Used parsing ext_conf_template.txt
62 *
63 * @var int
64 */
65 protected $rawPointer = 0;
66
67 /**
68 * Holding the value of the last comment
69 * Used parsing ext_conf_template.txt
70 *
71 * @var string
72 */
73 protected $lastComment = '';
74
75 /**
76 * Internal flag to create a multi-line comment (one of those like /* ... * /)
77 * Used parsing ext_conf_template.txt
78 *
79 * @var bool
80 */
81 protected $commentSet = false;
82
83 /**
84 * Internally set, when in brace. Counter.
85 * Used parsing ext_conf_template.txt
86 *
87 * @var int
88 */
89 protected $inBrace = 0;
90
91 /**
92 * Get a single configuration value, a sub array or the whole configuration.
93 *
94 * Examples:
95 * // Simple and typical usage: Get a single config value, or an array if the key is a "TypoScript"
96 * // a-like sub-path in ext_conf_template.txt "foo.bar = defaultValue"
97 * ->get('myExtension', 'aConfigKey');
98 *
99 * // Get all current configuration values, always an array
100 * ->get('myExtension');
101 *
102 * // Get a nested config value if the path is a "TypoScript" a-like sub-path
103 * // in ext_conf_template.txt "topLevelKey.subLevelKey = defaultValue"
104 * ->get('myExtension', 'topLevelKey/subLevelKey')
105 *
106 * Notes:
107 * - If a configuration or configuration path of an extension is not found, the
108 * code tries to synchronize configuration with ext_conf_template.txt first, only
109 * if still not found, it will throw exceptions.
110 * - Return values are NOT type safe: A boolean false could be returned as string 0.
111 * Cast accordingly.
112 * - This API throws exceptions if the path does not exist or the extension
113 * configuration is not available. The install tool takes care any new
114 * ext_conf_template.txt values are available TYPO3_CONF_VARS['EXTENSIONS'],
115 * a thrown exception indicates a programming error on developer side
116 * and should not be caught.
117 * - It is not checked if the extension in question is loaded at all,
118 * it's just checked the extension configuration path exists.
119 * - Extensions should typically not get configuration of a different extension.
120 *
121 * @param string $extension Extension name
122 * @param string $path Configuration path - eg. "featureCategory/coolThingIsEnabled"
123 * @return mixed The value. Can be a sub array or a single value.
124 * @throws ExtensionConfigurationExtensionNotConfiguredException If ext configuration does no exist
125 * @throws ExtensionConfigurationPathDoesNotExistException If a requested extension path does not exist
126 * @api
127 */
128 public function get(string $extension, string $path = '')
129 {
130 $hasBeenSynchronized = false;
131 if (!isset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension]) || !is_array($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension])) {
132 // This if() should not be hit at "casual" runtime, but only in early setup phases
133 $this->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions();
134 $hasBeenSynchronized = true;
135 if (!isset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension]) || !is_array($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension])) {
136 // If there is still no such entry, even after sync -> throw
137 throw new ExtensionConfigurationExtensionNotConfiguredException(
138 'No extension configuration for extension ' . $extension . ' found. Either this extension'
139 . ' has no extension configuration or the configuration is not up to date. Execute the'
140 . ' install tool to update configuration.',
141 1509654728
142 );
143 }
144 }
145 if (empty($path)) {
146 return $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension];
147 }
148 if (!ArrayUtility::isValidPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path)) {
149 // This if() should not be hit at "casual" runtime, but only in early setup phases
150 if (!$hasBeenSynchronized) {
151 $this->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions();
152 }
153 // If there is still no such entry, even after sync -> throw
154 if (!ArrayUtility::isValidPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path)) {
155 throw new ExtensionConfigurationPathDoesNotExistException(
156 'Path ' . $path . ' does not exist in extension configuration',
157 1509977699
158 );
159 }
160 }
161 return ArrayUtility::getValueByPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path);
162 }
163
164 /**
165 * Store a new or overwrite an existing configuration value.
166 *
167 * This is typically used by core internal low level tasks like the install
168 * tool but may become handy if an extension needs to update extension configuration
169 * on the fly for whatever reason.
170 *
171 * Examples:
172 * // Set a full extension configuration ($value could be a nested array, too)
173 * ->set('myExtension', ['aFeature' => 'true', 'aCustomClass' => 'css-foo'])
174 *
175 * // Unset a whole extension configuration
176 * ->set('myExtension')
177 *
178 * Notes:
179 * - Do NOT call this at arbitrary places during runtime (eg. NOT in ext_localconf.php or
180 * similar). ->set() is not supposed to be called each request since it writes LocalConfiguration
181 * each time. This API is however OK to be called from extension manager hooks.
182 * - Values are not type safe, if the install tool wrote them,
183 * boolean true could become string 1 on ->get()
184 * - It is not possible to store 'null' as value, giving $value=null
185 * or no value at all will unset the path
186 * - Setting a value and calling ->get() afterwards will still return the new value.
187 * - Warning on AdditionalConfiguration.php: If this file overwrites settings, it spoils the
188 * ->set() call and values may not end up as expected.
189 *
190 * @param string $extension Extension name
191 * @param string $path Configuration path to set - eg. "featureCategory/coolThingIsEnabled"
192 * @param null $value The value. If null, unset the path
193 * @internal
194 */
195 public function set(string $extension, string $path = '', $value = null)
196 {
197 if (empty($extension)) {
198 throw new \RuntimeException('extension name must not be empty', 1509715852);
199 }
200 if (!empty($path)) {
201 // @todo: this functionality can be removed once EXT:bootstrap_package is adapted to the new API.
202 $extensionConfiguration = $this->get($extension);
203 $value = ArrayUtility::setValueByPath($extensionConfiguration, $path, $value);
204 }
205 $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
206 if ($value === null) {
207 // Remove whole extension config
208 $configurationManager->removeLocalConfigurationKeysByPath(['EXTENSIONS/' . $extension]);
209 if (isset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension])) {
210 unset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension]);
211 }
212 } else {
213 // Set full extension config
214 $configurationManager->setLocalConfigurationValueByPath('EXTENSIONS/' . $extension, $value);
215 $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension] = $value;
216 }
217
218 // After TYPO3_CONF_VARS['EXTENSIONS'] has been written, update legacy layer TYPO3_CONF_VARS['EXTENSIONS']['extConf']
219 // @deprecated since TYPO3 v9, will be removed in v10 with removal of old serialized 'extConf' layer
220 if (!empty($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'])) {
221 $extConfArray = [];
222 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'] as $extensionName => $extensionConfig) {
223 $extConfArray[$extensionName] = serialize($this->addDotsToArrayKeysRecursiveForLegacyExtConf($extensionConfig));
224 }
225 $configurationManager->setLocalConfigurationValueByPath('EXT/extConf', $extConfArray);
226 $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'] = $extConfArray;
227 }
228 }
229
230 /**
231 * Set new configuration of all extensions and reload TYPO3_CONF_VARS.
232 * This is a "do all" variant of set() for all extensions that prevents
233 * writing and loading LocalConfiguration many times.
234 *
235 * @param array $configuration Configuration of all extensions
236 * @internal
237 */
238 public function setAll(array $configuration)
239 {
240 $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
241 $configurationManager->setLocalConfigurationValueByPath('EXTENSIONS', $configuration);
242 $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'] = $configuration;
243
244 // After TYPO3_CONF_VARS['EXTENSIONS'] has been written, update legacy layer TYPO3_CONF_VARS['EXTENSIONS']['extConf']
245 // @deprecated since TYPO3 v9, will be removed in v10 with removal of old serialized 'extConf' layer
246 $extConfArray = [];
247 foreach ($configuration as $extensionName => $extensionConfig) {
248 $extConfArray[$extensionName] = serialize($this->addDotsToArrayKeysRecursiveForLegacyExtConf($extensionConfig));
249 }
250 $configurationManager->setLocalConfigurationValueByPath('EXT/extConf', $extConfArray);
251 $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'] = $extConfArray;
252 }
253
254 /**
255 * If there are new config settings in ext_conf_template of an extension,
256 * they are found here and synchronized to LocalConfiguration['EXTENSIONS'].
257 *
258 * Used when entering the install tool, during installation and if calling ->get()
259 * with an extension or path that is not yet found in LocalConfiguration
260 *
261 * @internal
262 */
263 public function synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions()
264 {
265 $activePackages = GeneralUtility::makeInstance(PackageManager::class)->getActivePackages();
266 $fullConfiguration = [];
267 $currentLocalConfiguration = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'] ?? [];
268 foreach ($activePackages as $package) {
269 if (!@is_file($package->getPackagePath() . 'ext_conf_template.txt')) {
270 continue;
271 }
272 $extensionKey = $package->getPackageKey();
273 $currentExtensionConfig = $currentLocalConfiguration[$extensionKey] ?? [];
274 $extConfTemplateConfiguration = $this->getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots($extensionKey);
275 ArrayUtility::mergeRecursiveWithOverrule($extConfTemplateConfiguration, $currentExtensionConfig);
276 if (!empty($extConfTemplateConfiguration)) {
277 $fullConfiguration[$extensionKey] = $extConfTemplateConfiguration;
278 }
279 }
280 // Write new config if changed. Loose array comparison to not write if only array key order is different
281 if ($fullConfiguration != $currentLocalConfiguration) {
282 $this->setAll($fullConfiguration);
283 }
284 }
285
286 /**
287 * Read values from ext_conf_template, verify if they are in LocalConfiguration.php
288 * already and if not, add them.
289 *
290 * Used public by extension manager when updating extension
291 *
292 * @param string $extensionKey The extension to sync
293 * @internal
294 */
295 public function synchronizeExtConfTemplateWithLocalConfiguration(string $extensionKey)
296 {
297 $package = GeneralUtility::makeInstance(PackageManager::class)->getPackage($extensionKey);
298 if (!@is_file($package->getPackagePath() . 'ext_conf_template.txt')) {
299 return;
300 }
301 $currentLocalConfiguration = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extensionKey] ?? [];
302 $extConfTemplateConfiguration = $this->getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots($extensionKey);
303 ArrayUtility::mergeRecursiveWithOverrule($extConfTemplateConfiguration, $currentLocalConfiguration);
304 // Write new config if changed. Loose array comparison to not write if only array key order is different
305 if ($extConfTemplateConfiguration != $currentLocalConfiguration) {
306 $this->set($extensionKey, '', $extConfTemplateConfiguration);
307 }
308 }
309
310 /**
311 * The old EXT/extConf layer had '.' (dots) at the end of all nested array keys. This is created here
312 * to keep EXT/extConf format compatible with old not yet adapted extensions.
313 * But extensions may rely on ending dots if using legacy unserialize() on their extensions, too.
314 *
315 * A EXTENSIONS array like:
316 * TYPO3_CONF_VARS['EXTENSIONS']['someExtension'] => [
317 * 'someKey' => [
318 * 'someSubKey' => [
319 * 'someSubSubKey' => 'someValue',
320 * ],
321 * ],
322 * ]
323 * becomes (serialized) in old EXT/extConf (mind the dots and end of array keys for sub arrays):
324 * TYPO3_CONF_VARS['EXTENSIONS']['someExtension'] => [
325 * 'someKey.' => [
326 * 'someSubKey.' => [
327 * 'someSubSubKey' => 'someValue',
328 * ],
329 * ],
330 * ]
331 *
332 * @param array $extensionConfig
333 * @return array
334 * @deprecated since TYPO3 v9, will be removed in v10 with removal of old serialized 'extConf' layer
335 */
336 private function addDotsToArrayKeysRecursiveForLegacyExtConf(array $extensionConfig): array
337 {
338 $newArray = [];
339 foreach ($extensionConfig as $key => $value) {
340 if (is_array($value)) {
341 $newArray[$key . '.'] = $this->addDotsToArrayKeysRecursiveForLegacyExtConf($value);
342 } else {
343 $newArray[$key] = $value;
344 }
345 }
346 return $newArray;
347 }
348
349 /**
350 * Helper method of ext_conf_template.txt parsing.
351 *
352 * Poor man version of getDefaultConfigurationFromExtConfTemplateAsValuedArray() which ignores
353 * comments and returns ext_conf_template as array where nested keys have no dots.
354 *
355 * @param string $extensionKey
356 * @return array
357 */
358 protected function getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots(string $extensionKey): array
359 {
360 $configuration = $this->getParsedExtConfTemplate($extensionKey);
361 return $this->removeCommentsAndDotsRecursive($configuration);
362 }
363
364 /**
365 * Trigger main ext_conf_template.txt parsing logic.
366 * Needs to be public as it is used by install tool ExtensionConfigurationService
367 * which adds the comment parsing on top for display of options in install tool.
368 *
369 * @param string $extensionKey
370 * @return array
371 * @internal
372 */
373 public function getParsedExtConfTemplate(string $extensionKey): array
374 {
375 $rawConfigurationString = $this->getDefaultConfigurationRawString($extensionKey);
376 $configuration = [];
377 if ((string)$rawConfigurationString !== '') {
378 $this->raw = explode(LF, $rawConfigurationString);
379 $this->rawPointer = 0;
380 $this->setup = [];
381 $this->parseSub($this->setup);
382 if ($this->inBrace) {
383 throw new \RuntimeException(
384 'Line ' . ($this->rawPointer - 1) . ': The script is short of ' . $this->inBrace . ' end brace(s)',
385 1507645349
386 );
387 }
388 $configuration = $this->setup;
389 }
390 return $configuration;
391 }
392
393 /**
394 * Helper method of ext_conf_template.txt parsing.
395 *
396 * Return content of an extensions ext_conf_template.txt file if
397 * the file exists, empty string if file does not exist.
398 *
399 * @param string $extensionKey Extension key
400 * @return string
401 */
402 protected function getDefaultConfigurationRawString(string $extensionKey): string
403 {
404 $rawString = '';
405 $extConfTemplateFileLocation = GeneralUtility::getFileAbsFileName(
406 'EXT:' . $extensionKey . '/ext_conf_template.txt'
407 );
408 if (file_exists($extConfTemplateFileLocation)) {
409 $rawString = file_get_contents($extConfTemplateFileLocation);
410 }
411 return $rawString;
412 }
413
414 /**
415 * Helper method of ext_conf_template.txt parsing.
416 *
417 * "Comments" from the "TypoScript" parser below are identified by two (!) dots at the end of array keys
418 * and all array keys have a single dot at the end, if they have sub arrays. This is cleaned here.
419 *
420 * Incoming array:
421 * [
422 * 'automaticInstallation' => '1',
423 * 'automaticInstallation..' => '# cat=basic/enabled; ...'
424 * 'FE.' => [
425 * 'enabled' = '1',
426 * 'enabled..' => '# cat=basic/enabled; ...'
427 * ]
428 * ]
429 *
430 * Output array:
431 * [
432 * 'automaticInstallation' => '1',
433 * 'FE' => [
434 * 'enabled' => '1',
435 * ]
436 *
437 * @param array $config Incoming configuration
438 * @return array
439 */
440 protected function removeCommentsAndDotsRecursive(array $config): array
441 {
442 $cleanedConfig = [];
443 foreach ($config as $key => $value) {
444 if (substr($key, -2) === '..') {
445 continue;
446 }
447 if (substr($key, -1) === '.') {
448 $cleanedConfig[rtrim($key, '.')] = $this->removeCommentsAndDotsRecursive($value);
449 } else {
450 $cleanedConfig[$key] = $value;
451 }
452 }
453 return $cleanedConfig;
454 }
455
456 /**
457 * Helper method of ext_conf_template.txt parsing.
458 *
459 * Parsing the $this->raw TypoScript lines from pointer, $this->rawP
460 *
461 * @param array $setup Reference to the setup array in which to accumulate the values.
462 */
463 protected function parseSub(array &$setup)
464 {
465 while (isset($this->raw[$this->rawPointer])) {
466 $line = ltrim($this->raw[$this->rawPointer]);
467 $this->rawPointer++;
468 // Set comment flag?
469 if (strpos($line, '/*') === 0) {
470 $this->commentSet = 1;
471 }
472 if (!$this->commentSet && $line) {
473 if ($line[0] !== '}' && $line[0] !== '#' && $line[0] !== '/') {
474 // If not brace-end or comment
475 // Find object name string until we meet an operator
476 $varL = strcspn($line, TAB . ' {=<>(');
477 // check for special ":=" operator
478 if ($varL > 0 && substr($line, $varL - 1, 2) === ':=') {
479 --$varL;
480 }
481 // also remove tabs after the object string name
482 $objStrName = substr($line, 0, $varL);
483 if ($objStrName !== '') {
484 $r = [];
485 if (preg_match('/[^[:alnum:]_\\\\\\.:-]/i', $objStrName, $r)) {
486 throw new \RuntimeException(
487 'Line ' . ($this->rawPointer - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" contains invalid character "' . $r[0] . '". Must be alphanumeric or one of: "_:-\\."',
488 1507645381
489 );
490 }
491 $line = ltrim(substr($line, $varL));
492 if ($line === '') {
493 throw new \RuntimeException(
494 'Line ' . ($this->rawPointer - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({',
495 1507645417
496 );
497 }
498 switch ($line[0]) {
499 case '=':
500 if (strpos($objStrName, '.') !== false) {
501 $value = [];
502 $value[0] = trim(substr($line, 1));
503 $this->setVal($objStrName, $setup, $value);
504 } else {
505 $setup[$objStrName] = trim(substr($line, 1));
506 if ($this->lastComment) {
507 // Setting comment..
508 $setup[$objStrName . '..'] .= $this->lastComment;
509 }
510 }
511 break;
512 case '{':
513 $this->inBrace++;
514 if (strpos($objStrName, '.') !== false) {
515 $this->rollParseSub($objStrName, $setup);
516 } else {
517 if (!isset($setup[$objStrName . '.'])) {
518 $setup[$objStrName . '.'] = [];
519 }
520 $this->parseSub($setup[$objStrName . '.']);
521 }
522 break;
523 default:
524 throw new \RuntimeException(
525 'Line ' . ($this->rawPointer - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({',
526 1507645445
527 );
528 }
529
530 $this->lastComment = '';
531 }
532 } elseif ($line[0] === '}') {
533 $this->inBrace--;
534 $this->lastComment = '';
535 if ($this->inBrace < 0) {
536 throw new \RuntimeException(
537 'Line ' . ($this->rawPointer - 1) . ': An end brace is in excess.',
538 1507645489
539 );
540 }
541 break;
542 } else {
543 $this->lastComment .= rtrim($line) . LF;
544 }
545 }
546 // Unset comment
547 if ($this->commentSet) {
548 if (strpos($line, '*/') === 0) {
549 $this->commentSet = 0;
550 }
551 }
552 }
553 }
554
555 /**
556 * Helper method of ext_conf_template.txt parsing.
557 *
558 * Parsing of TypoScript keys inside a curly brace where the key is composite of at least two keys,
559 * thus having to recursively call itself to get the value.
560 *
561 * @param string $string The object sub-path, eg "thisprop.another_prot"
562 * @param array $setup The local setup array from the function calling this function
563 */
564 protected function rollParseSub($string, array &$setup)
565 {
566 if ((string)$string === '') {
567 return;
568 }
569 list($key, $remainingKey) = $this->parseNextKeySegment($string);
570 $key .= '.';
571 if (!isset($setup[$key])) {
572 $setup[$key] = [];
573 }
574 $remainingKey === ''
575 ? $this->parseSub($setup[$key])
576 : $this->rollParseSub($remainingKey, $setup[$key]);
577 }
578
579 /**
580 * Helper method of ext_conf_template.txt parsing.
581 *
582 * Setting a value/property of an object string in the setup array.
583 *
584 * @param string $string The object sub-path, eg "thisprop.another_prot
585 * @param array $setup The local setup array from the function calling this function.
586 * @param void
587 */
588 protected function setVal($string, array &$setup, $value)
589 {
590 if ((string)$string === '') {
591 return;
592 }
593
594 list($key, $remainingKey) = $this->parseNextKeySegment($string);
595 $subKey = $key . '.';
596 if ($remainingKey === '') {
597 if (isset($value[0])) {
598 $setup[$key] = $value[0];
599 }
600 if (isset($value[1])) {
601 $setup[$subKey] = $value[1];
602 }
603 if ($this->lastComment) {
604 $setup[$key . '..'] .= $this->lastComment;
605 }
606 } else {
607 if (!isset($setup[$subKey])) {
608 $setup[$subKey] = [];
609 }
610 $this->setVal($remainingKey, $setup[$subKey], $value);
611 }
612 }
613
614 /**
615 * Helper method of ext_conf_template.txt parsing.
616 *
617 * Determines the first key segment of a TypoScript key by searching for the first
618 * unescaped dot in the given key string.
619 *
620 * Since the escape characters are only needed to correctly determine the key
621 * segment any escape characters before the first unescaped dot are
622 * stripped from the key.
623 *
624 * @param string $key The key, possibly consisting of multiple key segments separated by unescaped dots
625 * @return array Array with key segment and remaining part of $key
626 */
627 protected function parseNextKeySegment($key): array
628 {
629 // if no dot is in the key, nothing to do
630 $dotPosition = strpos($key, '.');
631 if ($dotPosition === false) {
632 return [$key, ''];
633 }
634
635 if (strpos($key, '\\') !== false) {
636 // backslashes are in the key, so we do further parsing
637 while ($dotPosition !== false) {
638 if ($dotPosition > 0 && $key[$dotPosition - 1] !== '\\' || $dotPosition > 1 && $key[$dotPosition - 2] === '\\') {
639 break;
640 }
641 // escaped dot found, continue
642 $dotPosition = strpos($key, '.', $dotPosition + 1);
643 }
644
645 if ($dotPosition === false) {
646 // no regular dot found
647 $keySegment = $key;
648 $remainingKey = '';
649 } else {
650 if ($dotPosition > 1 && $key[$dotPosition - 2] === '\\' && $key[$dotPosition - 1] === '\\') {
651 $keySegment = substr($key, 0, $dotPosition - 1);
652 } else {
653 $keySegment = substr($key, 0, $dotPosition);
654 }
655 $remainingKey = substr($key, $dotPosition + 1);
656 }
657
658 // fix key segment by removing escape sequences
659 $keySegment = str_replace('\\.', '.', $keySegment);
660 } else {
661 // no backslash in the key, we're fine off
662 list($keySegment, $remainingKey) = explode('.', $key, 2);
663 }
664 return [$keySegment, $remainingKey];
665 }
666 }