Passed
Push — master ( 5afe60...881906 )
by
unknown
56:24 queued 42:09
created

AbstractItemProvider::sanitizeItemArray()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 6
nop 3
dl 0
loc 15
rs 10
c 0
b 0
f 0
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\Backend\Form\FormDataProvider;
17
18
use Doctrine\DBAL\Exception as DBALException;
19
use TYPO3\CMS\Backend\Utility\BackendUtility;
20
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
21
use TYPO3\CMS\Core\Database\ConnectionPool;
22
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
23
use TYPO3\CMS\Core\Database\Query\QueryHelper;
24
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
25
use TYPO3\CMS\Core\Database\RelationHandler;
26
use TYPO3\CMS\Core\Hooks\TcaItemsProcessorFunctions;
27
use TYPO3\CMS\Core\Imaging\IconFactory;
28
use TYPO3\CMS\Core\Localization\LanguageService;
29
use TYPO3\CMS\Core\Messaging\FlashMessage;
30
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
31
use TYPO3\CMS\Core\Messaging\FlashMessageService;
32
use TYPO3\CMS\Core\Resource\FileRepository;
33
use TYPO3\CMS\Core\Resource\ResourceStorage;
34
use TYPO3\CMS\Core\Type\Bitmask\Permission;
35
use TYPO3\CMS\Core\Utility\ArrayUtility;
36
use TYPO3\CMS\Core\Utility\GeneralUtility;
37
use TYPO3\CMS\Core\Utility\MathUtility;
38
39
/**
40
 * Contains methods used by Data providers that handle elements
41
 * with single items like select, radio and some more.
42
 */
43
abstract class AbstractItemProvider
44
{
45
    /**
46
     * Resolve "itemProcFunc" of elements.
47
     *
48
     * @param array $result Main result array
49
     * @param string $fieldName Field name to handle item list for
50
     * @param array $items Existing items array
51
     * @return array New list of item elements
52
     */
53
    protected function resolveItemProcessorFunction(array $result, $fieldName, array $items)
54
    {
55
        $table = $result['tableName'];
56
        $config = $result['processedTca']['columns'][$fieldName]['config'];
57
58
        $pageTsProcessorParameters = null;
59
        if (!empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['itemsProcFunc.'])) {
60
            $pageTsProcessorParameters = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['itemsProcFunc.'];
61
        }
62
        $processorParameters = [
63
            // Function manipulates $items directly and return nothing
64
            'items' => &$items,
65
            'config' => $config,
66
            'TSconfig' => $pageTsProcessorParameters,
67
            'table' => $table,
68
            'row' => $result['databaseRow'],
69
            'field' => $fieldName,
70
        ];
71
        if (!empty($result['flexParentDatabaseRow'])) {
72
            $processorParameters['flexParentDatabaseRow'] = $result['flexParentDatabaseRow'];
73
        }
74
75
        try {
76
            GeneralUtility::callUserFunction($config['itemsProcFunc'], $processorParameters, $this);
77
        } catch (\Exception $exception) {
78
            // The itemsProcFunc method may throw an exception, create a flash message if so
79
            $languageService = $this->getLanguageService();
0 ignored issues
show
Bug introduced by
The method getLanguageService() does not exist on null. ( Ignorable by Annotation )

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

79
            /** @scrutinizer ignore-call */ 
80
            $languageService = $this->getLanguageService();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
80
            $fieldLabel = $fieldName;
81
            if (!empty($result['processedTca']['columns'][$fieldName]['label'])) {
82
                $fieldLabel = $languageService->sL($result['processedTca']['columns'][$fieldName]['label']);
83
            }
84
            $message = sprintf(
85
                $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.items_proc_func_error'),
86
                $fieldLabel,
87
                $exception->getMessage()
88
            );
89
            /** @var FlashMessage $flashMessage */
90
            $flashMessage = GeneralUtility::makeInstance(
91
                FlashMessage::class,
92
                $message,
93
                '',
94
                FlashMessage::ERROR,
95
                true
96
            );
97
            /** @var \TYPO3\CMS\Core\Messaging\FlashMessageService $flashMessageService */
98
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
99
            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
100
            $defaultFlashMessageQueue->enqueue($flashMessage);
101
        }
102
103
        return $items;
104
    }
105
106
    /**
107
     * PageTsConfig addItems:
108
     *
109
     * TCEFORMS.aTable.aField[.types][.aType].addItems.aValue = aLabel,
110
     * with type specific options merged by pageTsConfig already
111
     *
112
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
113
     *
114
     * @param array $result result array
115
     * @param string $fieldName Current handle field name
116
     * @param array $items Incoming items
117
     * @return array Modified item array
118
     */
119
    protected function addItemsFromPageTsConfig(array $result, $fieldName, array $items)
