Completed
Push — master ( 17b078...3da3e5 )
by
unknown
14:54
created

addTcaSelectItemGroup()   C

Complexity

Conditions 13
Paths 22

Size

Total Lines 48
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 35
nc 22
nop 5
dl 0
loc 48
rs 6.6166
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Core\Utility;
17
18
use Psr\EventDispatcher\EventDispatcherInterface;
19
use Symfony\Component\Finder\Finder;
20
use TYPO3\CMS\Backend\Routing\Route;
21
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\Information\Typo3Version;
29
use TYPO3\CMS\Core\Log\LogManager;
30
use TYPO3\CMS\Core\Migrations\TcaMigration;
31
use TYPO3\CMS\Core\Package\PackageManager;
32
use TYPO3\CMS\Core\Preparations\TcaPreparation;
33
34
/**
35
 * Extension Management functions
36
 *
37
 * This class is never instantiated, rather the methods inside is called as functions like
38
 * \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('my_extension');
39
 */
40
class ExtensionManagementUtility
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 bootstrap.
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 EventDispatcherInterface
73
     */
74
    protected static $eventDispatcher;
75
76
    /**
77
     * Sets the event dispatcher to be available.
78
     *
79
     * @param EventDispatcherInterface $eventDispatcher
80
     * @internal only used for tests and the internal TYPO3 Bootstrap process
81
     */
82
    public static function setEventDispatcher(EventDispatcherInterface $eventDispatcher)
83
    {
84
        static::$eventDispatcher = $eventDispatcher;
85
    }
86
87
    /**
88
     * @var CacheManager
89
     */
90
    protected static $cacheManager;
91
92
    /**
93
     * Getter for the cache manager
94
     *
95
     * @return CacheManager
96
     */
97
    protected static function getCacheManager()
98
    {
99
        if (static::$cacheManager === null) {
100
            static::$cacheManager = GeneralUtility::makeInstance(CacheManager::class);
101
        }
102
        return static::$cacheManager;
103
    }
104
105
    /**************************************
106
     *
107
     * PATHS and other evaluation
108
     *
109
     ***************************************/
110
    /**
111
     * Returns TRUE if the extension with extension key $key is loaded.
112
     *
113
     * @param string $key Extension key to test
114
     * @return bool
115
     */
116
    public static function isLoaded($key)
117
    {
118
        return static::$packageManager->isPackageActive($key);
119
    }
120
121
    /**
122
     * Returns the absolute path to the extension with extension key $key.
123
     *
124
     * @param string $key Extension key
125
     * @param string $script $script is appended to the output if set.
126
     * @throws \BadFunctionCallException
127
     * @return string
128
     */
129
    public static function extPath($key, $script = '')
130
    {
131
        if (!static::$packageManager->isPackageActive($key)) {
132
            throw new \BadFunctionCallException('TYPO3 Fatal Error: Extension key "' . $key . '" is NOT loaded!', 1365429656);
133
        }
134
        return static::$packageManager->getPackage($key)->getPackagePath() . $script;
135
    }
136
137
    /**
138
     * Returns the correct class name prefix for the extension key $key
139
     *
140
     * @param string $key Extension key
141
     * @return string
142
     * @internal
143
     */
144
    public static function getCN($key)
145
    {
146
        return strpos($key, 'user_') === 0 ? 'user_' . str_replace('_', '', substr($key, 5)) : 'tx_' . str_replace('_', '', $key);
147
    }
148
149
    /**
150
     * Retrieves the version of an installed extension.
151
     * If the extension is not installed, this function returns an empty string.
152
     *
153
     * @param string $key The key of the extension to look up, must not be empty
154
     *
155
     * @throws \InvalidArgumentException
156
     * @throws \TYPO3\CMS\Core\Package\Exception
157
     * @return string The extension version as a string in the format "x.y.z",
158
     */
159
    public static function getExtensionVersion($key)
160
    {
161
        if (!is_string($key) || empty($key)) {
0 ignored issues
show
introduced by
The condition is_string($key) is always true.
Loading history...
162
            throw new \InvalidArgumentException('Extension key must be a non-empty string.', 1294586096);
163
        }
164
        if (!static::isLoaded($key)) {
165
            return '';
166
        }
167
        $version = static::$packageManager->getPackage($key)->getPackageMetaData()->getVersion();
168
        if (empty($version)) {
169
            throw new \TYPO3\CMS\Core\Package\Exception('Version number in composer manifest of package "' . $key . '" is missing or invalid', 1395614959);
170
        }
171
        return $version;
172
    }
173
174
    /**************************************
175
     *
176
     *	 Adding BACKEND features
177
     *	 (related to core features)
178
     *
179
     ***************************************/
180
    /**
181
     * Adding fields to an existing table definition in $GLOBALS['TCA']
182
     * Adds an array with $GLOBALS['TCA'] column-configuration to the $GLOBALS['TCA']-entry for that table.
183
     * 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!
184
     * So to have the fields displayed you must also call fx. addToAllTCAtypes or manually add the fields to the types list.
185
     * FOR USE IN files in Configuration/TCA/Overrides/*.php . Use in ext_tables.php FILES may break the frontend.
186
     *
187
     * @param string $table The table name of a table already present in $GLOBALS['TCA'] with a columns section
188
     * @param array $columnArray The array with the additional columns (typical some fields an extension wants to add)
189
     */
190
    public static function addTCAcolumns($table, $columnArray)
191
    {
192
        if (is_array($columnArray) && is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'])) {
193
            // Candidate for array_merge() if integer-keys will some day make trouble...
194
            $GLOBALS['TCA'][$table]['columns'] = array_merge($GLOBALS['TCA'][$table]['columns'], $columnArray);
195
        }
196
    }
197
198
    /**
199
     * Makes fields visible in the TCEforms, adding them to the end of (all) "types"-configurations
200
     *
201
     * Adds a string $string (comma separated list of field names) to all ["types"][xxx]["showitem"] entries for table $table (unless limited by $typeList)
202
     * This is needed to have new fields shown automatically in the TCEFORMS of a record from $table.
203
     * Typically this function is called after having added new columns (database fields) with the addTCAcolumns function
204
     * FOR USE IN files in Configuration/TCA/Overrides/*.php Use in ext_tables.php FILES may break the frontend.
205
     *
206
     * @param string $table Table name
207
     * @param string $newFieldsString Field list to add.
208
     * @param string $typeList List of specific types to add the field list to. (If empty, all type entries are affected)
209
     * @param string $position Insert fields before (default) or after one, or replace a field
210
     */
211
    public static function addToAllTCAtypes($table, $newFieldsString, $typeList = '', $position = '')
