1<?php
2namespace TYPO3\CMS\Backend\Form\FormDataProvider;
3
4/*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
18use TYPO3\CMS\Core\Utility\MathUtility;
19
20/**
21 * Resolve select items, set processed item list in processedTca, sanitize and resolve database field
22 */
23class TcaSelectItems extends AbstractItemProvider implements FormDataProviderInterface
24{
25    /**
26     * Resolve select items
27     *
28     * @param array $result
29     * @return array
30     * @throws \UnexpectedValueException
31     */
32    public function addData(array $result)
33    {
34        $table = $result['tableName'];
35
36        foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
37            if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'select') {
38                continue;
39            }
40
41            // Make sure we are only processing supported renderTypes
42            if (!$this->isTargetRenderType($fieldConfig)) {
43                continue;
44            }
45
46            $fieldConfig['config']['items'] = $this->sanitizeItemArray($fieldConfig['config']['items'] ?? [], $table, $fieldName);
47
48            $fieldConfig['config']['maxitems'] = MathUtility::forceIntegerInRange($fieldConfig['config']['maxitems'] ?? 0, 0, 99999);
49            if ($fieldConfig['config']['maxitems'] === 0) {
50                $fieldConfig['config']['maxitems'] = 99999;
51            }
52
53            $fieldConfig['config']['items'] = $this->addItemsFromSpecial($result, $fieldName, $fieldConfig['config']['items']);
54            $fieldConfig['config']['items'] = $this->addItemsFromFolder($result, $fieldName, $fieldConfig['config']['items']);
55
56            $fieldConfig['config']['items'] = $this->addItemsFromForeignTable($result, $fieldName, $fieldConfig['config']['items']);
57
58            // Resolve "itemsProcFunc"
59            if (!empty($fieldConfig['config']['itemsProcFunc'])) {
60                $fieldConfig['config']['items'] = $this->resolveItemProcessorFunction($result, $fieldName, $fieldConfig['config']['items']);
61                // itemsProcFunc must not be used anymore
62                unset($fieldConfig['config']['itemsProcFunc']);
63            }
64
65            // removing items before $dynamicItems and $removedItems have been built results in having them
66            // not populated to the dynamic database row and displayed as "invalid value" in the forms view
67            $fieldConfig['config']['items'] = $this->removeItemsByUserStorageRestriction($result, $fieldName, $fieldConfig['config']['items']);
68
69            $removedItems = $fieldConfig['config']['items'];
70
71            $fieldConfig['config']['items'] = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
72            $fieldConfig['config']['items'] = $this->addItemsFromPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
73            $fieldConfig['config']['items'] = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
74
75            $fieldConfig['config']['items'] = $this->removeItemsByUserLanguageFieldRestriction($result, $fieldName, $fieldConfig['config']['items']);
76            $fieldConfig['config']['items'] = $this->removeItemsByUserAuthMode($result, $fieldName, $fieldConfig['config']['items']);
77            $fieldConfig['config']['items'] = $this->removeItemsByDoktypeUserRestriction($result, $fieldName, $fieldConfig['config']['items']);
78
79            $removedItems = array_diff_key($removedItems, $fieldConfig['config']['items']);
80
81            $currentDatabaseValuesArray = $this->processDatabaseFieldValue($result['databaseRow'], $fieldName);
82            // Check if it's a new record to respect TCAdefaults
83            if (!empty($fieldConfig['config']['MM']) && $result['command'] !== 'new') {
84                // Getting the current database value on a mm relation doesn't make sense since the amount of selected
85                // relations is stored in the field and not the uids of the items
86                $currentDatabaseValuesArray = [];
87            }
88
89            $result['databaseRow'][$fieldName] = $currentDatabaseValuesArray;
90
91            // add item values as keys to determine which items are stored in the database and should be preselected
92            $itemArrayValues = array_column($fieldConfig['config']['items'], 1);
93            $itemArray = array_fill_keys(
94                $itemArrayValues,
95                $fieldConfig['config']['items']
96            );
97            $result['databaseRow'][$fieldName] = $this->processSelectFieldValue($result, $fieldName, $itemArray);
98
99            $fieldConfig['config']['items'] = $this->addInvalidItemsFromDatabase(
100                $result,
101                $table,
102                $fieldName,
103                $fieldConfig,
104                $currentDatabaseValuesArray,
105                $removedItems
106            );
107
108            // Translate labels
109            // skip file of sys_file_metadata which is not rendered anyway but can use all memory
110            if (!($table === 'sys_file_metadata' && $fieldName === 'file')) {
111                $fieldConfig['config']['items'] = $this->translateLabels($result, $fieldConfig['config']['items'], $table, $fieldName);
112            }
113
114            // Keys may contain table names, so a numeric array is created
115            $fieldConfig['config']['items'] = array_values($fieldConfig['config']['items']);
116
117            $result['processedTca']['columns'][$fieldName] = $fieldConfig;
118        }
119
120        return $result;
121    }
122
123    /**
124     * Add values that are currently listed in the database columns but not in the selectable items list
125     * back to the list.
126     *
127     * @param array $result The current result array.
128     * @param string $table The current table name
129     * @param string $fieldName The current field name
130     * @param array $fieldConf The configuration of the current field.
131     * @param array $databaseValues The item values from the database, can contain invalid items!
132     * @param array $removedItems Items removed by access checks and restrictions, must not be added as invalid values
133     * @return array
134     */
135    public function addInvalidItemsFromDatabase(array $result, $table, $fieldName, array $fieldConf, array $databaseValues, array $removedItems)
136    {
137        // Early return if there are no items or invalid values should not be displayed
138        if (empty($fieldConf['config']['items'])
139            || $fieldConf['config']['renderType'] !== 'selectSingle'
140            || ($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['disableNoMatchingValueElement'] ?? false)
141            || ($fieldConf['config']['disableNoMatchingValueElement'] ?? false)
142        ) {
143            return $fieldConf['config']['items'];
144        }
145
146        $languageService = $this->getLanguageService();
147        $noMatchingLabel = isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['noMatchingValue_label'])
148            ? $languageService->sL(trim($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['noMatchingValue_label']))
149            : '[ ' . $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue') . ' ]';
150
151        $unmatchedValues = array_diff(
152            array_values($databaseValues),
153            array_column($fieldConf['config']['items'], 1),
154            array_column($removedItems, 1)
155        );
156
157        foreach ($unmatchedValues as $unmatchedValue) {
158            $invalidItem = [
159                @sprintf($noMatchingLabel, $unmatchedValue),
160                $unmatchedValue
161            ];
162            array_unshift($fieldConf['config']['items'], $invalidItem);
163        }
164
165        return $fieldConf['config']['items'];
166    }
167
168    /**
169     * Determines whether the current field is a valid target for this DataProvider
170     *
171     * @param array $fieldConfig
172     * @return bool
173     */
174    protected function isTargetRenderType(array $fieldConfig)
175    {
176        return $fieldConfig['config']['renderType'] !== 'selectTree';
177    }
178}
179