120
    {
121
        $table = $result['tableName'];
122
        if (!empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['addItems.'])
123
            && is_array($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['addItems.'])
124
        ) {
125
            $addItemsArray = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['addItems.'];
126
            foreach ($addItemsArray as $value => $label) {
127
                // If the value ends with a dot, it is a subelement like "34.icon = mylabel.png", skip it
128
                if (substr($value, -1) === '.') {
129
                    continue;
130
                }
131
                // Check if value "34 = mylabel" also has a "34.icon = myImage.png"
132
                $iconIdentifier = null;
133
                if (isset($addItemsArray[$value . '.'])
134
                    && is_array($addItemsArray[$value . '.'])
135
                    && !empty($addItemsArray[$value . '.']['icon'])
136
                ) {
137
                    $iconIdentifier = $addItemsArray[$value . '.']['icon'];
138
                }
139
                $items[] = [$label, $value, $iconIdentifier];
140
            }
141
        }
142
        return $items;
143
    }
144
145
    /**
146
     * TCA config "special" evaluation. Add them to $items
147
     *
148
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
149
     *
150
     * @param array $result Result array
151
     * @param string $fieldName Current handle field name
152
     * @param array $items Incoming items
153
     * @return array Modified item array
154
     * @throws \UnexpectedValueException
155
     * @deprecated since v11, will be removed in v12
156
     */
157
    protected function addItemsFromSpecial(array $result, $fieldName, array $items)
158
    {
159
        // Guard
160
        if (empty($result['processedTca']['columns'][$fieldName]['config']['special'])
161
            || !is_string($result['processedTca']['columns'][$fieldName]['config']['special'])
162
        ) {
163
            return $items;
164
        }
165
166
        $special = $result['processedTca']['columns'][$fieldName]['config']['special'];
167
168
        trigger_error(
169
            'Using the TCA property \'special=' . $special . '\' is deprecated and will be removed in TYPO3 v12. Use a custom itemsProcFunc instead.',
170
            E_USER_DEPRECATED
171
        );
172
173
        switch (true) {
174
            case $special === 'tables':
175
                $fieldInformation = ['items' => $items];
176
                (new TcaItemsProcessorFunctions())->populateAvailableTables($fieldInformation);
177
                $items = $fieldInformation['items'];
178
                break;
179
            case $special === 'pagetypes':
180
                $fieldInformation = ['items' => $items];
181
                (new TcaItemsProcessorFunctions())->populateAvailablePageTypes($fieldInformation);
182
                $items = $fieldInformation['items'];
183
                break;
184
            case $special === 'exclude':
185
                $fieldInformation = ['items' => $items];
186
                (new TcaItemsProcessorFunctions())->populateExcludeFields($fieldInformation);
187
                $items = $fieldInformation['items'];
188
                break;
189
            case $special === 'explicitValues':
190
                $fieldInformation = ['items' => $items];
191
                (new TcaItemsProcessorFunctions())->populateExplicitAuthValues($fieldInformation);
192
                $items = $fieldInformation['items'];
193
                break;
194
            case $special === 'custom':
195
                $fieldInformation = ['items' => $items];
196
                (new TcaItemsProcessorFunctions())->populateCustomPermissionOptions($fieldInformation);
197
                $items = $fieldInformation['items'];
198
                break;
199
            case $special === 'modListGroup':
200
                $fieldInformation = ['items' => $items];
201
                (new TcaItemsProcessorFunctions())->populateAvailableGroupModules($fieldInformation);
202
                $items = $fieldInformation['items'];
203
                break;
204
            case $special === 'modListUser':
205
                $fieldInformation = ['items' => $items];
206
                (new TcaItemsProcessorFunctions())->populateAvailableUserModules($fieldInformation);
207
                $items = $fieldInformation['items'];
208
                break;
209
            default:
210
                throw new \UnexpectedValueException(
211
                    'Unknown special value ' . $special . ' for field ' . $fieldName . ' of table ' . $result['tableName'],
212
                    1439298496
213
                );
214
        }
215
        return $items;
216
    }
217
218
    /**
219
     * TCA config "fileFolder" evaluation. Add them to $items
220
     *
221
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
222
     *
223
     * @param array $result Result array
224
     * @param string $fieldName Current handle field name
225
     * @param array $items Incoming items
226
     * @return array Modified item array
227
     * @throws \RuntimeException
228
     */
229
    protected function addItemsFromFolder(array $result, $fieldName, array $items)
230
    {
231
        if (empty($result['processedTca']['columns'][$fieldName]['config']['fileFolder'])
232
            || !is_string($result['processedTca']['columns'][$fieldName]['config']['fileFolder'])
233
        ) {
234
            return $items;
235
        }
236
237
        $fileFolderRaw = $result['processedTca']['columns'][$fieldName]['config']['fileFolder'];
238
        $fileFolder = GeneralUtility::getFileAbsFileName($fileFolderRaw);
239
        if ($fileFolder === '') {
240
            throw new \RuntimeException(
241
                'Invalid folder given for item processing: ' . $fileFolderRaw . ' for table ' . $result['tableName'] . ', field ' . $fieldName,
242
                1479399227
243
            );
244
        }
245
        $fileFolder = rtrim($fileFolder, '/') . '/';
246
247
        if (@is_dir($fileFolder)) {
248
            $fileExtensionList = '';
249
            if (!empty($result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList'])
250
                && is_string($result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList'])
251
            ) {
252
                $fileExtensionList = $result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList'];
253
            }
254
            $recursionLevels = isset($result['processedTca']['columns'][$fieldName]['config']['fileFolder_recursions'])
255
                ? MathUtility::forceIntegerInRange($result['processedTca']['columns'][$fieldName]['config']['fileFolder_recursions'], 0, 99)
256
                : 99;
257
            $fileArray = GeneralUtility::getAllFilesAndFoldersInPath([], $fileFolder, $fileExtensionList, false, $recursionLevels);
258
            $fileArray = GeneralUtility::removePrefixPathFromList($fileArray, $fileFolder);
259
            foreach ($fileArray as $fileReference) {
260
                $fileInformation = pathinfo($fileReference);
261
                $icon = GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], strtolower($fileInformation['extension']))
262
                    ? $fileFolder . $fileReference
263
                    : '';
264
                $items[] = [
265
                    $fileReference,
266
                    $fileReference,
267
                    $icon
268
                ];
269
            }
270
        }
271
272
        return $items;
273
    }