212
    {
213
        $newFieldsString = trim($newFieldsString);
214
        if ($newFieldsString === '' || !is_array($GLOBALS['TCA'][$table]['types'] ?? false)) {
215
            return;
216
        }
217
        if ($position !== '') {
218
            [$positionIdentifier, $entityName] = GeneralUtility::trimExplode(':', $position);
219
        } else {
220
            $positionIdentifier = '';
221
            $entityName = '';
222
        }
223
        $palettesChanged = [];
224
225
        foreach ($GLOBALS['TCA'][$table]['types'] as $type => &$typeDetails) {
226
            // skip if we don't want to add the field for this type
227
            if ($typeList !== '' && !GeneralUtility::inList($typeList, $type)) {
228
                continue;
229
            }
230
            // skip if fields were already added
231
            if (!isset($typeDetails['showitem'])) {
232
                continue;
233
            }
234
235
            $fieldArray = GeneralUtility::trimExplode(',', $typeDetails['showitem'], true);
236
            if (in_array($newFieldsString, $fieldArray, true)) {
237
                continue;
238
            }
239
240
            $fieldExists = false;
241
            $newPosition = '';
242
            if (is_array($GLOBALS['TCA'][$table]['palettes'] ?? false)) {
243
                // Get the palette names used in current showitem
244
                $paletteCount = preg_match_all('/(?:^|,)                    # Line start or a comma
245
					(?:
246
					    \\s*\\-\\-palette\\-\\-;[^;]*;([^,$]*)|             # --palette--;label;paletteName
247
					    \\s*\\b[^;,]+\\b(?:;[^;]*;([^;,]+))?[^,]*           # field;label;paletteName
248
					)/x', $typeDetails['showitem'], $paletteMatches);
249
                if ($paletteCount > 0) {
250
                    $paletteNames = array_filter(array_merge($paletteMatches[1], $paletteMatches[2]));
251
                    if (!empty($paletteNames)) {
252
                        foreach ($paletteNames as $paletteName) {
253
                            if (!isset($GLOBALS['TCA'][$table]['palettes'][$paletteName])) {
254
                                continue;
255
                            }
256
                            $palette = $GLOBALS['TCA'][$table]['palettes'][$paletteName];
257
                            switch ($positionIdentifier) {
258
                                case 'after':
259
                                case 'before':
260
                                    if (preg_match('/\\b' . $entityName . '\\b/', $palette['showitem']) > 0) {
261
                                        $newPosition = $positionIdentifier . ':--palette--;;' . $paletteName;
262
                                    }
263
                                    break;
264
                                case 'replace':
265
                                    // check if fields have been added to palette before
266
                                    if (isset($palettesChanged[$paletteName])) {
267
                                        $fieldExists = true;
268
                                        continue 2;
269
                                    }
270
                                    if (preg_match('/\\b' . $entityName . '\\b/', $palette['showitem']) > 0) {
271
                                        self::addFieldsToPalette($table, $paletteName, $newFieldsString, $position);
272
                                        // Memorize that we already changed this palette, in case other types also use it
273
                                        $palettesChanged[$paletteName] = true;
274
                                        $fieldExists = true;
275
                                        continue 2;
276
                                    }
277
                                    break;
278
                                default:
279
                                    // Intentionally left blank
280
                            }
281
                        }
282
                    }
283
                }
284
            }
285
            if ($fieldExists === false) {
286
                $typeDetails['showitem'] = self::executePositionedStringInsertion(
287
                    $typeDetails['showitem'],
288
                    $newFieldsString,
289
                    $newPosition !== '' ? $newPosition : $position
290
                );
291
            }
292
        }
293
        unset($typeDetails);
294
    }
295
296
    /**
297
     * Adds new fields to all palettes that is defined after an existing field.
298
     * If the field does not have a following palette yet, it's created automatically
299
     * and gets called "generatedFor-$field".
300
     * FOR USE IN files in Configuration/TCA/Overrides/*.php Use in ext_tables.php FILES may break the frontend.
301
     *
302
     * See unit tests for more examples and edge cases.
303
     *
304
     * Example:
305
     *
306
     * 'aTable' => array(
307
     * 	'types' => array(
308
     * 		'aType' => array(
309
     * 			'showitem' => 'aField, --palette--;;aPalette',
310
     * 		),
311
     * 	),
312
     * 	'palettes' => array(
313
     * 		'aPalette' => array(
314
     * 			'showitem' => 'fieldB, fieldC',
315
     * 		),
316
     * 	),
317
     * ),
318
     *
319
     * Calling addFieldsToAllPalettesOfField('aTable', 'aField', 'newA', 'before: fieldC') results in:
320
     *
321
     * 'aTable' => array(
322
     * 	'types' => array(
323
     * 		'aType' => array(
324
     * 			'showitem' => 'aField, --palette--;;aPalette',
325
     * 		),
326
     * 	),
327
     * 	'palettes' => array(
328
     * 		'aPalette' => array(
329
     * 			'showitem' => 'fieldB, newA, fieldC',
330
     * 		),
331
     * 	),
332
     * ),
333
     *
334
     * @param string $table Name of the table
335
     * @param string $field Name of the field that has the palette to be extended
336
     * @param string $addFields List of fields to be added to the palette
337
     * @param string $insertionPosition Insert fields before (default) or after one
338
     */
339
    public static function addFieldsToAllPalettesOfField($table, $field, $addFields, $insertionPosition = '')
340
    {
341
        if (!isset($GLOBALS['TCA'][$table]['columns'][$field])) {
342
            return;
343
        }
344
        if (!is_array($GLOBALS['TCA'][$table]['types'])) {
345
            return;
346
        }
347
348
        // Iterate through all types and search for the field that defines the palette to be extended
349
        foreach ($GLOBALS['TCA'][$table]['types'] as $typeName => $typeArray) {
350
            // Continue if types has no showitem at all or if requested field is not in it
351
            if (!isset($typeArray['showitem']) || strpos($typeArray['showitem'], $field) === false) {
352
                continue;
353
            }
354
            $fieldArrayWithOptions = GeneralUtility::trimExplode(',', $typeArray['showitem']);
355
            // Find the field we're handling
356
            $newFieldStringArray = [];
357
            foreach ($fieldArrayWithOptions as $fieldNumber => $fieldString) {
358
                $newFieldStringArray[] = $fieldString;
359
                $fieldArray = GeneralUtility::trimExplode(';', $fieldString);
360
                if ($fieldArray[0] !== $field) {
361
                    continue;
362
                }
363
                if (
364
                    isset($fieldArrayWithOptions[$fieldNumber + 1])
365
                    && strpos($fieldArrayWithOptions[$fieldNumber + 1], '--palette--') === 0
366
                ) {
367
                    // Match for $field and next field is a palette - add fields to this one
368
                    $paletteName = GeneralUtility::trimExplode(';', $fieldArrayWithOptions[$fieldNumber + 1]);
369
                    $paletteName = $paletteName[2];
370
                    self::addFieldsToPalette($table, $paletteName, $addFields, $insertionPosition);
371
                } else {
372
                    // Match for $field but next field is no palette - create a new one
373
                    $newPaletteName = 'generatedFor-' . $field;
374
                    self::addFieldsToPalette($table, 'generatedFor-' . $field, $addFields, $insertionPosition);
375
                    $newFieldStringArray[] = '--palette--;;' . $newPaletteName;
376
                }
377
            }
378
            $GLOBALS['TCA'][$table]['types'][$typeName]['showitem'] = implode(', ', $newFieldStringArray);
379
        }
380
    }
381
382
    /**
383
     * Adds new fields to a palette.
384
     * If the palette does not exist yet, it's created automatically.
385
     * FOR USE IN files in Configuration/TCA/Overrides/*.php Use in ext_tables.php FILES may break the frontend.
386
     *
387
     * @param string $table Name of the table
388
     * @param string $palette Name of the palette to be extended
389
     * @param string $addFields List of fields to be added to the palette
390
     * @param string $insertionPosition Insert fields before (default) or after one
391
     */
392
    public static function addFieldsToPalette($table, $palette, $addFields, $insertionPosition = '')
393
    {
394
        if (isset($GLOBALS['TCA'][$table])) {
395
            $paletteData = &$GLOBALS['TCA'][$table]['palettes'][$palette];
396
            // If palette already exists, merge the data:
397
            if (is_array($paletteData)) {
398
                $paletteData['showitem'] = self::executePositionedStringInsertion($paletteData['showitem'], $addFields, $insertionPosition);
399
            } else {
400
                $paletteData['showitem'] = self::removeDuplicatesForInsertion($addFields);
401
            }
402
        }
403
    }
404
405
    /**
406
     * Add an item to a select field item list.
407
     *
408
     * Warning: Do not use this method for radio or check types, especially not
409
     * with $relativeToField and $relativePosition parameters. This would shift
410
     * existing database data 'off by one'.
411
     * FOR USE IN files in Configuration/TCA/Overrides/*.php Use in ext_tables.php FILES may break the frontend.
412
     *
413
     * As an example, this can be used to add an item to tt_content CType select
414
     * drop-down after the existing 'mailform' field with these parameters:
415
     * - $table = 'tt_content'
416
     * - $field = 'CType'
417
     * - $item = array(
418
     * 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.I.10',
419
     * 'login',
420
     * 'i/imagename.gif',
421
     * ),
422
     * - $relativeToField = mailform
423
     * - $relativePosition = after
424
     *
425
     * @throws \InvalidArgumentException If given parameters are not of correct
426
     * @throws \RuntimeException If reference to related position fields can not
427
     * @param string $table Name of TCA table
428
     * @param string $field Name of TCA field
429
     * @param array $item New item to add
430
     * @param string $relativeToField Add item relative to existing field
431
     * @param string $relativePosition Valid keywords: 'before', 'after'
432
     */
433
    public static function addTcaSelectItem($table, $field, array $item, $relativeToField = '', $relativePosition = '')
434
    {
435
        if (!is_string($table)) {
0 ignored issues
show
introduced by
The condition is_string($table) is always true.
Loading history...
436
            throw new \InvalidArgumentException('Given table is of type "' . gettype($table) . '" but a string is expected.', 1303236963);
437
        }
438
        if (!is_string($field)) {
0 ignored issues
show
introduced by
The condition is_string($field) is always true.
Loading history...
439
            throw new \InvalidArgumentException('Given field is of type "' . gettype($field) . '" but a string is expected.', 1303236964);
440
        }
441
        if (!is_string($relativeToField)) {
0 ignored issues
show
introduced by
The condition is_string($relativeToField) is always true.
Loading history...
442
            throw new \InvalidArgumentException('Given relative field is of type "' . gettype($relativeToField) . '" but a string is expected.', 1303236965);
443
        }
444
        if (!is_string($relativePosition)) {
0 ignored issues
show
introduced by
The condition is_string($relativePosition) is always true.
Loading history...
445
            throw new \InvalidArgumentException('Given relative position is of type "' . gettype($relativePosition) . '" but a string is expected.', 1303236966);
446
        }
447
        if ($relativePosition !== '' && $relativePosition !== 'before' && $relativePosition !== 'after' && $relativePosition !== 'replace') {
448
            throw new \InvalidArgumentException('Relative position must be either empty or one of "before", "after", "replace".', 1303236967);
449
        }
450
        if (!isset($GLOBALS['TCA'][$table]['columns'][$field]['config']['items'])
451
            || !is_array($GLOBALS['TCA'][$table]['columns'][$field]['config']['items'])
452
        ) {
453
            throw new \RuntimeException('Given select field item list was not found.', 1303237468);
454
        }
455
        // Make sure item keys are integers
456
        $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'] = array_values($GLOBALS['TCA'][$table]['columns'][$field]['config']['items']);
457
        if ($relativePosition !== '') {
458
            // Insert at specified position
459
            $matchedPosition = ArrayUtility::filterByValueRecursive($relativeToField, $GLOBALS['TCA'][$table]['columns'][$field]['config']['items']);
460
            if (!empty($matchedPosition)) {
461
                $relativeItemKey = key($matchedPosition);
462
                if ($relativePosition === 'replace') {
463
                    $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'][$relativeItemKey] = $item;
464
                } else {
465
                    if ($relativePosition === 'before') {
466
                        $offset = $relativeItemKey;
467
                    } else {
468
                        $offset = $relativeItemKey + 1;
469
                    }
470
                    array_splice($GLOBALS['TCA'][$table]['columns'][$field]['config']['items'], $offset, 0, [0 => $item]);
0 ignored issues
show
Bug introduced by
It seems like $offset can also be of type string; however, parameter $offset of array_splice() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

470
                    array_splice($GLOBALS['TCA'][$table]['columns'][$field]['config']['items'], /** @scrutinizer ignore-type */ $offset, 0, [0 => $item]);
Loading history...
471
                }
472
            } else {
473
                // Insert at new item at the end of the array if relative position was not found
474
                $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'][] = $item;
475
            }
476
        } else {
477
            // Insert at new item at the end of the array
478
            $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'][] = $item;
479
        }
480
    }
481
482
    /**
483
     * Adds an item group to a TCA select field, allows to add a group so addTcaSelectItem() can add a groupId
484
     * with a label and its position within other groups.
485
     *
486
     * @param string $table the table name in TCA - e.g. tt_content
487
     * @param string $field the field name in TCA - e.g. CType
488
     * @param string $groupId the unique identifier for a group, where all items from addTcaSelectItem() with a group ID are connected
489
     * @param string $groupLabel the label e.g. LLL:my_extension/Resources/Private/Language/locallang_tca.xlf:group.mygroupId
490
     * @param string|null $position e.g. "before:special", "after:default" (where the part after the colon is an existing groupId) or "top" or "bottom"
491
     */
492
    public static function addTcaSelectItemGroup(string $table, string $field, string $groupId, string $groupLabel, ?string $position = 'bottom'): void
493
    {
494
        if (!is_array($GLOBALS['TCA'][$table]['columns'][$field]['config'] ?? null)) {
495
            throw new \RuntimeException('Given select field item list was not found.', 1586728563);
496
        }
497
        $itemGroups = $GLOBALS['TCA'][$table]['columns'][$field]['config']['itemGroups'] ?? [];
498
        // Group has been defined already, nothing to do
499
        if (isset($itemGroups[$groupId])) {
500
            return;
501
        }
502
        $position = (string)$position;
503
        $positionGroupId = '';
504
        if (strpos($position, ':') !== false) {
505
            [$position, $positionGroupId] = explode(':', $position, 2);
506
        }
507
        // Referenced group was not not found, just append to the bottom
508
        if (!isset($itemGroups[$positionGroupId])) {
509
            $position = 'bottom';
510
        }
511
        switch ($position) {
512
            case 'after':
513
                $newItemGroups = [];
514
                foreach ($itemGroups as $existingGroupId => $existingGroupLabel) {
515
                    $newItemGroups[$existingGroupId] = $existingGroupLabel;
516
                    if ($positionGroupId === $existingGroupId) {
517
                        $newItemGroups[$groupId] = $groupLabel;
518
                    }
519
                }
520
                $itemGroups = $newItemGroups;
521
                break;
522
            case 'before':
523
                $newItemGroups = [];
524
                foreach ($itemGroups as $existingGroupId => $existingGroupLabel) {
525
                    if ($positionGroupId === $existingGroupId) {
526
                        $newItemGroups[$groupId] = $groupLabel;
527
                    }
528
                    $newItemGroups[$existingGroupId] = $existingGroupLabel;
529
                }
530
                $itemGroups = $newItemGroups;
531
                break;
532
            case 'top':
533
                $itemGroups = array_merge([$groupId => $groupLabel], $itemGroups);
534
                break;
535
            case 'bottom':
536
            default:
537
                $itemGroups[$groupId] = $groupLabel;
538
        }
539
        $GLOBALS['TCA'][$table]['columns'][$field]['config']['itemGroups'] = $itemGroups;
540
    }
541
542
    /**
543
     * Gets the TCA configuration for a field handling (FAL) files.
544
     *
545
     * @param string $fieldName Name of the field to be used
546
     * @param array $customSettingOverride Custom field settings overriding the basics
547
     * @param string $allowedFileExtensions Comma list of allowed file extensions (e.g. "jpg,gif,pdf")
548
     * @param string $disallowedFileExtensions
549
     *
550
     * @return array
551
     */
552
    public static function getFileFieldTCAConfig($fieldName, array $customSettingOverride = [], $allowedFileExtensions = '', $disallowedFileExtensions = '')
553
    {
554
        $fileFieldTCAConfig = [
555
            'type' => 'inline',
556
            'foreign_table' => 'sys_file_reference',
557
            'foreign_field' => 'uid_foreign',
558
            'foreign_sortby' => 'sorting_foreign',
559
            'foreign_table_field' => 'tablenames',
560
            'foreign_match_fields' => [
561
                'fieldname' => $fieldName
562
            ],
563
            'foreign_label' => 'uid_local',
564
            'foreign_selector' => 'uid_local',
565
            'overrideChildTca' => [
566
                'columns' => [
567
                    'uid_local' => [
568
                        'config' => [
569
                            'appearance' => [
570
                                'elementBrowserType' => 'file',
571
                                'elementBrowserAllowed' => $allowedFileExtensions
572
                            ],
573
                        ],
574
                    ],
575
                ],
576
            ],
577
            'filter' => [
578
                [
579
                    'userFunc' => \TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter::class . '->filterInlineChildren',
580
                    'parameters' => [
581
                        'allowedFileExtensions' => $allowedFileExtensions,
582
                        'disallowedFileExtensions' => $disallowedFileExtensions
583
                    ]
584
                ]
585
            ],
586
            'appearance' => [
587
                'useSortable' => true,
588
                'headerThumbnail' => [
589
                    'field' => 'uid_local',
590
                    'width' => '45',
591
                    'height' => '45c',
592
                ],
593
594
                'enabledControls' => [
595
                    'info' => true,
596
                    'new' => false,
597
                    'dragdrop' => true,
598
                    'sort' => false,
599
                    'hide' => true,
600
                    'delete' => true,
601
                ],
602
            ]
603
        ];
604
        ArrayUtility::mergeRecursiveWithOverrule($fileFieldTCAConfig, $customSettingOverride);
605
        return $fileFieldTCAConfig;
606
    }
607
608
    /**
609
     * Adds a list of new fields to the TYPO3 USER SETTINGS configuration "showitem" list, the array with
610
     * the new fields itself needs to be added additionally to show up in the user setup, like
611
     * $GLOBALS['TYPO3_USER_SETTINGS']['columns'] += $tempColumns
612
     *
613
     * @param string $addFields List of fields to be added to the user settings
614
     * @param string $insertionPosition Insert fields before (default) or after one
615
     */
616
    public static function addFieldsToUserSettings($addFields, $insertionPosition = '')
617
    {
618
        $GLOBALS['TYPO3_USER_SETTINGS']['showitem'] = self::executePositionedStringInsertion($GLOBALS['TYPO3_USER_SETTINGS']['showitem'], $addFields, $insertionPosition);
619
    }
620
621
    /**
622
     * Inserts as list of data into an existing list.
623
     * The insertion position can be defined accordant before of after existing list items.
624
     *
625
     * Example:
626
     * + list: 'field_a, field_b, field_c'
627
     * + insertionList: 'field_d, field_e'
628
     * + insertionPosition: 'after:field_b'
629
     * -> 'field_a, field_b, field_d, field_e, field_c'
630
     *
631
     * $insertPosition may contain ; and - characters: after:--palette--;;title
632
     *
633
     * @param string $list The list of items to be extended
634
     * @param string $insertionList The list of items to inserted
635
     * @param string $insertionPosition Insert fields before (default) or after one
636
     * @return string The extended list
637
     */
638
    protected static function executePositionedStringInsertion($list, $insertionList, $insertionPosition = '')
639
    {
640
        $list = $newList = trim($list, ", \t\n\r\0\x0B");
641
642
        if ($insertionPosition !== '') {
643
            [$location, $positionName] = GeneralUtility::trimExplode(':', $insertionPosition, false, 2);
644
        } else {
645
            $location = '';
646
            $positionName = '';
647
        }
648
649
        if ($location !== 'replace') {
650
            $insertionList = self::removeDuplicatesForInsertion($insertionList, $list);
651
        }
652
653
        if ($insertionList === '') {
654
            return $list;
655
        }
656
        if ($list === '') {
657
            return $insertionList;
658
        }
659
        if ($insertionPosition === '') {
660
            return $list . ', ' . $insertionList;
661
        }
662
663
        // The $insertPosition may be a palette: after:--palette--;;title
664
        // In the $list the palette may contain a LLL string in between the ;;
665
        // Adjust the regex to match that
666
        $positionName = preg_quote($positionName, '/');
667
        if (strpos($positionName, ';;') !== false) {
668
            $positionName = str_replace(';;', ';[^;]*;', $positionName);
669
        }
670
671
        $pattern = ('/(^|,\\s*)(' . $positionName . ')(;[^,$]+)?(,|$)/');
672
        switch ($location) {
673
            case 'after':
674
                $newList = preg_replace($pattern, '$1$2$3, ' . $insertionList . '$4', $list);
675
                break;
676
            case 'before':
677
                $newList = preg_replace($pattern, '$1' . $insertionList . ', $2$3$4', $list);
678
                break;
679
            case 'replace':
680
                $newList = preg_replace($pattern, '$1' . $insertionList . '$4', $list);
681
                break;
682
            default:
683
        }
684
685
        // When preg_replace did not replace anything; append the $insertionList.
686
        if ($list === $newList) {
687
            return $list . ', ' . $insertionList;
688
        }
689
        return $newList;
690
    }
691
692
    /**
693
     * Compares an existing list of items and a list of items to be inserted
694
     * and returns a duplicate-free variant of that insertion list.
695
     *
696
     * Example:
697
     * + list: 'field_a, field_b, field_c'
698
     * + insertion: 'field_b, field_d, field_c'
699
     * -> new insertion: 'field_d'
700
     *
701
     * Duplicate values in $insertionList are removed.
702
     *
703
     * @param string $insertionList The list of items to inserted
704
     * @param string $list The list of items to be extended (default: '')
705
     * @return string Duplicate-free list of items to be inserted
706
     */
707
    protected static function removeDuplicatesForInsertion($insertionList, $list = '')
708
    {
709
        $insertionListParts = preg_split('/\\s*,\\s*/', $insertionList);
710
        $listMatches = [];
711
        if ($list !== '') {
712
            preg_match_all('/(?:^|,)\\s*\\b([^;,]+)\\b[^,]*/', $list, $listMatches);
713
            $listMatches = $listMatches[1];
714
        }
715
716
        $cleanInsertionListParts = [];
717
        foreach ($insertionListParts as $fieldName) {
718
            $fieldNameParts = explode(';', $fieldName, 2);
719
            $cleanFieldName = $fieldNameParts[0];
720
            if (
721
                $cleanFieldName === '--linebreak--'
722
                || (
723
                    !in_array($cleanFieldName, $cleanInsertionListParts, true)
724
                    && !in_array($cleanFieldName, $listMatches, true)
725
                )
726
            ) {
727
                $cleanInsertionListParts[] = $fieldName;
728
            }
729
        }
730
        return implode(', ', $cleanInsertionListParts);
731
    }
732
733
    /**
734
     * Generates an array of fields/items with additional information such as e.g. the name of the palette.
735
     *
736
     * @param string $itemList List of fields/items to be splitted up
737
     * @return array An array with the names of the fields/items as keys and additional information
738
     */
739
    protected static function explodeItemList($itemList)
740
    {
741
        $items = [];
742
        $itemParts = GeneralUtility::trimExplode(',', $itemList, true);
743
        foreach ($itemParts as $itemPart) {
744
            $itemDetails = GeneralUtility::trimExplode(';', $itemPart, false, 5);
745
            $key = $itemDetails[0];
746
            if (strpos($key, '--') !== false) {
747
                // 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!
748
                $key .= count($items);
749
            }
750
            if (!isset($items[$key])) {
751
                $items[$key] = [
752
                    'rawData' => $itemPart,
753
                    'details' => []
754
                ];
755
                $details = [0 => 'field', 1 => 'label', 2 => 'palette'];
756
                foreach ($details as $id => $property) {
757
                    $items[$key]['details'][$property] = $itemDetails[$id] ?? '';
758
                }
759
            }
760
        }
761
        return $items;
762
    }
763
764
    /**
765
     * Generates a list of fields/items out of an array provided by the function getFieldsOfFieldList().
766
     *
767
     * @see explodeItemList
768
     * @param array $items The array of fields/items with optional additional information
769
     * @param bool $useRawData Use raw data instead of building by using the details (default: FALSE)
770
     * @return string The list of fields/items which gets used for $GLOBALS['TCA'][<table>]['types'][<type>]['showitem']
771
     */
772
    protected static function generateItemList(array $items, $useRawData = false)
773
    {
774
        $itemParts = [];
775
        foreach ($items as $item => $itemDetails) {
776
            if (strpos($item, '--') !== false) {
777
                // 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.
778
                $item = str_replace([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], '', $item);
779
            }
780
            if ($useRawData) {
781
                $itemParts[] = $itemDetails['rawData'];
782
            } else {
783
                if (count($itemDetails['details']) > 1) {
784
                    $details = ['palette', 'label', 'field'];
785
                    $elements = [];
786
                    $addEmpty = false;
787
                    foreach ($details as $property) {
788
                        if ($itemDetails['details'][$property] !== '' || $addEmpty) {
789
                            $addEmpty = true;
790
                            array_unshift($elements, $itemDetails['details'][$property]);
791
                        }
792
                    }
793
                    $item = implode(';', $elements);
794
                }
795
                $itemParts[] = $item;
796
            }
797
        }
798
        return implode(', ', $itemParts);
799
    }
800
801
    /**
802
     * Add tablename to default list of allowed tables on pages (in $PAGES_TYPES)
803
     * Will add the $table to the list of tables allowed by default on pages as setup by $PAGES_TYPES['default']['allowedTables']
804
     * FOR USE IN ext_tables.php FILES
805
     *
806
     * @param string $table Table name
807
     */
808
    public static function allowTableOnStandardPages($table)
809
    {
810
        $GLOBALS['PAGES_TYPES']['default']['allowedTables'] .= ',' . $table;
811
    }
812
813
    /**
814
     * Adds a module (main or sub) to the backend interface
815
     * FOR USE IN ext_tables.php FILES
816
     *
817
     * @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.
818
     * @param string $sub The submodule key. If $sub is not set a blank $main module is created.
819
     * @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.
820
     * @param string $path The absolute path to the module. Was used prior to TYPO3 v8, use $moduleConfiguration[routeTarget] now
821
     * @param array $moduleConfiguration additional configuration, previously put in "conf.php" of the module directory
822
     */
823
    public static function addModule($main, $sub = '', $position = '', $path = null, $moduleConfiguration = [])
824
    {
825
        if (!isset($GLOBALS['TBE_MODULES'])) {
826
            $GLOBALS['TBE_MODULES'] = [];
827
        }
828
        // If there is already a main module by this name:
829
        // Adding the submodule to the correct position:
830
        if (isset($GLOBALS['TBE_MODULES'][$main]) && $sub) {
831
            [$place, $modRef] = array_pad(GeneralUtility::trimExplode(':', $position, true), 2, null);
832
            $modules = ',' . $GLOBALS['TBE_MODULES'][$main] . ',';
833
            if ($place === null || ($modRef !== null && !GeneralUtility::inList($modules, $modRef))) {
834
                $place = 'bottom';
835
            }
836
            $modRef = ',' . $modRef . ',';
837
            if (!GeneralUtility::inList($modules, $sub)) {
838
                switch (strtolower($place)) {
839
                    case 'after':
840
                        $modules = str_replace($modRef, $modRef . $sub . ',', $modules);
841
                        break;
842
                    case 'before':
843
                        $modules = str_replace($modRef, ',' . $sub . $modRef, $modules);
844
                        break;
845
                    case 'top':
846
                        $modules = $sub . $modules;
847
                        break;
848
                    case 'bottom':
849
                    default:
850
                        $modules = $modules . $sub;
851
                }
852
            }
853
            // Re-inserting the submodule list:
854
            $GLOBALS['TBE_MODULES'][$main] = trim($modules, ',');
855
        } elseif (!isset($GLOBALS['TBE_MODULES'][$main]) && empty($sub)) {
856
            // Create a new main module, respecting the order, which is only possible when the module does not exist yet
857
            $conf = $GLOBALS['TBE_MODULES']['_configuration'] ?? [];
858
            unset($GLOBALS['TBE_MODULES']['_configuration']);
859
            $navigationComponents = $GLOBALS['TBE_MODULES']['_navigationComponents'] ?? [];
860
            unset($GLOBALS['TBE_MODULES']['_navigationComponents']);
861
862
            $modules = array_keys($GLOBALS['TBE_MODULES']);
863
            [$place, $moduleReference] = array_pad(GeneralUtility::trimExplode(':', $position, true), 2, null);
864
            if ($place === null || ($moduleReference !== null && !in_array($moduleReference, $modules, true))) {
865
                $place = 'bottom';
866
            }
867
            $newModules = [];
868
            switch (strtolower($place)) {
869
                case 'after':
870
                    foreach ($modules as $existingMainModule) {
871
                        $newModules[$existingMainModule] = $GLOBALS['TBE_MODULES'][$existingMainModule];
872
                        if ($moduleReference === $existingMainModule) {
873
                            $newModules[$main] = '';
874
                        }
875
                    }
876
                    break;
877
                case 'before':
878
                    foreach ($modules as $existingMainModule) {
879
                        if ($moduleReference === $existingMainModule) {
880
                            $newModules[$main] = '';
881
                        }
882
                        $newModules[$existingMainModule] = $GLOBALS['TBE_MODULES'][$existingMainModule];
883
                    }
884
                    break;
885
                case 'top':
886
                    $newModules[$main] = '';
887
                    $newModules += $GLOBALS['TBE_MODULES'];
888
                    break;
889
                case 'bottom':
890
                default:
891
                    $newModules = $GLOBALS['TBE_MODULES'];
892
                    $newModules[$main] = '';
893
            }
894
            $GLOBALS['TBE_MODULES'] = $newModules;
895
            $GLOBALS['TBE_MODULES']['_configuration'] = $conf;
896
            $GLOBALS['TBE_MODULES']['_navigationComponents'] = $navigationComponents;
897
        } else {
898
            // Create new main modules with only one submodule, $sub (or none if $sub is blank)
899
            $GLOBALS['TBE_MODULES'][$main] = $sub;
900
        }
901
902
        // add additional configuration
903
        $fullModuleSignature = $main . ($sub ? '_' . $sub : '');
904
        if (is_array($moduleConfiguration) && !empty($moduleConfiguration)) {
905
            // remove default icon if an icon identifier is available
906
            if (!empty($moduleConfiguration['iconIdentifier']) && $moduleConfiguration['icon'] === 'EXT:extbase/Resources/Public/Icons/Extension.png') {
907
                unset($moduleConfiguration['icon']);
908
            }
909
            if (!empty($moduleConfiguration['icon'])) {
910
                $iconRegistry = GeneralUtility::makeInstance(IconRegistry::class);
911
                $iconIdentifier = 'module-' . $fullModuleSignature;
912
                $iconProvider = $iconRegistry->detectIconProvider($moduleConfiguration['icon']);
913
                $iconRegistry->registerIcon(
914
                    $iconIdentifier,
915
                    $iconProvider,
916
                    ['source' => GeneralUtility::getFileAbsFileName($moduleConfiguration['icon'])]
917
                );
918
                $moduleConfiguration['iconIdentifier'] = $iconIdentifier;
919
                unset($moduleConfiguration['icon']);
920
            }
921
922
            $GLOBALS['TBE_MODULES']['_configuration'][$fullModuleSignature] = $moduleConfiguration;
923
        }
924
925
        // Also register the module as regular route
926
        $routeName = $moduleConfiguration['id'] ?? $fullModuleSignature;
927
        // Build Route objects from the data
928
        if (!empty($moduleConfiguration['path'])) {
929
            $path = $moduleConfiguration['path'];
930
            $path = '/' . ltrim($path, '/');
931
            $legacyPath = '';
932
        } else {
933
            $path = str_replace('_', '/', $fullModuleSignature);
934
            $path = trim($path, '/');
935
            $legacyPath = '/' . $path . '/';
936
            $path = '/module/' . $path;
937
        }
938
939
        $options = [
940
            'module' => true,
941
            'moduleName' => $fullModuleSignature,
942
            'access' => !empty($moduleConfiguration['access']) ? $moduleConfiguration['access'] : 'user,group'
943
        ];
944
        if (!empty($moduleConfiguration['routeTarget'])) {
945
            $options['target'] = $moduleConfiguration['routeTarget'];
946
        }
947
948
        $router = GeneralUtility::makeInstance(Router::class);
949
        $router->addRoute($routeName, GeneralUtility::makeInstance(Route::class, $path, $options));
950
        // bridge to allow to access old name like "/web/ts/" instead of "/module/web/ts/"
951
        // @deprecated since TYPO3 v10.0, will be removed in TYPO3 v11.0.
952
        if (!empty($legacyPath)) {
953
            $router->addRoute('_legacyroutetomodule_' . $routeName, GeneralUtility::makeInstance(Route::class, $legacyPath, $options));
954
        }
955
    }
956
957
    /**
958
     * Adds a "Function menu module" ('third level module') to an existing function menu for some other backend module
959
     * The arguments values are generally determined by which function menu this is supposed to interact with
960
     * See Inside TYPO3 for information on how to use this function.
961
     * FOR USE IN ext_tables.php FILES
962
     *
963
     * @param string $modname Module name
964
     * @param string $className Class name
965
     * @param string $_ unused
966
     * @param string $title Title of module
967
     * @param string $MM_key Menu array key - default is "function
968
     * @param string $WS Workspace conditions. Blank means all workspaces, any other string can be a comma list of "online", "offline" and "custom
969
     */
970
    public static function insertModuleFunction($modname, $className, $_ = null, $title, $MM_key = 'function', $WS = '')
0 ignored issues
show
Coding Style introduced by
Parameters which have default values should be placed at the end.

If you place a parameter with a default value before a parameter with a default value, the default value of the first parameter will never be used as it will always need to be passed anyway:

// $a must always be passed; it's default value is never used.
function someFunction($a = 5, $b) { }
Loading history...
971
    {
972
        $GLOBALS['TBE_MODULES_EXT'][$modname]['MOD_MENU'][$MM_key][$className] = [
973
            'name' => $className,
974
            'title' => $title,
975
            'ws' => $WS
976
        ];
977
    }
978
979
    /**
980
     * Adds $content to the default Page TSconfig as set in $GLOBALS['TYPO3_CONF_VARS'][BE]['defaultPageTSconfig']
981
     * Prefixed with a [GLOBAL] line
982
     * FOR USE IN ext_localconf.php FILE
983
     *
984
     * @param string $content Page TSconfig content
985
     */
986
    public static function addPageTSConfig($content)
987
    {
988
        $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] .= '
989
[GLOBAL]
990
' . $content;
991
    }
