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
16namespace TYPO3\CMS\Backend\Form\FormDataProvider;
17
18use Doctrine\DBAL\DBALException;
19use TYPO3\CMS\Backend\Module\ModuleLoader;
20use TYPO3\CMS\Backend\Utility\BackendUtility;
21use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
22use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidIdentifierException;
23use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
24use TYPO3\CMS\Core\Database\ConnectionPool;
25use TYPO3\CMS\Core\Database\Query\QueryBuilder;
26use TYPO3\CMS\Core\Database\Query\QueryHelper;
27use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
28use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
29use TYPO3\CMS\Core\Database\RelationHandler;
30use TYPO3\CMS\Core\Imaging\IconFactory;
31use TYPO3\CMS\Core\Imaging\IconRegistry;
32use TYPO3\CMS\Core\Localization\LanguageService;
33use TYPO3\CMS\Core\Messaging\FlashMessage;
34use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
35use TYPO3\CMS\Core\Messaging\FlashMessageService;
36use TYPO3\CMS\Core\Resource\FileRepository;
37use TYPO3\CMS\Core\Resource\ResourceStorage;
38use TYPO3\CMS\Core\Site\SiteFinder;
39use TYPO3\CMS\Core\Type\Bitmask\Permission;
40use TYPO3\CMS\Core\Utility\ArrayUtility;
41use TYPO3\CMS\Core\Utility\GeneralUtility;
42use TYPO3\CMS\Core\Utility\MathUtility;
43use TYPO3\CMS\Core\Versioning\VersionState;
44
45/**
46 * Contains methods used by Data providers that handle elements
47 * with single items like select, radio and some more.
48 */
49abstract class AbstractItemProvider
50{
51    /**
52     * Resolve "itemProcFunc" of elements.
53     *
54     * @param array $result Main result array
55     * @param string $fieldName Field name to handle item list for
56     * @param array $items Existing items array
57     * @return array New list of item elements
58     */
59    protected function resolveItemProcessorFunction(array $result, $fieldName, array $items)
60    {
61        $table = $result['tableName'];
62        $config = $result['processedTca']['columns'][$fieldName]['config'];
63
64        $pageTsProcessorParameters = null;
65        if (!empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['itemsProcFunc.'])) {
66            $pageTsProcessorParameters = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['itemsProcFunc.'];
67        }
68        $processorParameters = [
69            // Function manipulates $items directly and return nothing
70            'items' => &$items,
71            'config' => $config,
72            'TSconfig' => $pageTsProcessorParameters,
73            'table' => $table,
74            'row' => $result['databaseRow'],
75            'field' => $fieldName,
76            // IMPORTANT: Below fields are only available in FormEngine context.
77            // They are not used by the DataHandler when processing itemsProcFunc
78            // for checking if a submitted value is valid. This means, in case
79            // an item is added based on one of these fields, it won't be persisted
80            // by the DataHandler. This currently(!) only concerns columns of type "check"
81            // and type "radio", see checkValueForCheck() and checkValueForRadio().
82            // Therefore, no limitations when using those fields with other types
83            // like "select", but this may change in the future.
84            'inlineParentUid' => $result['inlineParentUid'],
85            'inlineParentTableName' => $result['inlineParentTableName'],
86            'inlineParentFieldName' => $result['inlineParentFieldName'],
87            'inlineParentConfig' => $result['inlineParentConfig'],
88            'inlineTopMostParentUid' => $result['inlineTopMostParentUid'],
89            'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'],
90            'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'],
91        ];
92        if (!empty($result['flexParentDatabaseRow'])) {
93            $processorParameters['flexParentDatabaseRow'] = $result['flexParentDatabaseRow'];
94        }
95
96        try {
97            GeneralUtility::callUserFunction($config['itemsProcFunc'], $processorParameters, $this);
98        } catch (\Exception $exception) {
99            // The itemsProcFunc method may throw an exception, create a flash message if so
100            $languageService = $this->getLanguageService();
101            $fieldLabel = $fieldName;
102            if (!empty($result['processedTca']['columns'][$fieldName]['label'])) {
103                $fieldLabel = $languageService->sL($result['processedTca']['columns'][$fieldName]['label']);
104            }
105            $message = sprintf(
106                $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.items_proc_func_error'),
107                $fieldLabel,
108                $exception->getMessage()
109            );
110            /** @var FlashMessage $flashMessage */
111            $flashMessage = GeneralUtility::makeInstance(
112                FlashMessage::class,
113                $message,
114                '',
115                FlashMessage::ERROR,
116                true
117            );
118            /** @var \TYPO3\CMS\Core\Messaging\FlashMessageService $flashMessageService */
119            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
120            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
121            $defaultFlashMessageQueue->enqueue($flashMessage);
122        }
123
124        return $items;
125    }
126
127    /**
128     * PageTsConfig addItems:
129     *
130     * TCEFORMS.aTable.aField[.types][.aType].addItems.aValue = aLabel,
131     * with type specific options merged by pageTsConfig already
132     *
133     * Used by TcaSelectItems and TcaSelectTreeItems data providers
134     *
135     * @param array $result result array
136     * @param string $fieldName Current handle field name
137     * @param array $items Incoming items
138     * @return array Modified item array
139     */
140    protected function addItemsFromPageTsConfig(array $result, $fieldName, array $items)
141    {
142        $table = $result['tableName'];
143        if (!empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['addItems.'])
144            && is_array($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['addItems.'])
145        ) {
146            $addItemsArray = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['addItems.'];
147            foreach ($addItemsArray as $value => $label) {
148                // If the value ends with a dot, it is a subelement like "34.icon = mylabel.png", skip it
149                if (substr($value, -1) === '.') {
150                    continue;
151                }
152                // Check if value "34 = mylabel" also has a "34.icon = myImage.png"
153                $iconIdentifier = null;
154                if (isset($addItemsArray[$value . '.'])
155                    && is_array($addItemsArray[$value . '.'])
156                    && !empty($addItemsArray[$value . '.']['icon'])
157                ) {
158                    $iconIdentifier = $addItemsArray[$value . '.']['icon'];
159                }
160                $items[] = [$label, $value, $iconIdentifier];
161            }
162        }
163        return $items;
164    }
165
166    /**
167     * TCA config "special" evaluation. Add them to $items
168     *
169     * Used by TcaSelectItems and TcaSelectTreeItems data providers
170     *
171     * @param array $result Result array
172     * @param string $fieldName Current handle field name
173     * @param array $items Incoming items
174     * @return array Modified item array
175     * @throws \UnexpectedValueException
176     */
177    protected function addItemsFromSpecial(array $result, $fieldName, array $items)
178    {
179        // Guard
180        if (empty($result['processedTca']['columns'][$fieldName]['config']['special'])
181            || !is_string($result['processedTca']['columns'][$fieldName]['config']['special'])
182        ) {
183            return $items;
184        }
185
186        $languageService = $this->getLanguageService();
187        $iconRegistry = GeneralUtility::makeInstance(IconRegistry::class);
188        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
189
190        $special = $result['processedTca']['columns'][$fieldName]['config']['special'];
191        switch (true) {
192            case $special === 'tables':
193                foreach ($GLOBALS['TCA'] as $currentTable => $_) {
194                    if (!empty($GLOBALS['TCA'][$currentTable]['ctrl']['adminOnly'])) {
195                        // Hide "admin only" tables
196                        continue;
197                    }
198                    $label = !empty($GLOBALS['TCA'][$currentTable]['ctrl']['title']) ? $GLOBALS['TCA'][$currentTable]['ctrl']['title'] : '';
199                    $icon = $iconFactory->mapRecordTypeToIconIdentifier($currentTable, []);
200                    $languageService->loadSingleTableDescription($currentTable);
201                    $helpText = (string)($GLOBALS['TCA_DESCR'][$currentTable]['columns']['']['description'] ?? '');
202                    $items[] = [$label, $currentTable, $icon, null, $helpText];
203                }
204                break;
205            case $special === 'pagetypes':
206                if (isset($GLOBALS['TCA']['pages']['columns']['doktype']['config']['items'])
207                    && is_array($GLOBALS['TCA']['pages']['columns']['doktype']['config']['items'])
208                ) {
209                    $specialItems = $GLOBALS['TCA']['pages']['columns']['doktype']['config']['items'];
210                    foreach ($specialItems as $specialItem) {
211                        if (!is_array($specialItem) || $specialItem[1] === '--div--') {
212                            // Skip non arrays and divider items
213                            continue;
214                        }
215                        $label = $specialItem[0];
216                        $value = $specialItem[1];
217                        $icon = $iconFactory->mapRecordTypeToIconIdentifier('pages', ['doktype' => $specialItem[1]]);
218                        $items[] = [$label, $value, $icon];
219                    }
220                }
221                break;
222            case $special === 'exclude':
223                $excludeArrays = $this->getExcludeFields();
224                foreach ($excludeArrays as $excludeArray) {
225                    // If the field comes from a FlexForm, the syntax is more complex
226                    if ($excludeArray['origin'] === 'flexForm') {
227                        // The field comes from a plugins FlexForm
228                        // Add header if not yet set for plugin section
229                        if (!isset($items[$excludeArray['sectionHeader']])) {
230                            // there is no icon handling for plugins - we take the icon from the table
231                            $icon = $iconFactory->mapRecordTypeToIconIdentifier($excludeArray['table'], []);
232                            $items[$excludeArray['sectionHeader']] = [
233                                $excludeArray['sectionHeader'],
234                                '--div--',
235                                $icon
236                            ];
237                        }
238                    } else {
239                        // Add header if not yet set for table
240                        if (!isset($items[$excludeArray['table']])) {
241                            $icon = $iconFactory->mapRecordTypeToIconIdentifier($excludeArray['table'], []);
242                            $items[$excludeArray['table']] = [
243                                $GLOBALS['TCA'][$excludeArray['table']]['ctrl']['title'],
244                                '--div--',
245                                $icon
246                            ];
247                        }
248                    }
249                    // Add help text
250                    $languageService->loadSingleTableDescription($excludeArray['table']);
251                    $helpText = (string)($GLOBALS['TCA_DESCR'][$excludeArray['table']]['columns'][$excludeArray['fullField']]['description'] ?? '');
252                    // Item configuration:
253                    $items[] = [
254                        rtrim($excludeArray['origin'] === 'flexForm' ? $excludeArray['fieldLabel'] : $languageService->sL($GLOBALS['TCA'][$excludeArray['table']]['columns'][$excludeArray['fieldName']]['label']), ':') . ' (' . $excludeArray['fieldName'] . ')',
255                        $excludeArray['table'] . ':' . $excludeArray['fullField'],
256                        'empty-empty',
257                        null,
258                        $helpText
259                    ];
260                }
261                break;
262            case $special === 'explicitValues':
263                $theTypes = $this->getExplicitAuthFieldValues();
264                $icons = [
265                    'ALLOW' => 'status-status-permission-granted',
266                    'DENY' => 'status-status-permission-denied'
267                ];
268                // Traverse types:
269                foreach ($theTypes as $tableFieldKey => $theTypeArrays) {
270                    if (!empty($theTypeArrays['items'])) {
271                        // Add header:
272                        $items[] = [
273                            $theTypeArrays['tableFieldLabel'],
274                            '--div--',
275                        ];
276                        // Traverse options for this field:
277                        foreach ($theTypeArrays['items'] as $itemValue => $itemContent) {
278                            // Add item to be selected:
279                            $items[] = [
280                                '[' . $itemContent[2] . '] ' . $itemContent[1],
281                                $tableFieldKey . ':' . preg_replace('/[:|,]/', '', $itemValue) . ':' . $itemContent[0],
282                                $icons[$itemContent[0]]
283                            ];
284                        }
285                    }
286                }
287                break;
288            case $special === 'languages':
289                $allLanguages = [];
290                if (($result['effectivePid'] ?? 0) === 0) {
291                    // This provides a list of all languages available for ALL sites
292                    // Due to the nature of the "sys_language_uid" field having no meaning currently,
293                    // We preserve the language ID and make a list of all languages
294                    $sites = $this->getAllSites();
295                    foreach ($sites as $site) {
296                        foreach ($site->getAllLanguages() as $language) {
297                            $languageId = $language->getLanguageId();
298                            if (isset($allLanguages[$languageId])) {
299                                // Language already provided by another site, just add the label separately
300                                $allLanguages[$languageId][0] .= ', ' . $language->getTitle() . ' [Site: ' . $site->getIdentifier() . ']';
301                            } else {
302                                $allLanguages[$languageId] = [
303                                    0 => $language->getTitle() . ' [Site: ' . $site->getIdentifier() . ']',
304                                    1 => $languageId,
305                                    2 => $language->getFlagIdentifier()
306                                ];
307                            }
308                        }
309                    }
310                    ksort($allLanguages);
311                }
312                if (!empty($allLanguages)) {
313                    foreach ($allLanguages as $item) {
314                        $items[] = $item;
315                    }
316                } else {
317                    // Happens for non-pid=0 records (e.g. "tt_content"), or when no site was configured
318                    foreach ($result['systemLanguageRows'] as $language) {
319                        if ($language['uid'] !== -1) {
320                            $items[] = [
321                                0 => $language['title'],
322                                1 => $language['uid'],
323                                2 => $language['flagIconIdentifier']
324                            ];
325                        }
326                    }
327                }
328
329                break;
330            case $special === 'custom':
331                $customOptions = $GLOBALS['TYPO3_CONF_VARS']['BE']['customPermOptions'];
332                if (is_array($customOptions)) {
333                    foreach ($customOptions as $coKey => $coValue) {
334                        if (is_array($coValue['items'])) {
335                            // Add header:
336                            $items[] = [
337                                $languageService->sL($coValue['header']),
338                                '--div--'
339                            ];
340                            // Traverse items:
341                            foreach ($coValue['items'] as $itemKey => $itemCfg) {
342                                $icon = 'empty-empty';
343                                $helpText = '';
344                                if (!empty($itemCfg[1])) {
345                                    if ($iconRegistry->isRegistered($itemCfg[1])) {
346                                        // Use icon identifier when registered
347                                        $icon = $itemCfg[1];
348                                    }
349                                }
350                                if (!empty($itemCfg[2])) {
351                                    $helpText = $languageService->sL($itemCfg[2]);
352                                }
353                                $items[] = [
354                                    $languageService->sL($itemCfg[0]),
355                                    $coKey . ':' . preg_replace('/[:|,]/', '', $itemKey),
356                                    $icon,
357                                    null,
358                                    $helpText
359                                ];
360                            }
361                        }
362                    }
363                }
364                break;
365            case $special === 'modListGroup' || $special === 'modListUser':
366                /** @var ModuleLoader $loadModules */
367                $loadModules = GeneralUtility::makeInstance(ModuleLoader::class);
368                $loadModules->load($GLOBALS['TBE_MODULES']);
369                $modList = $special === 'modListUser' ? $loadModules->modListUser : $loadModules->modListGroup;
370                if (is_array($modList)) {
371                    foreach ($modList as $theMod) {
372                        $moduleLabels = $loadModules->getLabelsForModule($theMod);
373                        $moduleArray = GeneralUtility::trimExplode('_', $theMod, true);
374                        $mainModule = $moduleArray[0] ?? '';
375                        $subModule = $moduleArray[1] ?? '';
376                        // Icon:
377                        if (!empty($subModule)) {
378                            $icon = $loadModules->modules[$mainModule]['sub'][$subModule]['iconIdentifier'];
379                        } else {
380                            $icon = $loadModules->modules[$theMod]['iconIdentifier'];
381                        }
382                        // Add help text
383                        $helpText = [
384                            'title' => $languageService->sL($moduleLabels['shortdescription']),
385                            'description' => $languageService->sL($moduleLabels['description'])
386                        ];
387
388                        $label = '';
389                        // Add label for main module if this is a submodule
390                        if (!empty($subModule)) {
391                            $mainModuleLabels = $loadModules->getLabelsForModule($mainModule);
392                            $label .= $languageService->sL($mainModuleLabels['title']) . '>';
393                        }
394                        // Add modules own label now
395                        $label .= $languageService->sL($moduleLabels['title']);
396
397                        // Item configuration
398                        $items[] = [$label, $theMod, $icon, null, $helpText];
399                    }
400                }
401                break;
402            default:
403                throw new \UnexpectedValueException(
404                    'Unknown special value ' . $special . ' for field ' . $fieldName . ' of table ' . $result['tableName'],
405                    1439298496
406                );
407        }
408        return $items;
409    }
410
411    /**
412     * TCA config "fileFolder" evaluation. Add them to $items
413     *
414     * Used by TcaSelectItems and TcaSelectTreeItems data providers
415     *
416     * @param array $result Result array
417     * @param string $fieldName Current handle field name
418     * @param array $items Incoming items
419     * @return array Modified item array
420     * @throws \RuntimeException
421     */
422    protected function addItemsFromFolder(array $result, $fieldName, array $items)
423    {
424        if (empty($result['processedTca']['columns'][$fieldName]['config']['fileFolder'])
425            || !is_string($result['processedTca']['columns'][$fieldName]['config']['fileFolder'])
426        ) {
427            return $items;
428        }
429
430        $fileFolderRaw = $result['processedTca']['columns'][$fieldName]['config']['fileFolder'];
431        $fileFolder = GeneralUtility::getFileAbsFileName($fileFolderRaw);
432        if ($fileFolder === '') {
433            throw new \RuntimeException(
434                'Invalid folder given for item processing: ' . $fileFolderRaw . ' for table ' . $result['tableName'] . ', field ' . $fieldName,
435                1479399227
436            );
437        }
438        $fileFolder = rtrim($fileFolder, '/') . '/';
439
440        if (@is_dir($fileFolder)) {
441            $fileExtensionList = '';
442            if (!empty($result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList'])
443                && is_string($result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList'])
444            ) {
445                $fileExtensionList = $result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList'];
446            }
447            $recursionLevels = isset($result['processedTca']['columns'][$fieldName]['config']['fileFolder_recursions'])
448                ? MathUtility::forceIntegerInRange($result['processedTca']['columns'][$fieldName]['config']['fileFolder_recursions'], 0, 99)
449                : 99;
450            $fileArray = GeneralUtility::getAllFilesAndFoldersInPath([], $fileFolder, $fileExtensionList, false, $recursionLevels);
451            $fileArray = GeneralUtility::removePrefixPathFromList($fileArray, $fileFolder);
452            foreach ($fileArray as $fileReference) {
453                $fileInformation = pathinfo($fileReference);
454                $icon = GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], strtolower($fileInformation['extension']))
455                    ? $fileFolder . $fileReference
456                    : '';
457                $items[] = [
458                    $fileReference,
459                    $fileReference,
460                    $icon
461                ];
462            }
463        }
464
465        return $items;
466    }
467
468    /**
469     * TCA config "foreign_table" evaluation. Add them to $items
470     *
471     * Used by TcaSelectItems and TcaSelectTreeItems data providers
472     *
473     * @param array $result Result array
474     * @param string $fieldName Current handle field name
475     * @param array $items Incoming items
476     * @return array Modified item array
477     * @throws \UnexpectedValueException
478     */
479    protected function addItemsFromForeignTable(array $result, $fieldName, array $items)
480    {
481        $databaseError = null;
482        $queryResult = null;
483        // Guard
484        if (empty($result['processedTca']['columns'][$fieldName]['config']['foreign_table'])
485            || !is_string($result['processedTca']['columns'][$fieldName]['config']['foreign_table'])
486        ) {
487            return $items;
488        }
489
490        $languageService = $this->getLanguageService();
491
492        $foreignTable = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'];
493
494        if (!isset($GLOBALS['TCA'][$foreignTable]) || !is_array($GLOBALS['TCA'][$foreignTable])) {
495            throw new \UnexpectedValueException(
496                'Field ' . $fieldName . ' of table ' . $result['tableName'] . ' reference to foreign table '
497                . $foreignTable . ', but this table is not defined in TCA',
498                1439569743
499            );
500        }
501
502        $queryBuilder = $this->buildForeignTableQueryBuilder($result, $fieldName);
503        try {
504            $queryResult = $queryBuilder->execute();
505        } catch (DBALException $e) {
506            $databaseError = $e->getPrevious()->getMessage();
507        }
508
509        // Early return on error with flash message
510        if (!empty($databaseError)) {
511            $msg = $databaseError . '. ';
512            $msg .= $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.database_schema_mismatch');
513            $msgTitle = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.database_schema_mismatch_title');
514            /** @var FlashMessage $flashMessage */
515            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, $msgTitle, FlashMessage::ERROR, true);
516            /** @var FlashMessageService $flashMessageService */
517            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
518            /** @var FlashMessageQueue $defaultFlashMessageQueue */
519            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
520            $defaultFlashMessageQueue->enqueue($flashMessage);
521            return $items;
522        }
523
524        $labelPrefix = '';
525        if (!empty($result['processedTca']['columns'][$fieldName]['config']['foreign_table_prefix'])) {
526            $labelPrefix = $result['processedTca']['columns'][$fieldName]['config']['foreign_table_prefix'];
527            $labelPrefix = $languageService->sL($labelPrefix);
528        }
529
530        $fileRepository = GeneralUtility::makeInstance(FileRepository::class);
531        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
532
533        while ($foreignRow = $queryResult->fetch()) {
534            BackendUtility::workspaceOL($foreignTable, $foreignRow);
535            // Only proceed in case the row was not unset and we don't deal with a delete placeholder
536            if (is_array($foreignRow)
537                && !VersionState::cast($foreignRow['t3ver_state'] ?? 0)->equals(VersionState::DELETE_PLACEHOLDER)
538            ) {
539                // If the foreign table sets selicon_field, this field can contain an image
540                // that represents this specific row.
541                $iconFieldName = '';
542                $isReferenceField = false;
543                if (!empty($GLOBALS['TCA'][$foreignTable]['ctrl']['selicon_field'])) {
544                    $iconFieldName = $GLOBALS['TCA'][$foreignTable]['ctrl']['selicon_field'];
545                    if (isset($GLOBALS['TCA'][$foreignTable]['columns'][$iconFieldName]['config']['type'])
546                        && $GLOBALS['TCA'][$foreignTable]['columns'][$iconFieldName]['config']['type'] === 'inline'
547                        && $GLOBALS['TCA'][$foreignTable]['columns'][$iconFieldName]['config']['foreign_table'] === 'sys_file_reference'
548                    ) {
549                        $isReferenceField = true;
550                    }
551                }
552                $icon = '';
553                if ($isReferenceField) {
554                    $references = $fileRepository->findByRelation($foreignTable, $iconFieldName, $foreignRow['uid']);
555                    if (is_array($references) && !empty($references)) {
556                        $icon = reset($references);
557                        $icon = $icon->getPublicUrl();
558                    }
559                } else {
560                    // Else, determine icon based on record type, or a generic fallback
561                    $icon = $iconFactory->mapRecordTypeToIconIdentifier($foreignTable, $foreignRow);
562                }
563                // Add the item
564                $items[] = [
565                    $labelPrefix . BackendUtility::getRecordTitle($foreignTable, $foreignRow),
566                    $foreignRow['uid'],
567                    $icon
568                ];
569            }
570        }
571
572        return $items;
573    }
574
575    /**
576     * Remove items using "keepItems" pageTsConfig
577     *
578     * Used by TcaSelectItems and TcaSelectTreeItems data providers
579     *
580     * @param array $result Result array
581     * @param string $fieldName Current handle field name
582     * @param array $items Incoming items
583     * @return array Modified item array
584     */
585    protected function removeItemsByKeepItemsPageTsConfig(array $result, $fieldName, array $items)
586    {
587        $table = $result['tableName'];
588        if (!isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'])
589            || !is_string($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'])
590        ) {
591            return $items;
592        }
593
594        // If keepItems is set but is an empty list all current items get removed
595        if ($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'] === '') {
596            return [];
597        }
598
599        return ArrayUtility::keepItemsInArray(
600            $items,
601            $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'],
602            function ($value) {
603                return $value[1];
604            }
605        );
606    }
607
608    /**
609     * Remove items using "removeItems" pageTsConfig
610     *
611     * Used by TcaSelectItems and TcaSelectTreeItems data providers
612     *
613     * @param array $result Result array
614     * @param string $fieldName Current handle field name
615     * @param array $items Incoming items
616     * @return array Modified item array
617     */
618    protected function removeItemsByRemoveItemsPageTsConfig(array $result, $fieldName, array $items)
619    {
620        $table = $result['tableName'];
621        if (!isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'])
622            || !is_string($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'])
623            || $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'] === ''
624        ) {
625            return $items;
626        }
627
628        $removeItems = array_flip(GeneralUtility::trimExplode(
629            ',',
630            $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'],
631            true
632        ));
633        foreach ($items as $key => $itemValues) {
634            if (isset($removeItems[$itemValues[1]])) {
635                unset($items[$key]);
636            }
637        }
638
639        return $items;
640    }
641
642    /**
643     * Remove items user restriction on language field
644     *
645     * Used by TcaSelectItems and TcaSelectTreeItems data providers
646     *
647     * @param array $result Result array
648     * @param string $fieldName Current handle field name
649     * @param array $items Incoming items
650     * @return array Modified item array
651     */
652    protected function removeItemsByUserLanguageFieldRestriction(array $result, $fieldName, array $items)
653    {
654        // Guard clause returns if not a language field is handled
655        if (empty($result['processedTca']['ctrl']['languageField'])
656            || $result['processedTca']['ctrl']['languageField'] !== $fieldName
657        ) {
658            return $items;
659        }
660
661        $backendUser = $this->getBackendUser();
662        foreach ($items as $key => $itemValues) {
663            if (!$backendUser->checkLanguageAccess($itemValues[1])) {
664                unset($items[$key]);
665            }
666        }
667
668        return $items;
669    }
670
671    /**
672     * Remove items by user restriction on authMode items
673     *
674     * Used by TcaSelectItems and TcaSelectTreeItems data providers
675     *
676     * @param array $result Result array
677     * @param string $fieldName Current handle field name
678     * @param array $items Incoming items
679     * @return array Modified item array
680     */
681    protected function removeItemsByUserAuthMode(array $result, $fieldName, array $items)
682    {
683        // Guard clause returns early if no authMode field is configured
684        if (!isset($result['processedTca']['columns'][$fieldName]['config']['authMode'])
685            || !is_string($result['processedTca']['columns'][$fieldName]['config']['authMode'])
686        ) {
687            return $items;
688        }
689
690        $backendUser = $this->getBackendUser();
691        $authMode = $result['processedTca']['columns'][$fieldName]['config']['authMode'];
692        foreach ($items as $key => $itemValues) {
693            // @todo: checkAuthMode() uses $GLOBAL access for "individual" authMode - get rid of this
694            if (!$backendUser->checkAuthMode($result['tableName'], $fieldName, $itemValues[1], $authMode)) {
695                unset($items[$key]);
696            }
697        }
698
699        return $items;
700    }
701
702    /**
703     * Remove items if doktype is handled for non admin users
704     *
705     * Used by TcaSelectItems and TcaSelectTreeItems data providers
706     *
707     * @param array $result Result array
708     * @param string $fieldName Current handle field name
709     * @param array $items Incoming items
710     * @return array Modified item array
711     */
712    protected function removeItemsByDoktypeUserRestriction(array $result, $fieldName, array $items)
713    {
714        $table = $result['tableName'];
715        $backendUser = $this->getBackendUser();
716        // Guard clause returns if not correct table and field or if user is admin
717        if ($table !== 'pages' || $fieldName !== 'doktype' || $backendUser->isAdmin()
718        ) {
719            return $items;
720        }
721
722        $allowedPageTypes = $backendUser->groupData['pagetypes_select'];
723        foreach ($items as $key => $itemValues) {
724            if (!GeneralUtility::inList($allowedPageTypes, $itemValues[1])) {
725                unset($items[$key]);
726            }
727        }
728
729        return $items;
730    }
731
732    /**
733     * Remove items if sys_file_storage is not allowed for non-admin users.
734     *
735     * Used by TcaSelectItems data providers
736     *
737     * @param array $result Result array
738     * @param string $fieldName Current handle field name
739     * @param array $items Incoming items
740     * @return array Modified item array
741     */
742    protected function removeItemsByUserStorageRestriction(array $result, $fieldName, array $items)
743    {
744        $referencedTableName = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'] ?? null;
745        if ($referencedTableName !== 'sys_file_storage') {
746            return $items;
747        }
748
749        $allowedStorageIds = array_map(
750            function (ResourceStorage $storage) {
751                return $storage->getUid();
752            },
753            $this->getBackendUser()->getFileStorages()
754        );
755
756        return array_filter(
757            $items,
758            function (array $item) use ($allowedStorageIds) {
759                $itemValue = $item[1] ?? null;
760                return empty($itemValue)
761                    || in_array((int)$itemValue, $allowedStorageIds, true);
762            }
763        );
764    }
765
766    /**
767     * Returns an array with the exclude fields as defined in TCA and FlexForms
768     * Used for listing the exclude fields in be_groups forms.
769     *
770     * @return array Array of arrays with excludeFields (fieldName, table:fieldName) from TCA
771     *               and FlexForms (fieldName, table:extKey;sheetName;fieldName)
772     */
773    protected function getExcludeFields()
774    {
775        $languageService = $this->getLanguageService();
776        $finalExcludeArray = [];
777
778        // Fetch translations for table names
779        $tableToTranslation = [];
780        // All TCA keys
781        foreach ($GLOBALS['TCA'] as $table => $conf) {
782            $tableToTranslation[$table] = $languageService->sL($conf['ctrl']['title']);
783        }
784        /** @var array<string, string> $tableToTranslation */
785        // Sort by translations
786        asort($tableToTranslation);
787        foreach ($tableToTranslation as $table => $translatedTable) {
788            $excludeArrayTable = [];
789
790            // All field names configured and not restricted to admins
791            if (is_array($GLOBALS['TCA'][$table]['columns'])
792                && empty($GLOBALS['TCA'][$table]['ctrl']['adminOnly'])
793                && (empty($GLOBALS['TCA'][$table]['ctrl']['rootLevel']) || !empty($GLOBALS['TCA'][$table]['ctrl']['security']['ignoreRootLevelRestriction']))
794            ) {
795                foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $_) {
796                    $isExcludeField = (bool)($GLOBALS['TCA'][$table]['columns'][$field]['exclude'] ?? false);
797                    $isOnlyVisibleForAdmins = ($GLOBALS['TCA'][$table]['columns'][$field]['displayCond'] ?? '') === 'HIDE_FOR_NON_ADMINS';
798                    // Only show fields that can be excluded for editors, or are hidden for non-admins
799                    if ($isExcludeField && !$isOnlyVisibleForAdmins) {
800                        // Get human readable names of fields
801                        $translatedField = $languageService->sL($GLOBALS['TCA'][$table]['columns'][$field]['label']);
802                        // Add entry, key 'labels' needed for sorting
803                        $excludeArrayTable[] = [
804                            'labels' => $translatedTable . ':' . $translatedField,
805                            'sectionHeader' => $translatedTable,
806                            'table' => $table,
807                            'tableField' => $field,
808                            'fieldName' => $field,
809                            'fullField' => $field,
810                            'fieldLabel' => $translatedField,
811                            'origin' => 'tca',
812                        ];
813                    }
814                }
815            }
816            // All FlexForm fields
817            $flexFormArray = $this->getRegisteredFlexForms($table);
818            foreach ($flexFormArray as $tableField => $flexForms) {
819                // Prefix for field label, e.g. "Plugin Options:"
820                $labelPrefix = '';
821                if (!empty($GLOBALS['TCA'][$table]['columns'][$tableField]['label'])) {
822                    $labelPrefix = $languageService->sL($GLOBALS['TCA'][$table]['columns'][$tableField]['label']);
823                }
824                // Get all sheets
825                foreach ($flexForms as $extIdent => $extConf) {
826                    // Get all fields in sheet
827                    foreach ($extConf['sheets'] as $sheetName => $sheet) {
828                        if (empty($sheet['ROOT']['el']) || !is_array($sheet['ROOT']['el'])) {
829                            continue;
830                        }
831                        foreach ($sheet['ROOT']['el'] as $pluginFieldName => $field) {
832                            // Use only fields that have exclude flag set
833                            if (empty($field['TCEforms']['exclude'])) {
834                                continue;
835                            }
836                            $fieldLabel = !empty($field['TCEforms']['label'])
837                                ? $languageService->sL($field['TCEforms']['label'])
838                                : $pluginFieldName;
839                            $excludeArrayTable[] = [
840                                'labels' => trim($translatedTable . ' ' . $labelPrefix . ' ' . $extIdent, ': ') . ':' . $fieldLabel,
841                                'sectionHeader' => trim($translatedTable . ' ' . $labelPrefix . ' ' . $extIdent, ':'),
842                                'table' => $table,
843                                'tableField' => $tableField,
844                                'extIdent' => $extIdent,
845                                'fieldName' => $pluginFieldName,
846                                'fullField' => $tableField . ';' . $extIdent . ';' . $sheetName . ';' . $pluginFieldName,
847                                'fieldLabel' => $fieldLabel,
848                                'origin' => 'flexForm',
849                            ];
850                        }
851                    }
852                }
853            }
854            // Sort fields by the translated value
855            if (!empty($excludeArrayTable)) {
856                usort($excludeArrayTable, function (array $array1, array $array2) {
857                    $array1 = reset($array1);
858                    $array2 = reset($array2);
859                    if (is_string($array1) && is_string($array2)) {
860                        return strcasecmp($array1, $array2);
861                    }
862                    return 0;
863                });
864                $finalExcludeArray = array_merge($finalExcludeArray, $excludeArrayTable);
865            }
866        }
867
868        return $finalExcludeArray;
869    }
870
871    /**
872     * Returns FlexForm data structures it finds. Used in select "special" for be_groups
873     * to set "exclude" flags for single flex form fields.
874     *
875     * This only finds flex forms registered in 'ds' config sections.
876     * This does not resolve other sophisticated flex form data structure references.
877     *
878     * @todo: This approach is limited and doesn't find everything. It works for casual tt_content plugins, though:
879     * @todo: The data structure identifier determination depends on data row, but we don't have all rows at hand here.
880     * @todo: The code thus "guesses" some standard data structure identifier scenarios and tries to resolve those.
881     * @todo: This guessing can not be solved in a good way. A general registry of "all" possible data structures is
882     * @todo: probably not wanted, since that wouldn't work for truly dynamic DS calculations. Probably the only
883     * @todo: thing we could do here is a hook to allow extensions declaring specific data structures to
884     * @todo: allow backend admins to set exclude flags for certain fields in those cases.
885     *
886     * @param string $table Table to handle
887     * @return array Data structures
888     */
889    protected function getRegisteredFlexForms($table)
890    {
891        if (empty($table) || empty($GLOBALS['TCA'][$table]['columns'])) {
892            return [];
893        }
894        $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
895        $flexForms = [];
896        foreach ($GLOBALS['TCA'][$table]['columns'] as $tableField => $fieldConf) {
897            if (!empty($fieldConf['config']['type']) && !empty($fieldConf['config']['ds']) && $fieldConf['config']['type'] === 'flex') {
898                $flexForms[$tableField] = [];
899                foreach (array_keys($fieldConf['config']['ds']) as $flexFormKey) {
900                    $flexFormKey = (string)$flexFormKey;
901                    // Get extension identifier (uses second value if it's not empty, "list" or "*", else first one)
902                    $identFields = GeneralUtility::trimExplode(',', $flexFormKey);
903                    $extIdent = $identFields[0];
904                    if (!empty($identFields[1]) && $identFields[1] !== 'list' && $identFields[1] !== '*') {
905                        $extIdent = $identFields[1];
906                    }
907                    $flexFormDataStructureIdentifier = json_encode([
908                        'type' => 'tca',
909                        'tableName' => $table,
910                        'fieldName' => $tableField,
911                        'dataStructureKey' => $flexFormKey,
912                    ]);
913                    try {
914                        $dataStructure = $flexFormTools->parseDataStructureByIdentifier($flexFormDataStructureIdentifier);
915                        $flexForms[$tableField][$extIdent] = $dataStructure;
916                    } catch (InvalidIdentifierException $e) {
917                        // Deliberately empty: The DS identifier is guesswork and the flex ds parser throws
918                        // this exception if it can not resolve to a valid data structure. This is "ok" here
919                        // and the exception is just eaten.
920                    }
921                }
922            }
923        }
924        return $flexForms;
925    }
926
927    /**
928     * Returns an array with explicit Allow/Deny fields.
929     * Used for listing these field/value pairs in be_groups forms
930     *
931     * @return array Array with information from all of $GLOBALS['TCA']
932     */
933    protected function getExplicitAuthFieldValues()
934    {
935        $languageService = static::getLanguageService();
936        $adLabel = [
937            'ALLOW' => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.allow'),
938            'DENY' => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.deny')
939        ];
940        $allowDenyOptions = [];
941        foreach ($GLOBALS['TCA'] as $table => $_) {
942            // All field names configured:
943            if (is_array($GLOBALS['TCA'][$table]['columns'])) {
944                foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $__) {
945                    $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
946                    if ($fieldConfig['type'] === 'select' && $fieldConfig['authMode']) {
947                        // Check for items
948                        if (is_array($fieldConfig['items'])) {
949                            // Get Human Readable names of fields and table:
950                            $allowDenyOptions[$table . ':' . $field]['tableFieldLabel'] =
951                                $languageService->sL($GLOBALS['TCA'][$table]['ctrl']['title']) . ': '
952                                . $languageService->sL($GLOBALS['TCA'][$table]['columns'][$field]['label']);
953                            foreach ($fieldConfig['items'] as $iVal) {
954                                $itemIdentifier = (string)$iVal[1];
955                                // Values '' and '--div--' are not controlled by this setting.
956                                if ($itemIdentifier === '' || $itemIdentifier === '--div--') {
957                                    continue;
958                                }
959                                // Find iMode
960                                $iMode = '';
961                                switch ((string)$fieldConfig['authMode']) {
962                                    case 'explicitAllow':
963                                        $iMode = 'ALLOW';
964                                        break;
965                                    case 'explicitDeny':
966                                        $iMode = 'DENY';
967                                        break;
968                                    case 'individual':
969                                        if ($iVal[5] ?? false) {
970                                            if ($iVal[5] === 'EXPL_ALLOW') {
971                                                $iMode = 'ALLOW';
972                                            } elseif ($iVal[5] === 'EXPL_DENY') {
973                                                $iMode = 'DENY';
974                                            }
975                                        }
976                                        break;
977                                }
978                                // Set iMode
979                                if ($iMode) {
980                                    $allowDenyOptions[$table . ':' . $field]['items'][$itemIdentifier] = [
981                                        $iMode,
982                                        $languageService->sL($iVal[0]),
983                                        $adLabel[$iMode]
984                                    ];
985                                }
986                            }
987                        }
988                    }
989                }
990            }
991        }
992        return $allowDenyOptions;
993    }
994
995    /**
996     * Build query to fetch foreign records. Helper method of
997     * addItemsFromForeignTable(), do not call otherwise.
998     *
999     * @param array $result Result array
1000     * @param string $localFieldName Current handle field name
1001     * @return QueryBuilder
1002     */
1003    protected function buildForeignTableQueryBuilder(array $result, string $localFieldName): QueryBuilder
1004    {
1005        $backendUser = $this->getBackendUser();
1006
1007        $foreignTableName = $result['processedTca']['columns'][$localFieldName]['config']['foreign_table'];
1008        $foreignTableClauseArray = $this->processForeignTableClause($result, $foreignTableName, $localFieldName);
1009
1010        $fieldList = BackendUtility::getCommonSelectFields($foreignTableName, $foreignTableName . '.');
1011        /** @var QueryBuilder $queryBuilder */
1012        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1013            ->getQueryBuilderForTable($foreignTableName);
1014
1015        $queryBuilder->getRestrictions()
1016            ->removeAll()
1017            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1018            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
1019
1020        $queryBuilder
1021            ->select(...GeneralUtility::trimExplode(',', $fieldList, true))
1022            ->from($foreignTableName)
1023            ->where($foreignTableClauseArray['WHERE']);
1024
1025        if (!empty($foreignTableClauseArray['GROUPBY'])) {
1026            $queryBuilder->groupBy(...$foreignTableClauseArray['GROUPBY']);
1027        }
1028
1029        if (!empty($foreignTableClauseArray['ORDERBY'])) {
1030            foreach ($foreignTableClauseArray['ORDERBY'] as $orderPair) {
1031                [$fieldName, $order] = $orderPair;
1032                $queryBuilder->addOrderBy($fieldName, $order);
1033            }
1034        } elseif (!empty($GLOBALS['TCA'][$foreignTableName]['ctrl']['default_sortby'])) {
1035            $orderByClauses = QueryHelper::parseOrderBy($GLOBALS['TCA'][$foreignTableName]['ctrl']['default_sortby']);
1036            foreach ($orderByClauses as $orderByClause) {
1037                if (!empty($orderByClause[0])) {
1038                    $queryBuilder->addOrderBy($foreignTableName . '.' . $orderByClause[0], $orderByClause[1]);
1039                }
1040            }
1041        }
1042
1043        if (!empty($foreignTableClauseArray['LIMIT'])) {
1044            if (!empty($foreignTableClauseArray['LIMIT'][1])) {
1045                $queryBuilder->setMaxResults($foreignTableClauseArray['LIMIT'][1]);
1046                $queryBuilder->setFirstResult($foreignTableClauseArray['LIMIT'][0]);
1047            } elseif (!empty($foreignTableClauseArray['LIMIT'][0])) {
1048                $queryBuilder->setMaxResults($foreignTableClauseArray['LIMIT'][0]);
1049            }
1050        }
1051
1052        // rootLevel = -1 means that elements can be on the rootlevel OR on any page (pid!=-1)
1053        // rootLevel = 0 means that elements are not allowed on root level
1054        // rootLevel = 1 means that elements are only on the root level (pid=0)
1055        $rootLevel = 0;
1056        if (isset($GLOBALS['TCA'][$foreignTableName]['ctrl']['rootLevel'])) {
1057            $rootLevel = (int)$GLOBALS['TCA'][$foreignTableName]['ctrl']['rootLevel'];
1058        }
1059
1060        if ($rootLevel === -1) {
1061            $queryBuilder->andWhere(
1062                $queryBuilder->expr()->neq(
1063                    $foreignTableName . '.pid',
1064                    $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
1065                )
1066            );
1067        } elseif ($rootLevel === 1) {
1068            $queryBuilder->andWhere(
1069                $queryBuilder->expr()->eq(
1070                    $foreignTableName . '.pid',
1071                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1072                )
1073            );
1074        } else {
1075            $queryBuilder->andWhere($backendUser->getPagePermsClause(Permission::PAGE_SHOW));
1076            if ($foreignTableName !== 'pages') {
1077                $queryBuilder
1078                    ->from('pages')
1079                    ->andWhere(
1080                        $queryBuilder->expr()->eq(
1081                            'pages.uid',
1082                            $queryBuilder->quoteIdentifier($foreignTableName . '.pid')
1083                        )
1084                    );
1085            }
1086        }
1087
1088        // @todo what about PID restriction?
1089        if ($this->getBackendUser()->workspace !== 0 && BackendUtility::isTableWorkspaceEnabled($foreignTableName)) {
1090            $queryBuilder
1091                ->andWhere(
1092                    $queryBuilder->expr()->neq(
1093                        $foreignTableName . '.t3ver_state',
1094                        $queryBuilder->createNamedParameter(VersionState::MOVE_PLACEHOLDER, \PDO::PARAM_INT)
1095                    )
1096                );
1097        }
1098
1099        return $queryBuilder;
1100    }
1101
1102    /**
1103     * Replace markers in a where clause from TCA foreign_table_where
1104     *
1105     * ###REC_FIELD_[field name]###
1106     * ###THIS_UID### - is current element uid (zero if new).
1107     * ###CURRENT_PID### - is the current page id (pid of the record).
1108     * ###SITEROOT###
1109     * ###PAGE_TSCONFIG_ID### - a value you can set from Page TSconfig dynamically.
1110     * ###PAGE_TSCONFIG_IDLIST### - a value you can set from Page TSconfig dynamically.
1111     * ###PAGE_TSCONFIG_STR### - a value you can set from Page TSconfig dynamically.
1112     *
1113     * @param array $result Result array
1114     * @param string $foreignTableName Name of foreign table
1115     * @param string $localFieldName Current handle field name
1116     * @return array Query parts with keys WHERE, ORDERBY, GROUPBY, LIMIT
1117     */
1118    protected function processForeignTableClause(array $result, $foreignTableName, $localFieldName)
1119    {
1120        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($foreignTableName);
1121        $localTable = $result['tableName'];
1122        $effectivePid = $result['effectivePid'];
1123
1124        $foreignTableClause = '';
1125        if (!empty($result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'])
1126            && is_string($result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'])
1127        ) {
1128            $foreignTableClause = $result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'];
1129            // Replace possible markers in query
1130            if (strpos($foreignTableClause, '###REC_FIELD_') !== false) {
1131                // " AND table.field='###REC_FIELD_field1###' AND ..." -> array(" AND table.field='", "field1###' AND ...")
1132                $whereClauseParts = explode('###REC_FIELD_', $foreignTableClause);
1133                foreach ($whereClauseParts as $key => $value) {
1134                    if ($key !== 0) {
1135                        // "field1###' AND ..." -> array("field1", "' AND ...")
1136                        $whereClauseSubParts = explode('###', $value, 2);
1137                        // @todo: Throw exception if there is no value? What happens for NEW records?
1138                        $databaseRowKey = empty($result['flexParentDatabaseRow']) ? 'databaseRow' : 'flexParentDatabaseRow';
1139                        $rowFieldValue = $result[$databaseRowKey][$whereClauseSubParts[0]] ?? '';
1140                        if (is_array($rowFieldValue)) {
1141                            // If a select or group field is used here, it may have been processed already and
1142                            // is now an array containing uid + table + title + row.
1143                            // See TcaGroup data provider for details.
1144                            // Pick the first one (always on 0), and use uid only.
1145                            $rowFieldValue = $rowFieldValue[0]['uid'] ?? $rowFieldValue[0];
1146                        }
1147                        if (substr($whereClauseParts[0], -1) === '\'' && $whereClauseSubParts[1][0] === '\'') {
1148                            $whereClauseParts[0] = substr($whereClauseParts[0], 0, -1);
1149                            $whereClauseSubParts[1] = substr($whereClauseSubParts[1], 1);
1150                        }
1151                        $whereClauseParts[$key] = $connection->quote($rowFieldValue) . $whereClauseSubParts[1];
1152                    }
1153                }
1154                $foreignTableClause = implode('', $whereClauseParts);
1155            }
1156            if (strpos($foreignTableClause, '###CURRENT_PID###') !== false) {
1157                // Use pid from parent page clause if in flex form context
1158                if (!empty($result['flexParentDatabaseRow']['pid'])) {
1159                    $effectivePid = $result['flexParentDatabaseRow']['pid'];
1160                } elseif (!$effectivePid && !empty($result['databaseRow']['pid'])) {
1161                    // Use pid from database row if in inline context
1162                    $effectivePid = $result['databaseRow']['pid'];
1163                }
1164            }
1165
1166            $siteRootUid = 0;
1167            foreach ($result['rootline'] as $rootlinePage) {
1168                if (!empty($rootlinePage['is_siteroot'])) {
1169                    $siteRootUid = (int)$rootlinePage['uid'];
1170                    break;
1171                }
1172            }
1173
1174            $pageTsConfigId = 0;
1175            if (isset($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID'])
1176                && $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID']
1177            ) {
1178                $pageTsConfigId = (int)$result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID'];
1179            }
1180
1181            $pageTsConfigIdList = 0;
1182            if (isset($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST'])
1183                && $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST']
1184            ) {
1185                $pageTsConfigIdList = $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST'];
1186            }
1187            $pageTsConfigIdListArray = GeneralUtility::trimExplode(',', $pageTsConfigIdList, true);
1188            $pageTsConfigIdList = [];
1189            foreach ($pageTsConfigIdListArray as $pageTsConfigIdListElement) {
1190                if (MathUtility::canBeInterpretedAsInteger($pageTsConfigIdListElement)) {
1191                    $pageTsConfigIdList[] = (int)$pageTsConfigIdListElement;
1192                }
1193            }
1194            $pageTsConfigIdList = implode(',', $pageTsConfigIdList);
1195
1196            $pageTsConfigString = '';
1197            if (isset($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR'])
1198                && $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR']
1199            ) {
1200                $pageTsConfigString = $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR'];
1201                $pageTsConfigString = $connection->quote($pageTsConfigString);
1202            }
1203
1204            $foreignTableClause = str_replace(
1205                [
1206                    '###CURRENT_PID###',
1207                    '###THIS_UID###',
1208                    '###SITEROOT###',
1209                    '###PAGE_TSCONFIG_ID###',
1210                    '###PAGE_TSCONFIG_IDLIST###',
1211                    '\'###PAGE_TSCONFIG_STR###\'',
1212                    '###PAGE_TSCONFIG_STR###'
1213                ],
1214                [
1215                    (int)$effectivePid,
1216                    (int)$result['databaseRow']['uid'],
1217                    $siteRootUid,
1218                    $pageTsConfigId,
1219                    $pageTsConfigIdList,
1220                    $pageTsConfigString,
1221                    $pageTsConfigString
1222                ],
1223                $foreignTableClause
1224            );
1225        }
1226
1227        // Split the clause into an array with keys WHERE, GROUPBY, ORDERBY, LIMIT
1228        // Prepend a space to make sure "[[:space:]]+" will find a space there for the first element.
1229        $foreignTableClause = ' ' . $foreignTableClause;
1230        $foreignTableClauseArray = [
1231            'WHERE' => '',
1232            'GROUPBY' => '',
1233            'ORDERBY' => '',
1234            'LIMIT' => '',
1235        ];
1236        // Find LIMIT
1237        $reg = [];
1238        if (preg_match('/^(.*)[[:space:]]+LIMIT[[:space:]]+([[:alnum:][:space:],._]+)$/is', $foreignTableClause, $reg)) {
1239            $foreignTableClauseArray['LIMIT'] = GeneralUtility::intExplode(',', trim($reg[2]), true);
1240            $foreignTableClause = $reg[1];
1241        }
1242        // Find ORDER BY
1243        $reg = [];
1244        if (preg_match('/^(.*)[[:space:]]+ORDER[[:space:]]+BY[[:space:]]+([[:alnum:][:space:],._()"]+)$/is', $foreignTableClause, $reg)) {
1245            $foreignTableClauseArray['ORDERBY'] = QueryHelper::parseOrderBy(trim($reg[2]));
1246            $foreignTableClause = $reg[1];
1247        }
1248        // Find GROUP BY
1249        $reg = [];
1250        if (preg_match('/^(.*)[[:space:]]+GROUP[[:space:]]+BY[[:space:]]+([[:alnum:][:space:],._()"]+)$/is', $foreignTableClause, $reg)) {
1251            $foreignTableClauseArray['GROUPBY'] = QueryHelper::parseGroupBy(trim($reg[2]));
1252            $foreignTableClause = $reg[1];
1253        }
1254        // Rest is assumed to be "WHERE" clause
1255        $foreignTableClauseArray['WHERE'] = QueryHelper::stripLogicalOperatorPrefix($foreignTableClause);
1256
1257        return $foreignTableClauseArray;
1258    }
1259
1260    /**
1261     * Convert the current database values into an array
1262     *
1263     * @param array $row database row
1264     * @param string $fieldName fieldname to process
1265     * @return array
1266     */
1267    protected function processDatabaseFieldValue(array $row, $fieldName)
1268    {
1269        $currentDatabaseValues = array_key_exists($fieldName, $row)
1270            ? $row[$fieldName]
1271            : '';
1272        if (!is_array($currentDatabaseValues)) {
1273            $currentDatabaseValues = GeneralUtility::trimExplode(',', $currentDatabaseValues, true);
1274        }
1275        return $currentDatabaseValues;
1276    }
1277
1278    /**
1279     * Validate and sanitize database row values of the select field with the given name.
1280     * Creates an array out of databaseRow[selectField] values.
1281     *
1282     * Used by TcaSelectItems and TcaSelectTreeItems data providers
1283     *
1284     * @param array $result The current result array.
1285     * @param string $fieldName Name of the current select field.
1286     * @param array $staticValues Array with statically defined items, item value is used as array key.
1287     * @return array
1288     */
1289    protected function processSelectFieldValue(array $result, $fieldName, array $staticValues)
1290    {
1291        $fieldConfig = $result['processedTca']['columns'][$fieldName];
1292
1293        $currentDatabaseValueArray = array_key_exists($fieldName, $result['databaseRow']) ? $result['databaseRow'][$fieldName] : [];
1294        $newDatabaseValueArray = [];
1295
1296        // Add all values that were defined by static methods and do not come from the relation
1297        // e.g. TCA, TSconfig, itemProcFunc etc.
1298        foreach ($currentDatabaseValueArray as $value) {
1299            if (isset($staticValues[$value])) {
1300                $newDatabaseValueArray[] = $value;
1301            }
1302        }
1303
1304        if (isset($fieldConfig['config']['foreign_table']) && !empty($fieldConfig['config']['foreign_table'])) {
1305            /** @var RelationHandler $relationHandler */
1306            $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
1307            $relationHandler->registerNonTableValues = !empty($fieldConfig['config']['allowNonIdValues']);
1308            if (!empty($fieldConfig['config']['MM']) && $result['command'] !== 'new') {
1309                // MM relation
1310                $relationHandler->start(
1311                    implode(',', $currentDatabaseValueArray),
1312                    $fieldConfig['config']['foreign_table'],
1313                    $fieldConfig['config']['MM'],
1314                    $result['databaseRow']['uid'],
1315                    $result['tableName'],
1316                    $fieldConfig['config']
1317                );
1318                $relationHandler->processDeletePlaceholder();
1319                $newDatabaseValueArray = array_merge($newDatabaseValueArray, $relationHandler->getValueArray());
1320            } else {
1321                // Non MM relation
1322                // If not dealing with MM relations, use default live uid, not versioned uid for record relations
1323                $relationHandler->start(
1324                    implode(',', $currentDatabaseValueArray),
1325                    $fieldConfig['config']['foreign_table'],
1326                    '',
1327                    $this->getLiveUid($result),
1328                    $result['tableName'],
1329                    $fieldConfig['config']
1330                );
1331                $relationHandler->processDeletePlaceholder();
1332                $databaseIds = array_merge($newDatabaseValueArray, $relationHandler->getValueArray());
1333                // remove all items from the current DB values if not available as relation or static value anymore
1334                $newDatabaseValueArray = array_values(array_intersect($currentDatabaseValueArray, $databaseIds));
1335            }
1336        }
1337
1338        if ($fieldConfig['config']['multiple'] ?? false) {
1339            return $newDatabaseValueArray;
1340        }
1341        return array_unique($newDatabaseValueArray);
1342    }
1343
1344    /**
1345     * Translate the item labels
1346     *
1347     * Used by TcaSelectItems and TcaSelectTreeItems data providers
1348     *
1349     * @param array $result Result array
1350     * @param array $itemArray Items
1351     * @param string $table
1352     * @param string $fieldName
1353     * @return array
1354     */
1355    public function translateLabels(array $result, array $itemArray, $table, $fieldName)
1356    {
1357        $languageService = $this->getLanguageService();
1358
1359        foreach ($itemArray as $key => $item) {
1360            if (isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]])
1361                && !empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]])
1362            ) {
1363                $label = $languageService->sL($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]]);
1364            } else {
1365                $label = $languageService->sL(trim($item[0]));
1366            }
1367            $value = strlen((string)$item[1]) > 0 ? $item[1] : '';
1368            $icon = !empty($item[2]) ? $item[2] : null;
1369            $groupId = $item[3] ?? null;
1370            $helpText = null;
1371            if (!empty($item[4])) {
1372                if (\is_string($item[4])) {
1373                    $helpText = $languageService->sL($item[4]);
1374                } else {
1375                    $helpText = $item[4];
1376                }
1377            }
1378            $itemArray[$key] = [
1379                $label,
1380                $value,
1381                $icon,
1382                $groupId,
1383                $helpText
1384            ];
1385        }
1386
1387        return $itemArray;
1388    }
1389
1390    /**
1391     * Add alternative icon using "altIcons" TSconfig
1392     *
1393     * @param array $result
1394     * @param array $items
1395     * @param string $table
1396     * @param string $fieldName
1397     *
1398     * @return array
1399     */
1400    public function addIconFromAltIcons(array $result, array $items, string $table, string $fieldName): array
1401    {
1402        foreach ($items as &$item) {
1403            if (isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altIcons.'][$item[1]])
1404                && !empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altIcons.'][$item[1]])
1405            ) {
1406                $item[2] = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altIcons.'][$item[1]];
1407            }
1408        }
1409
1410        return $items;
1411    }
1412
1413    /**
1414     * Sanitize incoming item array
1415     *
1416     * Used by TcaSelectItems and TcaSelectTreeItems data providers
1417     *
1418     * @param mixed $itemArray
1419     * @param string $tableName
1420     * @param string $fieldName
1421     * @throws \UnexpectedValueException
1422     * @return array
1423     */
1424    public function sanitizeItemArray($itemArray, $tableName, $fieldName)
1425    {
1426        if (!is_array($itemArray)) {
1427            $itemArray = [];
1428        }
1429        foreach ($itemArray as $item) {
1430            if (!is_array($item)) {
1431                throw new \UnexpectedValueException(
1432                    'An item in field ' . $fieldName . ' of table ' . $tableName . ' is not an array as expected',
1433                    1439288036
1434                );
1435            }
1436        }
1437
1438        return $itemArray;
1439    }
1440
1441    /**
1442     * Gets the record uid of the live default record. If already
1443     * pointing to the live record, the submitted record uid is returned.
1444     *
1445     * @param array $result Result array
1446     * @return int
1447     * @throws \UnexpectedValueException
1448     */
1449    protected function getLiveUid(array $result)
1450    {
1451        $table = $result['tableName'];
1452        $row = $result['databaseRow'];
1453        $uid = $row['uid'];
1454        if (BackendUtility::isTableWorkspaceEnabled($table) && (int)$row['t3ver_oid'] > 0) {
1455            $uid = $row['t3ver_oid'];
1456        }
1457        return $uid;
1458    }
1459
1460    protected function getAllSites(): array
1461    {
1462        return GeneralUtility::makeInstance(SiteFinder::class)->getAllSites();
1463    }
1464
1465    /**
1466     * @return LanguageService
1467     */
1468    protected function getLanguageService()
1469    {
1470        return $GLOBALS['LANG'];
1471    }
1472
1473    /**
1474     * @return BackendUserAuthentication
1475     */
1476    protected function getBackendUser()
1477    {
1478        return $GLOBALS['BE_USER'];
1479    }
1480}
1481