1<?php 2 3declare(strict_types=1); 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 18namespace TYPO3\CMS\Core\Configuration; 19 20use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException; 21use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException; 22use TYPO3\CMS\Core\Package\PackageManager; 23use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser; 24use TYPO3\CMS\Core\Utility\ArrayUtility; 25use TYPO3\CMS\Core\Utility\GeneralUtility; 26 27/** 28 * API to get() instance specific extension configuration options. 29 * 30 * Extension authors are encouraged to use this API - it is currently a simple 31 * wrapper to access TYPO3_CONF_VARS['EXTENSIONS'] but could later become something 32 * different in case core decides to store extension configuration elsewhere. 33 * 34 * Extension authors must not access TYPO3_CONF_VARS['EXTENSIONS'] on their own. 35 * 36 * Extension configurations are often 'feature flags' currently defined by 37 * ext_conf_template.txt files. The core (more specifically the install tool) 38 * takes care default values and overridden values are properly prepared upon 39 * loading or updating an extension. 40 * 41 * Note only ->get() is official API and other public methods are low level 42 * core internal API that is usually only used by extension manager and install tool. 43 */ 44class ExtensionConfiguration 45{ 46 /** 47 * Get a single configuration value, a sub array or the whole configuration. 48 * 49 * Examples: 50 * // Simple and typical usage: Get a single config value, or an array if the key is a "TypoScript" 51 * // a-like sub-path in ext_conf_template.txt "foo.bar = defaultValue" 52 * ->get('myExtension', 'aConfigKey'); 53 * 54 * // Get all current configuration values, always an array 55 * ->get('myExtension'); 56 * 57 * // Get a nested config value if the path is a "TypoScript" a-like sub-path 58 * // in ext_conf_template.txt "topLevelKey.subLevelKey = defaultValue" 59 * ->get('myExtension', 'topLevelKey/subLevelKey') 60 * 61 * Notes: 62 * - If a configuration or configuration path of an extension is not found, the 63 * code tries to synchronize configuration with ext_conf_template.txt first, only 64 * if still not found, it will throw exceptions. 65 * - Return values are NOT type safe: A boolean false could be returned as string 0. 66 * Cast accordingly. 67 * - This API throws exceptions if the path does not exist or the extension 68 * configuration is not available. The install tool takes care any new 69 * ext_conf_template.txt values are available TYPO3_CONF_VARS['EXTENSIONS'], 70 * a thrown exception indicates a programming error on developer side 71 * and should not be caught. 72 * - It is not checked if the extension in question is loaded at all, 73 * it's just checked the extension configuration path exists. 74 * - Extensions should typically not get configuration of a different extension. 75 * 76 * @param string $extension Extension name 77 * @param string $path Configuration path - e.g. "featureCategory/coolThingIsEnabled" 78 * @return mixed The value. Can be a sub array or a single value. 79 * @throws ExtensionConfigurationExtensionNotConfiguredException If the extension configuration does not exist 80 * @throws ExtensionConfigurationPathDoesNotExistException If a requested path in the extension configuration does not exist 81 */ 82 public function get(string $extension, string $path = '') 83 { 84 $hasBeenSynchronized = false; 85 if (!$this->hasConfiguration($extension)) { 86 // This if() should not be hit at "casual" runtime, but only in early setup phases 87 $this->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions(true); 88 $hasBeenSynchronized = true; 89 if (!$this->hasConfiguration($extension)) { 90 // If there is still no such entry, even after sync -> throw 91 throw new ExtensionConfigurationExtensionNotConfiguredException( 92 'No extension configuration for extension ' . $extension . ' found. Either this extension' 93 . ' has no extension configuration or the configuration is not up to date. Execute the' 94 . ' install tool to update configuration.', 95 1509654728 96 ); 97 } 98 } 99 if (empty($path)) { 100 return $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension]; 101 } 102 if (!ArrayUtility::isValidPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path)) { 103 // This if() should not be hit at "casual" runtime, but only in early setup phases 104 if (!$hasBeenSynchronized) { 105 $this->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions(true); 106 } 107 // If there is still no such entry, even after sync -> throw 108 if (!ArrayUtility::isValidPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path)) { 109 throw new ExtensionConfigurationPathDoesNotExistException( 110 'Path ' . $path . ' does not exist in extension configuration', 111 1509977699 112 ); 113 } 114 } 115 return ArrayUtility::getValueByPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path); 116 } 117 118 /** 119 * Store a new or overwrite an existing configuration value. 120 * 121 * This is typically used by core internal low level tasks like the install 122 * tool but may become handy if an extension needs to update extension configuration 123 * on the fly for whatever reason. 124 * 125 * Examples: 126 * // Set a full extension configuration ($value could be a nested array, too) 127 * ->set('myExtension', ['aFeature' => 'true', 'aCustomClass' => 'css-foo']) 128 * 129 * // Unset a whole extension configuration 130 * ->set('myExtension') 131 * 132 * Notes: 133 * - Do NOT call this at arbitrary places during runtime (eg. NOT in ext_localconf.php or 134 * similar). ->set() is not supposed to be called each request since it writes LocalConfiguration 135 * each time. This API is however OK to be called from extension manager hooks. 136 * - Values are not type safe, if the install tool wrote them, 137 * boolean true could become string 1 on ->get() 138 * - It is not possible to store 'null' as value, giving $value=null 139 * or no value at all will unset the path 140 * - Setting a value and calling ->get() afterwards will still return the new value. 141 * - Warning on AdditionalConfiguration.php: If this file overwrites settings, it spoils the 142 * ->set() call and values may not end up as expected. 143 * 144 * @param string $extension Extension name 145 * @param mixed|null $value The value. If null, unset the path 146 * @internal 147 */ 148 public function set(string $extension, $value = null): void 149 { 150 if (empty($extension)) { 151 throw new \RuntimeException('extension name must not be empty', 1509715852); 152 } 153 $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class); 154 if ($value === null) { 155 // Remove whole extension config 156 $configurationManager->removeLocalConfigurationKeysByPath(['EXTENSIONS/' . $extension]); 157 if (isset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension])) { 158 unset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension]); 159 } 160 } else { 161 // Set full extension config 162 $configurationManager->setLocalConfigurationValueByPath('EXTENSIONS/' . $extension, $value); 163 $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension] = $value; 164 } 165 } 166 167 /** 168 * Set new configuration of all extensions and reload TYPO3_CONF_VARS. 169 * This is a "do all" variant of set() for all extensions that prevents 170 * writing and loading LocalConfiguration many times. 171 * 172 * @param array $configuration Configuration of all extensions 173 * @param bool $skipWriteIfLocalConfiguationDoesNotExist 174 * @internal 175 */ 176 public function setAll(array $configuration, bool $skipWriteIfLocalConfiguationDoesNotExist = false): void 177 { 178 $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class); 179 if ($skipWriteIfLocalConfiguationDoesNotExist === false || @file_exists($configurationManager->getLocalConfigurationFileLocation())) { 180 $configurationManager->setLocalConfigurationValueByPath('EXTENSIONS', $configuration); 181 } 182 $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'] = $configuration; 183 } 184 185 /** 186 * If there are new config settings in ext_conf_template of an extension, 187 * they are found here and synchronized to LocalConfiguration['EXTENSIONS']. 188 * 189 * Used when entering the install tool, during installation and if calling ->get() 190 * with an extension or path that is not yet found in LocalConfiguration 191 * 192 * @param bool $skipWriteIfLocalConfiguationDoesNotExist 193 * @internal 194 */ 195 public function synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions(bool $skipWriteIfLocalConfiguationDoesNotExist = false): void 196 { 197 $activePackages = GeneralUtility::makeInstance(PackageManager::class)->getActivePackages(); 198 $fullConfiguration = []; 199 $currentLocalConfiguration = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'] ?? []; 200 foreach ($activePackages as $package) { 201 if (!@is_file($package->getPackagePath() . 'ext_conf_template.txt')) { 202 continue; 203 } 204 $extensionKey = $package->getPackageKey(); 205 $currentExtensionConfig = $currentLocalConfiguration[$extensionKey] ?? []; 206 $extConfTemplateConfiguration = $this->getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots($extensionKey); 207 ArrayUtility::mergeRecursiveWithOverrule($extConfTemplateConfiguration, $currentExtensionConfig); 208 if (!empty($extConfTemplateConfiguration)) { 209 $fullConfiguration[$extensionKey] = $extConfTemplateConfiguration; 210 } 211 } 212 // Write new config if changed. Loose array comparison to not write if only array key order is different 213 if ($fullConfiguration != $currentLocalConfiguration) { 214 $this->setAll($fullConfiguration, $skipWriteIfLocalConfiguationDoesNotExist); 215 } 216 } 217 218 /** 219 * Read values from ext_conf_template, verify if they are in LocalConfiguration.php 220 * already and if not, add them. 221 * 222 * Used public by extension manager when updating extension 223 * 224 * @param string $extensionKey The extension to sync 225 * @internal 226 */ 227 public function synchronizeExtConfTemplateWithLocalConfiguration(string $extensionKey): void 228 { 229 $package = GeneralUtility::makeInstance(PackageManager::class)->getPackage($extensionKey); 230 if (!@is_file($package->getPackagePath() . 'ext_conf_template.txt')) { 231 return; 232 } 233 $currentLocalConfiguration = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extensionKey] ?? []; 234 $extConfTemplateConfiguration = $this->getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots($extensionKey); 235 ArrayUtility::mergeRecursiveWithOverrule($extConfTemplateConfiguration, $currentLocalConfiguration); 236 // Write new config if changed. Loose array comparison to not write if only array key order is different 237 if ($extConfTemplateConfiguration != $currentLocalConfiguration) { 238 $this->set($extensionKey, $extConfTemplateConfiguration); 239 } 240 } 241 242 /** 243 * Helper method of ext_conf_template.txt parsing. 244 * 245 * Poor man version of getDefaultConfigurationFromExtConfTemplateAsValuedArray() which ignores 246 * comments and returns ext_conf_template as array where nested keys have no dots. 247 * 248 * @param string $extensionKey 249 * @return array 250 */ 251 protected function getExtConfTablesWithoutCommentsAsNestedArrayWithoutDots(string $extensionKey): array 252 { 253 $rawConfigurationString = $this->getDefaultConfigurationRawString($extensionKey); 254 $typoScriptParser = GeneralUtility::makeInstance(TypoScriptParser::class); 255 // we are parsing constants, so we need the instructions from comments 256 $typoScriptParser->regComments = true; 257 $typoScriptParser->parse($rawConfigurationString); 258 // setup contains the parsed constants string 259 $parsedTemplate = $typoScriptParser->setup; 260 return $this->removeCommentsAndDotsRecursive($parsedTemplate); 261 } 262 263 /** 264 * Helper method of ext_conf_template.txt parsing. 265 * 266 * Return content of an extensions ext_conf_template.txt file if 267 * the file exists, empty string if file does not exist. 268 * 269 * @param string $extensionKey Extension key 270 * @return string 271 */ 272 public function getDefaultConfigurationRawString(string $extensionKey): string 273 { 274 $rawString = ''; 275 $extConfTemplateFileLocation = GeneralUtility::getFileAbsFileName( 276 'EXT:' . $extensionKey . '/ext_conf_template.txt' 277 ); 278 if (file_exists($extConfTemplateFileLocation)) { 279 $rawString = (string)file_get_contents($extConfTemplateFileLocation); 280 } 281 return $rawString; 282 } 283 284 /** 285 * @param string $extension 286 * @return bool 287 */ 288 protected function hasConfiguration(string $extension): bool 289 { 290 return isset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension]) && is_array($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension]); 291 } 292 293 /** 294 * Helper method of ext_conf_template.txt parsing. 295 * 296 * "Comments" from the "TypoScript" parser below are identified by two (!) dots at the end of array keys 297 * and all array keys have a single dot at the end, if they have sub arrays. This is cleaned here. 298 * 299 * Incoming array: 300 * [ 301 * 'automaticInstallation' => '1', 302 * 'automaticInstallation..' => '# cat=basic/enabled; ...' 303 * 'FE.' => [ 304 * 'enabled' = '1', 305 * 'enabled..' => '# cat=basic/enabled; ...' 306 * ] 307 * ] 308 * Output array: 309 * [ 310 * 'automaticInstallation' => '1', 311 * 'FE' => [ 312 * 'enabled' => '1', 313 * ] 314 * 315 * @param array $config Incoming configuration 316 * @return array 317 */ 318 protected function removeCommentsAndDotsRecursive(array $config): array 319 { 320 $cleanedConfig = []; 321 foreach ($config as $key => $value) { 322 if (substr($key, -2) === '..') { 323 continue; 324 } 325 if (substr($key, -1) === '.') { 326 $cleanedConfig[rtrim($key, '.')] = $this->removeCommentsAndDotsRecursive($value); 327 } else { 328 $cleanedConfig[$key] = $value; 329 } 330 } 331 return $cleanedConfig; 332 } 333} 334