992
993
    /**
994
     * Adds $content to the default User TSconfig as set in $GLOBALS['TYPO3_CONF_VARS'][BE]['defaultUserTSconfig']
995
     * Prefixed with a [GLOBAL] line
996
     * FOR USE IN ext_localconf.php FILE
997
     *
998
     * @param string $content User TSconfig content
999
     */
1000
    public static function addUserTSConfig($content)
1001
    {
1002
        $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig'] .= '
1003
[GLOBAL]
1004
' . $content;
1005
    }
1006
1007
    /**
1008
     * Adds a reference to a locallang file with $GLOBALS['TCA_DESCR'] labels
1009
     * FOR USE IN ext_tables.php FILES
1010
     * 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.
1011
     *
1012
     * @param string $key Description key. Typically a database table (like "pages") but for applications can be other strings, but prefixed with "_MOD_")
1013
     * @param string $file File reference to locallang file, eg. "EXT:core/Resources/Private/Language/locallang_csh_pages.xlf" (or ".xml")
1014
     */
1015
    public static function addLLrefForTCAdescr($key, $file)
1016
    {
1017
        if (empty($key)) {
1018
            throw new \RuntimeException('No description key set in addLLrefForTCAdescr(). Provide it as first parameter', 1507321596);
1019
        }
1020
        if (!is_array($GLOBALS['TCA_DESCR'][$key] ?? false)) {
1021
            $GLOBALS['TCA_DESCR'][$key] = [];
1022
        }
1023
        if (!is_array($GLOBALS['TCA_DESCR'][$key]['refs'] ?? false)) {
1024
            $GLOBALS['TCA_DESCR'][$key]['refs'] = [];
1025
        }
1026
        $GLOBALS['TCA_DESCR'][$key]['refs'][] = $file;
1027
    }
