1<?php 2namespace TYPO3\CMS\Core\Utility; 3 4/* 5 * This file is part of the TYPO3 CMS project. 6 * 7 * It is free software; you can redistribute it and/or modify it under 8 * the terms of the GNU General Public License, either version 2 9 * of the License, or any later version. 10 * 11 * For the full copyright and license information, please read the 12 * LICENSE.txt file that was distributed with this source code. 13 * 14 * The TYPO3 project - inspiring people to share! 15 */ 16 17use Symfony\Component\Finder\Finder; 18use TYPO3\CMS\Backend\Routing\Route; 19use TYPO3\CMS\Backend\Routing\Router; 20use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; 21use TYPO3\CMS\Core\Category\CategoryRegistry; 22use TYPO3\CMS\Core\Core\Environment; 23use TYPO3\CMS\Core\Imaging\IconRegistry; 24use TYPO3\CMS\Core\Log\LogManager; 25use TYPO3\CMS\Core\Migrations\TcaMigration; 26use TYPO3\CMS\Core\Package\PackageManager; 27use TYPO3\CMS\Core\Preparations\TcaPreparation; 28 29/** 30 * Extension Management functions 31 * 32 * This class is never instantiated, rather the methods inside is called as functions like 33 * \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('my_extension'); 34 */ 35class ExtensionManagementUtility 36{ 37 /** 38 * @var array 39 */ 40 protected static $extensionKeyMap; 41 42 /** 43 * TRUE, if ext_tables file was read from cache for this script run. 44 * The frontend tends to do that multiple times, but the caching framework does 45 * not allow this (via a require_once call). This variable is used to track 46 * the access to the cache file to read the single ext_tables.php if it was 47 * already read from cache 48 * 49 * @todo See if we can get rid of the 'load multiple times' scenario in fe 50 * @var bool 51 */ 52 protected static $extTablesWasReadFromCacheOnce = false; 53 54 /** 55 * @var PackageManager 56 */ 57 protected static $packageManager; 58 59 /** 60 * Sets the package manager for all that backwards compatibility stuff, 61 * so it doesn't have to be fetched through the bootstap. 62 * 63 * @param PackageManager $packageManager 64 * @internal 65 */ 66 public static function setPackageManager(PackageManager $packageManager) 67 { 68 static::$packageManager = $packageManager; 69 } 70 71 /** 72 * @var \TYPO3\CMS\Core\Cache\CacheManager 73 */ 74 protected static $cacheManager; 75 76 /** 77 * Getter for the cache manager 78 * 79 * @return \TYPO3\CMS\Core\Cache\CacheManager 80 */ 81 protected static function getCacheManager() 82 { 83 if (static::$cacheManager === null) { 84 static::$cacheManager = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Cache\CacheManager::class); 85 } 86 return static::$cacheManager; 87 } 88 89 /** 90 * @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher 91 */ 92 protected static $signalSlotDispatcher; 93 94 /** 95 * Getter for the signal slot dispatcher 96 * 97 * @return \TYPO3\CMS\Extbase\SignalSlot\Dispatcher 98 */ 99 protected static function getSignalSlotDispatcher() 100 { 101 if (static::$signalSlotDispatcher === null) { 102 static::$signalSlotDispatcher = GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class); 103 } 104 return static::$signalSlotDispatcher; 105 } 106 107 /************************************** 108 * 109 * PATHS and other evaluation 110 * 111 ***************************************/ 112 /** 113 * Returns TRUE if the extension with extension key $key is loaded. 114 * 115 * @param string $key Extension key to test 116 * @param bool $exitOnError If $exitOnError is TRUE and the extension is not loaded the function will die with an error message, this is deprecated and will be removed in TYPO3 v10.0. 117 * @return bool 118 * @throws \BadFunctionCallException 119 */ 120 public static function isLoaded($key, $exitOnError = null) 121 { 122 // safety net for extensions checking for "EXT:version", can be removed in TYPO3 v10.0. 123 if ($key === 'version') { 124 trigger_error('EXT:version has been moved into EXT:workspaces, you should check against "workspaces", as this might lead to unexpected behaviour in the future.', E_USER_DEPRECATED); 125 $key = 'workspaces'; 126 } 127 if ($key === 'sv') { 128 trigger_error('EXT:sv has been moved into EXT:core, you should remove your check as code is always loaded, as this might lead to unexpected behaviour in the future.', E_USER_DEPRECATED); 129 return true; 130 } 131 if ($key === 'saltedpasswords') { 132 trigger_error('EXT:saltedpasswords has been moved into EXT:core, you should remove your check as code is always loaded, as this might lead to unexpected behaviour in the future.', E_USER_DEPRECATED); 133 return true; 134 } 135 if ($exitOnError !== null) { 136 trigger_error('Calling ExtensionManagementUtility::isLoaded() with a second argument via "exitOnError" will be removed in TYPO3 v10.0, handle an unloaded package yourself in the future.', E_USER_DEPRECATED); 137 } 138 $isLoaded = static::$packageManager->isPackageActive($key); 139 if ($exitOnError && !$isLoaded) { 140 // @deprecated, once $exitOnError is gone, this check can be removed. 141 throw new \BadFunctionCallException('TYPO3 Fatal Error: Extension "' . $key . '" is not loaded!', 1270853910); 142 } 143 return $isLoaded; 144 } 145 146 /** 147 * Returns the absolute path to the extension with extension key $key. 148 * 149 * @param string $key Extension key 150 * @param string $script $script is appended to the output if set. 151 * @throws \BadFunctionCallException 152 * @return string 153 */ 154 public static function extPath($key, $script = '') 155 { 156 if (!static::$packageManager->isPackageActive($key)) { 157 throw new \BadFunctionCallException('TYPO3 Fatal Error: Extension key "' . $key . '" is NOT loaded!', 1365429656); 158 } 159 return static::$packageManager->getPackage($key)->getPackagePath() . $script; 160 } 161 162 /** 163 * Returns the relative path to the extension as measured from the public web path 164 * If the extension is not loaded the function will die with an error message 165 * Useful for images and links from the frontend 166 * 167 * @param string $key Extension key 168 * @return string 169 * @deprecated use extPath() or GeneralUtility::getFileAbsFileName() together with PathUtility::getAbsoluteWebPath() instead. 170 */ 171 public static function siteRelPath($key) 172 { 173 trigger_error('ExtensionManagementUtility::siteRelPath() will be removed in TYPO3 v10.0, use extPath() in conjunction with PathUtility::getAbsoluteWebPath() instead.', E_USER_DEPRECATED); 174 return PathUtility::stripPathSitePrefix(self::extPath($key)); 175 } 176 177 /** 178 * Returns the correct class name prefix for the extension key $key 179 * 180 * @param string $key Extension key 181 * @return string 182 * @internal 183 */ 184 public static function getCN($key) 185 { 186 return strpos($key, 'user_') === 0 ? 'user_' . str_replace('_', '', substr($key, 5)) : 'tx_' . str_replace('_', '', $key); 187 } 188 189 /** 190 * Returns the real extension key like 'tt_news' from an extension prefix like 'tx_ttnews'. 191 * 192 * @param string $prefix The extension prefix (e.g. 'tx_ttnews') 193 * @return mixed Real extension key (string)or FALSE (bool) if something went wrong 194 * @deprecated since TYPO3 v9, just use the proper extension key directly 195 */ 196 public static function getExtensionKeyByPrefix($prefix) 197 { 198 trigger_error('ExtensionManagementUtility::getExtensionKeyByPrefix() will be removed in TYPO3 v10.0. Use extension keys directly.', E_USER_DEPRECATED); 199 $result = false; 200 // Build map of short keys referencing to real keys: 201 if (!isset(self::$extensionKeyMap)) { 202 self::$extensionKeyMap = []; 203 foreach (static::$packageManager->getActivePackages() as $package) { 204 $shortKey = str_replace('_', '', $package->getPackageKey()); 205 self::$extensionKeyMap[$shortKey] = $package->getPackageKey(); 206 } 207 } 208 // Lookup by the given short key: 209 $parts = explode('_', $prefix); 210 if (isset(self::$extensionKeyMap[$parts[1]])) { 211 $result = self::$extensionKeyMap[$parts[1]]; 212 } 213 return $result; 214 } 215 216 /** 217 * Clears the extension key map. 218 */ 219 public static function clearExtensionKeyMap() 220 { 221 self::$extensionKeyMap = null; 222 } 223 224 /** 225 * Retrieves the version of an installed extension. 226 * If the extension is not installed, this function returns an empty string. 227 * 228 * @param string $key The key of the extension to look up, must not be empty 229 * 230 * @throws \InvalidArgumentException 231 * @throws \TYPO3\CMS\Core\Package\Exception 232 * @return string The extension version as a string in the format "x.y.z", 233 */ 234 public static function getExtensionVersion($key) 235 { 236 if (!is_string($key) || empty($key)) { 237 throw new \InvalidArgumentException('Extension key must be a non-empty string.', 1294586096); 238 } 239 if (!static::isLoaded($key)) { 240 return ''; 241 } 242 $version = static::$packageManager->getPackage($key)->getPackageMetaData()->getVersion(); 243 if (empty($version)) { 244 throw new \TYPO3\CMS\Core\Package\Exception('Version number in composer manifest of package "' . $key . '" is missing or invalid', 1395614959); 245 } 246 return $version; 247 } 248 249 /************************************** 250 * 251 * Adding BACKEND features 252 * (related to core features) 253 * 254 ***************************************/ 255 /** 256 * Adding fields to an existing table definition in $GLOBALS['TCA'] 257 * Adds an array with $GLOBALS['TCA'] column-configuration to the $GLOBALS['TCA']-entry for that table. 258 * This function adds the configuration needed for rendering of the field in TCEFORMS - but it does NOT add the field names to the types lists! 259 * So to have the fields displayed you must also call fx. addToAllTCAtypes or manually add the fields to the types list. 260 * FOR USE IN files in Configuration/TCA/Overrides/*.php . Use in ext_tables.php FILES may break the frontend. 261 * 262 * @param string $table The table name of a table already present in $GLOBALS['TCA'] with a columns section 263 * @param array $columnArray The array with the additional columns (typical some fields an extension wants to add) 264 */ 265 public static function addTCAcolumns($table, $columnArray) 266 { 267 if (is_array($columnArray) && is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'])) { 268 // Candidate for array_merge() if integer-keys will some day make trouble... 269 $GLOBALS['TCA'][$table]['columns'] = array_merge($GLOBALS['TCA'][$table]['columns'], $columnArray); 270 } 271 } 272 273 /** 274 * Makes fields visible in the TCEforms, adding them to the end of (all) "types"-configurations 275 * 276 * Adds a string $string (comma separated list of field names) to all ["types"][xxx]["showitem"] entries for table $table (unless limited by $typeList) 277 * This is needed to have new fields shown automatically in the TCEFORMS of a record from $table. 278 * Typically this function is called after having added new columns (database fields) with the addTCAcolumns function 279 * FOR USE IN files in Configuration/TCA/Overrides/*.php Use in ext_tables.php FILES may break the frontend. 280 * 281 * @param string $table Table name 282 * @param string $newFieldsString Field list to add. 283 * @param string $typeList List of specific types to add the field list to. (If empty, all type entries are affected) 284 * @param string $position Insert fields before (default) or after one, or replace a field 285 */ 286 public static function addToAllTCAtypes($table, $newFieldsString, $typeList = '', $position = '') 287 { 288 $newFieldsString = trim($newFieldsString); 289 if ($newFieldsString === '' || !is_array($GLOBALS['TCA'][$table]['types'] ?? false)) { 290 return; 291 } 292 if ($position !== '') { 293 list($positionIdentifier, $entityName) = GeneralUtility::trimExplode(':', $position); 294 } else { 295 $positionIdentifier = ''; 296 $entityName = ''; 297 } 298 $palettesChanged = []; 299 300 foreach ($GLOBALS['TCA'][$table]['types'] as $type => &$typeDetails) { 301 // skip if we don't want to add the field for this type 302 if ($typeList !== '' && !GeneralUtility::inList($typeList, $type)) { 303 continue; 304 } 305 // skip if fields were already added 306 if (!isset($typeDetails['showitem'])) { 307 continue; 308 } 309 310 $fieldArray = GeneralUtility::trimExplode(',', $typeDetails['showitem'], true); 311 if (in_array($newFieldsString, $fieldArray, true)) { 312 continue; 313 } 314 315 $fieldExists = false; 316 $newPosition = ''; 317 if (is_array($GLOBALS['TCA'][$table]['palettes'] ?? false)) { 318 // Get the palette names used in current showitem 319 $paletteCount = preg_match_all('/(?:^|,) # Line start or a comma 320 (?: 321 \\s*\\-\\-palette\\-\\-;[^;]*;([^,$]*)| # --palette--;label;paletteName 322 \\s*\\b[^;,]+\\b(?:;[^;]*;([^;,]+))?[^,]* # field;label;paletteName 323 )/x', $typeDetails['showitem'], $paletteMatches); 324 if ($paletteCount > 0) { 325 $paletteNames = array_filter(array_merge($paletteMatches[1], $paletteMatches[2])); 326 if (!empty($paletteNames)) { 327 foreach ($paletteNames as $paletteName) { 328 if (!isset($GLOBALS['TCA'][$table]['palettes'][$paletteName])) { 329 continue; 330 } 331 $palette = $GLOBALS['TCA'][$table]['palettes'][$paletteName]; 332 switch ($positionIdentifier) { 333 case 'after': 334 case 'before': 335 if (preg_match('/\\b' . $entityName . '\\b/', $palette['showitem']) > 0) { 336 $newPosition = $positionIdentifier . ':--palette--;;' . $paletteName; 337 } 338 break; 339 case 'replace': 340 // check if fields have been added to palette before 341 if (isset($palettesChanged[$paletteName])) { 342 $fieldExists = true; 343 continue 2; 344 } 345 if (preg_match('/\\b' . $entityName . '\\b/', $palette['showitem']) > 0) { 346 self::addFieldsToPalette($table, $paletteName, $newFieldsString, $position); 347 // Memorize that we already changed this palette, in case other types also use it 348 $palettesChanged[$paletteName] = true; 349 $fieldExists = true; 350 continue 2; 351 } 352 break; 353 default: 354 // Intentionally left blank 355 } 356 } 357 } 358 } 359 } 360 if ($fieldExists === false) { 361 $typeDetails['showitem'] = self::executePositionedStringInsertion( 362 $typeDetails['showitem'], 363 $newFieldsString, 364 $newPosition !== '' ? $newPosition : $position 365 ); 366 } 367 } 368 unset($typeDetails); 369 } 370 371 /** 372 * Adds new fields to all palettes that is defined after an existing field. 373 * If the field does not have a following palette yet, it's created automatically 374 * and gets called "generatedFor-$field". 375 * FOR USE IN files in Configuration/TCA/Overrides/*.php Use in ext_tables.php FILES may break the frontend. 376 * 377 * See unit tests for more examples and edge cases. 378 * 379 * Example: 380 * 381 * 'aTable' => array( 382 * 'types' => array( 383 * 'aType' => array( 384 * 'showitem' => 'aField, --palette--;;aPalette', 385 * ), 386 * ), 387 * 'palettes' => array( 388 * 'aPallete' => array( 389 * 'showitem' => 'fieldB, fieldC', 390 * ), 391 * ), 392 * ), 393 * 394 * Calling addFieldsToAllPalettesOfField('aTable', 'aField', 'newA', 'before: fieldC') results in: 395 * 396 * 'aTable' => array( 397 * 'types' => array( 398 * 'aType' => array( 399 * 'showitem' => 'aField, --palette--;;aPalette', 400 * ), 401 * ), 402 * 'palettes' => array( 403 * 'aPallete' => array( 404 * 'showitem' => 'fieldB, newA, fieldC', 405 * ), 406 * ), 407 * ), 408 * 409 * @param string $table Name of the table 410 * @param string $field Name of the field that has the palette to be extended 411 * @param string $addFields List of fields to be added to the palette 412 * @param string $insertionPosition Insert fields before (default) or after one 413 */ 414 public static function addFieldsToAllPalettesOfField($table, $field, $addFields, $insertionPosition = '') 415 { 416 if (!isset($GLOBALS['TCA'][$table]['columns'][$field])) { 417 return; 418 } 419 if (!is_array($GLOBALS['TCA'][$table]['types'])) { 420 return; 421 } 422 423 // Iterate through all types and search for the field that defines the palette to be extended 424 foreach ($GLOBALS['TCA'][$table]['types'] as $typeName => $typeArray) { 425 // Continue if types has no showitem at all or if requested field is not in it 426 if (!isset($typeArray['showitem']) || strpos($typeArray['showitem'], $field) === false) { 427 continue; 428 } 429 $fieldArrayWithOptions = GeneralUtility::trimExplode(',', $typeArray['showitem']); 430 // Find the field we're handling 431 $newFieldStringArray = []; 432 foreach ($fieldArrayWithOptions as $fieldNumber => $fieldString) { 433 $newFieldStringArray[] = $fieldString; 434 $fieldArray = GeneralUtility::trimExplode(';', $fieldString); 435 if ($fieldArray[0] !== $field) { 436 continue; 437 } 438 if ( 439 isset($fieldArrayWithOptions[$fieldNumber + 1]) 440 && strpos($fieldArrayWithOptions[$fieldNumber + 1], '--palette--') === 0 441 ) { 442 // Match for $field and next field is a palette - add fields to this one 443 $paletteName = GeneralUtility::trimExplode(';', $fieldArrayWithOptions[$fieldNumber + 1]); 444 $paletteName = $paletteName[2]; 445 self::addFieldsToPalette($table, $paletteName, $addFields, $insertionPosition); 446 } else { 447 // Match for $field but next field is no palette - create a new one 448 $newPaletteName = 'generatedFor-' . $field; 449 self::addFieldsToPalette($table, 'generatedFor-' . $field, $addFields, $insertionPosition); 450 $newFieldStringArray[] = '--palette--;;' . $newPaletteName; 451 } 452 } 453 $GLOBALS['TCA'][$table]['types'][$typeName]['showitem'] = implode(', ', $newFieldStringArray); 454 } 455 } 456 457 /** 458 * Adds new fields to a palette. 459 * If the palette does not exist yet, it's created automatically. 460 * FOR USE IN files in Configuration/TCA/Overrides/*.php Use in ext_tables.php FILES may break the frontend. 461 * 462 * @param string $table Name of the table 463 * @param string $palette Name of the palette to be extended 464 * @param string $addFields List of fields to be added to the palette 465 * @param string $insertionPosition Insert fields before (default) or after one 466 */ 467 public static function addFieldsToPalette($table, $palette, $addFields, $insertionPosition = '') 468 { 469 if (isset($GLOBALS['TCA'][$table])) { 470 $paletteData = &$GLOBALS['TCA'][$table]['palettes'][$palette]; 471 // If palette already exists, merge the data: 472 if (is_array($paletteData)) { 473 $paletteData['showitem'] = self::executePositionedStringInsertion($paletteData['showitem'], $addFields, $insertionPosition); 474 } else { 475 $paletteData['showitem'] = self::removeDuplicatesForInsertion($addFields); 476 } 477 } 478 } 479 480 /** 481 * Add an item to a select field item list. 482 * 483 * Warning: Do not use this method for radio or check types, especially not 484 * with $relativeToField and $relativePosition parameters. This would shift 485 * existing database data 'off by one'. 486 * FOR USE IN files in Configuration/TCA/Overrides/*.php Use in ext_tables.php FILES may break the frontend. 487 * 488 * As an example, this can be used to add an item to tt_content CType select 489 * drop-down after the existing 'mailform' field with these parameters: 490 * - $table = 'tt_content' 491 * - $field = 'CType' 492 * - $item = array( 493 * 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.I.10', 494 * 'login', 495 * 'i/imagename.gif', 496 * ), 497 * - $relativeToField = mailform 498 * - $relativePosition = after 499 * 500 * @throws \InvalidArgumentException If given parameters are not of correct 501 * @throws \RuntimeException If reference to related position fields can not 502 * @param string $table Name of TCA table 503 * @param string $field Name of TCA field 504 * @param array $item New item to add 505 * @param string $relativeToField Add item relative to existing field 506 * @param string $relativePosition Valid keywords: 'before', 'after' 507 */ 508 public static function addTcaSelectItem($table, $field, array $item, $relativeToField = '', $relativePosition = '') 509 { 510 if (!is_string($table)) { 511 throw new \InvalidArgumentException('Given table is of type "' . gettype($table) . '" but a string is expected.', 1303236963); 512 } 513 if (!is_string($field)) { 514 throw new \InvalidArgumentException('Given field is of type "' . gettype($field) . '" but a string is expected.', 1303236964); 515 } 516 if (!is_string($relativeToField)) { 517 throw new \InvalidArgumentException('Given relative field is of type "' . gettype($relativeToField) . '" but a string is expected.', 1303236965); 518 } 519 if (!is_string($relativePosition)) { 520 throw new \InvalidArgumentException('Given relative position is of type "' . gettype($relativePosition) . '" but a string is expected.', 1303236966); 521 } 522 if ($relativePosition !== '' && $relativePosition !== 'before' && $relativePosition !== 'after' && $relativePosition !== 'replace') { 523 throw new \InvalidArgumentException('Relative position must be either empty or one of "before", "after", "replace".', 1303236967); 524 } 525 if (!isset($GLOBALS['TCA'][$table]['columns'][$field]['config']['items']) 526 || !is_array($GLOBALS['TCA'][$table]['columns'][$field]['config']['items']) 527 ) { 528 throw new \RuntimeException('Given select field item list was not found.', 1303237468); 529 } 530 // Make sure item keys are integers 531 $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'] = array_values($GLOBALS['TCA'][$table]['columns'][$field]['config']['items']); 532 if ($relativePosition !== '') { 533 // Insert at specified position 534 $matchedPosition = ArrayUtility::filterByValueRecursive($relativeToField, $GLOBALS['TCA'][$table]['columns'][$field]['config']['items']); 535 if (!empty($matchedPosition)) { 536 $relativeItemKey = key($matchedPosition); 537 if ($relativePosition === 'replace') { 538 $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'][$relativeItemKey] = $item; 539 } else { 540 if ($relativePosition === 'before') { 541 $offset = $relativeItemKey; 542 } else { 543 $offset = $relativeItemKey + 1; 544 } 545 array_splice($GLOBALS['TCA'][$table]['columns'][$field]['config']['items'], $offset, 0, [0 => $item]); 546 } 547 } else { 548 // Insert at new item at the end of the array if relative position was not found 549 $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'][] = $item; 550 } 551 } else { 552 // Insert at new item at the end of the array 553 $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'][] = $item; 554 } 555 } 556 557 /** 558 * Gets the TCA configuration for a field handling (FAL) files. 559 * 560 * @param string $fieldName Name of the field to be used 561 * @param array $customSettingOverride Custom field settings overriding the basics 562 * @param string $allowedFileExtensions Comma list of allowed file extensions (e.g. "jpg,gif,pdf") 563 * @param string $disallowedFileExtensions 564 * 565 * @return array 566 */ 567 public static function getFileFieldTCAConfig($fieldName, array $customSettingOverride = [], $allowedFileExtensions = '', $disallowedFileExtensions = '') 568 { 569 $fileFieldTCAConfig = [ 570 'type' => 'inline', 571 'foreign_table' => 'sys_file_reference', 572 'foreign_field' => 'uid_foreign', 573 'foreign_sortby' => 'sorting_foreign', 574 'foreign_table_field' => 'tablenames', 575 'foreign_match_fields' => [ 576 'fieldname' => $fieldName 577 ], 578 'foreign_label' => 'uid_local', 579 'foreign_selector' => 'uid_local', 580 'overrideChildTca' => [ 581 'columns' => [ 582 'uid_local' => [ 583 'config' => [ 584 'appearance' => [ 585 'elementBrowserType' => 'file', 586 'elementBrowserAllowed' => $allowedFileExtensions 587 ], 588 ], 589 ], 590 ], 591 ], 592 'filter' => [ 593 [ 594 'userFunc' => \TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter::class . '->filterInlineChildren', 595 'parameters' => [ 596 'allowedFileExtensions' => $allowedFileExtensions, 597 'disallowedFileExtensions' => $disallowedFileExtensions 598 ] 599 ] 600 ], 601 'appearance' => [ 602 'useSortable' => true, 603 'headerThumbnail' => [ 604 'field' => 'uid_local', 605 'width' => '45', 606 'height' => '45c', 607 ], 608 609 'enabledControls' => [ 610 'info' => true, 611 'new' => false, 612 'dragdrop' => true, 613 'sort' => false, 614 'hide' => true, 615 'delete' => true, 616 ], 617 ] 618 ]; 619 ArrayUtility::mergeRecursiveWithOverrule($fileFieldTCAConfig, $customSettingOverride); 620 return $fileFieldTCAConfig; 621 } 622 623 /** 624 * Adds a list of new fields to the TYPO3 USER SETTINGS configuration "showitem" list, the array with 625 * the new fields itself needs to be added additionally to show up in the user setup, like 626 * $GLOBALS['TYPO3_USER_SETTINGS']['columns'] += $tempColumns 627 * 628 * @param string $addFields List of fields to be added to the user settings 629 * @param string $insertionPosition Insert fields before (default) or after one 630 */ 631 public static function addFieldsToUserSettings($addFields, $insertionPosition = '') 632 { 633 $GLOBALS['TYPO3_USER_SETTINGS']['showitem'] = self::executePositionedStringInsertion($GLOBALS['TYPO3_USER_SETTINGS']['showitem'], $addFields, $insertionPosition); 634 } 635 636 /** 637 * Inserts as list of data into an existing list. 638 * The insertion position can be defined accordant before of after existing list items. 639 * 640 * Example: 641 * + list: 'field_a, field_b, field_c' 642 * + insertionList: 'field_d, field_e' 643 * + insertionPosition: 'after:field_b' 644 * -> 'field_a, field_b, field_d, field_e, field_c' 645 * 646 * $insertPosition may contain ; and - characters: after:--palette--;;title 647 * 648 * @param string $list The list of items to be extended 649 * @param string $insertionList The list of items to inserted 650 * @param string $insertionPosition Insert fields before (default) or after one 651 * @return string The extended list 652 */ 653 protected static function executePositionedStringInsertion($list, $insertionList, $insertionPosition = '') 654 { 655 $list = $newList = trim($list, ", \t\n\r\0\x0B"); 656 657 if ($insertionPosition !== '') { 658 list($location, $positionName) = GeneralUtility::trimExplode(':', $insertionPosition, false, 2); 659 } else { 660 $location = ''; 661 $positionName = ''; 662 } 663 664 if ($location !== 'replace') { 665 $insertionList = self::removeDuplicatesForInsertion($insertionList, $list); 666 } 667 668 if ($insertionList === '') { 669 return $list; 670 } 671 if ($list === '') { 672 return $insertionList; 673 } 674 if ($insertionPosition === '') { 675 return $list . ', ' . $insertionList; 676 } 677 678 // The $insertPosition may be a palette: after:--palette--;;title 679 // In the $list the palette may contain a LLL string in between the ;; 680 // Adjust the regex to match that 681 $positionName = preg_quote($positionName, '/'); 682 if (strpos($positionName, ';;') !== false) { 683 $positionName = str_replace(';;', ';[^;]*;', $positionName); 684 } 685 686 $pattern = ('/(^|,\\s*)(' . $positionName . ')(;[^,$]+)?(,|$)/'); 687 switch ($location) { 688 case 'after': 689 $newList = preg_replace($pattern, '$1$2$3, ' . $insertionList . '$4', $list); 690 break; 691 case 'before': 692 $newList = preg_replace($pattern, '$1' . $insertionList . ', $2$3$4', $list); 693 break; 694 case 'replace': 695 $newList = preg_replace($pattern, '$1' . $insertionList . '$4', $list); 696 break; 697 default: 698 } 699 700 // When preg_replace did not replace anything; append the $insertionList. 701 if ($list === $newList) { 702 return $list . ', ' . $insertionList; 703 } 704 return $newList; 705 } 706 707 /** 708 * Compares an existing list of items and a list of items to be inserted 709 * and returns a duplicate-free variant of that insertion list. 710 * 711 * Example: 712 * + list: 'field_a, field_b, field_c' 713 * + insertion: 'field_b, field_d, field_c' 714 * -> new insertion: 'field_d' 715 * 716 * Duplicate values in $insertionList are removed. 717 * 718 * @param string $insertionList The list of items to inserted 719 * @param string $list The list of items to be extended (default: '') 720 * @return string Duplicate-free list of items to be inserted 721 */ 722 protected static function removeDuplicatesForInsertion($insertionList, $list = '') 723 { 724 $insertionListParts = preg_split('/\\s*,\\s*/', $insertionList); 725 $listMatches = []; 726 if ($list !== '') { 727 preg_match_all('/(?:^|,)\\s*\\b([^;,]+)\\b[^,]*/', $list, $listMatches); 728 $listMatches = $listMatches[1]; 729 } 730 731 $cleanInsertionListParts = []; 732 foreach ($insertionListParts as $fieldName) { 733 $fieldNameParts = explode(';', $fieldName, 2); 734 $cleanFieldName = $fieldNameParts[0]; 735 if ( 736 $cleanFieldName === '--linebreak--' 737 || ( 738 !in_array($cleanFieldName, $cleanInsertionListParts, true) 739 && !in_array($cleanFieldName, $listMatches, true) 740 ) 741 ) { 742 $cleanInsertionListParts[] = $fieldName; 743 } 744 } 745 return implode(', ', $cleanInsertionListParts); 746 } 747 748 /** 749 * Generates an array of fields/items with additional information such as e.g. the name of the palette. 750 * 751 * @param string $itemList List of fields/items to be splitted up 752 * @return array An array with the names of the fields/items as keys and additional information 753 */ 754 protected static function explodeItemList($itemList) 755 { 756 $items = []; 757 $itemParts = GeneralUtility::trimExplode(',', $itemList, true); 758 foreach ($itemParts as $itemPart) { 759 $itemDetails = GeneralUtility::trimExplode(';', $itemPart, false, 5); 760 $key = $itemDetails[0]; 761 if (strpos($key, '--') !== false) { 762 // If $key is a separator (--div--) or palette (--palette--) then it will be appended by a unique number. This must be removed again when using this value! 763 $key .= count($items); 764 } 765 if (!isset($items[$key])) { 766 $items[$key] = [ 767 'rawData' => $itemPart, 768 'details' => [] 769 ]; 770 $details = [0 => 'field', 1 => 'label', 2 => 'palette']; 771 foreach ($details as $id => $property) { 772 $items[$key]['details'][$property] = $itemDetails[$id] ?? ''; 773 } 774 } 775 } 776 return $items; 777 } 778 779 /** 780 * Generates a list of fields/items out of an array provided by the function getFieldsOfFieldList(). 781 * 782 * @see explodeItemList 783 * @param array $items The array of fields/items with optional additional information 784 * @param bool $useRawData Use raw data instead of building by using the details (default: FALSE) 785 * @return string The list of fields/items which gets used for $GLOBALS['TCA'][<table>]['types'][<type>]['showitem'] 786 */ 787 protected static function generateItemList(array $items, $useRawData = false) 788 { 789 $itemParts = []; 790 foreach ($items as $item => $itemDetails) { 791 if (strpos($item, '--') !== false) { 792 // If $item is a separator (--div--) or palette (--palette--) then it may have been appended by a unique number. This must be stripped away here. 793 $item = str_replace([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], '', $item); 794 } 795 if ($useRawData) { 796 $itemParts[] = $itemDetails['rawData']; 797 } else { 798 if (count($itemDetails['details']) > 1) { 799 $details = ['palette', 'label', 'field']; 800 $elements = []; 801 $addEmpty = false; 802 foreach ($details as $property) { 803 if ($itemDetails['details'][$property] !== '' || $addEmpty) { 804 $addEmpty = true; 805 array_unshift($elements, $itemDetails['details'][$property]); 806 } 807 } 808 $item = implode(';', $elements); 809 } 810 $itemParts[] = $item; 811 } 812 } 813 return implode(', ', $itemParts); 814 } 815 816 /** 817 * Add tablename to default list of allowed tables on pages (in $PAGES_TYPES) 818 * Will add the $table to the list of tables allowed by default on pages as setup by $PAGES_TYPES['default']['allowedTables'] 819 * FOR USE IN ext_tables.php FILES 820 * 821 * @param string $table Table name 822 */ 823 public static function allowTableOnStandardPages($table) 824 { 825 $GLOBALS['PAGES_TYPES']['default']['allowedTables'] .= ',' . $table; 826 } 827 828 /** 829 * This method is called from \TYPO3\CMS\Backend\Module\ModuleLoader::checkMod 830 * and it replaces old conf.php. 831 * 832 * @param string $moduleSignature The module name 833 * @return array Configuration of the module 834 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, addModule() works the same way nowadays. 835 */ 836 public static function configureModule($moduleSignature) 837 { 838 trigger_error('ExtensionManagementUtility::configureModule will be removed in TYPO3 v10.0, as the same functionality is found in addModule() as well.', E_USER_DEPRECATED); 839 $moduleConfiguration = $GLOBALS['TBE_MODULES']['_configuration'][$moduleSignature]; 840 841 // Register the icon and move it too "iconIdentifier" 842 if (!empty($moduleConfiguration['icon'])) { 843 $iconRegistry = GeneralUtility::makeInstance(IconRegistry::class); 844 $iconIdentifier = 'module-' . $moduleSignature; 845 $iconProvider = $iconRegistry->detectIconProvider($moduleConfiguration['icon']); 846 $iconRegistry->registerIcon( 847 $iconIdentifier, 848 $iconProvider, 849 ['source' => GeneralUtility::getFileAbsFileName($moduleConfiguration['icon'])] 850 ); 851 $moduleConfiguration['iconIdentifier'] = $iconIdentifier; 852 unset($moduleConfiguration['icon']); 853 } 854 855 return $moduleConfiguration; 856 } 857 858 /** 859 * Adds a module (main or sub) to the backend interface 860 * FOR USE IN ext_tables.php FILES 861 * 862 * @param string $main The main module key, $sub is the submodule key. So $main would be an index in the $TBE_MODULES array and $sub could be an element in the lists there. 863 * @param string $sub The submodule key. If $sub is not set a blank $main module is created. 864 * @param string $position Can be used to set the position of the $sub module within the list of existing submodules for the main module. $position has this syntax: [cmd]:[submodule-key]. cmd can be "after", "before" or "top" (or blank which is default). If "after"/"before" then submodule will be inserted after/before the existing submodule with [submodule-key] if found. If not found, the bottom of list. If "top" the module is inserted in the top of the submodule list. 865 * @param string $path The absolute path to the module. Was used prior to TYPO3 v8, use $moduleConfiguration[routeTarget] now 866 * @param array $moduleConfiguration additional configuration, previously put in "conf.php" of the module directory 867 */ 868 public static function addModule($main, $sub = '', $position = '', $path = null, $moduleConfiguration = []) 869 { 870 if (($moduleConfiguration['navigationComponentId'] ?? '') === 'typo3-pagetree') { 871 trigger_error( 872 'Referencing the navigation component ID "typo3-pagetree" will be removed in TYPO3 v10.0.' 873 . 'Use "TYPO3/CMS/Backend/PageTree/PageTreeElement" instead. Module key: ' . $main . '-' . $sub, 874 E_USER_DEPRECATED 875 ); 876 $moduleConfiguration['navigationComponentId'] = 'TYPO3/CMS/Backend/PageTree/PageTreeElement'; 877 } 878 879 // If there is already a main module by this name: 880 // Adding the submodule to the correct position: 881 if (isset($GLOBALS['TBE_MODULES'][$main]) && $sub) { 882 list($place, $modRef) = array_pad(GeneralUtility::trimExplode(':', $position, true), 2, null); 883 $modules = ',' . $GLOBALS['TBE_MODULES'][$main] . ','; 884 if ($place === null || ($modRef !== null && !GeneralUtility::inList($modules, $modRef))) { 885 $place = 'bottom'; 886 } 887 $modRef = ',' . $modRef . ','; 888 if (!GeneralUtility::inList($modules, $sub)) { 889 switch (strtolower($place)) { 890 case 'after': 891 $modules = str_replace($modRef, $modRef . $sub . ',', $modules); 892 break; 893 case 'before': 894 $modules = str_replace($modRef, ',' . $sub . $modRef, $modules); 895 break; 896 case 'top': 897 $modules = $sub . $modules; 898 break; 899 case 'bottom': 900 default: 901 $modules = $modules . $sub; 902 } 903 } 904 // Re-inserting the submodule list: 905 $GLOBALS['TBE_MODULES'][$main] = trim($modules, ','); 906 } else { 907 // Create new main modules with only one submodule, $sub (or none if $sub is blank) 908 $GLOBALS['TBE_MODULES'][$main] = $sub; 909 } 910 911 // add additional configuration 912 $fullModuleSignature = $main . ($sub ? '_' . $sub : ''); 913 if (is_array($moduleConfiguration) && !empty($moduleConfiguration)) { 914 // remove default icon if an icon identifier is available 915 if (!empty($moduleConfiguration['iconIdentifier']) && $moduleConfiguration['icon'] === 'EXT:extbase/Resources/Public/Icons/Extension.png') { 916 unset($moduleConfiguration['icon']); 917 } 918 if (!empty($moduleConfiguration['icon'])) { 919 $iconRegistry = GeneralUtility::makeInstance(IconRegistry::class); 920 $iconIdentifier = 'module-' . $fullModuleSignature; 921 $iconProvider = $iconRegistry->detectIconProvider($moduleConfiguration['icon']); 922 $iconRegistry->registerIcon( 923 $iconIdentifier, 924 $iconProvider, 925 ['source' => GeneralUtility::getFileAbsFileName($moduleConfiguration['icon'])] 926 ); 927 $moduleConfiguration['iconIdentifier'] = $iconIdentifier; 928 unset($moduleConfiguration['icon']); 929 } 930 931 $GLOBALS['TBE_MODULES']['_configuration'][$fullModuleSignature] = $moduleConfiguration; 932 } 933 934 // Also register the module as regular route 935 $routeName = $moduleConfiguration['id'] ?? $fullModuleSignature; 936 // Build Route objects from the data 937 $path = $moduleConfiguration['path'] ?? str_replace('_', '/', $fullModuleSignature); 938 $path = '/' . trim($path, '/') . '/'; 939 940 $options = [ 941 'module' => true, 942 'moduleName' => $fullModuleSignature, 943 'access' => !empty($moduleConfiguration['access']) ? $moduleConfiguration['access'] : 'user,group' 944 ]; 945 if (!empty($moduleConfiguration['routeTarget'])) { 946 $options['target'] = $moduleConfiguration['routeTarget']; 947 } 948 949 $router = GeneralUtility::makeInstance(Router::class); 950 $router->addRoute($routeName, GeneralUtility::makeInstance(Route::class, $path, $options)); 951 } 952 953 /** 954 * Adds a "Function menu module" ('third level module') to an existing function menu for some other backend module 955 * The arguments values are generally determined by which function menu this is supposed to interact with 956 * See Inside TYPO3 for information on how to use this function. 957 * FOR USE IN ext_tables.php FILES 958 * 959 * @param string $modname Module name 960 * @param string $className Class name 961 * @param string $_ unused 962 * @param string $title Title of module 963 * @param string $MM_key Menu array key - default is "function 964 * @param string $WS Workspace conditions. Blank means all workspaces, any other string can be a comma list of "online", "offline" and "custom 965 * @see \TYPO3\CMS\Backend\Module\BaseScriptClass::mergeExternalItems() 966 */ 967 public static function insertModuleFunction($modname, $className, $_ = null, $title, $MM_key = 'function', $WS = '') 968 { 969 $GLOBALS['TBE_MODULES_EXT'][$modname]['MOD_MENU'][$MM_key][$className] = [ 970 'name' => $className, 971 'title' => $title, 972 'ws' => $WS 973 ]; 974 } 975 976 /** 977 * Adds $content to the default Page TSconfig as set in $GLOBALS['TYPO3_CONF_VARS'][BE]['defaultPageTSconfig'] 978 * Prefixed with a [GLOBAL] line 979 * FOR USE IN ext_localconf.php FILE 980 * 981 * @param string $content Page TSconfig content 982 */ 983 public static function addPageTSConfig($content) 984 { 985 $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] .= ' 986[GLOBAL] 987' . $content; 988 } 989 990 /** 991 * Adds $content to the default User TSconfig as set in $GLOBALS['TYPO3_CONF_VARS'][BE]['defaultUserTSconfig'] 992 * Prefixed with a [GLOBAL] line 993 * FOR USE IN ext_localconf.php FILE 994 * 995 * @param string $content User TSconfig content 996 */ 997 public static function addUserTSConfig($content) 998 { 999 $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig'] .= ' 1000[GLOBAL] 1001' . $content; 1002 } 1003 1004 /** 1005 * Adds a reference to a locallang file with $GLOBALS['TCA_DESCR'] labels 1006 * FOR USE IN ext_tables.php FILES 1007 * eg. \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addLLrefForTCAdescr('pages', 'EXT:core/Resources/Private/Language/locallang_csh_pages.xlf'); for the pages table or \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addLLrefForTCAdescr('_MOD_web_layout', 'EXT:frontend/Resources/Private/Language/locallang_csh_weblayout.xlf'); for the Web > Page module. 1008 * 1009 * @param string $key Description key. Typically a database table (like "pages") but for applications can be other strings, but prefixed with "_MOD_") 1010 * @param string $file File reference to locallang file, eg. "EXT:core/Resources/Private/Language/locallang_csh_pages.xlf" (or ".xml") 1011 */ 1012 public static function addLLrefForTCAdescr($key, $file) 1013 { 1014 if (empty($key)) { 1015 throw new \RuntimeException('No description key set in addLLrefForTCAdescr(). Provide it as first parameter', 1507321596); 1016 } 1017 if (!is_array($GLOBALS['TCA_DESCR'][$key] ?? false)) { 1018 $GLOBALS['TCA_DESCR'][$key] = []; 1019 } 1020 if (!is_array($GLOBALS['TCA_DESCR'][$key]['refs'] ?? false)) { 1021 $GLOBALS['TCA_DESCR'][$key]['refs'] = []; 1022 } 1023 $GLOBALS['TCA_DESCR'][$key]['refs'][] = $file; 1024 } 1025 1026 /** 1027 * Registers a navigation component e.g. page tree 1028 * 1029 * @param string $module 1030 * @param string $componentId componentId is also an RequireJS module name e.g. 'TYPO3/CMS/MyExt/MyNavComponent' 1031 * @param string $extensionKey 1032 * @throws \RuntimeException 1033 */ 1034 public static function addNavigationComponent($module, $componentId, $extensionKey) 1035 { 1036 if (empty($extensionKey)) { 1037 throw new \RuntimeException('No extensionKey set in addNavigationComponent(). Provide it as third parameter', 1404068039); 1038 } 1039 $GLOBALS['TBE_MODULES']['_navigationComponents'][$module] = [ 1040 'componentId' => $componentId, 1041 'extKey' => $extensionKey, 1042 'isCoreComponent' => false 1043 ]; 1044 } 1045 1046 /** 1047 * Registers a core navigation component 1048 * 1049 * @param string $module 1050 * @param string $componentId 1051 */ 1052 public static function addCoreNavigationComponent($module, $componentId) 1053 { 1054 self::addNavigationComponent($module, $componentId, 'core'); 1055 $GLOBALS['TBE_MODULES']['_navigationComponents'][$module]['isCoreComponent'] = true; 1056 } 1057 1058 /************************************** 1059 * 1060 * Adding SERVICES features 1061 * 1062 ***************************************/ 1063 /** 1064 * Adds a service to the global services array 1065 * 1066 * @param string $extKey Extension key 1067 * @param string $serviceType Service type, must not be prefixed "tx_" or "Tx_" 1068 * @param string $serviceKey Service key, must be prefixed "tx_", "Tx_" or "user_" 1069 * @param array $info Service description array 1070 */ 1071 public static function addService($extKey, $serviceType, $serviceKey, $info) 1072 { 1073 if (!$serviceType) { 1074 throw new \InvalidArgumentException('No serviceType given.', 1507321535); 1075 } 1076 if (!is_array($info)) { 1077 throw new \InvalidArgumentException('No information array given.', 1507321542); 1078 } 1079 $info['priority'] = max(0, min(100, $info['priority'])); 1080 $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey] = $info; 1081 $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey]['extKey'] = $extKey; 1082 $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey]['serviceKey'] = $serviceKey; 1083 $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey]['serviceType'] = $serviceType; 1084 // Change the priority (and other values) from $GLOBALS['TYPO3_CONF_VARS'] 1085 // $GLOBALS['TYPO3_CONF_VARS']['T3_SERVICES'][$serviceType][$serviceKey]['priority'] 1086 // even the activation is possible (a unix service might be possible on windows for some reasons) 1087 if (is_array($GLOBALS['TYPO3_CONF_VARS']['T3_SERVICES'][$serviceType][$serviceKey] ?? false)) { 1088 // No check is done here - there might be configuration values only the service type knows about, so 1089 // we pass everything 1090 $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey] = array_merge($GLOBALS['T3_SERVICES'][$serviceType][$serviceKey], $GLOBALS['TYPO3_CONF_VARS']['T3_SERVICES'][$serviceType][$serviceKey]); 1091 } 1092 // OS check 1093 // Empty $os means 'not limited to one OS', therefore a check is not needed 1094 if ($GLOBALS['T3_SERVICES'][$serviceType][$serviceKey]['available'] && $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey]['os'] != '') { 1095 $os_type = Environment::isWindows() ? 'WIN' : 'UNIX'; 1096 $os = GeneralUtility::trimExplode(',', strtoupper($GLOBALS['T3_SERVICES'][$serviceType][$serviceKey]['os'])); 1097 if (!in_array($os_type, $os, true)) { 1098 self::deactivateService($serviceType, $serviceKey); 1099 } 1100 } 1101 // Convert subtype list to array for quicker access 1102 $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey]['serviceSubTypes'] = []; 1103 $serviceSubTypes = GeneralUtility::trimExplode(',', $info['subtype']); 1104 foreach ($serviceSubTypes as $subtype) { 1105 $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey]['serviceSubTypes'][$subtype] = $subtype; 1106 } 1107 } 1108 1109 /** 1110 * Find the available service with highest priority 1111 * 1112 * @param string $serviceType Service type 1113 * @param string $serviceSubType Service sub type 1114 * @param mixed $excludeServiceKeys Service keys that should be excluded in the search for a service. Array or comma list. 1115 * @return mixed Service info array if a service was found, FALSE otherwise 1116 */ 1117 public static function findService($serviceType, $serviceSubType = '', $excludeServiceKeys = []) 1118 { 1119 $serviceKey = false; 1120 $serviceInfo = false; 1121 $priority = 0; 1122 $quality = 0; 1123 if (!is_array($excludeServiceKeys)) { 1124 $excludeServiceKeys = GeneralUtility::trimExplode(',', $excludeServiceKeys, true); 1125 } 1126 if (is_array($GLOBALS['T3_SERVICES'][$serviceType])) { 1127 foreach ($GLOBALS['T3_SERVICES'][$serviceType] as $key => $info) { 1128 if (in_array($key, $excludeServiceKeys)) { 1129 continue; 1130 } 1131 // Select a subtype randomly 1132 // Useful to start a service by service key without knowing his subtypes - for testing purposes 1133 if ($serviceSubType === '*') { 1134 $serviceSubType = key($info['serviceSubTypes']); 1135 } 1136 // This matches empty subtype too 1137 if ($info['available'] && ($info['subtype'] == $serviceSubType || $info['serviceSubTypes'][$serviceSubType]) && $info['priority'] >= $priority) { 1138 // Has a lower quality than the already found, therefore we skip this service 1139 if ($info['priority'] == $priority && $info['quality'] < $quality) { 1140 continue; 1141 } 1142 // Check if the service is available 1143 $info['available'] = self::isServiceAvailable($serviceType, $key, $info); 1144 // Still available after exec check? 1145 if ($info['available']) { 1146 $serviceKey = $key; 1147 $priority = $info['priority']; 1148 $quality = $info['quality']; 1149 } 1150 } 1151 } 1152 } 1153 if ($serviceKey) { 1154 $serviceInfo = $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey]; 1155 } 1156 return $serviceInfo; 1157 } 1158 1159 /** 1160 * Find a specific service identified by its key 1161 * Note that this completely bypasses the notions of priority and quality 1162 * 1163 * @param string $serviceKey Service key 1164 * @return array Service info array if a service was found 1165 * @throws \TYPO3\CMS\Core\Exception 1166 */ 1167 public static function findServiceByKey($serviceKey) 1168 { 1169 if (is_array($GLOBALS['T3_SERVICES'])) { 1170 // Loop on all service types 1171 // NOTE: we don't care about the actual type, we are looking for a specific key 1172 foreach ($GLOBALS['T3_SERVICES'] as $serviceType => $servicesPerType) { 1173 if (isset($servicesPerType[$serviceKey])) { 1174 $serviceDetails = $servicesPerType[$serviceKey]; 1175 // Test if service is available 1176 if (self::isServiceAvailable($serviceType, $serviceKey, $serviceDetails)) { 1177 // We have found the right service, return its information 1178 return $serviceDetails; 1179 } 1180 } 1181 } 1182 } 1183 throw new \TYPO3\CMS\Core\Exception('Service not found for key: ' . $serviceKey, 1319217244); 1184 } 1185 1186 /** 1187 * Check if a given service is available, based on the executable files it depends on 1188 * 1189 * @param string $serviceType Type of service 1190 * @param string $serviceKey Specific key of the service 1191 * @param array $serviceDetails Information about the service 1192 * @return bool Service availability 1193 */ 1194 public static function isServiceAvailable($serviceType, $serviceKey, $serviceDetails) 1195 { 1196 // If the service depends on external programs - check if they exists 1197 if (trim($serviceDetails['exec'])) { 1198 $executables = GeneralUtility::trimExplode(',', $serviceDetails['exec'], true); 1199 foreach ($executables as $executable) { 1200 // If at least one executable file is not available, exit early returning FALSE 1201 if (!CommandUtility::checkCommand($executable)) { 1202 self::deactivateService($serviceType, $serviceKey); 1203 return false; 1204 } 1205 } 1206 } 1207 // The service is available 1208 return true; 1209 } 1210 1211 /** 1212 * Deactivate a service 1213 * 1214 * @param string $serviceType Service type 1215 * @param string $serviceKey Service key 1216 */ 1217 public static function deactivateService($serviceType, $serviceKey) 1218 { 1219 // ... maybe it's better to move non-available services to a different array?? 1220 $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey]['available'] = false; 1221 } 1222 1223 /************************************** 1224 * 1225 * Adding FRONTEND features 1226 * 1227 ***************************************/ 1228 /** 1229 * Adds an entry to the list of plugins in content elements of type "Insert plugin" 1230 * Takes the $itemArray (label, value[,icon]) and adds to the items-array of $GLOBALS['TCA'][tt_content] elements with CType "listtype" (or another field if $type points to another fieldname) 1231 * If the value (array pos. 1) is already found in that items-array, the entry is substituted, otherwise the input array is added to the bottom. 1232 * Use this function to add a frontend plugin to this list of plugin-types - or more generally use this function to add an entry to any selectorbox/radio-button set in the FormEngine 1233 * 1234 * FOR USE IN files in Configuration/TCA/Overrides/*.php Use in ext_tables.php FILES may break the frontend. 1235 * 1236 * @param array $itemArray Numerical array: [0] => Plugin label, [1] => Plugin identifier / plugin key, ideally prefixed with a extension-specific name (e.g. "events2_list"), [2] => Path to plugin icon relative to TYPO3_mainDir 1237 * @param string $type Type (eg. "list_type") - basically a field from "tt_content" table 1238 * @param string $extensionKey The extension key 1239 * @throws \RuntimeException 1240 */ 1241 public static function addPlugin($itemArray, $type = 'list_type', $extensionKey = null) 1242 { 1243 if (!isset($extensionKey)) { 1244 throw new \InvalidArgumentException( 1245 'No extension key could be determined when calling addPlugin()!' 1246 . LF 1247 . 'This method is meant to be called from Configuration/TCA/Overrides files. ' 1248 . 'The extension key needs to be specified as third parameter. ' 1249 . 'Calling it from any other place e.g. ext_localconf.php does not work and is not supported.', 1250 1404068038 1251 ); 1252 } 1253 if (!isset($itemArray[2]) || !$itemArray[2]) { 1254 // @todo do we really set $itemArray[2], even if we cannot find an icon? (as that means it's set to 'EXT:foobar/') 1255 $itemArray[2] = 'EXT:' . $extensionKey . '/' . static::getExtensionIcon(static::$packageManager->getPackage($extensionKey)->getPackagePath()); 1256 } 1257 if (is_array($GLOBALS['TCA']['tt_content']['columns']) && is_array($GLOBALS['TCA']['tt_content']['columns'][$type]['config']['items'])) { 1258 foreach ($GLOBALS['TCA']['tt_content']['columns'][$type]['config']['items'] as $k => $v) { 1259 if ((string)$v[1] === (string)$itemArray[1]) { 1260 $GLOBALS['TCA']['tt_content']['columns'][$type]['config']['items'][$k] = $itemArray; 1261 return; 1262 } 1263 } 1264 $GLOBALS['TCA']['tt_content']['columns'][$type]['config']['items'][] = $itemArray; 1265 } 1266 } 1267 1268 /** 1269 * Adds an entry to the "ds" array of the tt_content field "pi_flexform". 1270 * This is used by plugins to add a flexform XML reference / content for use when they are selected as plugin or content element. 1271 * FOR USE IN files in Configuration/TCA/Overrides/*.php Use in ext_tables.php FILES may break the frontend. 1272 * 1273 * @param string $piKeyToMatch Plugin key as used in the list_type field. Use the asterisk * to match all list_type values. 1274 * @param string $value Either a reference to a flex-form XML file (eg. "FILE:EXT:newloginbox/flexform_ds.xml") or the XML directly. 1275 * @param string $CTypeToMatch Value of tt_content.CType (Content Type) to match. The default is "list" which corresponds to the "Insert Plugin" content element. Use the asterisk * to match all CType values. 1276 * @see addPlugin() 1277 */ 1278 public static function addPiFlexFormValue($piKeyToMatch, $value, $CTypeToMatch = 'list') 1279 { 1280 if (is_array($GLOBALS['TCA']['tt_content']['columns']) && is_array($GLOBALS['TCA']['tt_content']['columns']['pi_flexform']['config']['ds'])) { 1281 $GLOBALS['TCA']['tt_content']['columns']['pi_flexform']['config']['ds'][$piKeyToMatch . ',' . $CTypeToMatch] = $value; 1282 } 1283 } 1284 1285 /** 1286 * Adds the $table tablename to the list of tables allowed to be includes by content element type "Insert records" 1287 * By using $content_table and $content_field you can also use the function for other tables. 1288 * FOR USE IN files in Configuration/TCA/Overrides/*.php Use in ext_tables.php FILES may break the frontend. 1289 * 1290 * @param string $table Table name to allow for "insert record 1291 * @param string $content_table Table name TO WHICH the $table name is applied. See $content_field as well. 1292 * @param string $content_field Field name in the database $content_table in which $table is allowed to be added as a reference ("Insert Record") 1293 */ 1294 public static function addToInsertRecords($table, $content_table = 'tt_content', $content_field = 'records') 1295 { 1296 if (is_array($GLOBALS['TCA'][$content_table]['columns']) && isset($GLOBALS['TCA'][$content_table]['columns'][$content_field]['config']['allowed'])) { 1297 $GLOBALS['TCA'][$content_table]['columns'][$content_field]['config']['allowed'] .= ',' . $table; 1298 } 1299 } 1300 1301 /** 1302 * Add PlugIn to the default template rendering (previously called "Static Template #43") 1303 * 1304 * When adding a frontend plugin you will have to add both an entry to the TCA definition of tt_content table AND to the TypoScript template which must initiate the rendering. 1305 * 1306 * The naming of #43 has historic reason and is rooted inside code which is now put into a TER extension called 1307 * "statictemplates". Since the static template with uid 43 is the "content.default" and practically always used 1308 * for rendering the content elements it's very useful to have this function automatically adding the necessary 1309 * TypoScript for calling your plugin. 1310 * The logic is now generalized and called "defaultContentRendering", see addTypoScript() as well. 1311 * 1312 * $type determines the type of frontend plugin: 1313 * + list_type (default) - the good old "Insert plugin" entry 1314 * + menu_type - a "Menu/Sitemap" entry 1315 * + CType - a new content element type 1316 * + header_layout - an additional header type (added to the selection of layout1-5) 1317 * + includeLib - just includes the library for manual use somewhere in TypoScript. 1318 * (Remember that your $type definition should correspond to the column/items array in $GLOBALS['TCA'][tt_content] where you added the selector item for the element! See addPlugin() function) 1319 * FOR USE IN ext_localconf.php FILES 1320 * 1321 * @param string $key The extension key 1322 * @param string $_ unused since TYPO3 CMS 8 1323 * @param string $suffix Is used as a suffix of the class name (e.g. "_pi1") 1324 * @param string $type See description above 1325 * @param bool $cacheable If $cached is set as USER content object (cObject) is created - otherwise a USER_INT object is created. 1326 */ 1327 public static function addPItoST43($key, $_ = '', $suffix = '', $type = 'list_type', $cacheable = false) 1328 { 1329 $cN = self::getCN($key); 1330 // General plugin 1331 $pluginContent = trim(' 1332plugin.' . $cN . $suffix . ' = USER' . ($cacheable ? '' : '_INT') . ' 1333plugin.' . $cN . $suffix . '.userFunc = ' . $cN . $suffix . '->main 1334'); 1335 self::addTypoScript($key, 'setup', ' 1336# Setting ' . $key . ' plugin TypoScript 1337' . $pluginContent); 1338 // Add after defaultContentRendering 1339 switch ($type) { 1340 case 'list_type': 1341 $addLine = 'tt_content.list.20.' . $key . $suffix . ' = < plugin.' . $cN . $suffix; 1342 break; 1343 case 'menu_type': 1344 $addLine = 'tt_content.menu.20.' . $key . $suffix . ' = < plugin.' . $cN . $suffix; 1345 break; 1346 case 'CType': 1347 $addLine = trim(' 1348tt_content.' . $key . $suffix . ' =< lib.contentElement 1349tt_content.' . $key . $suffix . ' { 1350 templateName = Generic 1351 20 =< plugin.' . $cN . $suffix . ' 1352} 1353'); 1354 break; 1355 case 'header_layout': 1356 $addLine = 'lib.stdheader.10.' . $key . $suffix . ' = < plugin.' . $cN . $suffix; 1357 break; 1358 case 'includeLib': 1359 $addLine = 'page.1000 = < plugin.' . $cN . $suffix; 1360 break; 1361 default: 1362 $addLine = ''; 1363 } 1364 if ($addLine) { 1365 self::addTypoScript($key, 'setup', ' 1366# Setting ' . $key . ' plugin TypoScript 1367' . $addLine . ' 1368', 'defaultContentRendering'); 1369 } 1370 } 1371 1372 /** 1373 * Call this method to add an entry in the static template list found in sys_templates 1374 * FOR USE IN Configuration/TCA/Overrides/sys_template.php Use in ext_tables.php may break the frontend. 1375 * 1376 * @param string $extKey Is of course the extension key 1377 * @param string $path Is the path where the template files (fixed names) include_static.txt, constants.txt, setup.txt, and include_static_file.txt is found (relative to extPath, eg. 'static/'). The file include_static_file.txt, allows you to include other static templates defined in files, from your static template, and thus corresponds to the field 'include_static_file' in the sys_template table. The syntax for this is a comma separated list of static templates to include, like: EXT:fluid_styled_content/Configuration/TypoScript/,EXT:da_newsletter_subscription/static/,EXT:cc_random_image/pi2/static/ 1378 * @param string $title Is the title in the selector box. 1379 * @throws \InvalidArgumentException 1380 * @see addTypoScript() 1381 */ 1382 public static function addStaticFile($extKey, $path, $title) 1383 { 1384 if (!$extKey) { 1385 throw new \InvalidArgumentException('No extension key given.', 1507321291); 1386 } 1387 if (!$path) { 1388 throw new \InvalidArgumentException('No file path given.', 1507321297); 1389 } 1390 if (is_array($GLOBALS['TCA']['sys_template']['columns'])) { 1391 $value = str_replace(',', '', 'EXT:' . $extKey . '/' . $path); 1392 $itemArray = [trim($title . ' (' . $extKey . ')'), $value]; 1393 $GLOBALS['TCA']['sys_template']['columns']['include_static_file']['config']['items'][] = $itemArray; 1394 } 1395 } 1396 1397 /** 1398 * Call this method to add an entry in the pageTSconfig list found in pages 1399 * FOR USE in Configuration/TCA/Overrides/pages.php 1400 * 1401 * @param string $extKey The extension key 1402 * @param string $filePath The path where the TSconfig file is located 1403 * @param string $title The title in the selector box 1404 * @throws \InvalidArgumentException 1405 */ 1406 public static function registerPageTSConfigFile($extKey, $filePath, $title) 1407 { 1408 if (!$extKey) { 1409 throw new \InvalidArgumentException('No extension key given.', 1447789490); 1410 } 1411 if (!$filePath) { 1412 throw new \InvalidArgumentException('No file path given.', 1447789491); 1413 } 1414 if (!isset($GLOBALS['TCA']['pages']['columns']) || !is_array($GLOBALS['TCA']['pages']['columns'])) { 1415 throw new \InvalidArgumentException('No TCA definition for table "pages".', 1447789492); 1416 } 1417 1418 $value = str_replace(',', '', 'EXT:' . $extKey . '/' . $filePath); 1419 $itemArray = [trim($title . ' (' . $extKey . ')'), $value]; 1420 $GLOBALS['TCA']['pages']['columns']['tsconfig_includes']['config']['items'][] = $itemArray; 1421 } 1422 1423 /** 1424 * Adds $content to the default TypoScript setup code as set in $GLOBALS['TYPO3_CONF_VARS'][FE]['defaultTypoScript_setup'] 1425 * Prefixed with a [GLOBAL] line 1426 * FOR USE IN ext_localconf.php FILES 1427 * 1428 * @param string $content TypoScript Setup string 1429 */ 1430 public static function addTypoScriptSetup($content) 1431 { 1432 $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_setup'] .= ' 1433[GLOBAL] 1434' . $content; 1435 } 1436 1437 /** 1438 * Adds $content to the default TypoScript constants code as set in $GLOBALS['TYPO3_CONF_VARS'][FE]['defaultTypoScript_constants'] 1439 * Prefixed with a [GLOBAL] line 1440 * FOR USE IN ext_localconf.php FILES 1441 * 1442 * @param string $content TypoScript Constants string 1443 */ 1444 public static function addTypoScriptConstants($content) 1445 { 1446 $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_constants'] .= ' 1447[GLOBAL] 1448' . $content; 1449 } 1450 1451 /** 1452 * Adds $content to the default TypoScript code for either setup or constants as set in $GLOBALS['TYPO3_CONF_VARS'][FE]['defaultTypoScript_*'] 1453 * (Basically this function can do the same as addTypoScriptSetup and addTypoScriptConstants - just with a little more hazzle, but also with some more options!) 1454 * FOR USE IN ext_localconf.php FILES 1455 * Note: As of TYPO3 CMS 6.2, static template #43 (content: default) was replaced with "defaultContentRendering" which makes it 1456 * possible that a first extension like fluid_styled_content registers a "contentRendering" template (= a template that defines default content rendering TypoScript) 1457 * by adding itself to $TYPO3_CONF_VARS[FE][contentRenderingTemplates][] = 'myext/Configuration/TypoScript'. 1458 * An extension calling addTypoScript('myext', 'setup', $typoScript, 'defaultContentRendering') will add its TypoScript directly after; 1459 * For now, "43" and "defaultContentRendering" can be used, but "defaultContentRendering" is more descriptive and 1460 * should be used in the future. 1461 * 1462 * @param string $key Is the extension key (informative only). 1463 * @param string $type Is either "setup" or "constants" and obviously determines which kind of TypoScript code we are adding. 1464 * @param string $content Is the TS content, will be prefixed with a [GLOBAL] line and a comment-header. 1465 * @param int|string string pointing to the "key" of a static_file template ([reduced extension_key]/[local path]). The points is that the TypoScript you add is included only IF that static template is included (and in that case, right after). So effectively the TypoScript you set can specifically overrule settings from those static templates. 1466 * @throws \InvalidArgumentException 1467 */ 1468 public static function addTypoScript(string $key, string $type, string $content, $afterStaticUid = 0) 1469 { 1470 if ($type !== 'setup' && $type !== 'constants') { 1471 throw new \InvalidArgumentException('Argument $type must be set to either "setup" or "constants" when calling addTypoScript from extension "' . $key . '"', 1507321200); 1472 } 1473 $content = ' 1474 1475[GLOBAL] 1476############################################# 1477## TypoScript added by extension "' . $key . '" 1478############################################# 1479 1480' . $content; 1481 if ($afterStaticUid) { 1482 // If 'content (default)' is targeted (static uid 43), 1483 // the content is added after typoscript of type contentRendering, eg. fluid_styled_content, see EXT:frontend/TemplateService for more information on how the code is parsed 1484 if ($afterStaticUid === 'defaultContentRendering' || $afterStaticUid == 43) { 1485 if (!isset($GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $type . '.']['defaultContentRendering'])) { 1486 $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $type . '.']['defaultContentRendering'] = ''; 1487 } 1488 $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $type . '.']['defaultContentRendering'] .= $content; 1489 } else { 1490 if (!isset($GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $type . '.'][$afterStaticUid])) { 1491 $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $type . '.'][$afterStaticUid] = ''; 1492 } 1493 $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $type . '.'][$afterStaticUid] .= $content; 1494 } 1495 } else { 1496 if (!isset($GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $type])) { 1497 $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $type] = ''; 1498 } 1499 $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $type] .= $content; 1500 } 1501 } 1502 1503 /*************************************** 1504 * 1505 * Internal extension management methods 1506 * 1507 ***************************************/ 1508 /** 1509 * Find extension icon 1510 * 1511 * @param string $extensionPath Path to extension directory. 1512 * @param bool $returnFullPath Return full path of file. 1513 * 1514 * @return string 1515 */ 1516 public static function getExtensionIcon($extensionPath, $returnFullPath = false) 1517 { 1518 $icon = ''; 1519 $locationsToCheckFor = [ 1520 'Resources/Public/Icons/Extension.svg', 1521 'Resources/Public/Icons/Extension.png', 1522 'Resources/Public/Icons/Extension.gif', 1523 'ext_icon.svg', 1524 'ext_icon.png', 1525 'ext_icon.gif', 1526 ]; 1527 foreach ($locationsToCheckFor as $fileLocation) { 1528 if (file_exists($extensionPath . $fileLocation)) { 1529 $icon = $fileLocation; 1530 break; 1531 } 1532 } 1533 return $returnFullPath ? $extensionPath . $icon : $icon; 1534 } 1535 1536 /** 1537 * Execute all ext_localconf.php files of loaded extensions. 1538 * The method implements an optionally used caching mechanism that concatenates all 1539 * ext_localconf.php files in one file. 1540 * 1541 * This is an internal method. It is only used during bootstrap and 1542 * extensions should not use it! 1543 * 1544 * @param bool $allowCaching Whether or not to load / create concatenated cache file 1545 * @param FrontendInterface $codeCache 1546 * @internal 1547 */ 1548 public static function loadExtLocalconf($allowCaching = true, FrontendInterface $codeCache = null) 1549 { 1550 if ($allowCaching) { 1551 $codeCache = $codeCache ?? self::getCacheManager()->getCache('cache_core'); 1552 $cacheIdentifier = self::getExtLocalconfCacheIdentifier(); 1553 if ($codeCache->has($cacheIdentifier)) { 1554 $codeCache->require($cacheIdentifier); 1555 } else { 1556 self::loadSingleExtLocalconfFiles(); 1557 self::createExtLocalconfCacheEntry($codeCache); 1558 } 1559 } else { 1560 self::loadSingleExtLocalconfFiles(); 1561 } 1562 } 1563 1564 /** 1565 * Execute ext_localconf.php files from extensions 1566 */ 1567 protected static function loadSingleExtLocalconfFiles() 1568 { 1569 // This is the main array meant to be manipulated in the ext_localconf.php files 1570 // In general it is recommended to not rely on it to be globally defined in that 1571 // scope but to use $GLOBALS['TYPO3_CONF_VARS'] instead. 1572 // Nevertheless we define it here as global for backwards compatibility. 1573 global $TYPO3_CONF_VARS; 1574 foreach (static::$packageManager->getActivePackages() as $package) { 1575 $extLocalconfPath = $package->getPackagePath() . 'ext_localconf.php'; 1576 if (@file_exists($extLocalconfPath)) { 1577 // $_EXTKEY and $_EXTCONF are available in ext_localconf.php 1578 // and are explicitly set in cached file as well 1579 $_EXTKEY = $package->getPackageKey(); 1580 $_EXTCONF = $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][$_EXTKEY] ?? null; 1581 require $extLocalconfPath; 1582 } 1583 } 1584 } 1585 1586 /** 1587 * Create cache entry for concatenated ext_localconf.php files 1588 * 1589 * @param FrontendInterface $codeCache 1590 */ 1591 protected static function createExtLocalconfCacheEntry(FrontendInterface $codeCache) 1592 { 1593 $phpCodeToCache = []; 1594 // Set same globals as in loadSingleExtLocalconfFiles() 1595 $phpCodeToCache[] = '/**'; 1596 $phpCodeToCache[] = ' * Compiled ext_localconf.php cache file'; 1597 $phpCodeToCache[] = ' */'; 1598 $phpCodeToCache[] = ''; 1599 $phpCodeToCache[] = 'global $TYPO3_CONF_VARS, $T3_SERVICES, $T3_VAR;'; 1600 $phpCodeToCache[] = ''; 1601 // Iterate through loaded extensions and add ext_localconf content 1602 foreach (static::$packageManager->getActivePackages() as $package) { 1603 $extensionKey = $package->getPackageKey(); 1604 $extLocalconfPath = $package->getPackagePath() . 'ext_localconf.php'; 1605 if (@file_exists($extLocalconfPath)) { 1606 // Include a header per extension to make the cache file more readable 1607 $phpCodeToCache[] = '/**'; 1608 $phpCodeToCache[] = ' * Extension: ' . $extensionKey; 1609 $phpCodeToCache[] = ' * File: ' . $extLocalconfPath; 1610 $phpCodeToCache[] = ' */'; 1611 $phpCodeToCache[] = ''; 1612 // Set $_EXTKEY and $_EXTCONF for this extension 1613 $phpCodeToCache[] = '$_EXTKEY = \'' . $extensionKey . '\';'; 1614 $phpCodeToCache[] = '$_EXTCONF = $GLOBALS[\'TYPO3_CONF_VARS\'][\'EXT\'][\'extConf\'][$_EXTKEY] ?? null;'; 1615 $phpCodeToCache[] = ''; 1616 // Add ext_localconf.php content of extension 1617 $phpCodeToCache[] = trim(file_get_contents($extLocalconfPath)); 1618 $phpCodeToCache[] = ''; 1619 $phpCodeToCache[] = ''; 1620 } 1621 } 1622 $phpCodeToCache = implode(LF, $phpCodeToCache); 1623 // Remove all start and ending php tags from content 1624 $phpCodeToCache = preg_replace('/<\\?php|\\?>/is', '', $phpCodeToCache); 1625 $codeCache->set(self::getExtLocalconfCacheIdentifier(), $phpCodeToCache); 1626 } 1627 1628 /** 1629 * Cache identifier of concatenated ext_localconf file 1630 * 1631 * @return string 1632 */ 1633 protected static function getExtLocalconfCacheIdentifier() 1634 { 1635 return 'ext_localconf_' . sha1(TYPO3_version . Environment::getProjectPath() . 'extLocalconf' . serialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['runtimeActivatedPackages'])); 1636 } 1637 1638 /** 1639 * Wrapper for buildBaseTcaFromSingleFiles handling caching. 1640 * 1641 * This builds 'base' TCA that is later overloaded by ext_tables.php. 1642 * 1643 * Use a cache file if exists and caching is allowed. 1644 * 1645 * This is an internal method. It is only used during bootstrap and 1646 * extensions should not use it! 1647 * 1648 * @param bool $allowCaching Whether or not to load / create concatenated cache file 1649 * @internal 1650 */ 1651 public static function loadBaseTca($allowCaching = true, FrontendInterface $codeCache = null) 1652 { 1653 if ($allowCaching) { 1654 $codeCache = $codeCache ?? self::getCacheManager()->getCache('cache_core'); 1655 $cacheIdentifier = static::getBaseTcaCacheIdentifier(); 1656 $cacheData = $codeCache->require($cacheIdentifier); 1657 if ($cacheData) { 1658 $GLOBALS['TCA'] = $cacheData['tca']; 1659 GeneralUtility::setSingletonInstance( 1660 CategoryRegistry::class, 1661 unserialize( 1662 $cacheData['categoryRegistry'], 1663 ['allowed_classes' => [CategoryRegistry::class]] 1664 ) 1665 ); 1666 } else { 1667 static::buildBaseTcaFromSingleFiles(); 1668 static::createBaseTcaCacheFile($codeCache); 1669 } 1670 } else { 1671 static::buildBaseTcaFromSingleFiles(); 1672 } 1673 } 1674 1675 /** 1676 * Find all Configuration/TCA/* files of extensions and create base TCA from it. 1677 * The filename must be the table name in $GLOBALS['TCA'], and the content of 1678 * the file should return an array with content of a specific table. 1679 * 1680 * @see Extension core, extensionmanager and others for examples. 1681 */ 1682 protected static function buildBaseTcaFromSingleFiles() 1683 { 1684 $GLOBALS['TCA'] = []; 1685 1686 $activePackages = static::$packageManager->getActivePackages(); 1687 1688 // First load "full table" files from Configuration/TCA 1689 foreach ($activePackages as $package) { 1690 try { 1691 $finder = Finder::create()->files()->sortByName()->depth(0)->name('*.php')->in($package->getPackagePath() . 'Configuration/TCA'); 1692 } catch (\InvalidArgumentException $e) { 1693 // No such directory in this package 1694 continue; 1695 } 1696 foreach ($finder as $fileInfo) { 1697 $tcaOfTable = require $fileInfo->getPathname(); 1698 if (is_array($tcaOfTable)) { 1699 $tcaTableName = substr($fileInfo->getBasename(), 0, -4); 1700 $GLOBALS['TCA'][$tcaTableName] = $tcaOfTable; 1701 } 1702 } 1703 } 1704 1705 // Apply category stuff 1706 CategoryRegistry::getInstance()->applyTcaForPreRegisteredTables(); 1707 1708 // Execute override files from Configuration/TCA/Overrides 1709 foreach ($activePackages as $package) { 1710 try { 1711 $finder = Finder::create()->files()->sortByName()->depth(0)->name('*.php')->in($package->getPackagePath() . 'Configuration/TCA/Overrides'); 1712 } catch (\InvalidArgumentException $e) { 1713 // No such directory in this package 1714 continue; 1715 } 1716 foreach ($finder as $fileInfo) { 1717 require $fileInfo->getPathname(); 1718 } 1719 } 1720 1721 // TCA migration 1722 // @deprecated since TYPO3 CMS 7. Not removed in TYPO3 CMS 8 though. This call will stay for now to allow further TCA migrations in 8. 1723 $tcaMigration = GeneralUtility::makeInstance(TcaMigration::class); 1724 $GLOBALS['TCA'] = $tcaMigration->migrate($GLOBALS['TCA']); 1725 $messages = $tcaMigration->getMessages(); 1726 if (!empty($messages)) { 1727 $context = 'Automatic TCA migration done during bootstrap. Please adapt TCA accordingly, these migrations' 1728 . ' will be removed. The backend module "Configuration -> TCA" shows the modified values.' 1729 . ' Please adapt these areas:'; 1730 array_unshift($messages, $context); 1731 trigger_error(implode(LF, $messages), E_USER_DEPRECATED); 1732 } 1733 1734 // TCA preparation 1735 $tcaPreparation = GeneralUtility::makeInstance(TcaPreparation::class); 1736 $GLOBALS['TCA'] = $tcaPreparation->prepare($GLOBALS['TCA']); 1737 1738 static::emitTcaIsBeingBuiltSignal($GLOBALS['TCA']); 1739 } 1740 1741 /** 1742 * Emits the signal and uses the result of slots for the final TCA 1743 * This means, that *all* slots *must* return the complete TCA to 1744 * be effective. If a slot calls methods that manipulate the global array, 1745 * it needs to return the global array in the end. To be future proof, 1746 * a slot should manipulate the signal argument only and return it 1747 * after manipulation. 1748 * 1749 * @param array $tca 1750 */ 1751 protected static function emitTcaIsBeingBuiltSignal(array $tca) 1752 { 1753 list($tca) = static::getSignalSlotDispatcher()->dispatch(__CLASS__, 'tcaIsBeingBuilt', [$tca]); 1754 $GLOBALS['TCA'] = $tca; 1755 } 1756 1757 /** 1758 * Cache base $GLOBALS['TCA'] to cache file to require the whole thing in one 1759 * file for next access instead of cycling through all extensions again. 1760 * 1761 * @param FrontendInterface $codeCache 1762 */ 1763 protected static function createBaseTcaCacheFile(FrontendInterface $codeCache) 1764 { 1765 $codeCache->set( 1766 static::getBaseTcaCacheIdentifier(), 1767 'return ' 1768 . var_export(['tca' => $GLOBALS['TCA'], 'categoryRegistry' => serialize(CategoryRegistry::getInstance())], true) 1769 . ';' 1770 ); 1771 } 1772 1773 /** 1774 * Cache identifier of base TCA cache entry. 1775 * 1776 * @return string 1777 */ 1778 protected static function getBaseTcaCacheIdentifier() 1779 { 1780 return 'tca_base_' . sha1(TYPO3_version . Environment::getProjectPath() . 'tca_code' . serialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['runtimeActivatedPackages'])); 1781 } 1782 1783 /** 1784 * Execute all ext_tables.php files of loaded extensions. 1785 * The method implements an optionally used caching mechanism that concatenates all 1786 * ext_tables.php files in one file. 1787 * 1788 * This is an internal method. It is only used during bootstrap and 1789 * extensions should not use it! 1790 * 1791 * @param bool $allowCaching Whether to load / create concatenated cache file 1792 * @internal 1793 */ 1794 public static function loadExtTables($allowCaching = true) 1795 { 1796 if ($allowCaching && !self::$extTablesWasReadFromCacheOnce) { 1797 self::$extTablesWasReadFromCacheOnce = true; 1798 $cacheIdentifier = self::getExtTablesCacheIdentifier(); 1799 /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $codeCache */ 1800 $codeCache = self::getCacheManager()->getCache('cache_core'); 1801 if ($codeCache->has($cacheIdentifier)) { 1802 $codeCache->require($cacheIdentifier); 1803 } else { 1804 self::loadSingleExtTablesFiles(); 1805 self::createExtTablesCacheEntry(); 1806 } 1807 } else { 1808 self::loadSingleExtTablesFiles(); 1809 } 1810 } 1811 1812 /** 1813 * Load ext_tables.php as single files 1814 */ 1815 protected static function loadSingleExtTablesFiles() 1816 { 1817 // In general it is recommended to not rely on it to be globally defined in that 1818 // scope, but we can not prohibit this without breaking backwards compatibility 1819 global $T3_SERVICES, $T3_VAR, $TYPO3_CONF_VARS; 1820 global $TBE_MODULES, $TBE_MODULES_EXT, $TCA; 1821 global $PAGES_TYPES, $TBE_STYLES; 1822 global $_EXTKEY; 1823 // Load each ext_tables.php file of loaded extensions 1824 foreach (static::$packageManager->getActivePackages() as $package) { 1825 $extTablesPath = $package->getPackagePath() . 'ext_tables.php'; 1826 if (@file_exists($extTablesPath)) { 1827 // $_EXTKEY and $_EXTCONF are available in ext_tables.php 1828 // and are explicitly set in cached file as well 1829 $_EXTKEY = $package->getPackageKey(); 1830 $_EXTCONF = $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][$_EXTKEY] ?? null; 1831 require $extTablesPath; 1832 } 1833 } 1834 } 1835 1836 /** 1837 * Create concatenated ext_tables.php cache file 1838 */ 1839 protected static function createExtTablesCacheEntry() 1840 { 1841 $phpCodeToCache = []; 1842 // Set same globals as in loadSingleExtTablesFiles() 1843 $phpCodeToCache[] = '/**'; 1844 $phpCodeToCache[] = ' * Compiled ext_tables.php cache file'; 1845 $phpCodeToCache[] = ' */'; 1846 $phpCodeToCache[] = ''; 1847 $phpCodeToCache[] = 'global $T3_SERVICES, $T3_VAR, $TYPO3_CONF_VARS;'; 1848 $phpCodeToCache[] = 'global $TBE_MODULES, $TBE_MODULES_EXT, $TCA;'; 1849 $phpCodeToCache[] = 'global $PAGES_TYPES, $TBE_STYLES;'; 1850 $phpCodeToCache[] = 'global $_EXTKEY;'; 1851 $phpCodeToCache[] = ''; 1852 // Iterate through loaded extensions and add ext_tables content 1853 foreach (static::$packageManager->getActivePackages() as $package) { 1854 $extensionKey = $package->getPackageKey(); 1855 $extTablesPath = $package->getPackagePath() . 'ext_tables.php'; 1856 if (@file_exists($extTablesPath)) { 1857 // Include a header per extension to make the cache file more readable 1858 $phpCodeToCache[] = '/**'; 1859 $phpCodeToCache[] = ' * Extension: ' . $extensionKey; 1860 $phpCodeToCache[] = ' * File: ' . $extTablesPath; 1861 $phpCodeToCache[] = ' */'; 1862 $phpCodeToCache[] = ''; 1863 // Set $_EXTKEY and $_EXTCONF for this extension 1864 $phpCodeToCache[] = '$_EXTKEY = \'' . $extensionKey . '\';'; 1865 $phpCodeToCache[] = '$_EXTCONF = $GLOBALS[\'TYPO3_CONF_VARS\'][\'EXT\'][\'extConf\'][$_EXTKEY] ?? null;'; 1866 $phpCodeToCache[] = ''; 1867 // Add ext_tables.php content of extension 1868 $phpCodeToCache[] = trim(file_get_contents($extTablesPath)); 1869 $phpCodeToCache[] = ''; 1870 } 1871 } 1872 $phpCodeToCache = implode(LF, $phpCodeToCache); 1873 // Remove all start and ending php tags from content 1874 $phpCodeToCache = preg_replace('/<\\?php|\\?>/is', '', $phpCodeToCache); 1875 self::getCacheManager()->getCache('cache_core')->set(self::getExtTablesCacheIdentifier(), $phpCodeToCache); 1876 } 1877 1878 /** 1879 * Cache identifier for concatenated ext_tables.php files 1880 * 1881 * @return string 1882 */ 1883 protected static function getExtTablesCacheIdentifier() 1884 { 1885 return 'ext_tables_' . sha1(TYPO3_version . Environment::getProjectPath() . 'extTables' . serialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['runtimeActivatedPackages'])); 1886 } 1887 1888 /** 1889 * Remove cache files from php code cache, grouped by 'system' 1890 * 1891 * This removes the following cache entries: 1892 * - autoloader cache registry 1893 * - cache loaded extension array 1894 * - ext_localconf concatenation 1895 * - ext_tables concatenation 1896 * 1897 * This method is usually only used by extension that fiddle 1898 * with the loaded extensions. An example is the extension 1899 * manager and the install tool. 1900 * 1901 * @deprecated CacheManager provides the functionality directly 1902 */ 1903 public static function removeCacheFiles() 1904 { 1905 trigger_error('ExtensionManagementUtility::removeCacheFiles() will be removed in TYPO3 v10.0. Use CacheManager directly to flush all system caches.', E_USER_DEPRECATED); 1906 self::getCacheManager()->flushCachesInGroup('system'); 1907 } 1908 1909 /** 1910 * Gets an array of loaded extension keys 1911 * 1912 * @return array Loaded extensions 1913 */ 1914 public static function getLoadedExtensionListArray() 1915 { 1916 return array_keys(static::$packageManager->getActivePackages()); 1917 } 1918 1919 /** 1920 * Loads given extension 1921 * 1922 * Warning: This method only works if the ugrade wizard to transform 1923 * localconf.php to LocalConfiguration.php was already run 1924 * 1925 * @param string $extensionKey Extension key to load 1926 * @throws \RuntimeException 1927 */ 1928 public static function loadExtension($extensionKey) 1929 { 1930 if (static::$packageManager->isPackageActive($extensionKey)) { 1931 throw new \RuntimeException('Extension already loaded', 1342345486); 1932 } 1933 static::$packageManager->activatePackage($extensionKey); 1934 } 1935 1936 /** 1937 * Unloads given extension 1938 * 1939 * Warning: This method only works if the ugrade wizard to transform 1940 * localconf.php to LocalConfiguration.php was already run 1941 * 1942 * @param string $extensionKey Extension key to remove 1943 * @throws \RuntimeException 1944 */ 1945 public static function unloadExtension($extensionKey) 1946 { 1947 if (!static::$packageManager->isPackageActive($extensionKey)) { 1948 throw new \RuntimeException('Extension not loaded', 1342345487); 1949 } 1950 static::$packageManager->deactivatePackage($extensionKey); 1951 } 1952 1953 /** 1954 * Makes a table categorizable by adding value into the category registry. 1955 * FOR USE IN ext_localconf.php FILES or files in Configuration/TCA/Overrides/*.php Use the latter to benefit from TCA caching! 1956 * 1957 * @param string $extensionKey Extension key to be used 1958 * @param string $tableName Name of the table to be categorized 1959 * @param string $fieldName Name of the field to be used to store categories 1960 * @param array $options Additional configuration options 1961 * @param bool $override If TRUE, any category configuration for the same table / field is removed before the new configuration is added 1962 * @see addTCAcolumns 1963 * @see addToAllTCAtypes 1964 */ 1965 public static function makeCategorizable($extensionKey, $tableName, $fieldName = 'categories', array $options = [], $override = false) 1966 { 1967 // Update the category registry 1968 $result = CategoryRegistry::getInstance()->add($extensionKey, $tableName, $fieldName, $options, $override); 1969 if ($result === false) { 1970 GeneralUtility::makeInstance(LogManager::class) 1971 ->getLogger(__CLASS__) 1972 ->warning(sprintf( 1973 CategoryRegistry::class . ': no category registered for table "%s". Key was already registered.', 1974 $tableName 1975 )); 1976 } 1977 } 1978} 1979