ExtensionManagementUtility.php 85.6 KB
Newer Older
1
<?php
2

3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
7
8
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
9
 *
10
11
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
14
 * The TYPO3 project - inspiring people to share!
 */
Wouter Wolters's avatar
Wouter Wolters committed
15

16
17
namespace TYPO3\CMS\Core\Utility;

18
use Psr\EventDispatcher\EventDispatcherInterface;
19
use Symfony\Component\Finder\Finder;
20
21
use TYPO3\CMS\Backend\Routing\Route;
use TYPO3\CMS\Backend\Routing\Router;
22
use TYPO3\CMS\Core\Cache\CacheManager;
23
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
24
use TYPO3\CMS\Core\Category\CategoryRegistry;
25
use TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent;
26
use TYPO3\CMS\Core\Core\Environment;
27
use TYPO3\CMS\Core\Imaging\IconRegistry;
28
use TYPO3\CMS\Core\Log\LogManager;
29
use TYPO3\CMS\Core\Migrations\TcaMigration;
30
use TYPO3\CMS\Core\Package\Cache\PackageDependentCacheIdentifier;
31
use TYPO3\CMS\Core\Package\Exception as PackageException;
32
use TYPO3\CMS\Core\Package\PackageManager;
33
use TYPO3\CMS\Core\Preparations\TcaPreparation;
34
use TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter;
35