1028
1029
    /**
1030
     * Registers a navigation component e.g. page tree
1031
     *
1032
     * @param string $module
1033
     * @param string $componentId componentId is also a RequireJS module name e.g. 'TYPO3/CMS/MyExt/MyNavComponent'
1034
     * @param string $extensionKey
1035
     * @throws \RuntimeException
1036
     */
1037
    public static function addNavigationComponent($module, $componentId, $extensionKey)
1038
    {
1039
        if (empty($extensionKey)) {
1040
            throw new \RuntimeException('No extensionKey set in addNavigationComponent(). Provide it as third parameter', 1404068039);
1041
        }
1042
        $GLOBALS['TBE_MODULES']['_navigationComponents'][$module] = [
1043
            'componentId' => $componentId,
1044
            'extKey' => $extensionKey,
1045
            'isCoreComponent' => false
1046
        ];
1047
    }
1048
1049
    /**
1050
     * Registers a core navigation component
1051
     *
1052
     * @param string $module
1053
     * @param string $componentId
1054
     */
1055
    public static function addCoreNavigationComponent($module, $componentId)
1056
    {
1057
        self::addNavigationComponent($module, $componentId, 'core');
1058
        $GLOBALS['TBE_MODULES']['_navigationComponents'][$module]['isCoreComponent'] = true;
1059
    }
