c79e2db4d7e6b7add4627ecef02af2a41b93c4ac
[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 "FE.forceSalted = defaultValue"
104 * ->get('myExtension', 'FE/forceSalted')
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 * Most prominent usage is ext:saltedpasswords which uses sub keys like FE.forceSalted and BE.forceSalted,
314 * but extensions may rely on ending dots if using legacy unserialize() on their extensions, too.
315 *
316 * A EXTENSIONS array like:
317 * TYPO3_CONF_VARS['EXTENSIONS']['someExtension'] => [
318 * 'someKey' => [
319 * 'someSubKey' => [
320 * 'someSubSubKey' => 'someValue',
321 * ],
322 * ],
323 * ]
324 * becomes (serialized) in old EXT/extConf (mind the dots and end of array keys for sub arrays):
325 * TYPO3_CONF_VARS['EXTENSIONS']['someExtension'] => [
326 * 'someKey.' => [
327 * 'someSubKey.' => [
328 * 'someSubSubKey' => 'someValue',
329 * ],
330 * ],
331 * ]
332 *
333 * @param array $extensionConfig
334 * @return array
335 * @deprecated since TYPO3 v9, will be removed in v10 with removal of old serialized 'extConf' layer
336 */
337 private function addDotsToArrayKeysRecursiveForLegacyExtConf(array $extensionConfig): array
338 {
339 $newArray = [];
340 foreach ($extensionConfig as $key => $value) {
341 if (is_array($value)) {
342 $newArray[$key . '.'] = $this->addDotsToArrayKeysRecursiveForLegacyExtConf($value);
343 } else {
344 $newArray[$key] = $value;
345 }
346 }
347 return $newArray;
348 }
349
350 /**
351 * Helper method of ext_conf_template.txt parsing.
352 *
353 * Poor man version of getDefaultConfigurationFromExtConfTemplateAsValuedArray() which ignores
354 * comments and returns ext_conf_template as array where nested keys have no dots.
355 *
356 * @param string $extensionKey
357 * @return array
358 */
359 protected function getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots(string $extensionKey): array
360 {
361 $configuration = $this->getParsedExtConfTemplate($extensionKey);
362 return $this->removeCommentsAndDotsRecursive($configuration);
363 }
364
365 /**
366 * Trigger main ext_conf_template.txt parsing logic.
367 * Needs to be public as it is used by install tool ExtensionConfigurationService
368 * which adds the comment parsing on top for display of options in install tool.
369 *
370 * @param string $extensionKey
371 * @return array
372 * @internal
373 */
374 public function getParsedExtConfTemplate(string $extensionKey): array
375 {
376 $rawConfigurationString = $this->getDefaultConfigurationRawString($extensionKey);
377 $configuration = [];
378 if ((string)$rawConfigurationString !== '') {
379 $this->raw = explode(LF, $rawConfigurationString);
380 $this->rawPointer = 0;
381 $this->setup = [];
382 $this->parseSub($this->setup);
383 if ($this->inBrace) {
384 throw new \RuntimeException(
385 'Line ' . ($this->rawPointer - 1) . ': The script is short of ' . $this->inBrace . ' end brace(s)',
386 1507645349
387 );
388 }
389 $configuration = $this->setup;
390 }
391 return $configuration;
392 }
393
394 /**
395 * Helper method of ext_conf_template.txt parsing.
396 *
397 * Return content of an extensions ext_conf_template.txt file if
398 * the file exists, empty string if file does not exist.
399 *
400 * @param string $extensionKey Extension key
401 * @return string
402 */
403 protected function getDefaultConfigurationRawString(string $extensionKey): string
404 {
405 $rawString = '';
406 $extConfTemplateFileLocation = GeneralUtility::getFileAbsFileName(
407 'EXT:' . $extensionKey . '/ext_conf_template.txt'
408 );
409 if (file_exists($extConfTemplateFileLocation)) {
410 $rawString = file_get_contents($extConfTemplateFileLocation);
411 }
412 return $rawString;
413 }
414
415 /**
416 * Helper method of ext_conf_template.txt parsing.
417 *
418 * "Comments" from the "TypoScript" parser below are identified by two (!) dots at the end of array keys
419 * and all array keys have a single dot at the end, if they have sub arrays. This is cleaned here.
420 *
421 * Incoming array:
422 * [
423 * 'automaticInstallation' => '1',
424 * 'automaticInstallation..' => '# cat=basic/enabled; ...'
425 * 'FE.' => [
426 * 'enabled' = '1',
427 * 'enabled..' => '# cat=basic/enabled; ...'
428 * ]
429 * ]
430 *
431 * Output array:
432 * [
433 * 'automaticInstallation' => '1',
434 * 'FE' => [
435 * 'enabled' => '1',
436 * ]
437 *
438 * @param array $config Incoming configuration
439 * @return array
440 */
441 protected function removeCommentsAndDotsRecursive(array $config): array
442 {
443 $cleanedConfig = [];
444 foreach ($config as $key => $value) {
445 if (substr($key, -2) === '..') {
446 continue;
447 }
448 if (substr($key, -1) === '.') {
449 $cleanedConfig[rtrim($key, '.')] = $this->removeCommentsAndDotsRecursive($value);
450 } else {
451 $cleanedConfig[$key] = $value;
452 }
453 }
454 return $cleanedConfig;
455 }
456
457 /**
458 * Helper method of ext_conf_template.txt parsing.
459 *
460 * Parsing the $this->raw TypoScript lines from pointer, $this->rawP
461 *
462 * @param array $setup Reference to the setup array in which to accumulate the values.
463 */
464 protected function parseSub(array &$setup)
465 {
466 while (isset($this->raw[$this->rawPointer])) {
467 $line = ltrim($this->raw[$this->rawPointer]);
468 $this->rawPointer++;
469 // Set comment flag?
470 if (strpos($line, '/*') === 0) {
471 $this->commentSet = 1;
472 }
473 if (!$this->commentSet && $line) {
474 if ($line[0] !== '}' && $line[0] !== '#' && $line[0] !== '/') {
475 // If not brace-end or comment
476 // Find object name string until we meet an operator
477 $varL = strcspn($line, TAB . ' {=<>(');
478 // check for special ":=" operator
479 if ($varL > 0 && substr($line, $varL - 1, 2) === ':=') {
480 --$varL;
481 }
482 // also remove tabs after the object string name
483 $objStrName = substr($line, 0, $varL);
484 if ($objStrName !== '') {
485 $r = [];
486 if (preg_match('/[^[:alnum:]_\\\\\\.:-]/i', $objStrName, $r)) {
487 throw new \RuntimeException(
488 'Line ' . ($this->rawPointer - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" contains invalid character "' . $r[0] . '". Must be alphanumeric or one of: "_:-\\."',
489 1507645381
490 );
491 }
492 $line = ltrim(substr($line, $varL));
493 if ($line === '') {
494 throw new \RuntimeException(
495 'Line ' . ($this->rawPointer - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({',
496 1507645417
497 );
498 }
499 switch ($line[0]) {
500 case '=':
501 if (strpos($objStrName, '.') !== false) {
502 $value = [];
503 $value[0] = trim(substr($line, 1));
504 $this->setVal($objStrName, $setup, $value);
505 } else {
506 $setup[$objStrName] = trim(substr($line, 1));
507 if ($this->lastComment) {
508 // Setting comment..
509 $setup[$objStrName . '..'] .= $this->lastComment;
510 }
511 }
512 break;
513 case '{':
514 $this->inBrace++;
515 if (strpos($objStrName, '.') !== false) {
516 $this->rollParseSub($objStrName, $setup);
517 } else {
518 if (!isset($setup[$objStrName . '.'])) {
519 $setup[$objStrName . '.'] = [];
520 }
521 $this->parseSub($setup[$objStrName . '.']);
522 }
523 break;
524 default:
525 throw new \RuntimeException(
526 'Line ' . ($this->rawPointer - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({',
527 1507645445
528 );
529 }
530
531 $this->lastComment = '';
532 }
533 } elseif ($line[0] === '}') {
534 $this->inBrace--;
535 $this->lastComment = '';
536 if ($this->inBrace < 0) {
537 throw new \RuntimeException(
538 'Line ' . ($this->rawPointer - 1) . ': An end brace is in excess.',
539 1507645489
540 );
541 }
542 break;
543 } else {
544 $this->lastComment .= rtrim($line) . LF;
545 }
546 }
547 // Unset comment
548 if ($this->commentSet) {
549 if (strpos($line, '*/') === 0) {
550 $this->commentSet = 0;
551 }
552 }
553 }
554 }
555
556 /**
557 * Helper method of ext_conf_template.txt parsing.
558 *
559 * Parsing of TypoScript keys inside a curly brace where the key is composite of at least two keys,
560 * thus having to recursively call itself to get the value.
561 *
562 * @param string $string The object sub-path, eg "thisprop.another_prot"
563 * @param array $setup The local setup array from the function calling this function
564 */
565 protected function rollParseSub($string, array &$setup)
566 {
567 if ((string)$string === '') {
568 return;
569 }
570 list($key, $remainingKey) = $this->parseNextKeySegment($string);
571 $key .= '.';
572 if (!isset($setup[$key])) {
573 $setup[$key] = [];
574 }
575 $remainingKey === ''
576 ? $this->parseSub($setup[$key])
577 : $this->rollParseSub($remainingKey, $setup[$key]);
578 }
579
580 /**
581 * Helper method of ext_conf_template.txt parsing.
582 *
583 * Setting a value/property of an object string in the setup array.
584 *
585 * @param string $string The object sub-path, eg "thisprop.another_prot
586 * @param array $setup The local setup array from the function calling this function.
587 * @param void
588 */
589 protected function setVal($string, array &$setup, $value)
590 {
591 if ((string)$string === '') {
592 return;
593 }
594
595 list($key, $remainingKey) = $this->parseNextKeySegment($string);
596 $subKey = $key . '.';
597 if ($remainingKey === '') {
598 if (isset($value[0])) {
599 $setup[$key] = $value[0];
600 }
601 if (isset($value[1])) {
602 $setup[$subKey] = $value[1];
603 }
604 if ($this->lastComment) {
605 $setup[$key . '..'] .= $this->lastComment;
606 }
607 } else {
608 if (!isset($setup[$subKey])) {
609 $setup[$subKey] = [];
610 }
611 $this->setVal($remainingKey, $setup[$subKey], $value);
612 }
613 }
614
615 /**
616 * Helper method of ext_conf_template.txt parsing.
617 *
618 * Determines the first key segment of a TypoScript key by searching for the first
619 * unescaped dot in the given key string.
620 *
621 * Since the escape characters are only needed to correctly determine the key
622 * segment any escape characters before the first unescaped dot are
623 * stripped from the key.
624 *
625 * @param string $key The key, possibly consisting of multiple key segments separated by unescaped dots
626 * @return array Array with key segment and remaining part of $key
627 */
628 protected function parseNextKeySegment($key): array
629 {
630 // if no dot is in the key, nothing to do
631 $dotPosition = strpos($key, '.');
632 if ($dotPosition === false) {
633 return [$key, ''];
634 }
635
636 if (strpos($key, '\\') !== false) {
637 // backslashes are in the key, so we do further parsing
638 while ($dotPosition !== false) {
639 if ($dotPosition > 0 && $key[$dotPosition - 1] !== '\\' || $dotPosition > 1 && $key[$dotPosition - 2] === '\\') {
640 break;
641 }
642 // escaped dot found, continue
643 $dotPosition = strpos($key, '.', $dotPosition + 1);
644 }
645
646 if ($dotPosition === false) {
647 // no regular dot found
648 $keySegment = $key;
649 $remainingKey = '';
650 } else {
651 if ($dotPosition > 1 && $key[$dotPosition - 2] === '\\' && $key[$dotPosition - 1] === '\\') {
652 $keySegment = substr($key, 0, $dotPosition - 1);
653 } else {
654 $keySegment = substr($key, 0, $dotPosition);
655 }
656 $remainingKey = substr($key, $dotPosition + 1);
657 }
658
659 // fix key segment by removing escape sequences
660 $keySegment = str_replace('\\.', '.', $keySegment);
661 } else {
662 // no backslash in the key, we're fine off
663 list($keySegment, $remainingKey) = explode('.', $key, 2);
664 }
665 return [$keySegment, $remainingKey];
666 }
667 }