e07a20e4b72a0d54d89581d97138bffb38a89408
[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 * If the first record in the chain is translatable the language
60 * UID of this record is stored in this variable.
61 *
62 * @var int
63 */
64 protected $originalLanguageUid = NULL;
65
66 /**
67 * Inline first pid
68 *
69 * @var integer
70 */
71 protected $inlineFirstPid;
72
73 /**
74 * General prefix of forms
75 *
76 * @var string
77 */
78 protected $prependFormFieldNames;
79
80 /**
81 * Traverses the array of given field names by using the TCA.
82 *
83 * @param array $fieldNameArray The field names that should be traversed.
84 * @param string $tableName The starting table name.
85 * @param array $row The starting record row.
86 * @param int $inlineFirstPid Inline first pid
87 * @param string $prependFormFieldNames General prefix of forms
88 * @return mixed The value of the last field in the chain.
89 */
90 public function getTraversedFieldValue(array $fieldNameArray, $tableName, array $row, $inlineFirstPid, $prependFormFieldNames) {
91 $this->currentTable = $tableName;
92 $this->currentRow = $row;
93 $this->inlineFirstPid = $inlineFirstPid;
94 $this->prependFormFieldNames = $prependFormFieldNames;
95 $fieldValue = '';
96 if (count($fieldNameArray) > 0) {
97 $this->initializeOriginalLanguageUid();
98 $fieldValue = $this->getFieldValueRecursive($fieldNameArray);
99 }
100 return $fieldValue;
101 }
102
103 /**
104 * Checks if the current table is translatable and initializes the
105 * originalLanguageUid with the value of the languageField of the
106 * current row.
107 *
108 * @return void
109 */
110 protected function initializeOriginalLanguageUid() {
111 $fieldCtrlConfig = $GLOBALS['TCA'][$this->currentTable]['ctrl'];
112 if (!empty($fieldCtrlConfig['languageField']) && isset($this->currentRow[$fieldCtrlConfig['languageField']])) {
113 $this->originalLanguageUid = (int)$this->currentRow[$fieldCtrlConfig['languageField']];
114 } else {
115 $this->originalLanguageUid = FALSE;
116 }
117 }
118
119 /**
120 * Traverses the fields in the $fieldNameArray and tries to read
121 * the field values.
122 *
123 * @param array $fieldNameArray The field names that should be traversed.
124 * @return mixed The value of the last field.
125 */
126 protected function getFieldValueRecursive(array $fieldNameArray) {
127 $value = '';
128
129 foreach ($fieldNameArray as $fieldName) {
130 // Skip if a defined field was actually not present in the database row
131 // Using array_key_exists here, since TYPO3 supports NULL values as well
132 if (!array_key_exists($fieldName, $this->currentRow)) {
133 $value = '';
134 break;
135 }
136
137 $value = $this->currentRow[$fieldName];
138 if (empty($value)) {
139 break;
140 }
141
142 $this->currentRow = $this->getRelatedRecordRow($fieldName, $value);
143 if ($this->currentRow === FALSE) {
144 break;
145 }
146 }
147
148 if ((empty($value) || $this->forceAlternativeFieldValueUse) && !empty($this->alternativeFieldValue)) {
149 $value = $this->alternativeFieldValue;
150 }
151
152 return $value;
153 }
154
155 /**
156 * Tries to read the related record from the database depending on
157 * the TCA. Supported types are group (db), select and inline.
158 *
159 * @param string $fieldName The name of the field of which the related record should be fetched.
160 * @param string $value The current field value.
161 * @return array|boolean The related row if it could be found otherwise FALSE.
162 */
163 protected function getRelatedRecordRow($fieldName, $value) {
164 $fieldConfig = $GLOBALS['TCA'][$this->currentTable]['columns'][$fieldName]['config'];
165 $possibleUids = array();
166
167 switch ($fieldConfig['type']) {
168 case 'group':
169 $possibleUids = $this->getRelatedGroupFieldUids($fieldConfig, $value);
170 break;
171 case 'select':
172 // @todo: This misuses SelectElement and should be solved differently
173 /** @var $selectObject \TYPO3\CMS\Backend\Form\Element\SelectElement */
174 $selectObject = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Form\Element\SelectElement::class);
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 $inlineRelatedRecordResolver = GeneralUtility::makeInstance(InlineRelatedRecordResolver::class);
247 $items = $inlineRelatedRecordResolver->getRelatedRecords($this->currentTable, $fieldName, $this->currentRow, $PA, $fieldConfig, $this->inlineFirstPid, $this->prependFormFieldNames);
248 if ($items['count'] > 0) {
249 $this->currentTable = $fieldConfig['foreign_table'];
250 foreach ($items['records'] as $inlineRecord) {
251 $relatedUids[] = $inlineRecord['uid'];
252 }
253 }
254
255 return $relatedUids;
256 }
257
258 /**
259 * Will read the "allowed" value from the given field configuration
260 * and returns FALSE if none was defined or more than one.
261 *
262 * If exactly one table was defined the name of that table is returned.
263 *
264 * @param array $fieldConfig "config" section of a group field from the TCA.
265 * @return bool|string FALSE if none ore more than one table was found, otherwise the name of the table.
266 */
267 protected function getAllowedTableForGroupField(array $fieldConfig) {
268 $allowedTable = FALSE;
269
270 $allowedTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed'], TRUE);
271 if (count($allowedTables) === 1) {
272 $allowedTable = $allowedTables[0];
273 }
274
275 return $allowedTable;
276 }
277
278 /**
279 * Uses the DataPreprocessor to read a value from the database.
280 *
281 * The table name is read from the currentTable class variable.
282 *
283 * @param int $uid The UID of the record that should be fetched.
284 * @return array|boolean FALSE if the record can not be accessed, otherwise the data of the requested record.
285 */
286 protected function getRecordRow($uid) {
287 /** @var \TYPO3\CMS\Backend\Form\DataPreprocessor $dataPreprocessor */
288 $dataPreprocessor = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Form\DataPreprocessor::class);
289 $dataPreprocessor->fetchRecord($this->currentTable, $uid, '');
290 return reset($dataPreprocessor->regTableItems_data);
291 }
292
293 /**
294 * Tries to get the correct record based on the parent translation by
295 * traversing all given related UIDs and checking if their language UID
296 * matches the stored original language UID.
297 *
298 * If exactly one match was found for the original language the resulting
299 * row is returned, otherwise FALSE.
300 *
301 * @param array $relatedUids All possible matching UIDs.
302 * @return bool|array The row data if a matching record was found, FALSE otherwise.
303 */
304 protected function getMatchingRecordRowByTranslation(array $relatedUids) {
305 if ($this->originalLanguageUid === FALSE) {
306 return FALSE;
307 }
308
309 $fieldCtrlConfig = $GLOBALS['TCA'][$this->currentTable]['ctrl'];
310 if (empty($fieldCtrlConfig['languageField'])) {
311 return FALSE;
312 }
313
314 $languageField = $fieldCtrlConfig['languageField'];
315 $foundRows = array();
316 foreach ($relatedUids as $relatedUid) {
317 $currentRow = $this->getRecordRow($relatedUid);
318 if (!isset($currentRow[$languageField])) {
319 continue;
320 }
321 if ((int)$currentRow[$languageField] === $this->originalLanguageUid) {
322 $foundRows[] = $currentRow;
323 }
324 }
325
326 $relatedRow = FALSE;
327 if (count($foundRows) === 1) {
328 $relatedRow = $foundRows[0];
329 }
330
331 return $relatedRow;
332 }
333
334 }