1060
1061
    /**************************************
1062
     *
1063
     *	 Adding SERVICES features
1064
     *
1065
     ***************************************/
1066
    /**
1067
     * Adds a service to the global services array
1068
     *
1069
     * @param string $extKey Extension key
1070
     * @param string $serviceType Service type, must not be prefixed "tx_" or "Tx_"
1071
     * @param string $serviceKey Service key, must be prefixed "tx_", "Tx_" or "user_"
1072
     * @param array $info Service description array
1073
     */
1074
    public static function addService($extKey, $serviceType, $serviceKey, $info)
1075
    {
1076
        if (!$serviceType) {
1077
            throw new \InvalidArgumentException('No serviceType given.', 1507321535);
1078
        }
1079
        if (!is_array($info)) {
0 ignored issues
show
introduced by
The condition is_array($info) is always true.
Loading history...
1080
            throw new \InvalidArgumentException('No information array given.', 1507321542);
1081
        }
1082
        $info['priority'] = max(0, min(100, $info['priority']));
1083
        $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey] = $info;
1084
        $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey]['extKey'] = $extKey;
1085
        $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey]['serviceKey'] = $serviceKey;
1086
        $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey]['serviceType'] = $serviceType;
1087
        // Change the priority (and other values) from $GLOBALS['TYPO3_CONF_VARS']
1088
        // $GLOBALS['TYPO3_CONF_VARS']['T3_SERVICES'][$serviceType][$serviceKey]['priority']
1089
        // even the activation is possible (a unix service might be possible on windows for some reasons)
1090
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['T3_SERVICES'][$serviceType][$serviceKey] ?? false)) {
1091
            // No check is done here - there might be configuration values only the service type knows about, so
1092
            // we pass everything
1093
            $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey] = array_merge($GLOBALS['T3_SERVICES'][$serviceType][$serviceKey], $GLOBALS['TYPO3_CONF_VARS']['T3_SERVICES'][$serviceType][$serviceKey]);
1094
        }
1095
        // OS check
1096
        // Empty $os means 'not limited to one OS', therefore a check is not needed
1097
        if ($GLOBALS['T3_SERVICES'][$serviceType][$serviceKey]['available'] && $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey]['os'] != '') {
1098
            $os_type = Environment::isWindows() ? 'WIN' : 'UNIX';
1099
            $os = GeneralUtility::trimExplode(',', strtoupper($GLOBALS['T3_SERVICES'][$serviceType][$serviceKey]['os']));
1100
            if (!in_array($os_type, $os, true)) {
1101
                self::deactivateService($serviceType, $serviceKey);
1102
            }
1103
        }
1104
        // Convert subtype list to array for quicker access
1105
        $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey]['serviceSubTypes'] = [];
1106
        $serviceSubTypes = GeneralUtility::trimExplode(',', $info['subtype']);
1107
        foreach ($serviceSubTypes as $subtype) {
1108
            $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey]['serviceSubTypes'][$subtype] = $subtype;
1109
        }
1110
    }
1111
1112
    /**
1113
     * Find the available service with highest priority
1114
     *
1115
     * @param string $serviceType Service type
1116
     * @param string $serviceSubType Service sub type
1117
     * @param mixed $excludeServiceKeys Service keys that should be excluded in the search for a service. Array or comma list.
1118
     * @return mixed Service info array if a service was found, FALSE otherwise
1119
     */
1120
    public static function findService($serviceType, $serviceSubType = '', $excludeServiceKeys = [])
1121
    {
1122
        $serviceKey = false;
1123
        $serviceInfo = false;
1124
        $priority = 0;
1125
        $quality = 0;
1126
        if (!is_array($excludeServiceKeys)) {
1127
            trigger_error('ExtensionManagementUtility::findService() expects the third method argument to be an array instead of a comma-separated string. TYPO3 v11.0 will only support arrays as third argument for $excludeServiceKeys', E_USER_DEPRECATED);
1128
            $excludeServiceKeys = GeneralUtility::trimExplode(',', $excludeServiceKeys, true);
1129
        }
1130
        if (is_array($GLOBALS['T3_SERVICES'][$serviceType])) {
1131
            foreach ($GLOBALS['T3_SERVICES'][$serviceType] as $key => $info) {
1132
                if (in_array($key, $excludeServiceKeys)) {
1133
                    continue;
1134
                }
1135
                // Select a subtype randomly
1136
                // Useful to start a service by service key without knowing his subtypes - for testing purposes
1137
                if ($serviceSubType === '*') {
1138
                    $serviceSubType = key($info['serviceSubTypes']);
1139
                }
1140
                // This matches empty subtype too
1141
                if ($info['available'] && ($info['subtype'] == $serviceSubType || $info['serviceSubTypes'][$serviceSubType]) && $info['priority'] >= $priority) {
1142
                    // Has a lower quality than the already found, therefore we skip this service
1143
                    if ($info['priority'] == $priority && $info['quality'] < $quality) {
1144
                        continue;
1145
                    }
1146
                    // Check if the service is available
1147
                    $info['available'] = self::isServiceAvailable($serviceType, $key, $info);
1148
                    // Still available after exec check?
1149
                    if ($info['available']) {
1150
                        $serviceKey = $key;
1151
                        $priority = $info['priority'];
1152
                        $quality = $info['quality'];
1153
                    }
1154
                }
1155
            }
1156
        }
1157
        if ($serviceKey) {
0 ignored issues
show
introduced by
$serviceKey is of type mixed, thus it always evaluated to false.
Loading history...
1158
            $serviceInfo = $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey];
1159
        }
1160
        return $serviceInfo;
1161
    }
1162
1163
    /**
1164
     * Find a specific service identified by its key
1165
     * Note that this completely bypasses the notions of priority and quality
1166
     *
1167
     * @param string $serviceKey Service key
1168
     * @return array Service info array if a service was found
1169
     * @throws \TYPO3\CMS\Core\Exception
1170
     */
1171
    public static function findServiceByKey($serviceKey)
1172
    {
1173
        if (is_array($GLOBALS['T3_SERVICES'])) {
1174
            // Loop on all service types
1175
            // NOTE: we don't care about the actual type, we are looking for a specific key
1176
            foreach ($GLOBALS['T3_SERVICES'] as $serviceType => $servicesPerType) {
1177
                if (isset($servicesPerType[$serviceKey])) {
1178
                    $serviceDetails = $servicesPerType[$serviceKey];
1179
                    // Test if service is available
1180
                    if (self::isServiceAvailable($serviceType, $serviceKey, $serviceDetails)) {
1181
                        // We have found the right service, return its information
1182
                        return $serviceDetails;
1183
                    }
1184
                }
1185
            }
1186
        }
1187
        throw new \TYPO3\CMS\Core\Exception('Service not found for key: ' . $serviceKey, 1319217244);
1188
    }
1189
1190
    /**
1191
     * Check if a given service is available, based on the executable files it depends on
1192
     *
1193
     * @param string $serviceType Type of service
1194
     * @param string $serviceKey Specific key of the service
1195
     * @param array $serviceDetails Information about the service
1196
     * @return bool Service availability
1197
     */
1198
    public static function isServiceAvailable($serviceType, $serviceKey, $serviceDetails)
1199
    {
1200
        // If the service depends on external programs - check if they exists
1201
        if (trim($serviceDetails['exec'])) {
1202
            $executables = GeneralUtility::trimExplode(',', $serviceDetails['exec'], true);
1203
            foreach ($executables as $executable) {
1204
                // If at least one executable file is not available, exit early returning FALSE
1205
                if (!CommandUtility::checkCommand($executable)) {
1206
                    self::deactivateService($serviceType, $serviceKey);
1207
                    return false;
1208
                }
1209
            }
1210
        }
1211
        // The service is available
1212
        return true;
1213
    }
1214
1215
    /**
1216
     * Deactivate a service
1217
     *
1218
     * @param string $serviceType Service type
1219
     * @param string $serviceKey Service key
1220
     */
1221
    public static function deactivateService($serviceType, $serviceKey)
1222
    {
1223
        // ... maybe it's better to move non-available services to a different array??
1224
        $GLOBALS['T3_SERVICES'][$serviceType][$serviceKey]['available'] = false;
1225
    }
1226
1227
    /**************************************
1228
     *
1229
     *	 Adding FRONTEND features
1230
     *
1231
     ***************************************/
1232
    /**
1233
     * Adds an entry to the list of plugins in content elements of type "Insert plugin"
1234
     * 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)
1235
     * 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.
1236
     * 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
1237
     *
1238
     * FOR USE IN files in Configuration/TCA/Overrides/*.php Use in ext_tables.php FILES may break the frontend.
1239
     *
1240
     * @param array $itemArray Numerical array: [0] => Plugin label, [1] => Plugin identifier / plugin key, ideally prefixed with an extension-specific name (e.g. "events2_list"), [2] => Path to plugin icon, [3] => an optional "group" ID, falls back to "default"
1241
     * @param string $type Type (eg. "list_type") - basically a field from "tt_content" table
1242
     * @param string $extensionKey The extension key
1243
     * @throws \RuntimeException
1244
     */