36
37
38
39
/**
 * Extension Management functions
 *
 * This class is never instantiated, rather the methods inside is called as functions like
40
 * \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('my_extension');
41
 */
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class ExtensionManagementUtility
{
    /**
     * TRUE, if ext_tables file was read from cache for this script run.
     * The frontend tends to do that multiple times, but the caching framework does
     * not allow this (via a require_once call). This variable is used to track
     * the access to the cache file to read the single ext_tables.php if it was
     * already read from cache
     *
     * @todo See if we can get rid of the 'load multiple times' scenario in fe
     * @var bool
     */
    protected static $extTablesWasReadFromCacheOnce = false;

    /**
     * @var PackageManager
     */
    protected static $packageManager;

    /**
     * Sets the package manager for all that backwards compatibility stuff,
63
     * so it doesn't have to be fetched through the bootstrap.
64
65
66
67
68
69
70
71
72
73
     *
     * @param PackageManager $packageManager
     * @internal
     */
    public static function setPackageManager(PackageManager $packageManager)
    {
        static::$packageManager = $packageManager;
    }

    /**
74
     * @var EventDispatcherInterface
75
     */
76
    protected static $eventDispatcher;
77
78

    /**
79
     * Sets the event dispatcher to be available.
80
     *
81
82
     * @param EventDispatcherInterface $eventDispatcher
     * @internal only used for tests and the internal TYPO3 Bootstrap process
83
     */
84
    public static function setEventDispatcher(EventDispatcherInterface $eventDispatcher)
85
    {
86
        static::$eventDispatcher = $eventDispatcher;
87
88
89
    }

    /**
90
     * @var CacheManager
91
     */
92
    protected static $cacheManager;
93
94

    /**
95
     * Getter for the cache manager
96
     *
97
     * @return CacheManager
98
     */
99
    protected static function getCacheManager()
100
    {
101
102
        if (static::$cacheManager === null) {
            static::$cacheManager = GeneralUtility::makeInstance(CacheManager::class);
103
        }
104
        return static::$cacheManager;
105
106
107
108
109
110
111
112
113
114
115
116
117
    }

    /**************************************
     *
     * PATHS and other evaluation
     *
     ***************************************/
    /**
     * Returns TRUE if the extension with extension key $key is loaded.
     *
     * @param string $key Extension key to test
     * @return bool
     */
118
    public static function isLoaded($key)
119
    {
120
        return static::$packageManager->isPackageActive($key);
121
122
    }

123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
    /**
     * Temporary helper method to resolve paths with the PackageManager, because
     * it is statically injected to this class already. This method will be removed
     * without substitution in TYPO3 12 once a proper resource API is introduced.
     *
     * @param string $path
     * @return string
     * @throws PackageException
     * @internal This method is only allowed to be called from GeneralUtility::getFileAbsFileName()! DONT'T introduce other usages!
     */
    public static function resolvePackagePath(string $path): string
    {
        return static::$packageManager->resolvePackagePath($path);
    }

138
139
140
    /**
     * Returns the absolute path to the extension with extension key $key.
     *
141
142
     * @param string $key Extension key
     * @param string $script $script is appended to the output if set.
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
     * @throws \BadFunctionCallException
     * @return string
     */
    public static function extPath($key, $script = '')
    {
        if (!static::$packageManager->isPackageActive($key)) {
            throw new \BadFunctionCallException('TYPO3 Fatal Error: Extension key "' . $key . '" is NOT loaded!', 1365429656);
        }
        return static::$packageManager->getPackage($key)->getPackagePath() . $script;
    }

    /**
     * Returns the correct class name prefix for the extension key $key
     *
     * @param string $key Extension key
     * @return string
     * @internal
     */
    public static function getCN($key)
    {
        return strpos($key, 'user_') === 0 ? 'user_' . str_replace('_', '', substr($key, 5)) : 'tx_' . str_replace('_', '', $key);
    }

    /**
     * Retrieves the version of an installed extension.
     * If the extension is not installed, this function returns an empty string.
     *
     * @param string $key The key of the extension to look up, must not be empty
     *
     * @throws \InvalidArgumentException
     * @throws \TYPO3\CMS\Core\Package\Exception
     * @return string The extension version as a string in the format "x.y.z",
     */
    public static function getExtensionVersion($key)
    {
        if (!is_string($key) || empty($key)) {
            throw new \InvalidArgumentException('Extension key must be a non-empty string.', 1294586096);
        }
        if (!static::isLoaded($key)) {
            return '';
        }
        $version = static::$packageManager->getPackage($key)->getPackageMetaData()->getVersion();
        if (empty($version)) {
186
            throw new PackageException('Version number in composer manifest of package "' . $key . '" is missing or invalid', 1395614959);
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
        }
        return $version;
    }

    /**************************************
     *
     *	 Adding BACKEND features
     *	 (related to core features)
     *
     ***************************************/
    /**
     * Adding fields to an existing table definition in $GLOBALS['TCA']
     * Adds an array with $GLOBALS['TCA'] column-configuration to the $GLOBALS['TCA']-entry for that table.
     * 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!
     * So to have the fields displayed you must also call fx. addToAllTCAtypes or manually add the fields to the types list.
202
     * FOR USE IN files in Configuration/TCA/Overrides/*.php . Use in ext_tables.php FILES may break the frontend.
203
204
205
206
     *
     * @param string $table The table name of a table already present in $GLOBALS['TCA'] with a columns section
     * @param array $columnArray The array with the additional columns (typical some fields an extension wants to add)
     */
207
    public static function addTCAcolumns($table, $columnArray)
208
209
210
211
212
213
214
215
216
217
218
219
220
    {
        if (is_array($columnArray) && is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'])) {
            // Candidate for array_merge() if integer-keys will some day make trouble...
            $GLOBALS['TCA'][$table]['columns'] = array_merge($GLOBALS['TCA'][$table]['columns'], $columnArray);
        }
    }

    /**
     * Makes fields visible in the TCEforms, adding them to the end of (all) "types"-configurations
     *
     * Adds a string $string (comma separated list of field names) to all ["types"][xxx]["showitem"] entries for table $table (unless limited by $typeList)
     * This is needed to have new fields shown automatically in the TCEFORMS of a record from $table.
     * Typically this function is called after having added new columns (database fields) with the addTCAcolumns function
221
     * FOR USE IN files in Configuration/TCA/Overrides/*.php Use in ext_tables.php FILES may break the frontend.
222
223
224
225
226
227
228
229
230
     *
     * @param string $table Table name
     * @param string $newFieldsString Field list to add.
     * @param string $typeList List of specific types to add the field list to. (If empty, all type entries are affected)
     * @param string $position Insert fields before (default) or after one, or replace a field
     */
    public static function addToAllTCAtypes($table, $newFieldsString, $typeList = '', $position = '')
    {
        $newFieldsString = trim($newFieldsString);
231
        if ($newFieldsString === '' || !is_array($GLOBALS['TCA'][$table]['types'] ?? false)) {
232
233
            return;
        }
234
        if ($position !== '') {
235
            [$positionIdentifier, $entityName] = GeneralUtility::trimExplode(':', $position);
236
237
238
239
        } else {
            $positionIdentifier = '';
            $entityName = '';
        }
240
        $palettesChanged = [];
241
242
243
244
245
246
247

        foreach ($GLOBALS['TCA'][$table]['types'] as $type => &$typeDetails) {
            // skip if we don't want to add the field for this type
            if ($typeList !== '' && !GeneralUtility::inList($typeList, $type)) {
                continue;
            }
            // skip if fields were already added
248
249
250
251
252
253
            if (!isset($typeDetails['showitem'])) {
                continue;
            }

            $fieldArray = GeneralUtility::trimExplode(',', $typeDetails['showitem'], true);
            if (in_array($newFieldsString, $fieldArray, true)) {
254
255
256
257
258
                continue;
            }

            $fieldExists = false;
            $newPosition = '';
259
            if (is_array($GLOBALS['TCA'][$table]['palettes'] ?? false)) {
260
261
                // Get the palette names used in current showitem
                $paletteCount = preg_match_all('/(?:^|,)                    # Line start or a comma
262
					(?:
263
264
					    \\s*\\-\\-palette\\-\\-;[^;]*;([^,$]*)|             # --palette--;label;paletteName
					    \\s*\\b[^;,]+\\b(?:;[^;]*;([^;,]+))?[^,]*           # field;label;paletteName
265
					)/x', $typeDetails['showitem'], $paletteMatches);
266
267
268
269
                if ($paletteCount > 0) {
                    $paletteNames = array_filter(array_merge($paletteMatches[1], $paletteMatches[2]));
                    if (!empty($paletteNames)) {
                        foreach ($paletteNames as $paletteName) {
270
271
272
                            if (!isset($GLOBALS['TCA'][$table]['palettes'][$paletteName])) {
                                continue;
                            }
273
274
275
276
277
278
279
280
281
282
283
284
                            $palette = $GLOBALS['TCA'][$table]['palettes'][$paletteName];
                            switch ($positionIdentifier) {
                                case 'after':
                                case 'before':
                                    if (preg_match('/\\b' . $entityName . '\\b/', $palette['showitem']) > 0) {
                                        $newPosition = $positionIdentifier . ':--palette--;;' . $paletteName;
                                    }
                                    break;
                                case 'replace':
                                    // check if fields have been added to palette before
                                    if (isset($palettesChanged[$paletteName])) {
                                        $fieldExists = true;
285
                                        continue 2;
286
287
288
289
290
291
                                    }
                                    if (preg_match('/\\b' . $entityName . '\\b/', $palette['showitem']) > 0) {
                                        self::addFieldsToPalette($table, $paletteName, $newFieldsString, $position);
                                        // Memorize that we already changed this palette, in case other types also use it
                                        $palettesChanged[$paletteName] = true;
                                        $fieldExists = true;
292
                                        continue 2;
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
                                    }
                                    break;
                                default:
                                    // Intentionally left blank
                            }
                        }
                    }
                }
            }
            if ($fieldExists === false) {
                $typeDetails['showitem'] = self::executePositionedStringInsertion(
                    $typeDetails['showitem'],
                    $newFieldsString,
                    $newPosition !== '' ? $newPosition : $position
                );
            }
        }
        unset($typeDetails);
    }

    /**
     * Adds new fields to all palettes that is defined after an existing field.
     * If the field does not have a following palette yet, it's created automatically
     * and gets called "generatedFor-$field".
317
     * FOR USE IN files in Configuration/TCA/Overrides/*.php Use in ext_tables.php FILES may break the frontend.
318
319
320
321
322
323
324
325
326
327
328
329
     *
     * See unit tests for more examples and edge cases.
     *
     * Example:
     *
     * 'aTable' => array(
     * 	'types' => array(
     * 		'aType' => array(
     * 			'showitem' => 'aField, --palette--;;aPalette',
     * 		),
     * 	),
     * 	'palettes' => array(
330
     * 		'aPalette' => array(
331
332
333
334
335
336
337
338
339
340
341
342
343
344
     * 			'showitem' => 'fieldB, fieldC',
     * 		),
     * 	),
     * ),
     *
     * Calling addFieldsToAllPalettesOfField('aTable', 'aField', 'newA', 'before: fieldC') results in:
     *
     * 'aTable' => array(
     * 	'types' => array(
     * 		'aType' => array(
     * 			'showitem' => 'aField, --palette--;;aPalette',
     * 		),
     * 	),
     * 	'palettes' => array(
345
     * 		'aPalette' => array(
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
     * 			'showitem' => 'fieldB, newA, fieldC',
     * 		),
     * 	),
     * ),
     *
     * @param string $table Name of the table
     * @param string $field Name of the field that has the palette to be extended
     * @param string $addFields List of fields to be added to the palette
     * @param string $insertionPosition Insert fields before (default) or after one
     */
    public static function addFieldsToAllPalettesOfField($table, $field, $addFields, $insertionPosition = '')
    {
        if (!isset($GLOBALS['TCA'][$table]['columns'][$field])) {
            return;
        }
        if (!is_array($GLOBALS['TCA'][$table]['types'])) {
            return;
        }

        // Iterate through all types and search for the field that defines the palette to be extended
        foreach ($GLOBALS['TCA'][$table]['types'] as $typeName => $typeArray) {
            // Continue if types has no showitem at all or if requested field is not in it
368
            if (!isset($typeArray['showitem']) || !str_contains($typeArray['showitem'], $field)) {
369
370
371
372
                continue;
            }
            $fieldArrayWithOptions = GeneralUtility::trimExplode(',', $typeArray['showitem']);
            // Find the field we're handling
373
            $newFieldStringArray = [];
374
375
376
377
378
379
380
381
            foreach ($fieldArrayWithOptions as $fieldNumber => $fieldString) {
                $newFieldStringArray[] = $fieldString;
                $fieldArray = GeneralUtility::trimExplode(';', $fieldString);
                if ($fieldArray[0] !== $field) {
                    continue;
                }
                if (
                    isset($fieldArrayWithOptions[$fieldNumber + 1])
382
                    && strpos($fieldArrayWithOptions[$fieldNumber + 1], '--palette--') === 0
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
                ) {
                    // Match for $field and next field is a palette - add fields to this one
                    $paletteName = GeneralUtility::trimExplode(';', $fieldArrayWithOptions[$fieldNumber + 1]);
                    $paletteName = $paletteName[2];
                    self::addFieldsToPalette($table, $paletteName, $addFields, $insertionPosition);
                } else {
                    // Match for $field but next field is no palette - create a new one
                    $newPaletteName = 'generatedFor-' . $field;
                    self::addFieldsToPalette($table, 'generatedFor-' . $field, $addFields, $insertionPosition);
                    $newFieldStringArray[] = '--palette--;;' . $newPaletteName;
                }
            }
            $GLOBALS['TCA'][$table]['types'][$typeName]['showitem'] = implode(', ', $newFieldStringArray);
        }
    }

    /**
     * Adds new fields to a palette.
     * If the palette does not exist yet, it's created automatically.
402
     * FOR USE IN files in Configuration/TCA/Overrides/*.php Use in ext_tables.php FILES may break the frontend.
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
     *
     * @param string $table Name of the table
     * @param string $palette Name of the palette to be extended
     * @param string $addFields List of fields to be added to the palette
     * @param string $insertionPosition Insert fields before (default) or after one
     */
    public static function addFieldsToPalette($table, $palette, $addFields, $insertionPosition = '')
    {
        if (isset($GLOBALS['TCA'][$table])) {
            $paletteData = &$GLOBALS['TCA'][$table]['palettes'][$palette];
            // If palette already exists, merge the data:
            if (is_array($paletteData)) {
                $paletteData['showitem'] = self::executePositionedStringInsertion($paletteData['showitem'], $addFields, $insertionPosition);
            } else {
                $paletteData['showitem'] = self::removeDuplicatesForInsertion($addFields);
            }
        }
    }

    /**
     * Add an item to a select field item list.
     *
     * Warning: Do not use this method for radio or check types, especially not
     * with $relativeToField and $relativePosition parameters. This would shift
     * existing database data 'off by one'.
428
     * FOR USE IN files in Configuration/TCA/Overrides/*.php Use in ext_tables.php FILES may break the frontend.
429
430
431
432
433
434
435
436
     *
     * As an example, this can be used to add an item to tt_content CType select
     * drop-down after the existing 'mailform' field with these parameters:
     * - $table = 'tt_content'
     * - $field = 'CType'
     * - $item = array(
     * 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.I.10',
     * 'login',
437
     * 'i/imagename.gif',
438
439
440
441
     * ),
     * - $relativeToField = mailform
     * - $relativePosition = after
     *
442
443
444
     * $item has an optional fourth parameter for the groupId (string), to attach the
     * new item to. The groupname is defined when a group is added with addTcaSelectItemGroup
     *
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
     * @throws \InvalidArgumentException If given parameters are not of correct
     * @throws \RuntimeException If reference to related position fields can not
     * @param string $table Name of TCA table
     * @param string $field Name of TCA field
     * @param array $item New item to add
     * @param string $relativeToField Add item relative to existing field
     * @param string $relativePosition Valid keywords: 'before', 'after'
     */
    public static function addTcaSelectItem($table, $field, array $item, $relativeToField = '', $relativePosition = '')
    {
        if (!is_string($table)) {
            throw new \InvalidArgumentException('Given table is of type "' . gettype($table) . '" but a string is expected.', 1303236963);
        }
        if (!is_string($field)) {
            throw new \InvalidArgumentException('Given field is of type "' . gettype($field) . '" but a string is expected.', 1303236964);
        }
        if (!is_string($relativeToField)) {
            throw new \InvalidArgumentException('Given relative field is of type "' . gettype($relativeToField) . '" but a string is expected.', 1303236965);
        }
        if (!is_string($relativePosition)) {
            throw new \InvalidArgumentException('Given relative position is of type "' . gettype($relativePosition) . '" but a string is expected.', 1303236966);
        }
        if ($relativePosition !== '' && $relativePosition !== 'before' && $relativePosition !== 'after' && $relativePosition !== 'replace') {
            throw new \InvalidArgumentException('Relative position must be either empty or one of "before", "after", "replace".', 1303236967);
        }
470
471
472
        if (!isset($GLOBALS['TCA'][$table]['columns'][$field]['config']['items'])
            || !is_array($GLOBALS['TCA'][$table]['columns'][$field]['config']['items'])
        ) {
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
            throw new \RuntimeException('Given select field item list was not found.', 1303237468);
        }
        // Make sure item keys are integers
        $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'] = array_values($GLOBALS['TCA'][$table]['columns'][$field]['config']['items']);
        if ($relativePosition !== '') {
            // Insert at specified position
            $matchedPosition = ArrayUtility::filterByValueRecursive($relativeToField, $GLOBALS['TCA'][$table]['columns'][$field]['config']['items']);
            if (!empty($matchedPosition)) {
                $relativeItemKey = key($matchedPosition);
                if ($relativePosition === 'replace') {
                    $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'][$relativeItemKey] = $item;
                } else {
                    if ($relativePosition === 'before') {
                        $offset = $relativeItemKey;
                    } else {
                        $offset = $relativeItemKey + 1;
                    }
490
                    array_splice($GLOBALS['TCA'][$table]['columns'][$field]['config']['items'], $offset, 0, [0 => $item]);
491
492
493
494
495
496
497
498
499
500
501
                }
            } else {
                // Insert at new item at the end of the array if relative position was not found
                $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'][] = $item;
            }
        } else {
            // Insert at new item at the end of the array
            $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'][] = $item;
        }
    }

502
503
504
505
506
507
508
    /**
     * Adds an item group to a TCA select field, allows to add a group so addTcaSelectItem() can add a groupId
     * with a label and its position within other groups.
     *
     * @param string $table the table name in TCA - e.g. tt_content
     * @param string $field the field name in TCA - e.g. CType
     * @param string $groupId the unique identifier for a group, where all items from addTcaSelectItem() with a group ID are connected
509
     * @param string $groupLabel the label e.g. LLL:EXT:my_extension/Resources/Private/Language/locallang_tca.xlf:group.mygroupId
510
511
512
513
514
515
516
517
518
519
520
521
522
523
     * @param string|null $position e.g. "before:special", "after:default" (where the part after the colon is an existing groupId) or "top" or "bottom"
     */
    public static function addTcaSelectItemGroup(string $table, string $field, string $groupId, string $groupLabel, ?string $position = 'bottom'): void
    {
        if (!is_array($GLOBALS['TCA'][$table]['columns'][$field]['config'] ?? null)) {
            throw new \RuntimeException('Given select field item list was not found.', 1586728563);
        }
        $itemGroups = $GLOBALS['TCA'][$table]['columns'][$field]['config']['itemGroups'] ?? [];
        // Group has been defined already, nothing to do
        if (isset($itemGroups[$groupId])) {
            return;
        }
        $position = (string)$position;
        $positionGroupId = '';
524
        if (str_contains($position, ':')) {
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
            [$position, $positionGroupId] = explode(':', $position, 2);
        }
        // Referenced group was not not found, just append to the bottom
        if (!isset($itemGroups[$positionGroupId])) {
            $position = 'bottom';
        }
        switch ($position) {
            case 'after':
                $newItemGroups = [];
                foreach ($itemGroups as $existingGroupId => $existingGroupLabel) {
                    $newItemGroups[$existingGroupId] = $existingGroupLabel;
                    if ($positionGroupId === $existingGroupId) {
                        $newItemGroups[$groupId] = $groupLabel;
                    }
                }
                $itemGroups = $newItemGroups;
                break;
            case 'before':
                $newItemGroups = [];
                foreach ($itemGroups as $existingGroupId => $existingGroupLabel) {
                    if ($positionGroupId === $existingGroupId) {
                        $newItemGroups[$groupId] = $groupLabel;
                    }
                    $newItemGroups[$existingGroupId] = $existingGroupLabel;
                }
                $itemGroups = $newItemGroups;
                break;
            case 'top':
                $itemGroups = array_merge([$groupId => $groupLabel], $itemGroups);
                break;
            case 'bottom':
            default:
                $itemGroups[$groupId] = $groupLabel;
        }
        $GLOBALS['TCA'][$table]['columns'][$field]['config']['itemGroups'] = $itemGroups;
    }

562
563
564
565
566
567
568
569
570
571
    /**
     * Gets the TCA configuration for a field handling (FAL) files.
     *
     * @param string $fieldName Name of the field to be used
     * @param array $customSettingOverride Custom field settings overriding the basics
     * @param string $allowedFileExtensions Comma list of allowed file extensions (e.g. "jpg,gif,pdf")
     * @param string $disallowedFileExtensions
     *
     * @return array
     */
572
    public static function getFileFieldTCAConfig($fieldName, array $customSettingOverride = [], $allowedFileExtensions = '', $disallowedFileExtensions = '')
573
    {
574
        $fileFieldTCAConfig = [
575
576
577
578
579
            'type' => 'inline',
            'foreign_table' => 'sys_file_reference',
            'foreign_field' => 'uid_foreign',
            'foreign_sortby' => 'sorting_foreign',
            'foreign_table_field' => 'tablenames',
580
            'foreign_match_fields' => [
581
                'fieldname' => $fieldName,
582
            ],
583
584
            'foreign_label' => 'uid_local',
            'foreign_selector' => 'uid_local',
585
586
587
588
589
590
            'overrideChildTca' => [
                'columns' => [
                    'uid_local' => [
                        'config' => [
                            'appearance' => [
                                'elementBrowserType' => 'file',
591
                                'elementBrowserAllowed' => $allowedFileExtensions,
592
593
594
595
                            ],
                        ],
                    ],
                ],
596
597
598
            ],
            'filter' => [
                [
599
                    'userFunc' => FileExtensionFilter::class . '->filterInlineChildren',
600
                    'parameters' => [
601
                        'allowedFileExtensions' => $allowedFileExtensions,
602
603
604
                        'disallowedFileExtensions' => $disallowedFileExtensions,
                    ],
                ],
605
606
            ],
            'appearance' => [
607
                'useSortable' => true,
608
                'headerThumbnail' => [
609
                    'field' => 'uid_local',
610
                    'height' => '45m',
611
                ],
612

613
                'enabledControls' => [
614
615
616
617
618
619
                    'info' => true,
                    'new' => false,
                    'dragdrop' => true,
                    'sort' => false,
                    'hide' => true,
                    'delete' => true,
620
                ],
621
            ],
622
        ];
623
624
625
626
627
628
629
630
631
632
633
634
635
636
        ArrayUtility::mergeRecursiveWithOverrule($fileFieldTCAConfig, $customSettingOverride);
        return $fileFieldTCAConfig;
    }

    /**
     * Adds a list of new fields to the TYPO3 USER SETTINGS configuration "showitem" list, the array with
     * the new fields itself needs to be added additionally to show up in the user setup, like
     * $GLOBALS['TYPO3_USER_SETTINGS']['columns'] += $tempColumns
     *
     * @param string $addFields List of fields to be added to the user settings
     * @param string $insertionPosition Insert fields before (default) or after one
     */
    public static function addFieldsToUserSettings($addFields, $insertionPosition = '')
    {
637
        $GLOBALS['TYPO3_USER_SETTINGS']['showitem'] = self::executePositionedStringInsertion($GLOBALS['TYPO3_USER_SETTINGS']['showitem'] ?? '', $addFields, $insertionPosition);
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
    }

    /**
     * Inserts as list of data into an existing list.
     * The insertion position can be defined accordant before of after existing list items.
     *
     * Example:
     * + list: 'field_a, field_b, field_c'
     * + insertionList: 'field_d, field_e'
     * + insertionPosition: 'after:field_b'
     * -> 'field_a, field_b, field_d, field_e, field_c'
     *
     * $insertPosition may contain ; and - characters: after:--palette--;;title
     *
     * @param string $list The list of items to be extended
     * @param string $insertionList The list of items to inserted
     * @param string $insertionPosition Insert fields before (default) or after one
     * @return string The extended list
     */
    protected static function executePositionedStringInsertion($list, $insertionList, $insertionPosition = '')
    {
        $list = $newList = trim($list, ", \t\n\r\0\x0B");

661
        if ($insertionPosition !== '') {
662
            [$location, $positionName] = GeneralUtility::trimExplode(':', $insertionPosition, false, 2);
663
664
665
666
        } else {
            $location = '';
            $positionName = '';
        }
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684

        if ($location !== 'replace') {
            $insertionList = self::removeDuplicatesForInsertion($insertionList, $list);
        }

        if ($insertionList === '') {
            return $list;
        }
        if ($list === '') {
            return $insertionList;
        }
        if ($insertionPosition === '') {
            return $list . ', ' . $insertionList;
        }

        // The $insertPosition may be a palette: after:--palette--;;title
        // In the $list the palette may contain a LLL string in between the ;;
        // Adjust the regex to match that
685
        $positionName = preg_quote($positionName, '/');
686
        if (str_contains($positionName, ';;')) {
687
688
689
            $positionName = str_replace(';;', ';[^;]*;', $positionName);
        }

690
        $pattern = ('/(^|,\\s*)(' . $positionName . ')(;[^,$]+)?(,|$)/');
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
        switch ($location) {
            case 'after':
                $newList = preg_replace($pattern, '$1$2$3, ' . $insertionList . '$4', $list);
                break;
            case 'before':
                $newList = preg_replace($pattern, '$1' . $insertionList . ', $2$3$4', $list);
                break;
            case 'replace':
                $newList = preg_replace($pattern, '$1' . $insertionList . '$4', $list);
                break;
            default:
        }

        // When preg_replace did not replace anything; append the $insertionList.
        if ($list === $newList) {
            return $list . ', ' . $insertionList;
        }
        return $newList;
    }

    /**
     * Compares an existing list of items and a list of items to be inserted
     * and returns a duplicate-free variant of that insertion list.
     *
     * Example:
     * + list: 'field_a, field_b, field_c'
     * + insertion: 'field_b, field_d, field_c'
     * -> new insertion: 'field_d'
     *
     * Duplicate values in $insertionList are removed.
     *
     * @param string $insertionList The list of items to inserted
     * @param string $list The list of items to be extended (default: '')
     * @return string Duplicate-free list of items to be inserted
     */
    protected static function removeDuplicatesForInsertion($insertionList, $list = '')
    {
        $insertionListParts = preg_split('/\\s*,\\s*/', $insertionList);
729
        $listMatches = [];
730
731
732
733
734
        if ($list !== '') {
            preg_match_all('/(?:^|,)\\s*\\b([^;,]+)\\b[^,]*/', $list, $listMatches);
            $listMatches = $listMatches[1];
        }

735
        $cleanInsertionListParts = [];
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
        foreach ($insertionListParts as $fieldName) {
            $fieldNameParts = explode(';', $fieldName, 2);
            $cleanFieldName = $fieldNameParts[0];
            if (
                $cleanFieldName === '--linebreak--'
                || (
                    !in_array($cleanFieldName, $cleanInsertionListParts, true)
                    && !in_array($cleanFieldName, $listMatches, true)
                )
            ) {
                $cleanInsertionListParts[] = $fieldName;
            }
        }
        return implode(', ', $cleanInsertionListParts);
    }

    /**
     * Add tablename to default list of allowed tables on pages (in $PAGES_TYPES)
     * Will add the $table to the list of tables allowed by default on pages as setup by $PAGES_TYPES['default']['allowedTables']
     * FOR USE IN ext_tables.php FILES
     *
     * @param string $table Table name
     */
    public static function allowTableOnStandardPages($table)
    {
761
        $GLOBALS['PAGES_TYPES']['default']['allowedTables'] ??= '';
762
763
764
765
766
767
768
769
770
771
        $GLOBALS['PAGES_TYPES']['default']['allowedTables'] .= ',' . $table;
    }

    /**
     * Adds a module (main or sub) to the backend interface
     * FOR USE IN ext_tables.php FILES
     *
     * @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.
     * @param string $sub The submodule key. If $sub is not set a blank $main module is created.
     * @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.
772
     * @param string $path The absolute path to the module. Was used prior to TYPO3 v8, use $moduleConfiguration[routeTarget] now
773
774
     * @param array $moduleConfiguration additional configuration, previously put in "conf.php" of the module directory
     */
775
    public static function addModule($main, $sub = '', $position = '', $path = null, $moduleConfiguration = [])
776
    {
777
778
779
        if (!isset($GLOBALS['TBE_MODULES'])) {
            $GLOBALS['TBE_MODULES'] = [];
        }
780
781
782
        // If there is already a main module by this name:
        // Adding the submodule to the correct position:
        if (isset($GLOBALS['TBE_MODULES'][$main]) && $sub) {
783
            [$place, $modRef] = array_pad(GeneralUtility::trimExplode(':', $position, true), 2, null);
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
            $modules = ',' . $GLOBALS['TBE_MODULES'][$main] . ',';
            if ($place === null || ($modRef !== null && !GeneralUtility::inList($modules, $modRef))) {
                $place = 'bottom';
            }
            $modRef = ',' . $modRef . ',';
            if (!GeneralUtility::inList($modules, $sub)) {
                switch (strtolower($place)) {
                    case 'after':
                        $modules = str_replace($modRef, $modRef . $sub . ',', $modules);
                        break;
                    case 'before':
                        $modules = str_replace($modRef, ',' . $sub . $modRef, $modules);
                        break;
                    case 'top':
                        $modules = $sub . $modules;
                        break;
                    case 'bottom':
                    default:
                        $modules = $modules . $sub;
                }
            }
            // Re-inserting the submodule list:
            $GLOBALS['TBE_MODULES'][$main] = trim($modules, ',');
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
        } elseif (!isset($GLOBALS['TBE_MODULES'][$main]) && empty($sub)) {
            // Create a new main module, respecting the order, which is only possible when the module does not exist yet
            $conf = $GLOBALS['TBE_MODULES']['_configuration'] ?? [];
            unset($GLOBALS['TBE_MODULES']['_configuration']);
            $navigationComponents = $GLOBALS['TBE_MODULES']['_navigationComponents'] ?? [];
            unset($GLOBALS['TBE_MODULES']['_navigationComponents']);

            $modules = array_keys($GLOBALS['TBE_MODULES']);
            [$place, $moduleReference] = array_pad(GeneralUtility::trimExplode(':', $position, true), 2, null);
            if ($place === null || ($moduleReference !== null && !in_array($moduleReference, $modules, true))) {
                $place = 'bottom';
            }
            $newModules = [];
            switch (strtolower($place)) {
                case 'after':
                    foreach ($modules as $existingMainModule) {
                        $newModules[$existingMainModule] = $GLOBALS['TBE_MODULES'][$existingMainModule];
                        if ($moduleReference === $existingMainModule) {
                            $newModules[$main] = '';
                        }
                    }
                    break;
                case 'before':
                    foreach ($modules as $existingMainModule) {
                        if ($moduleReference === $existingMainModule) {
                            $newModules[$main] = '';
                        }
                        $newModules[$existingMainModule] = $GLOBALS['TBE_MODULES'][$existingMainModule];
                    }
                    break;
                case 'top':
                    $newModules[$main] = '';
                    $newModules += $GLOBALS['TBE_MODULES'];
                    break;
                case 'bottom':
                default:
                    $newModules = $GLOBALS['TBE_MODULES'];
                    $newModules[$main] = '';
            }
            $GLOBALS['TBE_MODULES'] = $newModules;
            $GLOBALS['TBE_MODULES']['_configuration'] = $conf;
            $GLOBALS['TBE_MODULES']['_navigationComponents'] = $navigationComponents;
849
850
851
852
853
854
        } else {
            // Create new main modules with only one submodule, $sub (or none if $sub is blank)
            $GLOBALS['TBE_MODULES'][$main] = $sub;
        }

        // add additional configuration
855
        $fullModuleSignature = $main . ($sub ? '_' . $sub : '');
856
        if (is_array($moduleConfiguration) && !empty($moduleConfiguration)) {
857
            // remove default icon if an icon identifier is available
858
859
860
861
            if (!empty($moduleConfiguration['iconIdentifier'])
                && !empty($moduleConfiguration['icon'])
                && $moduleConfiguration['icon'] === 'EXT:extbase/Resources/Public/Icons/Extension.png'
            ) {
862
863
                unset($moduleConfiguration['icon']);
            }
864
865
866
867
868
869
870

            // file_navframe is now used as SVG-based navigation component
            // @deprecated this migration can be removed in TYPO3 v12.0
            if (($moduleConfiguration['navigationFrameModule'] ?? '') === 'file_navframe') {
                trigger_error('Module ' . $fullModuleSignature . ' uses "file_navframe" as "navigationFrameModule" configuration, but was migrated to the FileStorageTreeContainer navigation component. This Fallback will be removed in TYPO3 v12.0.', E_USER_DEPRECATED);
                unset($moduleConfiguration['navigationFrameModule']);
                $moduleConfiguration['navigationComponentId'] = 'TYPO3/CMS/Backend/Tree/FileStorageTreeContainer';
871
872
            } elseif (isset($moduleConfiguration['navigationFrameModule']) || isset($moduleConfiguration['navFrameScript'])) {
                trigger_error('Module ' . $fullModuleSignature . ' is using a "navigationFrameModule" configuration which renders an iframe for the navigation part, which is deprecated in favor of the navigationComponent functionality. The frame will be removed in TYPO3 v12.0.', E_USER_DEPRECATED);
873
874
            }

875
            if (!empty($moduleConfiguration['icon'])) {
876
877
878
879
                $iconPath = $moduleConfiguration['icon'];
                if (!PathUtility::isExtensionPath($iconPath)) {
                    $iconPath = GeneralUtility::getFileAbsFileName($iconPath);
                }
880
                $iconRegistry = GeneralUtility::makeInstance(IconRegistry::class);
881
                $iconIdentifier = 'module-' . $fullModuleSignature;
882
                $iconProvider = $iconRegistry->detectIconProvider($iconPath);
883
884
885
                $iconRegistry->registerIcon(
                    $iconIdentifier,
                    $iconProvider,
886
                    ['source' => $iconPath]
887
                );
888
889
                $moduleConfiguration['iconIdentifier'] = $iconIdentifier;
                unset($moduleConfiguration['icon']);
890
891
            }

892
893
            $GLOBALS['TBE_MODULES']['_configuration'][$fullModuleSignature] = $moduleConfiguration;
        }
894
895

        // Also register the module as regular route
896
        $routeName = $moduleConfiguration['id'] ?? $fullModuleSignature;
897
        // Build Route objects from the data
898
899
900
901
902
        if (!empty($moduleConfiguration['path'])) {
            $path = $moduleConfiguration['path'];
            $path = '/' . ltrim($path, '/');
        } else {
            $path = str_replace('_', '/', $fullModuleSignature);
903
            $path = '/module/' . trim($path, '/');
904
        }
905
906
907
908

        $options = [
            'module' => true,
            'moduleName' => $fullModuleSignature,
909
            'access' => !empty($moduleConfiguration['access']) ? $moduleConfiguration['access'] : 'user,group',
910
        ];
911
        if (!empty($moduleConfiguration['routeTarget'])) {
912
913
914
            $options['target'] = $moduleConfiguration['routeTarget'];
        }

915
916
        $router = GeneralUtility::makeInstance(Router::class);
        $router->addRoute($routeName, GeneralUtility::makeInstance(Route::class, $path, $options));
917
918
919
920
921
922
923
924
925
926
    }

    /**
     * Adds a "Function menu module" ('third level module') to an existing function menu for some other backend module
     * The arguments values are generally determined by which function menu this is supposed to interact with
     * See Inside TYPO3 for information on how to use this function.
     * FOR USE IN ext_tables.php FILES
     *
     * @param string $modname Module name
     * @param string $className Class name
927
     * @param string $_unused not in use anymore
928
929
930
931
     * @param string $title Title of module
     * @param string $MM_key Menu array key - default is "function
     * @param string $WS Workspace conditions. Blank means all workspaces, any other string can be a comma list of "online", "offline" and "custom
     */
932
    public static function insertModuleFunction($modname, $className, $_unused, $title, $MM_key = 'function', $WS = '')
933
    {
934
        $GLOBALS['TBE_MODULES_EXT'][$modname]['MOD_MENU'][$MM_key][$className] = [
935
936
            'name' => $className,
            'title' => $title,
937
            'ws' => $WS,
938
        ];
939
940
941
942
943
    }

    /**
     * Adds $content to the default Page TSconfig as set in $GLOBALS['TYPO3_CONF_VARS'][BE]['defaultPageTSconfig']
     * Prefixed with a [GLOBAL] line
944
     * FOR USE IN ext_localconf.php FILE
945
946
947
948
949
     *
     * @param string $content Page TSconfig content
     */
    public static function addPageTSConfig($content)
    {
950
        $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] .= '
951
[GLOBAL]
952
' . $content;
953
954
955
956
957
    }

    /**
     * Adds $content to the default User TSconfig as set in $GLOBALS['TYPO3_CONF_VARS'][BE]['defaultUserTSconfig']
     * Prefixed with a [GLOBAL] line
958
     * FOR USE IN ext_localconf.php FILE
959
960
961
962
963
     *
     * @param string $content User TSconfig content
     */
    public static function addUserTSConfig($content)
    {
964
        $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig'] .= '
965
[GLOBAL]
966
' . $content;
967
968
969
970
971
    }

    /**
     * Adds a reference to a locallang file with $GLOBALS['TCA_DESCR'] labels
     * FOR USE IN ext_tables.php FILES
972
     * 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.
973
     *
974
     * @param string $key Description key. Typically a database table (like "pages") but for applications can be other strings, but prefixed with "_MOD_")
975
     * @param string $file File reference to locallang file, eg. "EXT:core/Resources/Private/Language/locallang_csh_pages.xlf"
976
     */
977
    public static function addLLrefForTCAdescr($key, $file)
978
    {
979
        if (empty($key)) {
980
            throw new \RuntimeException('No description key set in addLLrefForTCAdescr(). Provide it as first parameter', 1507321596);
981
        }
982
        if (!is_array($GLOBALS['TCA_DESCR'][$key] ?? false)) {
983
984
            $GLOBALS['TCA_DESCR'][$key] = [];
        }
985
        if (!is_array($GLOBALS['TCA_DESCR'][$key]['refs'] ?? false)) {
986
            $GLOBALS['TCA_DESCR'][$key]['refs'] = [];
987
        }
988
        $GLOBALS['TCA_DESCR'][$key]['refs'][] = $file;
989
990
991
    }

    /**
992
     * Registers a navigation component e.g. page tree
993
994
     *
     * @param string $module
995
     * @param string $componentId componentId is also a RequireJS module name e.g. 'TYPO3/CMS/MyExt/MyNavComponent'
996
997
998
     * @param string $extensionKey
     * @throws \RuntimeException
     */
999
    public static function addNavigationComponent($module, $componentId, $extensionKey)
1000
    {
For faster browsing, not all history is shown. View entire blame