274
275
    /**
276
     * TCA config "foreign_table" evaluation. Add them to $items
277
     *
278
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
279
     *
280
     * @param array $result Result array
281
     * @param string $fieldName Current handle field name
282
     * @param array $items Incoming items
283
     * @return array Modified item array
284
     * @throws \UnexpectedValueException
285
     */
286
    protected function addItemsFromForeignTable(array $result, $fieldName, array $items)
287
    {
288
        $databaseError = null;
289
        $queryResult = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $queryResult is dead and can be removed.
Loading history...
290
        // Guard
291
        if (empty($result['processedTca']['columns'][$fieldName]['config']['foreign_table'])
292
            || !is_string($result['processedTca']['columns'][$fieldName]['config']['foreign_table'])
293
        ) {
294
            return $items;
295
        }
296
297
        $languageService = $this->getLanguageService();
298
299
        $foreignTable = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'];
300
301
        if (!isset($GLOBALS['TCA'][$foreignTable]) || !is_array($GLOBALS['TCA'][$foreignTable])) {
302
            throw new \UnexpectedValueException(
303
                'Field ' . $fieldName . ' of table ' . $result['tableName'] . ' reference to foreign table '
304
                . $foreignTable . ', but this table is not defined in TCA',
305
                1439569743
306
            );
307
        }
308
309
        $queryBuilder = $this->buildForeignTableQueryBuilder($result, $fieldName);
310
        try {
311
            $queryResult = $queryBuilder->execute();
312
        } catch (DBALException $e) {
313
            $databaseError = $e->getPrevious()->getMessage();
314
        }
315
316
        // Early return on error with flash message
317
        if (!empty($databaseError)) {
318
            $msg = $databaseError . '. ';
319
            $msg .= $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.database_schema_mismatch');
320
            $msgTitle = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.database_schema_mismatch_title');
321
            /** @var FlashMessage $flashMessage */
322
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, $msgTitle, FlashMessage::ERROR, true);
323
            /** @var FlashMessageService $flashMessageService */
324
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
325
            /** @var FlashMessageQueue $defaultFlashMessageQueue */
326
            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
327
            $defaultFlashMessageQueue->enqueue($flashMessage);
328
            return $items;
329
        }
330
331
        $labelPrefix = '';
332
        if (!empty($result['processedTca']['columns'][$fieldName]['config']['foreign_table_prefix'])) {
333
            $labelPrefix = $result['processedTca']['columns'][$fieldName]['config']['foreign_table_prefix'];
334
            $labelPrefix = $languageService->sL($labelPrefix);
335
        }
336
337
        $fileRepository = GeneralUtility::makeInstance(FileRepository::class);
338
        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
339
340
        while ($foreignRow = $queryResult->fetch()) {
341
            BackendUtility::workspaceOL($foreignTable, $foreignRow);
342
            if (is_array($foreignRow)) {
343
                // If the foreign table sets selicon_field, this field can contain an image
344
                // that represents this specific row.
345
                $iconFieldName = '';
346
                $isReferenceField = false;
347
                if (!empty($GLOBALS['TCA'][$foreignTable]['ctrl']['selicon_field'])) {
348
                    $iconFieldName = $GLOBALS['TCA'][$foreignTable]['ctrl']['selicon_field'];
349
                    if (isset($GLOBALS['TCA'][$foreignTable]['columns'][$iconFieldName]['config']['type'])
350
                        && $GLOBALS['TCA'][$foreignTable]['columns'][$iconFieldName]['config']['type'] === 'inline'
351
                        && $GLOBALS['TCA'][$foreignTable]['columns'][$iconFieldName]['config']['foreign_table'] === 'sys_file_reference'
352
                    ) {
353
                        $isReferenceField = true;
354
                    }
355
                }
356
                $icon = '';
357
                if ($isReferenceField) {
358
                    $references = $fileRepository->findByRelation($foreignTable, $iconFieldName, $foreignRow['uid']);
359
                    if (is_array($references) && !empty($references)) {
360
                        $icon = reset($references);
361
                        $icon = $icon->getPublicUrl();
362
                    }
363
                } else {
364
                    // Else, determine icon based on record type, or a generic fallback
365
                    $icon = $iconFactory->mapRecordTypeToIconIdentifier($foreignTable, $foreignRow);
366
                }
367
                // Add the item
368
                $items[] = [
369
                    $labelPrefix . BackendUtility::getRecordTitle($foreignTable, $foreignRow),
370
                    $foreignRow['uid'],
371
                    $icon
372
                ];
373
            }
374
        }