1245
    public static function addPlugin($itemArray, $type = 'list_type', $extensionKey = null)
1246
    {
1247
        if (!isset($extensionKey)) {
1248
            throw new \InvalidArgumentException(
1249
                'No extension key could be determined when calling addPlugin()!'
1250
                . LF
1251
                . 'This method is meant to be called from Configuration/TCA/Overrides files. '
1252
                . 'The extension key needs to be specified as third parameter. '
1253
                . 'Calling it from any other place e.g. ext_localconf.php does not work and is not supported.',
1254
                1404068038
1255
            );
1256
        }
1257
        if (!isset($itemArray[2]) || !$itemArray[2]) {
1258
            // @todo do we really set $itemArray[2], even if we cannot find an icon? (as that means it's set to 'EXT:foobar/')
1259
            $itemArray[2] = 'EXT:' . $extensionKey . '/' . static::getExtensionIcon(static::$packageManager->getPackage($extensionKey)->getPackagePath());
1260
        }
1261
        if (!isset($itemArray[3])) {
1262
            $itemArray[3] = 'default';
1263
        }
1264
        if (is_array($GLOBALS['TCA']['tt_content']['columns']) && is_array($GLOBALS['TCA']['tt_content']['columns'][$type]['config']['items'])) {
1265
            foreach ($GLOBALS['TCA']['tt_content']['columns'][$type]['config']['items'] as $k => $v) {
1266
                if ((string)$v[1] === (string)$itemArray[1]) {
1267
                    $GLOBALS['TCA']['tt_content']['columns'][$type]['config']['items'][$k] = $itemArray;
1268
                    return;
1269
                }
1270
            }
1271
            $GLOBALS['TCA']['tt_content']['columns'][$type]['config']['items'][] = $itemArray;
1272
        }
1273
    }
1274
1275
    /**
1276
     * Adds an entry to the "ds" array of the tt_content field "pi_flexform".
1277
     * This is used by plugins to add a flexform XML reference / content for use when they are selected as plugin or content element.
1278
     * FOR USE IN files in Configuration/TCA/Overrides/*.php Use in ext_tables.php FILES may break the frontend.
1279
     *
1280
     * @param string $piKeyToMatch Plugin key as used in the list_type field. Use the asterisk * to match all list_type values.
1281
     * @param string $value Either a reference to a flex-form XML file (eg. "FILE:EXT:newloginbox/flexform_ds.xml") or the XML directly.
1282
     * @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.
1283
     * @see addPlugin()
1284
     */
1285
    public static function addPiFlexFormValue($piKeyToMatch, $value, $CTypeToMatch = 'list')
1286
    {
1287
        if (is_array($GLOBALS['TCA']['tt_content']['columns']) && is_array($GLOBALS['TCA']['tt_content']['columns']['pi_flexform']['config']['ds'])) {
1288
            $GLOBALS['TCA']['tt_content']['columns']['pi_flexform']['config']['ds'][$piKeyToMatch . ',' . $CTypeToMatch] = $value;
1289
        }
1290
    }
1291
1292
    /**
1293
     * Adds the $table tablename to the list of tables allowed to be includes by content element type "Insert records"
1294
     * By using $content_table and $content_field you can also use the function for other tables.
1295
     * FOR USE IN files in Configuration/TCA/Overrides/*.php Use in ext_tables.php FILES may break the frontend.
1296
     *
1297
     * @param string $table Table name to allow for "insert record
1298
     * @param string $content_table Table name TO WHICH the $table name is applied. See $content_field as well.
1299
     * @param string $content_field Field name in the database $content_table in which $table is allowed to be added as a reference ("Insert Record")
1300
     */
1301
    public static function addToInsertRecords($table, $content_table = 'tt_content', $content_field = 'records')
1302
    {
1303
        if (is_array($GLOBALS['TCA'][$content_table]['columns']) && isset($GLOBALS['TCA'][$content_table]['columns'][$content_field]['config']['allowed'])) {
1304
            $GLOBALS['TCA'][$content_table]['columns'][$content_field]['config']['allowed'] .= ',' . $table;
1305
        }
1306
    }
1307
1308
    /**
1309
     * Add PlugIn to the default template rendering (previously called "Static Template #43")
1310
     *
1311
     * 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.
1312
     *
1313
     * The naming of #43 has historic reason and is rooted inside code which is now put into a TER extension called
1314
     * "statictemplates". Since the static template with uid 43 is the "content.default" and practically always used
1315
     * for rendering the content elements it's very useful to have this function automatically adding the necessary
1316
     * TypoScript for calling your plugin.
1317
     * The logic is now generalized and called "defaultContentRendering", see addTypoScript() as well.
1318
     *
1319
     * $type determines the type of frontend plugin:
1320
     * + list_type (default) - the good old "Insert plugin" entry
1321
     * + CType - a new content element type
1322
     * + header_layout - an additional header type (added to the selection of layout1-5)
1323
     * + includeLib - just includes the library for manual use somewhere in TypoScript.
1324
     * (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)
1325
     * FOR USE IN ext_localconf.php FILES
1326
     *
1327
     * @param string $key The extension key
1328
     * @param string $_ unused since TYPO3 CMS 8
1329
     * @param string $suffix Is used as a suffix of the class name (e.g. "_pi1")
1330
     * @param string $type See description above
1331
     * @param bool $cacheable If $cached is set as USER content object (cObject) is created - otherwise a USER_INT object is created.
1332
     */
1333
    public static function addPItoST43($key, $_ = '', $suffix = '', $type = 'list_type', $cacheable = false)
1334
    {
1335
        $cN = self::getCN($key);
1336
        // General plugin
1337
        $pluginContent = trim('
1338
plugin.' . $cN . $suffix . ' = USER' . ($cacheable ? '' : '_INT') . '
1339
plugin.' . $cN . $suffix . '.userFunc = ' . $cN . $suffix . '->main
1340
');
1341
        self::addTypoScript($key, 'setup', '
1342
# Setting ' . $key . ' plugin TypoScript
1343
' . $pluginContent);
1344
        // Add after defaultContentRendering
1345
        switch ($type) {
1346
            case 'list_type':
1347
                $addLine = 'tt_content.list.20.' . $key . $suffix . ' = < plugin.' . $cN . $suffix;
1348
                break;
1349
            case 'CType':
1350
                $addLine = trim('
1351
tt_content.' . $key . $suffix . ' =< lib.contentElement
1352
tt_content.' . $key . $suffix . ' {
1353
    templateName = Generic
1354
    20 =< plugin.' . $cN . $suffix . '
1355
}
1356
');
1357
                break;
1358
            case 'header_layout':
1359
                $addLine = 'lib.stdheader.10.' . $key . $suffix . ' = < plugin.' . $cN . $suffix;
1360
                break;
1361
            case 'includeLib':
1362
                $addLine = 'page.1000 = < plugin.' . $cN . $suffix;
1363
                break;
1364
            default:
1365
                $addLine = '';
1366
        }
1367
        if ($addLine) {
1368
            self::addTypoScript($key, 'setup', '
1369
# Setting ' . $key . ' plugin TypoScript
1370
' . $addLine . '
1371
', 'defaultContentRendering');
1372
        }
1373
    }
1374
1375
    /**
1376
     * Call this method to add an entry in the static template list found in sys_templates
1377
     * FOR USE IN Configuration/TCA/Overrides/sys_template.php Use in ext_tables.php may break the frontend.
1378
     *
1379
     * @param string $extKey Is of course the extension key
1380
     * @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/
1381
     * @param string $title Is the title in the selector box.
1382
     * @throws \InvalidArgumentException
1383
     * @see addTypoScript()
1384
     */
1385
    public static function addStaticFile($extKey, $path, $title)
1386
    {
1387
        if (!$extKey) {
1388
            throw new \InvalidArgumentException('No extension key given.', 1507321291);
1389
        }
1390
        if (!$path) {
1391
            throw new \InvalidArgumentException('No file path given.', 1507321297);
1392
        }
1393
        if (is_array($GLOBALS['TCA']['sys_template']['columns'])) {
1394
            $value = str_replace(',', '', 'EXT:' . $extKey . '/' . $path);
1395
            $itemArray = [trim($title . ' (' . $extKey . ')'), $value];
1396
            $GLOBALS['TCA']['sys_template']['columns']['include_static_file']['config']['items'][] = $itemArray;
1397
        }
1398
    }
1399
1400
    /**
1401
     * Call this method to add an entry in the pageTSconfig list found in pages
1402
     * FOR USE in Configuration/TCA/Overrides/pages.php
1403
     *
1404
     * @param string $extKey The extension key
1405
     * @param string $filePath The path where the TSconfig file is located
1406
     * @param string $title The title in the selector box
1407
     * @throws \InvalidArgumentException
1408
     */
1409
    public static function registerPageTSConfigFile($extKey, $filePath, $title)
1410
    {
1411
        if (!$extKey) {
1412
            throw new \InvalidArgumentException('No extension key given.', 1447789490);
1413
        }
1414
        if (!$filePath) {
1415
            throw new \InvalidArgumentException('No file path given.', 1447789491);
1416
        }
1417
        if (!isset($GLOBALS['TCA']['pages']['columns']) || !is_array($GLOBALS['TCA']['pages']['columns'])) {
1418
            throw new \InvalidArgumentException('No TCA definition for table "pages".', 1447789492);
1419
        }
1420
1421
        $value = str_replace(',', '', 'EXT:' . $extKey . '/' . $filePath);
1422
        $itemArray = [trim($title . ' (' . $extKey . ')'), $value];
1423
        $GLOBALS['TCA']['pages']['columns']['tsconfig_includes']['config']['items'][] = $itemArray;
1424
    }
1425
1426
    /**
1427
     * Adds $content to the default TypoScript setup code as set in $GLOBALS['TYPO3_CONF_VARS'][FE]['defaultTypoScript_setup']
1428
     * Prefixed with a [GLOBAL] line
1429
     * FOR USE IN ext_localconf.php FILES
1430
     *
1431
     * @param string $content TypoScript Setup string
1432
     */
1433
    public static function addTypoScriptSetup($content)
1434
    {
1435
        $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_setup'] .= '
1436
[GLOBAL]
1437
' . $content;
1438
    }
1439
1440
    /**
1441
     * Adds $content to the default TypoScript constants code as set in $GLOBALS['TYPO3_CONF_VARS'][FE]['defaultTypoScript_constants']
1442
     * Prefixed with a [GLOBAL] line
1443
     * FOR USE IN ext_localconf.php FILES
1444
     *
1445
     * @param string $content TypoScript Constants string
1446
     */
1447
    public static function addTypoScriptConstants($content)
1448
    {
1449
        $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_constants'] .= '
1450
[GLOBAL]
1451
' . $content;
1452
    }
1453
1454
    /**
1455
     * Adds $content to the default TypoScript code for either setup or constants as set in $GLOBALS['TYPO3_CONF_VARS'][FE]['defaultTypoScript_*']
1456
     * (Basically this function can do the same as addTypoScriptSetup and addTypoScriptConstants - just with a little more hazzle, but also with some more options!)
1457
     * FOR USE IN ext_localconf.php FILES
1458
     * Note: As of TYPO3 CMS 6.2, static template #43 (content: default) was replaced with "defaultContentRendering" which makes it
1459
     * possible that a first extension like fluid_styled_content registers a "contentRendering" template (= a template that defines default content rendering TypoScript)
1460
     * by adding itself to $TYPO3_CONF_VARS[FE][contentRenderingTemplates][] = 'myext/Configuration/TypoScript'.
1461
     * An extension calling addTypoScript('myext', 'setup', $typoScript, 'defaultContentRendering') will add its TypoScript directly after;
1462
     * For now, "43" and "defaultContentRendering" can be used, but "defaultContentRendering" is more descriptive and
1463
     * should be used in the future.
1464
     *
1465
     * @param string $key Is the extension key (informative only).
1466
     * @param string $type Is either "setup" or "constants" and obviously determines which kind of TypoScript code we are adding.
1467
     * @param string $content Is the TS content, will be prefixed with a [GLOBAL] line and a comment-header.
1468
     * @param int|string $afterStaticUid 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.
1469
     * @throws \InvalidArgumentException
1470
     */
1471
    public static function addTypoScript(string $key, string $type, string $content, $afterStaticUid = 0)
1472
    {
1473
        if ($type !== 'setup' && $type !== 'constants') {
1474
            throw new \InvalidArgumentException('Argument $type must be set to either "setup" or "constants" when calling addTypoScript from extension "' . $key . '"', 1507321200);
1475
        }
1476
        $content = '
1477
1478
[GLOBAL]
1479
#############################################
1480
## TypoScript added by extension "' . $key . '"
1481
#############################################
1482
1483
' . $content;
1484
        if ($afterStaticUid) {
1485
            // If 'content (default)' is targeted (static uid 43),
1486
            // 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
1487
            if ($afterStaticUid === 'defaultContentRendering' || $afterStaticUid == 43) {
1488
                if (!isset($GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $type . '.']['defaultContentRendering'])) {
1489
                    $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $type . '.']['defaultContentRendering'] = '';
1490
                }
1491
                $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $type . '.']['defaultContentRendering'] .= $content;
1492
            } else {
1493
                if (!isset($GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $type . '.'][$afterStaticUid])) {
1494
                    $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $type . '.'][$afterStaticUid] = '';
1495
                }
1496
                $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $type . '.'][$afterStaticUid] .= $content;
1497
            }
