[SECURITY] XSS in TCA type inline
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / FormDataProvider / TcaRecordTitle.php
1 <?php
2 namespace TYPO3\CMS\Backend\Form\FormDataProvider;
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\FormDataProviderInterface;
18 use TYPO3\CMS\Backend\Utility\BackendUtility;
19 use TYPO3\CMS\Core\Database\DatabaseConnection;
20 use TYPO3\CMS\Core\Utility\GeneralUtility;
21 use TYPO3\CMS\Lang\LanguageService;
22
23 /**
24 * Determine the title of a record and write it to $result['recordTitle'].
25 *
26 * TCA ctrl fields like label and label_alt are evaluated and their
27 * current values from databaseRow used to create the title.
28 */
29 class TcaRecordTitle implements FormDataProviderInterface
30 {
31 /**
32 * Enrich the processed record information with the resolved title
33 *
34 * @param array $result Incoming result array
35 * @return array Modified array
36 */
37 public function addData(array $result)
38 {
39 if (!isset($result['processedTca']['ctrl']['label'])) {
40 throw new \UnexpectedValueException(
41 'TCA of table ' . $result['tableName'] . ' misses required [\'ctrl\'][\'label\'] definition.',
42 1443706103
43 );
44 }
45
46 if ($result['isInlineChild'] && isset($result['processedTca']['ctrl']['formattedLabel_userFunc'])) {
47 // inline child with formatted user func is first
48 $parameters = [
49 'table' => $result['tableName'],
50 'row' => $result['databaseRow'],
51 'title' => '',
52 'isOnSymmetricSide' => $result['isOnSymmetricSide'],
53 'options' => isset($result['processedTca']['ctrl']['formattedLabel_userFunc_options'])
54 ? $result['processedTca']['ctrl']['formattedLabel_userFunc_options']
55 : [],
56 'parent' => [
57 'uid' => $result['databaseRow']['uid'],
58 'config' => $result['inlineParentConfig']
59 ]
60 ];
61 // callUserFunction requires a third parameter, but we don't want to give $this as reference!
62 $null = null;
63 GeneralUtility::callUserFunction($result['processedTca']['ctrl']['formattedLabel_userFunc'], $parameters, $null);
64 $result['recordTitle'] = $parameters['title'];
65 } elseif ($result['isInlineChild'] && (isset($result['inlineParentConfig']['foreign_label'])
66 || isset($result['inlineParentConfig']['symmetric_label']))
67 ) {
68 // inline child with foreign label or symmetric inline child with symmetric_label
69 $fieldName = $result['isOnSymmetricSide']
70 ? $result['inlineParentConfig']['symmetric_label']
71 : $result['inlineParentConfig']['foreign_label'];
72 // @todo: this is a mixup here. problem is the prep method cuts the string, but also hsc's the thing.
73 // @todo: this is uncool for the userfuncs, so it is applied only here. however, the OuterWrapContainer
74 // @todo: also prep()'s the title created by the else patch below ... find a better separation and clean this up!
75 $result['recordTitle'] = BackendUtility::getRecordTitlePrep($this->getRecordTitleForField($fieldName, $result));
76 } elseif (isset($result['processedTca']['ctrl']['label_userFunc'])) {
77 // userFunc takes precedence over everything else
78 $parameters = [
79 'table' => $result['tableName'],
80 'row' => $result['databaseRow'],
81 'title' => '',
82 'options' => isset($result['processedTca']['ctrl']['label_userFunc_options'])
83 ? $result['processedTca']['ctrl']['label_userFunc_options']
84 : [],
85 ];
86 $null = null;
87 GeneralUtility::callUserFunction($result['processedTca']['ctrl']['label_userFunc'], $parameters, $null);
88 $result['recordTitle'] = $parameters['title'];
89 } else {
90 // standard record
91 $result = $this->getRecordTitleByLabelProperties($result);
92 }
93
94 return $result;
95 }
96
97 /**
98 * Build the record title from label, label_alt and label_alt_force properties
99 *
100 * @param array $result Incoming result array
101 * @return array Modified result array
102 */
103 protected function getRecordTitleByLabelProperties(array $result)
104 {
105 $titles = [];
106 $titleByLabel = $this->getRecordTitleForField($result['processedTca']['ctrl']['label'], $result);
107 if (!empty($titleByLabel)) {
108 $titles[] = $titleByLabel;
109 }
110
111 $labelAltForce = isset($result['processedTca']['ctrl']['label_alt_force'])
112 ? (bool)$result['processedTca']['ctrl']['label_alt_force']
113 : false;
114 if (!empty($result['processedTca']['ctrl']['label_alt']) && ($labelAltForce || empty($titleByLabel))) {
115 // Dive into label_alt evaluation if label_alt_force is set or if label did not came up with a title yet
116 $labelAltFields = GeneralUtility::trimExplode(',', $result['processedTca']['ctrl']['label_alt'], true);
117 foreach ($labelAltFields as $fieldName) {
118 $titleByLabelAlt = $this->getRecordTitleForField($fieldName, $result);
119 if (!empty($titleByLabelAlt)) {
120 $titles[] = $titleByLabelAlt;
121 }
122 if (!$labelAltForce && !empty($titleByLabelAlt)) {
123 // label_alt_force creates a comma separated list of multiple fields.
124 // If not set, one found field with content is enough
125 break;
126 }
127 }
128 }
129
130 $result['recordTitle'] = htmlspecialchars(implode(', ', $titles));
131 return $result;
132 }
133
134 /**
135 * Record title of a single field
136 *
137 * @param string $fieldName Field to handle
138 * @param array $result Incoming result array
139 * @return string
140 */
141 protected function getRecordTitleForField($fieldName, $result)
142 {
143 if ($fieldName === 'uid') {
144 // uid return field content directly since it usually has not TCA definition
145 return $result['databaseRow']['uid'];
146 }
147
148 if (!isset($result['processedTca']['columns'][$fieldName]['config']['type'])
149 || !is_string($result['processedTca']['columns'][$fieldName]['config']['type'])
150 ) {
151 return '';
152 }
153
154 $recordTitle = '';
155 $rawValue = null;
156 if (array_key_exists($fieldName, $result['databaseRow'])) {
157 $rawValue = $result['databaseRow'][$fieldName];
158 }
159 $fieldConfig = $result['processedTca']['columns'][$fieldName]['config'];
160 switch ($fieldConfig['type']) {
161 case 'radio':
162 $recordTitle = $this->getRecordTitleForRadioType($rawValue, $fieldConfig);
163 break;
164 case 'inline':
165 // intentional fall-through
166 case 'select':
167 $recordTitle = $this->getRecordTitleForSelectType($rawValue, $fieldConfig);
168 break;
169 case 'group':
170 $recordTitle = $this->getRecordTitleForGroupType($rawValue, $fieldConfig);
171 break;
172 case 'check':
173 $recordTitle = $this->getRecordTitleForCheckboxType($rawValue, $fieldConfig);
174 break;
175 case 'input':
176 $recordTitle = $this->getRecordTitleForInputType($rawValue, $fieldConfig);
177 break;
178 case 'text':
179 $recordTitle = $this->getRecordTitleForTextType($rawValue);
180 case 'flex':
181 // @todo: Check if and how a label could be generated from flex field data
182 default:
183
184 }
185
186 return $recordTitle;
187 }
188
189 /**
190 * Return the record title for radio fields
191 *
192 * @param mixed $value Current database value of this field
193 * @param array $fieldConfig TCA field configuration
194 * @return string
195 */
196 protected function getRecordTitleForRadioType($value, $fieldConfig)
197 {
198 if (!isset($fieldConfig['items']) || !is_array($fieldConfig['items'])) {
199 return '';
200 }
201 foreach ($fieldConfig['items'] as $item) {
202 list($itemLabel, $itemValue) = $item;
203 if ((string)$value === (string)$itemValue) {
204 return $itemLabel;
205 }
206 }
207 return '';
208 }
209
210 /**
211 * Return the record title for database records
212 *
213 * @param mixed $value Current database value of this field
214 * @param array $fieldConfig TCA field configuration
215 * @return string
216 */
217 protected function getRecordTitleForSelectType($value, $fieldConfig)
218 {
219 if (!is_array($value)) {
220 return '';
221 }
222 $labelParts = [];
223 foreach ($value as $itemValue) {
224 $itemKey = array_search($itemValue, array_column($fieldConfig['items'], 1));
225 if ($itemKey !== false) {
226 $labelParts[] = $fieldConfig['items'][$itemKey][0];
227 }
228 }
229 $title = implode(', ', $labelParts);
230 if (empty($title) && !empty($value)) {
231 $title = implode(', ', $value);
232 }
233 return $title;
234 }
235
236 /**
237 * Return the record title for database records
238 *
239 * @param mixed $value Current database value of this field
240 * @param array $fieldConfig TCA field configuration
241 * @return string
242 */
243 protected function getRecordTitleForGroupType($value, $fieldConfig)
244 {
245 if ($fieldConfig['internal_type'] !== 'db') {
246 return implode(', ', GeneralUtility::trimExplode(',', $value, true));
247 }
248 $labelParts = array_map(
249 function ($rawLabelItem) {
250 return array_pop(GeneralUtility::trimExplode('|', $rawLabelItem, true, 2));
251 },
252 GeneralUtility::trimExplode(',', $value, true)
253 );
254 if (!empty($labelParts)) {
255 sort($labelParts);
256 return implode(', ', $labelParts);
257 }
258 return '';
259 }
260
261 /**
262 * Returns the record title for checkbox fields
263 *
264 * @param mixed $value Current database value of this field
265 * @param array $fieldConfig TCA field configuration
266 * @return string
267 */
268 protected function getRecordTitleForCheckboxType($value, $fieldConfig)
269 {
270 $languageService = $this->getLanguageService();
271 if (empty($fieldConfig['items']) || !is_array($fieldConfig['items'])) {
272 $title = (bool)$value
273 ? $languageService->sL('LLL:EXT:lang/locallang_common.xlf:yes')
274 : $languageService->sL('LLL:EXT:lang/locallang_common.xlf:no');
275 } else {
276 $labelParts = [];
277 foreach ($fieldConfig['items'] as $key => $val) {
278 if ($value & pow(2, $key)) {
279 $labelParts[] = $val[0];
280 }
281 }
282 $title = implode(', ', $labelParts);
283 }
284 return $title;
285 }
286
287 /**
288 * Returns the record title for input fields
289 *
290 * @param mixed $value Current database value of this field
291 * @param array $fieldConfig TCA field configuration
292 * @return string
293 */
294 protected function getRecordTitleForInputType($value, $fieldConfig)
295 {
296 if (!isset($value)) {
297 return '';
298 }
299 $title = $value;
300 if (GeneralUtility::inList($fieldConfig['eval'], 'date')) {
301 if (isset($fieldConfig['dbType']) && $fieldConfig['dbType'] === 'date') {
302 $value = $value === '0000-00-00' ? 0 : (int)strtotime($value);
303 } else {
304 $value = (int)$value;
305 }
306 if (!empty($value)) {
307 $ageSuffix = '';
308 // Generate age suffix as long as not explicitly suppressed
309 if (!isset($fieldConfig['disableAgeDisplay']) || (bool)$fieldConfig['disableAgeDisplay'] === false) {
310 $ageDelta = $GLOBALS['EXEC_TIME'] - $value;
311 $calculatedAge = BackendUtility::calcAge(
312 abs($ageDelta),
313 $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.minutesHoursDaysYears')
314 );
315 $ageSuffix = ' (' . ($ageDelta > 0 ? '-' : '') . $calculatedAge . ')';
316 }
317 $title = BackendUtility::date($value) . $ageSuffix;
318 }
319 } elseif (GeneralUtility::inList($fieldConfig['eval'], 'time')) {
320 if (!empty($value)) {
321 $title = BackendUtility::time((int)$value, false);
322 }
323 } elseif (GeneralUtility::inList($fieldConfig['eval'], 'timesec')) {
324 if (!empty($value)) {
325 $title = BackendUtility::time((int)$value);
326 }
327 } elseif (GeneralUtility::inList($fieldConfig['eval'], 'datetime')) {
328 // Handle native date/time field
329 if (isset($fieldConfig['dbType']) && $fieldConfig['dbType'] === 'datetime') {
330 $value = $value === '0000-00-00 00:00:00' ? 0 : (int)strtotime($value);
331 } else {
332 $value = (int)$value;
333 }
334 if (!empty($value)) {
335 $title = BackendUtility::datetime($value);
336 }
337 }
338 return $title;
339 }
340
341 /**
342 * Returns the record title for text fields
343 *
344 * @param mixed $value Current database value of this field
345 * @return string
346 */
347 protected function getRecordTitleForTextType($value)
348 {
349 return trim(strip_tags($value));
350 }
351
352 /**
353 * @return DatabaseConnection
354 */
355 protected function getDatabaseConnection()
356 {
357 return $GLOBALS['TYPO3_DB'];
358 }
359
360 /**
361 * @return LanguageService
362 */
363 protected function getLanguageService()
364 {
365 return $GLOBALS['LANG'];
366 }
367 }