375
376
        return $items;
377
    }
378
379
    /**
380
     * Remove items using "keepItems" pageTsConfig
381
     *
382
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
383
     *
384
     * @param array $result Result array
385
     * @param string $fieldName Current handle field name
386
     * @param array $items Incoming items
387
     * @return array Modified item array
388
     */
389
    protected function removeItemsByKeepItemsPageTsConfig(array $result, $fieldName, array $items)
390
    {
391
        $table = $result['tableName'];
392
        if (!isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'])
393
            || !is_string($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'])
394
        ) {
395
            return $items;
396
        }
397
398
        // If keepItems is set but is an empty list all current items get removed
399
        if ($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'] === '') {
400
            return [];
401
        }
402
403
        return ArrayUtility::keepItemsInArray(
404
            $items,
405
            $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'],
406
            function ($value) {
407
                return $value[1];
408
            }
409
        );
410
    }
411
412
    /**
413
     * Remove items using "removeItems" pageTsConfig
414
     *
415
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
416
     *
417
     * @param array $result Result array
418
     * @param string $fieldName Current handle field name
419
     * @param array $items Incoming items
420
     * @return array Modified item array
421
     */
422
    protected function removeItemsByRemoveItemsPageTsConfig(array $result, $fieldName, array $items)
423
    {
424
        $table = $result['tableName'];
425
        if (!isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'])
426
            || !is_string($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'])
427
            || $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'] === ''
428
        ) {
429
            return $items;
430
        }
431
432
        $removeItems = array_flip(GeneralUtility::trimExplode(
433
            ',',
434
            $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'],
435
            true
436
        ));
437
        foreach ($items as $key => $itemValues) {
438
            if (isset($removeItems[$itemValues[1]])) {
439
                unset($items[$key]);
440
            }
441
        }
442
443
        return $items;
444
    }
445
446
    /**
447
     * Remove items user restriction on language field
448
     *
449
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
450
     *
451
     * @param array $result Result array
452
     * @param string $fieldName Current handle field name
453
     * @param array $items Incoming items
454
     * @return array Modified item array
455
     */
456
    protected function removeItemsByUserLanguageFieldRestriction(array $result, $fieldName, array $items)
457
    {
458
        // Guard clause returns if not a language field is handled
459
        if (empty($result['processedTca']['ctrl']['languageField'])
460
            || $result['processedTca']['ctrl']['languageField'] !== $fieldName
461
        ) {
462
            return $items;
463
        }
464
465
        $backendUser = $this->getBackendUser();
466
        foreach ($items as $key => $itemValues) {
467
            if (!$backendUser->checkLanguageAccess($itemValues[1])) {
468
                unset($items[$key]);
469
            }
470
        }
471
472
        return $items;
473
    }
474
475
    /**
476
     * Remove items by user restriction on authMode items
477
     *
478
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
479
     *
480
     * @param array $result Result array
481
     * @param string $fieldName Current handle field name
482
     * @param array $items Incoming items
483
     * @return array Modified item array
484
     */
485
    protected function removeItemsByUserAuthMode(array $result, $fieldName, array $items)
486
    {
487
        // Guard clause returns early if no authMode field is configured
488
        if (!isset($result['processedTca']['columns'][$fieldName]['config']['authMode'])
489
            || !is_string($result['processedTca']['columns'][$fieldName]['config']['authMode'])
490
        ) {
491
            return $items;
492
        }
493
494
        $backendUser = $this->getBackendUser();
495
        $authMode = $result['processedTca']['columns'][$fieldName]['config']['authMode'];
496
        foreach ($items as $key => $itemValues) {
497
            // @todo: checkAuthMode() uses $GLOBAL access for "individual" authMode - get rid of this
498
            if (!$backendUser->checkAuthMode($result['tableName'], $fieldName, $itemValues[1], $authMode)) {
499
                unset($items[$key]);
500
            }
501
        }
502
503
        return $items;
504
    }
505
506
    /**
507
     * Remove items if doktype is handled for non admin users
508
     *
509
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
510
     *
511
     * @param array $result Result array
512
     * @param string $fieldName Current handle field name
513
     * @param array $items Incoming items
514
     * @return array Modified item array
515
     */
516
    protected function removeItemsByDoktypeUserRestriction(array $result, $fieldName, array $items)
517
    {
518
        $table = $result['tableName'];
519
        $backendUser = $this->getBackendUser();
520
        // Guard clause returns if not correct table and field or if user is admin
521
        if ($table !== 'pages' || $fieldName !== 'doktype' || $backendUser->isAdmin()
522
        ) {
523
            return $items;
524
        }
525
526
        $allowedPageTypes = $backendUser->groupData['pagetypes_select'];
527
        foreach ($items as $key => $itemValues) {
528
            if (!GeneralUtility::inList($allowedPageTypes, $itemValues[1])) {
529
                unset($items[$key]);
530
            }
531
        }
532
533
        return $items;
534
    }
535
536
    /**
537
     * Remove items if sys_file_storage is not allowed for non-admin users.
538
     *
539
     * Used by TcaSelectItems data providers
540
     *
541
     * @param array $result Result array
542
     * @param string $fieldName Current handle field name
543
     * @param array $items Incoming items
544
     * @return array Modified item array
545
     */
546
    protected function removeItemsByUserStorageRestriction(array $result, $fieldName, array $items)
547
    {
548
        $referencedTableName = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'] ?? null;
549
        if ($referencedTableName !== 'sys_file_storage') {
550
            return $items;
551
        }
552
553
        $allowedStorageIds = array_map(
554
            function (ResourceStorage $storage) {
555
                return $storage->getUid();
556
            },
557
            $this->getBackendUser()->getFileStorages()
558
        );
559
560
        return array_filter(
561
            $items,
562
            function (array $item) use ($allowedStorageIds) {
563
                $itemValue = $item[1] ?? null;
564
                return empty($itemValue)
565
                    || in_array((int)$itemValue, $allowedStorageIds, true);
566
            }
567
        );
568
    }
569
570
    /**
571
     * Build query to fetch foreign records. Helper method of
572
     * addItemsFromForeignTable(), do not call otherwise.
573
     *
574
     * @param array $result Result array
575
     * @param string $localFieldName Current handle field name
576
     * @return QueryBuilder
577
     */
578
    protected function buildForeignTableQueryBuilder(array $result, string $localFieldName): QueryBuilder
579
    {
580
        $backendUser = $this->getBackendUser();
581
582
        $foreignTableName = $result['processedTca']['columns'][$localFieldName]['config']['foreign_table'];
583
        $foreignTableClauseArray = $this->processForeignTableClause($result, $foreignTableName, $localFieldName);
584
585
        $fieldList = BackendUtility::getCommonSelectFields($foreignTableName, $foreignTableName . '.');
586
        /** @var QueryBuilder $queryBuilder */
587
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
588
            ->getQueryBuilderForTable($foreignTableName);
589
590
        $queryBuilder->getRestrictions()
591
            ->removeAll()
592
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
593
594
        $queryBuilder
595
            ->select(...GeneralUtility::trimExplode(',', $fieldList, true))
596
            ->from($foreignTableName)
597
            ->where($foreignTableClauseArray['WHERE']);
598
599
        if (!empty($foreignTableClauseArray['GROUPBY'])) {
600
            $queryBuilder->groupBy(...$foreignTableClauseArray['GROUPBY']);
601
        }
602
603
        if (!empty($foreignTableClauseArray['ORDERBY'])) {
604
            foreach ($foreignTableClauseArray['ORDERBY'] as $orderPair) {
605
                [$fieldName, $order] = $orderPair;
606
                $queryBuilder->addOrderBy($fieldName, $order);
607
            }
608
        } elseif (!empty($GLOBALS['TCA'][$foreignTableName]['ctrl']['default_sortby'])) {
609
            $orderByClauses = QueryHelper::parseOrderBy($GLOBALS['TCA'][$foreignTableName]['ctrl']['default_sortby']);
610
            foreach ($orderByClauses as $orderByClause) {
611
                if (!empty($orderByClause[0])) {
612
                    $queryBuilder->addOrderBy($foreignTableName . '.' . $orderByClause[0], $orderByClause[1]);
613
                }
614
            }
615
        }
616
617
        if (!empty($foreignTableClauseArray['LIMIT'])) {
618
            if (!empty($foreignTableClauseArray['LIMIT'][1])) {
619
                $queryBuilder->setMaxResults($foreignTableClauseArray['LIMIT'][1]);
620
                $queryBuilder->setFirstResult($foreignTableClauseArray['LIMIT'][0]);
621
            } elseif (!empty($foreignTableClauseArray['LIMIT'][0])) {
622
                $queryBuilder->setMaxResults($foreignTableClauseArray['LIMIT'][0]);
623
            }
624
        }
625
626
        // rootLevel = -1 means that elements can be on the rootlevel OR on any page (pid!=-1)
627
        // rootLevel = 0 means that elements are not allowed on root level
628
        // rootLevel = 1 means that elements are only on the root level (pid=0)
629
        $rootLevel = 0;
630
        if (isset($GLOBALS['TCA'][$foreignTableName]['ctrl']['rootLevel'])) {
631
            $rootLevel = (int)$GLOBALS['TCA'][$foreignTableName]['ctrl']['rootLevel'];
632
        }
633
634
        if ($rootLevel === -1) {
635
            $queryBuilder->andWhere(
636
                $queryBuilder->expr()->neq(
637
                    $foreignTableName . '.pid',
638
                    $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
639
                )
640
            );
641
        } elseif ($rootLevel === 1) {
642
            $queryBuilder->andWhere(
643
                $queryBuilder->expr()->eq(
644
                    $foreignTableName . '.pid',
645
                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
646
                )
647
            );
648
        } else {
649
            $queryBuilder->andWhere($backendUser->getPagePermsClause(Permission::PAGE_SHOW));
650
            if ($foreignTableName !== 'pages') {
651
                $queryBuilder
652
                    ->from('pages')
653
                    ->andWhere(
654
                        $queryBuilder->expr()->eq(
655
                            'pages.uid',
656
                            $queryBuilder->quoteIdentifier($foreignTableName . '.pid')
657
                        )
658
                    );
659
            }
660
        }
661
662
        return $queryBuilder;
663
    }
664
665
    /**
666
     * Replace markers in a where clause from TCA foreign_table_where
667
     *
668
     * ###REC_FIELD_[field name]###
669
     * ###THIS_UID### - is current element uid (zero if new).
670
     * ###CURRENT_PID### - is the current page id (pid of the record).
671
     * ###SITEROOT###
672
     * ###PAGE_TSCONFIG_ID### - a value you can set from Page TSconfig dynamically.
673
     * ###PAGE_TSCONFIG_IDLIST### - a value you can set from Page TSconfig dynamically.
674
     * ###PAGE_TSCONFIG_STR### - a value you can set from Page TSconfig dynamically.
675
     *
676
     * @param array $result Result array
677
     * @param string $foreignTableName Name of foreign table
678
     * @param string $localFieldName Current handle field name
679
     * @return array Query parts with keys WHERE, ORDERBY, GROUPBY, LIMIT
680
     */
681
    protected function processForeignTableClause(array $result, $foreignTableName, $localFieldName)
682
    {
683
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($foreignTableName);
684
        $localTable = $result['tableName'];
685
        $effectivePid = $result['effectivePid'];
686
687
        $foreignTableClause = '';
688
        if (!empty($result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'])
689
            && is_string($result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'])
690
        ) {
691
            $foreignTableClause = $result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'];
692
            // Replace possible markers in query
693
            if (strpos($foreignTableClause, '###REC_FIELD_') !== false) {
694
                // " AND table.field='###REC_FIELD_field1###' AND ..." -> array(" AND table.field='", "field1###' AND ...")
695
                $whereClauseParts = explode('###REC_FIELD_', $foreignTableClause);
696
                foreach ($whereClauseParts as $key => $value) {
697
                    if ($key !== 0) {
698
                        // "field1###' AND ..." -> array("field1", "' AND ...")
699
                        $whereClauseSubParts = explode('###', $value, 2);
700
                        // @todo: Throw exception if there is no value? What happens for NEW records?
701
                        $databaseRowKey = empty($result['flexParentDatabaseRow']) ? 'databaseRow' : 'flexParentDatabaseRow';
702
                        $rowFieldValue = $result[$databaseRowKey][$whereClauseSubParts[0]] ?? '';
703
                        if (is_array($rowFieldValue)) {
704
                            // If a select or group field is used here, it may have been processed already and
705
                            // is now an array containing uid + table + title + row.
706
                            // See TcaGroup data provider for details.
707
                            // Pick the first one (always on 0), and use uid only.
708
                            $rowFieldValue = $rowFieldValue[0]['uid'] ?? $rowFieldValue[0];
709
                        }
710
                        if (substr($whereClauseParts[0], -1) === '\'' && $whereClauseSubParts[1][0] === '\'') {
711
                            $whereClauseParts[0] = substr($whereClauseParts[0], 0, -1);
712
                            $whereClauseSubParts[1] = substr($whereClauseSubParts[1], 1);
713
                        }
714
                        $whereClauseParts[$key] = $connection->quote($rowFieldValue) . $whereClauseSubParts[1];
715
                    }
716
                }
717
                $foreignTableClause = implode('', $whereClauseParts);
718
            }
719
            if (strpos($foreignTableClause, '###CURRENT_PID###') !== false) {
720
                // Use pid from parent page clause if in flex form context
721
                if (!empty($result['flexParentDatabaseRow']['pid'])) {
722
                    $effectivePid = $result['flexParentDatabaseRow']['pid'];
723
                } elseif (!$effectivePid && !empty($result['databaseRow']['pid'])) {
724
                    // Use pid from database row if in inline context
725
                    $effectivePid = $result['databaseRow']['pid'];
726
                }
727
            }
728
729
            $siteRootUid = 0;
730
            foreach ($result['rootline'] as $rootlinePage) {
731
                if (!empty($rootlinePage['is_siteroot'])) {
732
                    $siteRootUid = (int)$rootlinePage['uid'];
733
                    break;
734
                }
735
            }
736
737
            $pageTsConfigId = 0;
738
            if (isset($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID'])
739
                && $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID']
740
            ) {
741
                $pageTsConfigId = (int)$result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID'];
742
            }
743
744
            $pageTsConfigIdList = 0;
745
            if (isset($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST'])
746
                && $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST']
747
            ) {
748
                $pageTsConfigIdList = $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST'];
749
            }
750
            $pageTsConfigIdListArray = GeneralUtility::trimExplode(',', $pageTsConfigIdList, true);
751
            $pageTsConfigIdList = [];
752
            foreach ($pageTsConfigIdListArray as $pageTsConfigIdListElement) {
753
                if (MathUtility::canBeInterpretedAsInteger($pageTsConfigIdListElement)) {
754
                    $pageTsConfigIdList[] = (int)$pageTsConfigIdListElement;
755
                }
756
            }
757
            $pageTsConfigIdList = implode(',', $pageTsConfigIdList);
758
759
            $pageTsConfigString = '';
760
            if (isset($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR'])
761
                && $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR']
762
            ) {
763
                $pageTsConfigString = $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR'];
764
                $pageTsConfigString = $connection->quote($pageTsConfigString);
765
            }
766
767
            $foreignTableClause = str_replace(
768
                [
769
                    '###CURRENT_PID###',
770
                    '###THIS_UID###',
771
                    '###SITEROOT###',
772
                    '###PAGE_TSCONFIG_ID###',
773
                    '###PAGE_TSCONFIG_IDLIST###',
774
                    '\'###PAGE_TSCONFIG_STR###\'',
775
                    '###PAGE_TSCONFIG_STR###'
776
                ],
777
                [
778
                    (int)$effectivePid,
779
                    (int)$result['databaseRow']['uid'],
780
                    $siteRootUid,
781
                    $pageTsConfigId,
782
                    $pageTsConfigIdList,
783
                    $pageTsConfigString,
784
                    $pageTsConfigString
785
                ],
786
                $foreignTableClause
787
            );
788
        }
789
790
        // Split the clause into an array with keys WHERE, GROUPBY, ORDERBY, LIMIT
791
        // Prepend a space to make sure "[[:space:]]+" will find a space there for the first element.
792
        $foreignTableClause = ' ' . $foreignTableClause;
793
        $foreignTableClauseArray = [
794
            'WHERE' => '',
795
            'GROUPBY' => '',
796
            'ORDERBY' => '',
797
            'LIMIT' => '',
798
        ];
799
        // Find LIMIT
800
        $reg = [];
801
        if (preg_match('/^(.*)[[:space:]]+LIMIT[[:space:]]+([[:alnum:][:space:],._]+)$/is', $foreignTableClause, $reg)) {
802
            $foreignTableClauseArray['LIMIT'] = GeneralUtility::intExplode(',', trim($reg[2]), true);
803
            $foreignTableClause = $reg[1];
804
        }
805
        // Find ORDER BY
806
        $reg = [];
807
        if (preg_match('/^(.*)[[:space:]]+ORDER[[:space:]]+BY[[:space:]]+([[:alnum:][:space:],._()"]+)$/is', $foreignTableClause, $reg)) {
808
            $foreignTableClauseArray['ORDERBY'] = QueryHelper::parseOrderBy(trim($reg[2]));
809
            $foreignTableClause = $reg[1];
810
        }
811
        // Find GROUP BY
812
        $reg = [];
813
        if (preg_match('/^(.*)[[:space:]]+GROUP[[:space:]]+BY[[:space:]]+([[:alnum:][:space:],._()"]+)$/is', $foreignTableClause, $reg)) {
814
            $foreignTableClauseArray['GROUPBY'] = QueryHelper::parseGroupBy(trim($reg[2]));
815
            $foreignTableClause = $reg[1];
816
        }
817
        // Rest is assumed to be "WHERE" clause
818
        $foreignTableClauseArray['WHERE'] = QueryHelper::stripLogicalOperatorPrefix($foreignTableClause);
819
820
        return $foreignTableClauseArray;
821
    }
822
823
    /**
824
     * Convert the current database values into an array
825
     *
826
     * @param array $row database row
827
     * @param string $fieldName fieldname to process
828
     * @return array
829
     */
830
    protected function processDatabaseFieldValue(array $row, $fieldName)
831
    {
832
        $currentDatabaseValues = array_key_exists($fieldName, $row)
833
            ? $row[$fieldName]
834
            : '';
835
        if (!is_array($currentDatabaseValues)) {
836
            $currentDatabaseValues = GeneralUtility::trimExplode(',', $currentDatabaseValues, true);
837
        }
838
        return $currentDatabaseValues;
839
    }
840
841
    /**
842
     * Validate and sanitize database row values of the select field with the given name.
843
     * Creates an array out of databaseRow[selectField] values.
844
     *
845
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
846
     *
847
     * @param array $result The current result array.
848
     * @param string $fieldName Name of the current select field.
849
     * @param array $staticValues Array with statically defined items, item value is used as array key.
850
     * @return array
851
     */
852
    protected function processSelectFieldValue(array $result, $fieldName, array $staticValues)
853
    {
854
        $fieldConfig = $result['processedTca']['columns'][$fieldName];
855
856
        $currentDatabaseValueArray = array_key_exists($fieldName, $result['databaseRow']) ? $result['databaseRow'][$fieldName] : [];
857
        $newDatabaseValueArray = [];
858
859
        // Add all values that were defined by static methods and do not come from the relation
860
        // e.g. TCA, TSconfig, itemProcFunc etc.
861
        foreach ($currentDatabaseValueArray as $value) {
862
            if (isset($staticValues[$value])) {
863
                $newDatabaseValueArray[] = $value;
864
            }
865
        }
866
867
        if (isset($fieldConfig['config']['foreign_table']) && !empty($fieldConfig['config']['foreign_table'])) {
868
            /** @var RelationHandler $relationHandler */
869
            $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
870
            $relationHandler->registerNonTableValues = !empty($fieldConfig['config']['allowNonIdValues']);
871
            if (!empty($fieldConfig['config']['MM']) && $result['command'] !== 'new') {
872
                // MM relation
873
                $relationHandler->start(
874
                    implode(',', $currentDatabaseValueArray),
875
                    $fieldConfig['config']['foreign_table'],
876
                    $fieldConfig['config']['MM'],
877
                    $result['databaseRow']['uid'],
878
                    $result['tableName'],
879
                    $fieldConfig['config']
880
                );
881
                $newDatabaseValueArray = array_merge($newDatabaseValueArray, $relationHandler->getValueArray());
882
            } else {
883
                // Non MM relation
884
                // If not dealing with MM relations, use default live uid, not versioned uid for record relations
885
                $relationHandler->start(
886
                    implode(',', $currentDatabaseValueArray),
887
                    $fieldConfig['config']['foreign_table'],
888
                    '',
889
                    $this->getLiveUid($result),
890
                    $result['tableName'],
891
                    $fieldConfig['config']
892
                );
893
                $databaseIds = array_merge($newDatabaseValueArray, $relationHandler->getValueArray());
894
                // remove all items from the current DB values if not available as relation or static value anymore
895
                $newDatabaseValueArray = array_values(array_intersect($currentDatabaseValueArray, $databaseIds));
896
            }
897
        }
898
899
        if ($fieldConfig['config']['multiple'] ?? false) {
900
            return $newDatabaseValueArray;
901
        }
902
        return array_unique($newDatabaseValueArray);
903
    }
904
905
    /**
906
     * Translate the item labels
907
     *
908
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
909
     *
910
     * @param array $result Result array
911
     * @param array $itemArray Items
912
     * @param string $table
913
     * @param string $fieldName
914
     * @return array
915
     */
916
    public function translateLabels(array $result, array $itemArray, $table, $fieldName)
917
    {
918
        $languageService = $this->getLanguageService();
919
920
        foreach ($itemArray as $key => $item) {
921
            if (isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]])
922
                && !empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]])
923
            ) {
924
                $label = $languageService->sL($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]]);
925
            } else {
926
                $label = $languageService->sL(trim($item[0]));
927
            }
928
            $value = strlen((string)$item[1]) > 0 ? $item[1] : '';
929
            $icon = !empty($item[2]) ? $item[2] : null;
930
            $groupId = $item[3] ?? null;
931
            $helpText = null;
932
            if (!empty($item[4])) {
933
                if (\is_string($item[4])) {
934
                    $helpText = $languageService->sL($item[4]);
935
                } else {
936
                    $helpText = $item[4];
937
                }
938
            }
939
            $itemArray[$key] = [
940
                $label,
941
                $value,
942
                $icon,
943
                $groupId,
944
                $helpText
945
            ];
946
        }