1498
        } else {
1499
            if (!isset($GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $type])) {
1500
                $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $type] = '';
1501
            }
1502
            $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $type] .= $content;
1503
        }
1504
    }
1505
1506
    /***************************************
1507
     *
1508
     * Internal extension management methods
1509
     *
1510
     ***************************************/
1511
    /**
1512
     * Find extension icon
1513
     *
1514
     * @param string $extensionPath Path to extension directory.
1515
     * @param bool $returnFullPath Return full path of file.
1516
     *
1517
     * @return string
1518
     */
1519
    public static function getExtensionIcon($extensionPath, $returnFullPath = false)
1520
    {
1521
        $icon = '';
1522
        $locationsToCheckFor = [
1523
            'Resources/Public/Icons/Extension.svg',
1524
            'Resources/Public/Icons/Extension.png',
1525
            'Resources/Public/Icons/Extension.gif',
1526
            'ext_icon.svg',
1527
            'ext_icon.png',
1528
            'ext_icon.gif',
1529
        ];
1530
        foreach ($locationsToCheckFor as $fileLocation) {
1531
            if (file_exists($extensionPath . $fileLocation)) {
1532
                $icon = $fileLocation;
1533
                break;
1534
            }
1535
        }
1536
        return $returnFullPath ? $extensionPath . $icon : $icon;
1537
    }
1538
1539
    /**
1540
     * Execute all ext_localconf.php files of loaded extensions.
1541
     * The method implements an optionally used caching mechanism that concatenates all
1542
     * ext_localconf.php files in one file.
1543
     *
1544
     * This is an internal method. It is only used during bootstrap and
1545
     * extensions should not use it!
1546
     *
1547
     * @param bool $allowCaching Whether or not to load / create concatenated cache file
1548
     * @param FrontendInterface $codeCache
1549
     * @internal
1550
     */
1551
    public static function loadExtLocalconf($allowCaching = true, FrontendInterface $codeCache = null)
1552
    {
1553
        if ($allowCaching) {
1554
            $codeCache = $codeCache ?? self::getCacheManager()->getCache('core');
1555
            $cacheIdentifier = self::getExtLocalconfCacheIdentifier();
1556
            $hasCache = $codeCache->require($cacheIdentifier) !== false;
0 ignored issues
show
Bug introduced by
The method require() does not exist on TYPO3\CMS\Core\Cache\Frontend\FrontendInterface. It seems like you code against a sub-type of TYPO3\CMS\Core\Cache\Frontend\FrontendInterface such as TYPO3\CMS\Core\Cache\Frontend\PhpFrontend. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1556
            $hasCache = $codeCache->/** @scrutinizer ignore-call */ require($cacheIdentifier) !== false;
Loading history...
1557
            if (!$hasCache) {
1558
                self::loadSingleExtLocalconfFiles();
1559
                self::createExtLocalconfCacheEntry($codeCache);
1560
            }
1561
        } else {
1562
            self::loadSingleExtLocalconfFiles();
1563
        }
1564
    }
1565
1566
    /**
1567
     * Execute ext_localconf.php files from extensions
1568
     */
1569
    protected static function loadSingleExtLocalconfFiles()
1570
    {
1571
        foreach (static::$packageManager->getActivePackages() as $package) {
1572
            $extLocalconfPath = $package->getPackagePath() . 'ext_localconf.php';
1573
            if (@file_exists($extLocalconfPath)) {
1574
                require $extLocalconfPath;
1575
            }
1576
        }
1577
    }
1578
1579
    /**
1580
     * Create cache entry for concatenated ext_localconf.php files
1581
     *
1582
     * @param FrontendInterface $codeCache
1583
     */
1584
    protected static function createExtLocalconfCacheEntry(FrontendInterface $codeCache)
1585
    {
1586
        $phpCodeToCache = [];
1587
        // Set same globals as in loadSingleExtLocalconfFiles()
1588
        $phpCodeToCache[] = '/**';
1589
        $phpCodeToCache[] = ' * Compiled ext_localconf.php cache file';
1590
        $phpCodeToCache[] = ' */';
1591
        $phpCodeToCache[] = '';
1592
        // Iterate through loaded extensions and add ext_localconf content
1593
        foreach (static::$packageManager->getActivePackages() as $package) {
1594
            $extensionKey = $package->getPackageKey();
1595
            $extLocalconfPath = $package->getPackagePath() . 'ext_localconf.php';
1596
            if (@file_exists($extLocalconfPath)) {
1597
                // Include a header per extension to make the cache file more readable
1598
                $phpCodeToCache[] = '/**';
1599
                $phpCodeToCache[] = ' * Extension: ' . $extensionKey;
1600
                $phpCodeToCache[] = ' * File: ' . $extLocalconfPath;
1601
                $phpCodeToCache[] = ' */';
1602
                $phpCodeToCache[] = '';
1603
                // Add ext_localconf.php content of extension
1604
                $phpCodeToCache[] = trim(file_get_contents($extLocalconfPath));
1605
                $phpCodeToCache[] = '';
1606
                $phpCodeToCache[] = '';
1607
            }
1608
        }
1609
        $phpCodeToCache = implode(LF, $phpCodeToCache);
1610
        // Remove all start and ending php tags from content
1611
        $phpCodeToCache = preg_replace('/<\\?php|\\?>/is', '', $phpCodeToCache);
1612
        $codeCache->set(self::getExtLocalconfCacheIdentifier(), $phpCodeToCache);
1613
    }
1614
1615
    /**
1616
     * Cache identifier of concatenated ext_localconf file
1617
     *
1618
     * @return string
1619
     */
1620
    protected static function getExtLocalconfCacheIdentifier()
1621
    {
1622
        return 'ext_localconf_' . sha1((string)(new Typo3Version()) . Environment::getProjectPath() . 'extLocalconf' . serialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['runtimeActivatedPackages']));
1623
    }
1624
1625
    /**
1626
     * Wrapper for buildBaseTcaFromSingleFiles handling caching.
1627
     *
1628
     * This builds 'base' TCA that is later overloaded by ext_tables.php.
1629
     *
1630
     * Use a cache file if exists and caching is allowed.
1631
     *
1632
     * This is an internal method. It is only used during bootstrap and
1633
     * extensions should not use it!
1634
     *
1635
     * @param bool $allowCaching Whether or not to load / create concatenated cache file
1636
     * @internal
1637
     */
1638
    public static function loadBaseTca($allowCaching = true, FrontendInterface $codeCache = null)
1639
    {
1640
        if ($allowCaching) {
1641
            $codeCache = $codeCache ?? self::getCacheManager()->getCache('core');
1642
            $cacheIdentifier = static::getBaseTcaCacheIdentifier();
1643
            $cacheData = $codeCache->require($cacheIdentifier);
1644
            if ($cacheData) {
1645
                $GLOBALS['TCA'] = $cacheData['tca'];
1646
                GeneralUtility::setSingletonInstance(
1647
                    CategoryRegistry::class,
1648
                    unserialize(
1649
                        $cacheData['categoryRegistry'],
1650
                        ['allowed_classes' => [CategoryRegistry::class]]
1651
                    )
1652
                );
1653
            } else {
1654
                static::buildBaseTcaFromSingleFiles();
1655
                static::createBaseTcaCacheFile($codeCache);
1656
            }
1657
        } else {
1658
            static::buildBaseTcaFromSingleFiles();
1659
        }
1660
    }
1661
1662
    /**
1663
     * Find all Configuration/TCA/* files of extensions and create base TCA from it.
1664
     * The filename must be the table name in $GLOBALS['TCA'], and the content of
1665
     * the file should return an array with content of a specific table.
1666
     *
1667
     * @see Extension core, extensionmanager and others for examples.
1668
     */
1669
    protected static function buildBaseTcaFromSingleFiles()
