[SECURITY] Disallow insecure deserialization for l18n_diffsource
[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 */
127 public function get(string $extension, string $path = '')
128 {
129 $hasBeenSynchronized = false;
130 if (!isset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension]) || !is_array($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension])) {
131 // This if() should not be hit at "casual" runtime, but only in early setup phases
132 $this->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions();
133 $hasBeenSynchronized = true;
134 if (!isset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension]) || !is_array($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension])) {
135 // If there is still no such entry, even after sync -> throw
136 throw new ExtensionConfigurationExtensionNotConfiguredException(
137 'No extension configuration for extension ' . $extension . ' found. Either this extension'
138 . ' has no extension configuration or the configuration is not up to date. Execute the'
139 . ' install tool to update configuration.',
140 1509654728
141 );
142 }
143 }
144 if (empty($path)) {
145 return $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension];
146 }
147 if (!ArrayUtility::isValidPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path)) {
148 // This if() should not be hit at "casual" runtime, but only in early setup phases
149 if (!$hasBeenSynchronized) {
150 $this->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions();
151 }
152 // If there is still no such entry, even after sync -> throw
153 if (!ArrayUtility::isValidPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path)) {
154 throw new ExtensionConfigurationPathDoesNotExistException(
155 'Path ' . $path . ' does not exist in extension configuration',
156 1509977699
157 );
158 }
159 }
160 return ArrayUtility::getValueByPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path);
161 }
162
163 /**
164 * Store a new or overwrite an existing configuration value.
165 *
166 * This is typically used by core internal low level tasks like the install
167 * tool but may become handy if an extension needs to update extension configuration
168 * on the fly for whatever reason.
169 *
170 * Examples:
171 * // Set a full extension configuration ($value could be a nested array, too)
172 * ->set('myExtension', ['aFeature' => 'true', 'aCustomClass' => 'css-foo'])
173 *
174 * // Unset a whole extension configuration
175 * ->set('myExtension')
176 *
177 * Notes:
178 * - Do NOT call this at arbitrary places during runtime (eg. NOT in ext_localconf.php or
179 * similar). ->set() is not supposed to be called each request since it writes LocalConfiguration
180 * each time. This API is however OK to be called from extension manager hooks.
181 * - Values are not type safe, if the install tool wrote them,
182 * boolean true could become string 1 on ->get()
183 * - It is not possible to store 'null' as value, giving $value=null
184 * or no value at all will unset the path
185 * - Setting a value and calling ->get() afterwards will still return the new value.
186 * - Warning on AdditionalConfiguration.php: If this file overwrites settings, it spoils the
187 * ->set() call and values may not end up as expected.
188 *
189 * @param string $extension Extension name
190 * @param string $path Configuration path to set - eg. "featureCategory/coolThingIsEnabled"
191 * @param null $value The value. If null, unset the path
192 * @internal
193 */
194 public function set(string $extension, string $path = '', $value = null)
195 {
196 if (empty($extension)) {
197 throw new \RuntimeException('extension name must not be empty', 1509715852);
198 }
199 if (!empty($path)) {
200 // @todo: this functionality can be removed once EXT:bootstrap_package is adapted to the new API.
201 $extensionConfiguration = $this->get($extension);
202 $value = ArrayUtility::setValueByPath($extensionConfiguration, $path, $value);
203 }
204 $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
205 if ($value === null) {
206 // Remove whole extension config
207 $configurationManager->removeLocalConfigurationKeysByPath(['EXTENSIONS/' . $extension]);
208 if (isset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension])) {
209 unset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension]);
210 }
211 } else {
212 // Set full extension config
213 $configurationManager->setLocalConfigurationValueByPath('EXTENSIONS/' . $extension, $value);
214 $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension] = $value;
215 }
216 }
217
218 /**
219 * Set new configuration of all extensions and reload TYPO3_CONF_VARS.
220 * This is a "do all" variant of set() for all extensions that prevents
221 * writing and loading LocalConfiguration many times.
222 *
223 * @param array $configuration Configuration of all extensions
224 * @internal
225 */
226 public function setAll(array $configuration)
227 {
228 $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
229 $configurationManager->setLocalConfigurationValueByPath('EXTENSIONS', $configuration);
230 $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'] = $configuration;
231 }
232
233 /**
234 * If there are new config settings in ext_conf_template of an extension,
235 * they are found here and synchronized to LocalConfiguration['EXTENSIONS'].
236 *
237 * Used when entering the install tool, during installation and if calling ->get()
238 * with an extension or path that is not yet found in LocalConfiguration
239 *
240 * @internal
241 */
242 public function synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions()
243 {
244 $activePackages = GeneralUtility::makeInstance(PackageManager::class)->getActivePackages();
245 $fullConfiguration = [];
246 $currentLocalConfiguration = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'] ?? [];
247 foreach ($activePackages as $package) {
248 if (!@is_file($package->getPackagePath() . 'ext_conf_template.txt')) {
249 continue;
250 }
251 $extensionKey = $package->getPackageKey();
252 $currentExtensionConfig = $currentLocalConfiguration[$extensionKey] ?? [];
253 $extConfTemplateConfiguration = $this->getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots($extensionKey);
254 ArrayUtility::mergeRecursiveWithOverrule($extConfTemplateConfiguration, $currentExtensionConfig);
255 if (!empty($extConfTemplateConfiguration)) {
256 $fullConfiguration[$extensionKey] = $extConfTemplateConfiguration;
257 }
258 }
259 // Write new config if changed. Loose array comparison to not write if only array key order is different
260 if ($fullConfiguration != $currentLocalConfiguration) {
261 $this->setAll($fullConfiguration);
262 }
263 }
264
265 /**
266 * Read values from ext_conf_template, verify if they are in LocalConfiguration.php
267 * already and if not, add them.
268 *
269 * Used public by extension manager when updating extension
270 *
271 * @param string $extensionKey The extension to sync
272 * @internal
273 */
274 public function synchronizeExtConfTemplateWithLocalConfiguration(string $extensionKey)
275 {
276 $package = GeneralUtility::makeInstance(PackageManager::class)->getPackage($extensionKey);
277 if (!@is_file($package->getPackagePath() . 'ext_conf_template.txt')) {
278 return;
279 }
280 $currentLocalConfiguration = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extensionKey] ?? [];
281 $extConfTemplateConfiguration = $this->getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots($extensionKey);
282 ArrayUtility::mergeRecursiveWithOverrule($extConfTemplateConfiguration, $currentLocalConfiguration);
283 // Write new config if changed. Loose array comparison to not write if only array key order is different
284 if ($extConfTemplateConfiguration != $currentLocalConfiguration) {
285 $this->set($extensionKey, '', $extConfTemplateConfiguration);
286 }
287 }
288
289 /**
290 * Helper method of ext_conf_template.txt parsing.
291 *
292 * Poor man version of getDefaultConfigurationFromExtConfTemplateAsValuedArray() which ignores
293 * comments and returns ext_conf_template as array where nested keys have no dots.
294 *
295 * @param string $extensionKey
296 * @return array
297 */
298 protected function getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots(string $extensionKey): array
299 {
300 $configuration = $this->getParsedExtConfTemplate($extensionKey);
301 return $this->removeCommentsAndDotsRecursive($configuration);
302 }
303
304 /**
305 * Trigger main ext_conf_template.txt parsing logic.
306 * Needs to be public as it is used by install tool ExtensionConfigurationService
307 * which adds the comment parsing on top for display of options in install tool.
308 *
309 * @param string $extensionKey
310 * @return array
311 * @internal
312 */
313 public function getParsedExtConfTemplate(string $extensionKey): array
314 {
315 $rawConfigurationString = $this->getDefaultConfigurationRawString($extensionKey);
316 $configuration = [];
317 if ((string)$rawConfigurationString !== '') {
318 $this->raw = explode(LF, $rawConfigurationString);
319 $this->rawPointer = 0;
320 $this->setup = [];
321 $this->parseSub($this->setup);
322 if ($this->inBrace) {
323 throw new \RuntimeException(
324 'Line ' . ($this->rawPointer - 1) . ': The script is short of ' . $this->inBrace . ' end brace(s)',
325 1507645349
326 );
327 }
328 $configuration = $this->setup;
329 }
330 return $configuration;
331 }
332
333 /**
334 * Helper method of ext_conf_template.txt parsing.
335 *
336 * Return content of an extensions ext_conf_template.txt file if
337 * the file exists, empty string if file does not exist.
338 *
339 * @param string $extensionKey Extension key
340 * @return string
341 */
342 protected function getDefaultConfigurationRawString(string $extensionKey): string
343 {
344 $rawString = '';
345 $extConfTemplateFileLocation = GeneralUtility::getFileAbsFileName(
346 'EXT:' . $extensionKey . '/ext_conf_template.txt'
347 );
348 if (file_exists($extConfTemplateFileLocation)) {
349 $rawString = file_get_contents($extConfTemplateFileLocation);
350 }
351 return $rawString;
352 }
353
354 /**
355 * Helper method of ext_conf_template.txt parsing.
356 *
357 * "Comments" from the "TypoScript" parser below are identified by two (!) dots at the end of array keys
358 * and all array keys have a single dot at the end, if they have sub arrays. This is cleaned here.
359 *
360 * Incoming array:
361 * [
362 * 'automaticInstallation' => '1',
363 * 'automaticInstallation..' => '# cat=basic/enabled; ...'
364 * 'FE.' => [
365 * 'enabled' = '1',
366 * 'enabled..' => '# cat=basic/enabled; ...'
367 * ]
368 * ]
369 *
370 * Output array:
371 * [
372 * 'automaticInstallation' => '1',
373 * 'FE' => [
374 * 'enabled' => '1',
375 * ]
376 *
377 * @param array $config Incoming configuration
378 * @return array
379 */
380 protected function removeCommentsAndDotsRecursive(array $config): array
381 {
382 $cleanedConfig = [];
383 foreach ($config as $key => $value) {
384 if (substr($key, -2) === '..') {
385 continue;
386 }
387 if (substr($key, -1) === '.') {
388 $cleanedConfig[rtrim($key, '.')] = $this->removeCommentsAndDotsRecursive($value);
389 } else {
390 $cleanedConfig[$key] = $value;
391 }
392 }
393 return $cleanedConfig;
394 }
395
396 /**
397 * Helper method of ext_conf_template.txt parsing.
398 *
399 * Parsing the $this->raw TypoScript lines from pointer, $this->rawP
400 *
401 * @param array $setup Reference to the setup array in which to accumulate the values.
402 */
403 protected function parseSub(array &$setup)
404 {
405 while (isset($this->raw[$this->rawPointer])) {
406 $line = ltrim($this->raw[$this->rawPointer]);
407 $this->rawPointer++;
408 // Set comment flag?
409 if (strpos($line, '/*') === 0) {
410 $this->commentSet = 1;
411 }
412 if (!$this->commentSet && $line) {
413 if ($line[0] !== '}' && $line[0] !== '#' && $line[0] !== '/') {
414 // If not brace-end or comment
415 // Find object name string until we meet an operator
416 $varL = strcspn($line, "\t" . ' {=<>(');
417 // check for special ":=" operator
418 if ($varL > 0 && substr($line, $varL - 1, 2) === ':=') {
419 --$varL;
420 }
421 // also remove tabs after the object string name
422 $objStrName = substr($line, 0, $varL);
423 if ($objStrName !== '') {
424 $r = [];
425 if (preg_match('/[^[:alnum:]_\\\\\\.:-]/i', $objStrName, $r)) {
426 throw new \RuntimeException(
427 'Line ' . ($this->rawPointer - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" contains invalid character "' . $r[0] . '". Must be alphanumeric or one of: "_:-\\."',
428 1507645381
429 );
430 }
431 $line = ltrim(substr($line, $varL));
432 if ($line === '') {
433 throw new \RuntimeException(
434 'Line ' . ($this->rawPointer - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({',
435 1507645417
436 );
437 }
438 switch ($line[0]) {
439 case '=':
440 if (strpos($objStrName, '.') !== false) {
441 $value = [];
442 $value[0] = trim(substr($line, 1));
443 $this->setVal($objStrName, $setup, $value);
444 } else {
445 $setup[$objStrName] = trim(substr($line, 1));
446 if ($this->lastComment) {
447 // Setting comment..
448 $setup[$objStrName . '..'] .= $this->lastComment;
449 }
450 }
451 break;
452 case '{':
453 $this->inBrace++;
454 if (strpos($objStrName, '.') !== false) {
455 $this->rollParseSub($objStrName, $setup);
456 } else {
457 if (!isset($setup[$objStrName . '.'])) {
458 $setup[$objStrName . '.'] = [];
459 }
460 $this->parseSub($setup[$objStrName . '.']);
461 }
462 break;
463 default:
464 throw new \RuntimeException(
465 'Line ' . ($this->rawPointer - 1) . ': Object Name String, "' . htmlspecialchars($objStrName) . '" was not followed by any operator, =<>({',
466 1507645445
467 );
468 }
469
470 $this->lastComment = '';
471 }
472 } elseif ($line[0] === '}') {
473 $this->inBrace--;
474 $this->lastComment = '';
475 if ($this->inBrace < 0) {
476 throw new \RuntimeException(
477 'Line ' . ($this->rawPointer - 1) . ': An end brace is in excess.',
478 1507645489
479 );
480 }
481 break;
482 } else {
483 $this->lastComment .= rtrim($line) . LF;
484 }
485 }
486 // Unset comment
487 if ($this->commentSet) {
488 if (strpos($line, '*/') === 0) {
489 $this->commentSet = 0;
490 }
491 }
492 }
493 }
494
495 /**
496 * Helper method of ext_conf_template.txt parsing.
497 *
498 * Parsing of TypoScript keys inside a curly brace where the key is composite of at least two keys,
499 * thus having to recursively call itself to get the value.
500 *
501 * @param string $string The object sub-path, eg "thisprop.another_prot"
502 * @param array $setup The local setup array from the function calling this function
503 */
504 protected function rollParseSub($string, array &$setup)
505 {
506 if ((string)$string === '') {
507 return;
508 }
509 list($key, $remainingKey) = $this->parseNextKeySegment($string);
510 $key .= '.';
511 if (!isset($setup[$key])) {
512 $setup[$key] = [];
513 }
514 $remainingKey === ''
515 ? $this->parseSub($setup[$key])
516 : $this->rollParseSub($remainingKey, $setup[$key]);
517 }
518
519 /**
520 * Helper method of ext_conf_template.txt parsing.
521 *
522 * Setting a value/property of an object string in the setup array.
523 *
524 * @param string $string The object sub-path, eg "thisprop.another_prot
525 * @param array $setup The local setup array from the function calling this function.
526 * @param void
527 */
528 protected function setVal($string, array &$setup, $value)
529 {
530 if ((string)$string === '') {
531 return;
532 }
533
534 list($key, $remainingKey) = $this->parseNextKeySegment($string);
535 $subKey = $key . '.';
536 if ($remainingKey === '') {
537 if (isset($value[0])) {
538 $setup[$key] = $value[0];
539 }
540 if (isset($value[1])) {
541 $setup[$subKey] = $value[1];
542 }
543 if ($this->lastComment) {
544 $setup[$key . '..'] .= $this->lastComment;
545 }
546 } else {
547 if (!isset($setup[$subKey])) {
548 $setup[$subKey] = [];
549 }
550 $this->setVal($remainingKey, $setup[$subKey], $value);
551 }
552 }
553
554 /**
555 * Helper method of ext_conf_template.txt parsing.
556 *
557 * Determines the first key segment of a TypoScript key by searching for the first
558 * unescaped dot in the given key string.
559 *
560 * Since the escape characters are only needed to correctly determine the key
561 * segment any escape characters before the first unescaped dot are
562 * stripped from the key.
563 *
564 * @param string $key The key, possibly consisting of multiple key segments separated by unescaped dots
565 * @return array Array with key segment and remaining part of $key
566 */
567 protected function parseNextKeySegment($key): array
568 {
569 // if no dot is in the key, nothing to do
570 $dotPosition = strpos($key, '.');
571 if ($dotPosition === false) {
572 return [$key, ''];
573 }
574
575 if (strpos($key, '\\') !== false) {
576 // backslashes are in the key, so we do further parsing
577 while ($dotPosition !== false) {
578 if ($dotPosition > 0 && $key[$dotPosition - 1] !== '\\' || $dotPosition > 1 && $key[$dotPosition - 2] === '\\') {
579 break;
580 }
581 // escaped dot found, continue
582 $dotPosition = strpos($key, '.', $dotPosition + 1);
583 }
584
585 if ($dotPosition === false) {
586 // no regular dot found
587 $keySegment = $key;
588 $remainingKey = '';
589 } else {
590 if ($dotPosition > 1 && $key[$dotPosition - 2] === '\\' && $key[$dotPosition - 1] === '\\') {
591 $keySegment = substr($key, 0, $dotPosition - 1);
592 } else {
593 $keySegment = substr($key, 0, $dotPosition);
594 }
595 $remainingKey = substr($key, $dotPosition + 1);
596 }
597
598 // fix key segment by removing escape sequences
599 $keySegment = str_replace('\\.', '.', $keySegment);
600 } else {
601 // no backslash in the key, we're fine off
602 list($keySegment, $remainingKey) = explode('.', $key, 2);
603 }
604 return [$keySegment, $remainingKey];
605 }
606 }