947
948
        return $itemArray;
949
    }
950
951
    /**
952
     * Add alternative icon using "altIcons" TSconfig
953
     *
954
     * @param array $result
955
     * @param array $items
956
     * @param string $table
957
     * @param string $fieldName
958
     *
959
     * @return array
960
     */
961
    public function addIconFromAltIcons(array $result, array $items, string $table, string $fieldName): array
962
    {
963
        foreach ($items as &$item) {
964
            if (isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altIcons.'][$item[1]])
965
                && !empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altIcons.'][$item[1]])
966
            ) {
967
                $item[2] = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altIcons.'][$item[1]];
968
            }
969
        }
970
971
        return $items;
972
    }
973
974
    /**
975
     * Sanitize incoming item array
976
     *
977
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
978
     *
979
     * @param mixed $itemArray
980
     * @param string $tableName
981
     * @param string $fieldName
982
     * @throws \UnexpectedValueException
983
     * @return array
984
     */
985
    public function sanitizeItemArray($itemArray, $tableName, $fieldName)
986
    {
987
        if (!is_array($itemArray)) {
988
            $itemArray = [];
989
        }
990
        foreach ($itemArray as $item) {
991
            if (!is_array($item)) {
992
                throw new \UnexpectedValueException(
993
                    'An item in field ' . $fieldName . ' of table ' . $tableName . ' is not an array as expected',
994
                    1439288036
995
                );
996
            }
997
        }
998
999
        return $itemArray;
1000
    }
1001
1002
    /**
1003
     * Gets the record uid of the live default record. If already
1004
     * pointing to the live record, the submitted record uid is returned.
1005
     *
1006
     * @param array $result Result array
1007
     * @return int
1008
     * @throws \UnexpectedValueException
1009
     */
1010
    protected function getLiveUid(array $result)
1011
    {
1012
        $table = $result['tableName'];
1013
        $row = $result['databaseRow'];
1014
        $uid = $row['uid'];
1015
        if (BackendUtility::isTableWorkspaceEnabled($table) && (int)$row['t3ver_oid'] > 0) {
1016
            $uid = $row['t3ver_oid'];
1017
        }
1018
        return $uid;
1019
    }
1020
1021
    /**
1022
     * @return LanguageService
1023
     */
1024
    protected function getLanguageService()
1025
    {
1026
        return $GLOBALS['LANG'];
1027
    }
1028
1029
    /**
1030
     * @return BackendUserAuthentication
1031
     */
1032
    protected function getBackendUser()
1033
    {
1034
        return $GLOBALS['BE_USER'];
1035
    }
1036
}
1037