1670
    {
1671
        $GLOBALS['TCA'] = [];
1672
1673
        $activePackages = static::$packageManager->getActivePackages();
1674
1675
        // First load "full table" files from Configuration/TCA
1676
        foreach ($activePackages as $package) {
1677
            try {
1678
                $finder = Finder::create()->files()->sortByName()->depth(0)->name('*.php')->in($package->getPackagePath() . 'Configuration/TCA');
1679
            } catch (\InvalidArgumentException $e) {
1680
                // No such directory in this package
1681
                continue;
1682
            }
1683
            foreach ($finder as $fileInfo) {
1684
                $tcaOfTable = require $fileInfo->getPathname();
1685
                if (is_array($tcaOfTable)) {
1686
                    $tcaTableName = substr($fileInfo->getBasename(), 0, -4);
1687
                    $GLOBALS['TCA'][$tcaTableName] = $tcaOfTable;
1688
                }
1689
            }
1690
        }
1691
1692
        // Apply category stuff
1693
        CategoryRegistry::getInstance()->applyTcaForPreRegisteredTables();
1694
1695
        // Execute override files from Configuration/TCA/Overrides
1696
        foreach ($activePackages as $package) {
1697
            try {
1698
                $finder = Finder::create()->files()->sortByName()->depth(0)->name('*.php')->in($package->getPackagePath() . 'Configuration/TCA/Overrides');
1699
            } catch (\InvalidArgumentException $e) {
1700
                // No such directory in this package
1701
                continue;
1702
            }
1703
            foreach ($finder as $fileInfo) {
1704
                require $fileInfo->getPathname();
1705
            }
1706
        }
1707
1708
        // Call the TcaMigration and log any deprecations.
1709
        $tcaMigration = GeneralUtility::makeInstance(TcaMigration::class);
1710
        $GLOBALS['TCA'] = $tcaMigration->migrate($GLOBALS['TCA']);
1711
        $messages = $tcaMigration->getMessages();
1712
        if (!empty($messages)) {
1713
            $context = 'Automatic TCA migration done during bootstrap. Please adapt TCA accordingly, these migrations'
1714
                . ' will be removed. The backend module "Configuration -> TCA" shows the modified values.'
1715
                . ' Please adapt these areas:';
1716
            array_unshift($messages, $context);
1717
            trigger_error(implode(LF, $messages), E_USER_DEPRECATED);
1718
        }
1719
1720
        // TCA preparation
1721
        $tcaPreparation = GeneralUtility::makeInstance(TcaPreparation::class);
1722
        $GLOBALS['TCA'] = $tcaPreparation->prepare($GLOBALS['TCA']);
1723
1724
        static::dispatchTcaIsBeingBuiltEvent($GLOBALS['TCA']);
1725
    }
1726
1727
    /**
1728
     * Triggers an event for manipulating the final TCA
1729
     *
1730
     * @param array $tca
1731
     */
1732
    protected static function dispatchTcaIsBeingBuiltEvent(array $tca)
1733
    {
1734
        $GLOBALS['TCA'] = static::$eventDispatcher->dispatch(new AfterTcaCompilationEvent($tca))->getTca();
1735
    }
1736
1737
    /**
1738
     * Cache base $GLOBALS['TCA'] to cache file to require the whole thing in one
1739
     * file for next access instead of cycling through all extensions again.
1740
     *
1741
     * @param FrontendInterface $codeCache
1742
     */
1743
    protected static function createBaseTcaCacheFile(FrontendInterface $codeCache)
1744
    {
1745
        $codeCache->set(
1746
            static::getBaseTcaCacheIdentifier(),
1747
            'return '
1748
                . var_export(['tca' => $GLOBALS['TCA'], 'categoryRegistry' => serialize(CategoryRegistry::getInstance())], true)
1749
                . ';'
1750
        );
1751
    }
1752
1753
    /**
1754
     * Cache identifier of base TCA cache entry.
1755
     *
1756
     * @return string
1757
     */
1758
    protected static function getBaseTcaCacheIdentifier()
1759
    {
1760
        return 'tca_base_' . sha1((string)(new Typo3Version()) . Environment::getProjectPath() . 'tca_code' . serialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['runtimeActivatedPackages']));
1761
    }
1762
1763
    /**
1764
     * Execute all ext_tables.php files of loaded extensions.
1765
     * The method implements an optionally used caching mechanism that concatenates all
1766
     * ext_tables.php files in one file.
1767
     *
1768
     * This is an internal method. It is only used during bootstrap and
1769
     * extensions should not use it!
1770
     *
1771
     * @param bool $allowCaching Whether to load / create concatenated cache file
1772
     * @internal
1773
     */
1774
    public static function loadExtTables($allowCaching = true)
1775
    {
1776
        if ($allowCaching && !self::$extTablesWasReadFromCacheOnce) {
1777
            self::$extTablesWasReadFromCacheOnce = true;
1778
            $cacheIdentifier = self::getExtTablesCacheIdentifier();
1779
            /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $codeCache */
1780
            $codeCache = self::getCacheManager()->getCache('core');
1781
            $hasCache = $codeCache->require($cacheIdentifier) !== false;
1782
            if (!$hasCache) {
1783
                self::loadSingleExtTablesFiles();
1784
                self::createExtTablesCacheEntry();
1785
            }
1786
        } else {
1787
            self::loadSingleExtTablesFiles();
1788
        }
1789
    }
1790
1791
    /**
1792
     * Load ext_tables.php as single files
1793
     */
1794
    protected static function loadSingleExtTablesFiles()
1795
    {
1796
        // Load each ext_tables.php file of loaded extensions
1797
        foreach (static::$packageManager->getActivePackages() as $package) {
1798
            $extTablesPath = $package->getPackagePath() . 'ext_tables.php';
1799
            if (@file_exists($extTablesPath)) {
1800
                require $extTablesPath;
1801
            }
1802
        }
1803
    }
1804
1805
    /**
1806
     * Create concatenated ext_tables.php cache file
1807
     */
1808
    protected static function createExtTablesCacheEntry()
1809
    {
1810
        $phpCodeToCache = [];
1811
        // Set same globals as in loadSingleExtTablesFiles()
1812
        $phpCodeToCache[] = '/**';
1813
        $phpCodeToCache[] = ' * Compiled ext_tables.php cache file';
1814
        $phpCodeToCache[] = ' */';
1815
        $phpCodeToCache[] = '';
1816
        // Iterate through loaded extensions and add ext_tables content
1817
        foreach (static::$packageManager->getActivePackages() as $package) {
1818
            $extensionKey = $package->getPackageKey();
1819
            $extTablesPath = $package->getPackagePath() . 'ext_tables.php';
1820
            if (@file_exists($extTablesPath)) {
1821
                // Include a header per extension to make the cache file more readable
1822
                $phpCodeToCache[] = '/**';
1823
                $phpCodeToCache[] = ' * Extension: ' . $extensionKey;
1824
                $phpCodeToCache[] = ' * File: ' . $extTablesPath;
1825
                $phpCodeToCache[] = ' */';
1826
                $phpCodeToCache[] = '';
1827
                // Add ext_tables.php content of extension
1828
                $phpCodeToCache[] = trim(file_get_contents($extTablesPath));
1829
                $phpCodeToCache[] = '';
1830
            }
1831
        }
1832
        $phpCodeToCache = implode(LF, $phpCodeToCache);
1833
        // Remove all start and ending php tags from content
1834
        $phpCodeToCache = preg_replace('/<\\?php|\\?>/is', '', $phpCodeToCache);
1835
        self::getCacheManager()->getCache('core')->set(self::getExtTablesCacheIdentifier(), $phpCodeToCache);
1836
    }
1837
1838
    /**
1839
     * Cache identifier for concatenated ext_tables.php files
1840
     *
1841
     * @return string
1842
     */
1843
    protected static function getExtTablesCacheIdentifier()
1844
    {
1845
        return 'ext_tables_' . sha1((string)(new Typo3Version()) . Environment::getProjectPath() . 'extTables' . serialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['runtimeActivatedPackages']));
1846
    }
1847
1848
    /**
1849
     * Gets an array of loaded extension keys
1850
     *
1851
     * @return array Loaded extensions
1852
     */
1853
    public static function getLoadedExtensionListArray()
1854
    {
1855
        return array_keys(static::$packageManager->getActivePackages());
1856
    }
1857
1858
    /**
1859
     * Loads given extension
1860
     *
1861
     * Warning: This method only works if the upgrade wizard to transform
1862
     * localconf.php to LocalConfiguration.php was already run
1863
     *
1864
     * @param string $extensionKey Extension key to load
1865
     * @throws \RuntimeException
1866
     */
1867
    public static function loadExtension($extensionKey)
1868
    {
1869
        if (static::$packageManager->isPackageActive($extensionKey)) {
1870
            throw new \RuntimeException('Extension already loaded', 1342345486);
1871
        }
1872
        static::$packageManager->activatePackage($extensionKey);
1873
    }
1874
1875
    /**
1876
     * Unloads given extension
1877
     *
1878
     * Warning: This method only works if the upgrade wizard to transform
1879
     * localconf.php to LocalConfiguration.php was already run
1880
     *
1881
     * @param string $extensionKey Extension key to remove
1882
     * @throws \RuntimeException
1883
     */
1884
    public static function unloadExtension($extensionKey)
1885
    {
1886
        if (!static::$packageManager->isPackageActive($extensionKey)) {
1887
            throw new \RuntimeException('Extension not loaded', 1342345487);
1888
        }
1889
        static::$packageManager->deactivatePackage($extensionKey);
1890
    }
1891
1892
    /**
1893
     * Makes a table categorizable by adding value into the category registry.
1894
     * FOR USE IN ext_localconf.php FILES or files in Configuration/TCA/Overrides/*.php Use the latter to benefit from TCA caching!
1895
     *
1896
     * @param string $extensionKey Extension key to be used
1897
     * @param string $tableName Name of the table to be categorized
1898
     * @param string $fieldName Name of the field to be used to store categories
1899
     * @param array $options Additional configuration options
1900
     * @param bool $override If TRUE, any category configuration for the same table / field is removed before the new configuration is added
1901
     * @see addTCAcolumns
1902
     * @see addToAllTCAtypes
1903
     */
1904
    public static function makeCategorizable($extensionKey, $tableName, $fieldName = 'categories', array $options = [], $override = false)
1905
    {
1906
        // Update the category registry
1907
        $result = CategoryRegistry::getInstance()->add($extensionKey, $tableName, $fieldName, $options, $override);
1908
        if ($result === false) {
1909
            GeneralUtility::makeInstance(LogManager::class)
1910
                ->getLogger(__CLASS__)
1911
                ->warning(sprintf(
1912
                    CategoryRegistry::class . ': no category registered for table "%s". Key was already registered.',
1913
                    $tableName
1914
                ));
1915
        }
1916
    }
1917
}
1918