[BUGFIX] Fix calls to undefined methods in FormDataTraverser
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / FormDataTraverser.php
1 <?php
2 namespace TYPO3\CMS\Backend\Form;
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 TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
18 use TYPO3\CMS\Backend\Utility\BackendUtility;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
20
21 /**
22 * Utility class for traversing related fields in the TCA.
23 *
24 * This thing basically resolves __row|field syntax for the placeholder stuff. It follows a
25 * given path and returns a value of a connected foreign record.
26 *
27 * @todo: This whole class is very hard to understand and hard to debug. It should be
28 * @todo: done as a data provider to work on prepared structures instead.
29 *
30 * @internal: Do not use in extensions, class will be changed / removed in the future
31 */
32 class FormDataTraverser {
33
34 /**
35 * If this value is set during traversal and the traversal chain can
36 * not be walked to the end this value will be returned instead.
37 *
38 * @var string
39 */
40 protected $alternativeFieldValue;
41
42 /**
43 * If this is TRUE the alternative field value will be used even if
44 * the detected field value is not empty.
45 *
46 * @var bool
47 */
48 protected $forceAlternativeFieldValueUse = FALSE;
49
50 /**
51 * The row data of the record that is currently traversed.
52 *
53 * @var array
54 */
55 protected $currentRow;
56
57 /**
58 * Name of the table that is currently traversed.
59 *
60 * @var string
61 */
62 protected $currentTable;
63
64 /**
65 * If the first record in the chain is translatable the language
66 * UID of this record is stored in this variable.
67 *
68 * @var int
69 */
70 protected $originalLanguageUid = NULL;
71
72 /**
73 * Inline first pid
74 *
75 * @var integer
76 */
77 protected $inlineFirstPid;
78
79 /**
80 * Traverses the array of given field names by using the TCA.
81 *
82 * @param array $fieldNameArray The field names that should be traversed.
83 * @param string $tableName The starting table name.
84 * @param array $row The starting record row.
85 * @param int $inlineFirstPid Inline first pid
86 * @return mixed The value of the last field in the chain.
87 */
88 public function getTraversedFieldValue(array $fieldNameArray, $tableName, array $row, $inlineFirstPid) {
89 $this->currentTable = $tableName;
90 $this->currentRow = $row;
91 $this->inlineFirstPid = $inlineFirstPid;
92 $fieldValue = '';
93 if (!empty($fieldNameArray)) {
94 $this->initializeOriginalLanguageUid();
95 $fieldValue = $this->getFieldValue($fieldNameArray);
96 }
97 return $fieldValue;
98 }
99
100 /**
101 * Checks if the current table is translatable and initializes the
102 * originalLanguageUid with the value of the languageField of the
103 * current row.
104 *
105 * @return void
106 */
107 protected function initializeOriginalLanguageUid() {
108 $fieldCtrlConfig = $GLOBALS['TCA'][$this->currentTable]['ctrl'];
109 if (!empty($fieldCtrlConfig['languageField']) && isset($this->currentRow[$fieldCtrlConfig['languageField']])) {
110 $this->originalLanguageUid = (int)$this->currentRow[$fieldCtrlConfig['languageField']];
111 } else {
112 $this->originalLanguageUid = FALSE;
113 }
114 }
115
116 /**
117 * Traverses the fields in the $fieldNameArray and tries to read
118 * the field values.
119 *
120 * @param array $fieldNameArray The field names that should be traversed.
121 * @return mixed The value of the last field.
122 */
123 protected function getFieldValue(array $fieldNameArray) {
124 $value = '';
125
126 $count = 0;
127 foreach ($fieldNameArray as $fieldName) {
128 $count = $count + 1;
129 // Skip if a defined field was actually not present in the database row
130 // Using array_key_exists here, since TYPO3 supports NULL values as well
131 if (!array_key_exists($fieldName, $this->currentRow) || $this->currentRow[$fieldName] === "0") {
132 $value = '';
133 break;
134 }
135
136 $value = $this->currentRow[$fieldName];
137 // @todo: the first value was not processed with old data preprocessor and contains no |
138 // @todo: this is simuluated here
139 if ($count === 1) {
140 $value = explode('|', $value);
141 $value = $value[0];
142 }
143
144 if (empty($value)) {
145 break;
146 }
147
148 $this->currentRow = $this->getRelatedRecordRow($fieldName, $value);
149 if ($this->currentRow === FALSE) {
150 break;
151 }
152 }
153
154 if ((empty($value) || $this->forceAlternativeFieldValueUse) && !empty($this->alternativeFieldValue)) {
155 $value = $this->alternativeFieldValue;
156 }
157
158 return $value;
159 }
160
161 /**
162 * Tries to read the related record from the database depending on
163 * the TCA. Supported types are group (db), select and inline.
164 *
165 * @param string $fieldName The name of the field of which the related record should be fetched.
166 * @param string $value The current field value.
167 * @return array|boolean The related row if it could be found otherwise FALSE.
168 */
169 protected function getRelatedRecordRow($fieldName, $value) {
170 $fieldConfig = $GLOBALS['TCA'][$this->currentTable]['columns'][$fieldName]['config'];
171 $possibleUids = array();
172
173 switch ($fieldConfig['type']) {
174 case 'group':
175 $possibleUids = $this->getRelatedGroupFieldUids($fieldConfig, $value);
176 break;
177 case 'select':
178 $possibleUids = $this->getRelatedSelectFieldUids($fieldConfig, $fieldName, $value);
179 break;
180 case 'inline':
181 $possibleUids = $this->getRelatedInlineFieldUids($fieldConfig, $fieldName, $value);
182 break;
183 }
184
185 $relatedRow = FALSE;
186 $possibleUidsCount = count($possibleUids);
187 if ($possibleUidsCount === 1) {
188 $relatedRow = $this->getRecordRow($possibleUids[0]);
189 }
190
191 return $relatedRow;
192 }
193
194 /**
195 * Tries to get the related UIDs of a group field.
196 *
197 * @param array $fieldConfig "config" section from the TCA for the current field.
198 * @param string $value The current value (normally a comma separated record list, possibly consisting of multiple parts [table]_[uid]|[title]).
199 * @return array Array of related UIDs.
200 */
201 protected function getRelatedGroupFieldUids(array $fieldConfig, $value) {
202 $relatedUids = array();
203 $allowedTable = $this->getAllowedTableForGroupField($fieldConfig);
204
205 if (($fieldConfig['internal_type'] !== 'db') || ($allowedTable === FALSE)) {
206 return $relatedUids;
207 }
208
209 $values = GeneralUtility::trimExplode(',', $value, TRUE);
210 foreach ($values as $groupValue) {
211 list($foreignIdentifier, $foreignTitle) = GeneralUtility::trimExplode('|', $groupValue);
212 list($recordForeignTable, $foreignUid) = BackendUtility::splitTable_Uid($foreignIdentifier);
213 // skip records that do not match the allowed table
214 if (!empty($recordForeignTable) && ($recordForeignTable !== $allowedTable)) {
215 continue;
216 }
217 if (!empty($foreignTitle)) {
218 $this->alternativeFieldValue = rawurldecode($foreignTitle);
219 }
220 $relatedUids[] = $foreignUid;
221 }
222
223 if (!empty($relatedUids)) {
224 $this->currentTable = $allowedTable;
225 }
226
227 return $relatedUids;
228 }
229
230 /**
231 * Tries to get the related UID of an inline field.
232 *
233 * @param array $fieldConfig "config" section of the TCA configuration of the related inline field.
234 * @param string $fieldName The name of the inline field.
235 * @param string $value The value in the local table (normally a comma separated list of the inline record UIDs).
236 * @return array Array of related UIDs.
237 */
238 protected function getRelatedInlineFieldUids(array $fieldConfig, $fieldName, $value) {
239 $relatedUids = array();
240
241 $PA = array('itemFormElValue' => $value);
242 $inlineRelatedRecordResolver = GeneralUtility::makeInstance(InlineRelatedRecordResolver::class);
243 $items = $inlineRelatedRecordResolver->getRelatedRecords($this->currentTable, $fieldName, $this->currentRow, $PA, $fieldConfig, $this->inlineFirstPid);
244 if ($items['count'] > 0) {
245 $this->currentTable = $fieldConfig['foreign_table'];
246 foreach ($items['records'] as $inlineRecord) {
247 $relatedUids[] = $inlineRecord['uid'];
248 }
249 }
250
251 return $relatedUids;
252 }
253
254 /**
255 * Will read the "allowed" value from the given field configuration
256 * and returns FALSE if none was defined or more than one.
257 *
258 * If exactly one table was defined the name of that table is returned.
259 *
260 * @param array $fieldConfig "config" section of a group field from the TCA.
261 * @return bool|string FALSE if none ore more than one table was found, otherwise the name of the table.
262 */
263 protected function getAllowedTableForGroupField(array $fieldConfig) {
264 $allowedTable = FALSE;
265
266 $allowedTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed'], TRUE);
267 if (count($allowedTables) === 1) {
268 $allowedTable = $allowedTables[0];
269 }
270
271 return $allowedTable;
272 }
273
274 /**
275 * Fetch a record from the database
276 *
277 * @param int $uid The UID of the record that should be fetched.
278 * @return array|boolean FALSE if the record can not be accessed, otherwise the data of the requested record.
279 */
280 protected function getRecordRow($uid) {
281 $selectDataInput = [
282 'command' => 'edit',
283 'vanillaUid' => (int)$uid,
284 'tableName' => $this->currentTable,
285 ];
286 /** @var TcaDatabaseRecord $formDataGroup */
287 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
288 /** @var FormDataCompiler $formDataCompiler */
289 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
290 $compilerResult = $formDataCompiler->compile($selectDataInput);
291 $databaseRow = $compilerResult['databaseRow'];
292 $newDatabaseRow = [];
293 foreach ($databaseRow as $fieldName => $value) {
294 $newDatabaseRow[$fieldName] = $value;
295 if (is_array($value)) {
296 $newDatabaseRow[$fieldName] = implode(',', $value);
297 }
298 }
299 return $newDatabaseRow;
300 }
301
302 /**
303 * Tries to get the correct record based on the parent translation by
304 * traversing all given related UIDs and checking if their language UID
305 * matches the stored original language UID.
306 *
307 * If exactly one match was found for the original language the resulting
308 * row is returned, otherwise FALSE.
309 *
310 * @param array $relatedUids All possible matching UIDs.
311 * @return bool|array The row data if a matching record was found, FALSE otherwise.
312 */
313 protected function getMatchingRecordRowByTranslation(array $relatedUids) {
314 if ($this->originalLanguageUid === FALSE) {
315 return FALSE;
316 }
317
318 $fieldCtrlConfig = $GLOBALS['TCA'][$this->currentTable]['ctrl'];
319 if (empty($fieldCtrlConfig['languageField'])) {
320 return FALSE;
321 }
322
323 $languageField = $fieldCtrlConfig['languageField'];
324 $foundRows = array();
325 foreach ($relatedUids as $relatedUid) {
326 $currentRow = $this->getRecordRow($relatedUid);
327 if (!isset($currentRow[$languageField])) {
328 continue;
329 }
330 if ((int)$currentRow[$languageField] === $this->originalLanguageUid) {
331 $foundRows[] = $currentRow;
332 }
333 }
334
335 $relatedRow = FALSE;
336 if (count($foundRows) === 1) {
337 $relatedRow = $foundRows[0];
338 }
339
340 return $relatedRow;
341 }
342
343 /**
344 * If the select field is build by a foreign_table the related UIDs
345 * will be returned.
346 *
347 * Otherwise the label of the currently selected value will be written
348 * to the alternativeFieldValue class property.
349 *
350 * @param array $fieldConfig The "config" section of the TCA for the current select field.
351 * @param string $fieldName The name of the select field.
352 * @param string $value The current value in the local record, usually a comma separated list of selected values.
353 * @return array Array of related UIDs.
354 */
355 protected function getRelatedSelectFieldUids(array $fieldConfig, $fieldName, $value) {
356 $relatedUids = array();
357
358 $isTraversable = FALSE;
359 if (isset($fieldConfig['foreign_table'])) {
360 $isTraversable = TRUE;
361 // if a foreign_table is used we pre-filter the records for performance
362 $fieldConfig['foreign_table_where'] .= ' AND ' . $fieldConfig['foreign_table'] . '.uid IN (' . $value . ')';
363 }
364
365 /** @var TcaDatabaseRecord $formDataGroup */
366 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
367 /** @var FormDataCompiler $formDataCompiler */
368 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
369 $formDataInput = [
370 'vanillaUid' => (int)$this->currentRow['uid'],
371 'tableName' => $this->currentTable,
372 'command' => 'edit',
373 ];
374 $formDataResult = $formDataCompiler->compile($formDataInput);
375 $selectItemArray = [];
376 if (isset($formDataResult['processedTca']['columns'][$fieldName]['config']['items'])
377 && is_array($formDataResult['processedTca']['columns'][$fieldName]['config']['items'])
378 ) {
379 $selectItemArray = $formDataResult['processedTca']['columns'][$fieldName]['config']['items'];
380 }
381
382 if ($isTraversable && !empty($selectItemArray)) {
383 $this->currentTable = $fieldConfig['foreign_table'];
384 $relatedUids = $this->getSelectedValuesFromSelectItemArray($selectItemArray, $value);
385 } else {
386 $selectedLabels = $this->getSelectedValuesFromSelectItemArray($selectItemArray, $value, 1, TRUE);
387 if (count($selectedLabels) === 1) {
388 $this->alternativeFieldValue = $selectedLabels[0];
389 $this->forceAlternativeFieldValueUse = TRUE;
390 }
391 }
392
393 return $relatedUids;
394 }
395
396 /**
397 * Extracts the selected values from a given array of select items.
398 *
399 * @param array $selectItemArray The select item array generated by \TYPO3\CMS\Backend\Form\FormEngine->getSelectItems.
400 * @param string $value The currently selected value(s) as comma separated list.
401 * @param int|NULL $maxItems Optional value, if set processing is skipped and an empty array will be returned when the number of selected values is larger than the provided value.
402 * @param bool $returnLabels If TRUE the select labels will be returned instead of the values.
403 * @return array
404 */
405 protected function getSelectedValuesFromSelectItemArray(array $selectItemArray, $value, $maxItems = NULL, $returnLabels = FALSE) {
406 $values = GeneralUtility::trimExplode(',', $value);
407 $selectedValues = array();
408
409 if ($maxItems !== NULL && (count($values) > (int)$maxItems)) {
410 return $selectedValues;
411 }
412
413 foreach ($selectItemArray as $selectItem) {
414 $selectItemValue = $selectItem[1];
415 if (in_array($selectItemValue, $values)) {
416 if ($returnLabels) {
417 $selectedValues[] = $selectItem[0];
418 } else {
419 $selectedValues[] = $selectItemValue;
420 }
421 }
422 }
423
424 return $selectedValues;
425 }
426
427 }