[BUGFIX] Fatal Error in filelist when editing
[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 /**
172 * @var $selectObject \TYPO3\CMS\Backend\Form\Element\SelectElement
173 */
174 $selectObject = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Form\Element\SelectElement::class, $this->formEngine);
175 $selectObject->setCurrentRow($this->currentRow);
176 $selectObject->setCurrentTable($this->currentTable);
177 $selectObject->setAlternativeFieldValue($this->alternativeFieldValue);
178 $selectObject->setForceAlternativeFieldValueUse($this->forceAlternativeFieldValueUse);
179 $possibleUids = $selectObject->getRelatedSelectFieldUids($fieldConfig, $fieldName, $value);
180 $this->alternativeFieldValue = $selectObject->getAlternativeFieldValue();
181 $this->forceAlternativeFieldValueUse = $selectObject->isForceAlternativeFieldValueUse();
182 break;
183 case 'inline':
184 $possibleUids = $this->getRelatedInlineFieldUids($fieldConfig, $fieldName, $value);
185 break;
186 }
187
188 $relatedRow = FALSE;
189 if (count($possibleUids) === 1) {
190 $relatedRow = $this->getRecordRow($possibleUids[0]);
191 } elseif (count($possibleUids) > 1) {
192 $relatedRow = $this->getMatchingRecordRowByTranslation($possibleUids, $fieldConfig);
193 }
194
195 return $relatedRow;
196 }
197
198 /**
199 * Tries to get the related UIDs of a group field.
200 *
201 * @param array $fieldConfig "config" section from the TCA for the current field.
202 * @param string $value The current value (normally a comma separated record list, possibly consisting of multiple parts [table]_[uid]|[title]).
203 * @return array Array of related UIDs.
204 */
205 protected function getRelatedGroupFieldUids(array $fieldConfig, $value) {
206 $relatedUids = array();
207 $allowedTable = $this->getAllowedTableForGroupField($fieldConfig);
208
209 if (($fieldConfig['internal_type'] !== 'db') || ($allowedTable === FALSE)) {
210 return $relatedUids;
211 }
212
213 $values = GeneralUtility::trimExplode(',', $value, TRUE);
214 foreach ($values as $groupValue) {
215 list($foreignIdentifier, $foreignTitle) = GeneralUtility::trimExplode('|', $groupValue);
216 list($recordForeignTable, $foreignUid) = BackendUtility::splitTable_Uid($foreignIdentifier);
217 // skip records that do not match the allowed table
218 if (!empty($recordForeignTable) && ($recordForeignTable !== $allowedTable)) {
219 continue;
220 }
221 if (!empty($foreignTitle)) {
222 $this->alternativeFieldValue = rawurldecode($foreignTitle);
223 }
224 $relatedUids[] = $foreignUid;
225 }
226
227 if (count($relatedUids) > 0) {
228 $this->currentTable = $allowedTable;
229 }
230
231 return $relatedUids;
232 }
233
234 /**
235 * Tries to get the related UID of an inline field.
236 *
237 * @param array $fieldConfig "config" section of the TCA configuration of the related inline field.
238 * @param string $fieldName The name of the inline field.
239 * @param string $value The value in the local table (normally a comma separated list of the inline record UIDs).
240 * @return array Array of related UIDs.
241 */
242 protected function getRelatedInlineFieldUids(array $fieldConfig, $fieldName, $value) {
243 $relatedUids = array();
244
245 $PA = array('itemFormElValue' => $value);
246 $items = $this->formEngine->inline->getRelatedRecords($this->currentTable, $fieldName, $this->currentRow, $PA, $fieldConfig);
247 if ($items['count'] > 0) {
248 $this->currentTable = $fieldConfig['foreign_table'];
249 foreach ($items['records'] as $inlineRecord) {
250 $relatedUids[] = $inlineRecord['uid'];
251 }
252 }
253
254 return $relatedUids;
255 }
256
257 /**
258 * Will read the "allowed" value from the given field configuration
259 * and returns FALSE if none was defined or more than one.
260 *
261 * If exactly one table was defined the name of that table is returned.
262 *
263 * @param array $fieldConfig "config" section of a group field from the TCA.
264 * @return bool|string FALSE if none ore more than one table was found, otherwise the name of the table.
265 */
266 protected function getAllowedTableForGroupField(array $fieldConfig) {
267 $allowedTable = FALSE;
268
269 $allowedTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed'], TRUE);
270 if (count($allowedTables) === 1) {
271 $allowedTable = $allowedTables[0];
272 }
273
274 return $allowedTable;
275 }
276
277 /**
278 * Uses the DataPreprocessor to read a value from the database.
279 *
280 * The table name is read from the currentTable class variable.
281 *
282 * @param int $uid The UID of the record that should be fetched.
283 * @return array|boolean FALSE if the record can not be accessed, otherwise the data of the requested record.
284 */
285 protected function getRecordRow($uid) {
286 /** @var \TYPO3\CMS\Backend\Form\DataPreprocessor $dataPreprocessor */
287 $dataPreprocessor = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Form\DataPreprocessor::class);
288 $dataPreprocessor->fetchRecord($this->currentTable, $uid, '');
289 return reset($dataPreprocessor->regTableItems_data);
290 }
291
292 /**
293 * Tries to get the correct record based on the parent translation by
294 * traversing all given related UIDs and checking if their language UID
295 * matches the stored original language UID.
296 *
297 * If exactly one match was found for the original language the resulting
298 * row is returned, otherwise FALSE.
299 *
300 * @param array $relatedUids All possible matching UIDs.
301 * @return bool|array The row data if a matching record was found, FALSE otherwise.
302 */
303 protected function getMatchingRecordRowByTranslation(array $relatedUids) {
304 if ($this->originalLanguageUid === FALSE) {
305 return FALSE;
306 }
307
308 $fieldCtrlConfig = $GLOBALS['TCA'][$this->currentTable]['ctrl'];
309 if (empty($fieldCtrlConfig['languageField'])) {
310 return FALSE;
311 }
312
313 $languageField = $fieldCtrlConfig['languageField'];
314 $foundRows = array();
315 foreach ($relatedUids as $relatedUid) {
316 $currentRow = $this->getRecordRow($relatedUid);
317 if (!isset($currentRow[$languageField])) {
318 continue;
319 }
320 if ((int)$currentRow[$languageField] === $this->originalLanguageUid) {
321 $foundRows[] = $currentRow;
322 }
323 }
324
325 $relatedRow = FALSE;
326 if (count($foundRows) === 1) {
327 $relatedRow = $foundRows[0];
328 }
329
330 return $relatedRow;
331 }
332 }