[BUGFIX] Fix translated file relation with sql_mode=ONLY_FULL_GROUP_BY
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / FormDataProvider / TcaInputPlaceholders.php
1 <?php
2 namespace 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
17 use Doctrine\DBAL\Connection;
18 use TYPO3\CMS\Backend\Form\FormDataCompiler;
19 use TYPO3\CMS\Backend\Form\FormDataGroup\TcaInputPlaceholderRecord;
20 use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
21 use TYPO3\CMS\Core\Database\ConnectionPool;
22 use TYPO3\CMS\Core\Localization\LanguageService;
23 use TYPO3\CMS\Core\Utility\GeneralUtility;
24
25 /**
26 * Resolve placeholders for fields of type input or text. The placeholder value
27 * in the processedTca section of the result will be replaced with the resolved
28 * value.
29 */
30 class TcaInputPlaceholders implements FormDataProviderInterface
31 {
32 /**
33 * Resolve placeholders for input/text fields. Placeholders that are simple
34 * strings will be returned unmodified. Placeholders beginning with __row are
35 * being resolved, possibly traversing multiple tables.
36 *
37 * @param array $result
38 * @return array
39 */
40 public function addData(array $result)
41 {
42 foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
43 // Placeholders are only valid for input and text type fields
44 if (
45 ($fieldConfig['config']['type'] !== 'input' && $fieldConfig['config']['type'] !== 'text')
46 || !isset($fieldConfig['config']['placeholder'])
47 ) {
48 continue;
49 }
50
51 // Resolve __row|field type placeholders
52 if (strpos($fieldConfig['config']['placeholder'], '__row|') === 0) {
53 // split field names into array and remove the __row indicator
54 $fieldNameArray = array_slice(
55 GeneralUtility::trimExplode('|', $fieldConfig['config']['placeholder'], true),
56 1
57 );
58 $result['processedTca']['columns'][$fieldName]['config']['placeholder'] = $this->getPlaceholderValue($fieldNameArray, $result);
59 }
60
61 // Resolve placeholders from language files
62 if (strpos($fieldConfig['config']['placeholder'], 'LLL:') === 0) {
63 $result['processedTca']['columns'][$fieldName]['config']['placeholder'] = $this->getLanguageService()->sL($fieldConfig['config']['placeholder']);
64 }
65
66 // Remove empty placeholders
67 if (empty($result['processedTca']['columns'][$fieldName]['config']['placeholder'])) {
68 unset($result['processedTca']['columns'][$fieldName]['config']['placeholder']);
69 }
70 }
71
72 return $result;
73 }
74
75 /**
76 * Recursively resolve the placeholder value. A placeholder string with a
77 * syntax of __row|field1|field2|field3 will be recursively resolved to a
78 * final value.
79 *
80 * @param array $fieldNameArray
81 * @param array $result
82 * @param int $recursionLevel
83 * @return string
84 */
85 protected function getPlaceholderValue($fieldNameArray, $result, $recursionLevel = 0)
86 {
87 if ($recursionLevel > 99) {
88 // This should not happen, treat as misconfiguration
89 return '';
90 }
91
92 $fieldName = array_shift($fieldNameArray);
93
94 // Skip if a defined field was actually not present in the database row
95 // Using array_key_exists here, since NULL values are valid as well.
96 if (!array_key_exists($fieldName, $result['databaseRow'])) {
97 return '';
98 }
99
100 $value = $result['databaseRow'][$fieldName];
101
102 if (!isset($result['processedTca']['columns'][$fieldName]['config'])
103 || !is_array($result['processedTca']['columns'][$fieldName]['config'])
104 ) {
105 return (string)$value;
106 }
107
108 $fieldConfig = $result['processedTca']['columns'][$fieldName]['config'];
109
110 switch ($fieldConfig['type']) {
111 case 'select':
112 // The FormDataProviders already resolved the select items to an array of uids,
113 // filter out empty values that occur when no related record has been selected.
114 $possibleUids = array_filter($value);
115 $foreignTableName = $fieldConfig['foreign_table'];
116 break;
117 case 'group':
118 $possibleUids = $this->getRelatedGroupFieldUids($fieldConfig, $value);
119 $foreignTableName = $this->getAllowedTableForGroupField($fieldConfig);
120 break;
121 case 'inline':
122 $possibleUids = array_filter(GeneralUtility::trimExplode(',', $value, true));
123 $foreignTableName = $fieldConfig['foreign_table'];
124 break;
125 default:
126 $possibleUids = [];
127 $foreignTableName = '';
128 }
129
130 if (!empty($possibleUids) && !empty($fieldNameArray)) {
131 if (count($possibleUids) > 1
132 && !empty($GLOBALS['TCA'][$foreignTableName]['ctrl']['languageField'])
133 && isset($result['currentSysLanguage'])
134 ) {
135 $possibleUids = $this->getPossibleUidsByCurrentSysLanguage($possibleUids, $foreignTableName, $result['currentSysLanguage']);
136 }
137 $relatedFormData = $this->getRelatedFormData($foreignTableName, $possibleUids[0], $fieldNameArray[0]);
138 if (!empty($GLOBALS['TCA'][$result['tableName']]['ctrl']['languageField'])
139 && isset($result['databaseRow'][$GLOBALS['TCA'][$result['tableName']]['ctrl']['languageField']])
140 ) {
141 $relatedFormData['currentSysLanguage'] = $result['databaseRow'][$GLOBALS['TCA'][$result['tableName']]['ctrl']['languageField']][0];
142 }
143 $value = $this->getPlaceholderValue($fieldNameArray, $relatedFormData, $recursionLevel + 1);
144 }
145
146 if ($recursionLevel === 0 && is_array($value)) {
147 $value = implode(', ', $value);
148 }
149 return (string)$value;
150 }
151
152 /**
153 * Compile a formdata result set based on the tablename and record uid.
154 *
155 * @param string $tableName Name of the table for which to compile formdata
156 * @param int $uid UID of the record for which to compile the formdata
157 * @param string $columnToProcess The column that is required from the record
158 * @return array The compiled formdata
159 */
160 protected function getRelatedFormData($tableName, $uid, $columnToProcess)
161 {
162 $fakeDataInput = [
163 'command' => 'edit',
164 'vanillaUid' => (int)$uid,
165 'tableName' => $tableName,
166 'inlineCompileExistingChildren' => false,
167 'columnsToProcess' => [$columnToProcess],
168 ];
169 /** @var TcaInputPlaceholderRecord $formDataGroup */
170 $formDataGroup = GeneralUtility::makeInstance(TcaInputPlaceholderRecord::class);
171 /** @var FormDataCompiler $formDataCompiler */
172 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
173 $compilerResult = $formDataCompiler->compile($fakeDataInput);
174 return $compilerResult;
175 }
176
177 /**
178 * Return uids of related records for group type fields. Uids consisting of
179 * multiple parts like [table]_[uid]|[title] will be reduced to integers and
180 * validated against the allowed table. Uids without a table prefix are
181 * accepted in any case.
182 *
183 * @param array $fieldConfig TCA "config" section for the group type field.
184 * @param string $value A comma separated list of records
185 * @return array
186 */
187 protected function getRelatedGroupFieldUids(array $fieldConfig, $value)
188 {
189 $relatedUids = [];
190 $allowedTable = $this->getAllowedTableForGroupField($fieldConfig);
191
192 // Skip if it's not a database relation with a resolvable foreign table
193 if (($fieldConfig['internal_type'] !== 'db') || ($allowedTable === false)) {
194 return $relatedUids;
195 }
196
197 // Related group values have been prepared by TcaGroup data provider, an array is expected here
198 foreach ($value as $singleValue) {
199 $relatedUids[] = $singleValue['uid'];
200 }
201
202 return $relatedUids;
203 }
204
205 /**
206 * Will read the "allowed" value from the given field configuration
207 * and returns FALSE if none or more than one has been defined.
208 * Otherwise the name of the allowed table will be returned.
209 *
210 * @param array $fieldConfig TCA "config" section for the group type field.
211 * @return bool|string
212 */
213 protected function getAllowedTableForGroupField(array $fieldConfig)
214 {
215 $allowedTable = false;
216
217 $allowedTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed'], true);
218 if (count($allowedTables) === 1) {
219 $allowedTable = $allowedTables[0];
220 }
221
222 return $allowedTable;
223 }
224
225 /**
226 * E.g. sys_file is not translatable, thus the uid of the translation of it's metadata has to be retrieved here.
227 *
228 * Get the uid of e.g. a file metadata entry for a given sys_language_uid and the possible translated data.
229 * If there is no translation available, return the uid of default language.
230 * If there is no value at all, return the "possible uids".
231 *
232 * @param array $possibleUids
233 * @param string $foreignTableName
234 * @param int $currentLanguage
235 * @return array
236 */
237 protected function getPossibleUidsByCurrentSysLanguage(array $possibleUids, $foreignTableName, $currentLanguage)
238 {
239 $languageField = $GLOBALS['TCA'][$foreignTableName]['ctrl']['languageField'];
240 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($foreignTableName);
241 $possibleRecords = $queryBuilder->select('uid', $languageField)
242 ->from($foreignTableName)
243 ->where(
244 $queryBuilder->expr()->in(
245 'uid',
246 $queryBuilder->createNamedParameter($possibleUids, Connection::PARAM_INT_ARRAY)
247 ),
248 $queryBuilder->expr()->in(
249 $languageField,
250 $queryBuilder->createNamedParameter([$currentLanguage, 0], Connection::PARAM_INT_ARRAY)
251 )
252 )
253 ->groupBy($languageField, 'uid')
254 ->execute()
255 ->fetchAll();
256
257 if (!empty($possibleRecords)) {
258 // Either only one record or first record matches language
259 if (count($possibleRecords) === 1
260 || (int)$possibleRecords[0][$languageField] === (int)$currentLanguage
261 ) {
262 return [$possibleRecords[0]['uid']];
263 }
264
265 // Language of second record matches language
266 return [$possibleRecords[1]['uid']];
267 }
268
269 return $possibleUids;
270 }
271
272 /**
273 * @return LanguageService
274 */
275 protected function getLanguageService()
276 {
277 return $GLOBALS['LANG'];
278 }
279 }