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