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