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