[BUGFIX] Correct record title escaping
[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 $result['recordTitle'] = $this->getRecordTitleForField($fieldName, $result);
73 } elseif (isset($result['processedTca']['ctrl']['label_userFunc'])) {
74 // userFunc takes precedence over everything else
75 $parameters = [
76 'table' => $result['tableName'],
77 'row' => $result['databaseRow'],
78 'title' => '',
79 'options' => isset($result['processedTca']['ctrl']['label_userFunc_options'])
80 ? $result['processedTca']['ctrl']['label_userFunc_options']
81 : [],
82 ];
83 $null = null;
84 GeneralUtility::callUserFunction($result['processedTca']['ctrl']['label_userFunc'], $parameters, $null);
85 $result['recordTitle'] = $parameters['title'];
86 } else {
87 // standard record
88 $result = $this->getRecordTitleByLabelProperties($result);
89 }
90
91 return $result;
92 }
93
94 /**
95 * Build the record title from label, label_alt and label_alt_force properties
96 *
97 * @param array $result Incoming result array
98 * @return array Modified result array
99 */
100 protected function getRecordTitleByLabelProperties(array $result)
101 {
102 $titles = [];
103 $titleByLabel = $this->getRecordTitleForField($result['processedTca']['ctrl']['label'], $result);
104 if (!empty($titleByLabel)) {
105 $titles[] = $titleByLabel;
106 }
107
108 $labelAltForce = isset($result['processedTca']['ctrl']['label_alt_force'])
109 ? (bool)$result['processedTca']['ctrl']['label_alt_force']
110 : false;
111 if (!empty($result['processedTca']['ctrl']['label_alt']) && ($labelAltForce || empty($titleByLabel))) {
112 // Dive into label_alt evaluation if label_alt_force is set or if label did not came up with a title yet
113 $labelAltFields = GeneralUtility::trimExplode(',', $result['processedTca']['ctrl']['label_alt'], true);
114 foreach ($labelAltFields as $fieldName) {
115 $titleByLabelAlt = $this->getRecordTitleForField($fieldName, $result);
116 if (!empty($titleByLabelAlt)) {
117 $titles[] = $titleByLabelAlt;
118 }
119 if (!$labelAltForce && !empty($titleByLabelAlt)) {
120 // label_alt_force creates a comma separated list of multiple fields.
121 // If not set, one found field with content is enough
122 break;
123 }
124 }
125 }
126
127 $result['recordTitle'] = implode(', ', $titles);
128 return $result;
129 }
130
131 /**
132 * Record title of a single field
133 *
134 * @param string $fieldName Field to handle
135 * @param array $result Incoming result array
136 * @return string
137 */
138 protected function getRecordTitleForField($fieldName, $result)
139 {
140 if ($fieldName === 'uid') {
141 // uid return field content directly since it usually has not TCA definition
142 return $result['databaseRow']['uid'];
143 }
144
145 if (!isset($result['processedTca']['columns'][$fieldName]['config']['type'])
146 || !is_string($result['processedTca']['columns'][$fieldName]['config']['type'])
147 ) {
148 return '';
149 }
150
151 $recordTitle = '';
152 $rawValue = null;
153 if (array_key_exists($fieldName, $result['databaseRow'])) {
154 $rawValue = $result['databaseRow'][$fieldName];
155 }
156 $fieldConfig = $result['processedTca']['columns'][$fieldName]['config'];
157 switch ($fieldConfig['type']) {
158 case 'radio':
159 $recordTitle = $this->getRecordTitleForRadioType($rawValue, $fieldConfig);
160 break;
161 case 'inline':
162 // intentional fall-through
163 case 'select':
164 $recordTitle = $this->getRecordTitleForSelectType($rawValue, $fieldConfig);
165 break;
166 case 'group':
167 $recordTitle = $this->getRecordTitleForGroupType($rawValue, $fieldConfig);
168 break;
169 case 'check':
170 $recordTitle = $this->getRecordTitleForCheckboxType($rawValue, $fieldConfig);
171 break;
172 case 'input':
173 $recordTitle = $this->getRecordTitleForInputType($rawValue, $fieldConfig);
174 break;
175 case 'text':
176 $recordTitle = $this->getRecordTitleForTextType($rawValue);
177 case 'flex':
178 // @todo: Check if and how a label could be generated from flex field data
179 default:
180
181 }
182
183 return $recordTitle;
184 }
185
186 /**
187 * Return the record title for radio fields
188 *
189 * @param mixed $value Current database value of this field
190 * @param array $fieldConfig TCA field configuration
191 * @return string
192 */
193 protected function getRecordTitleForRadioType($value, $fieldConfig)
194 {
195 if (!isset($fieldConfig['items']) || !is_array($fieldConfig['items'])) {
196 return '';
197 }
198 foreach ($fieldConfig['items'] as $item) {
199 list($itemLabel, $itemValue) = $item;
200 if ((string)$value === (string)$itemValue) {
201 return $itemLabel;
202 }
203 }
204 return '';
205 }
206
207 /**
208 * Return the record title for database records
209 *
210 * @param mixed $value Current database value of this field
211 * @param array $fieldConfig TCA field configuration
212 * @return string
213 */
214 protected function getRecordTitleForSelectType($value, $fieldConfig)
215 {
216 if (!is_array($value)) {
217 return '';
218 }
219 $labelParts = [];
220 foreach ($value as $itemValue) {
221 $itemKey = array_search($itemValue, array_column($fieldConfig['items'], 1));
222 if ($itemKey !== false) {
223 $labelParts[] = $fieldConfig['items'][$itemKey][0];
224 }
225 }
226 $title = implode(', ', $labelParts);
227 if (empty($title) && !empty($value)) {
228 $title = implode(', ', $value);
229 }
230 return $title;
231 }
232
233 /**
234 * Return the record title for database records
235 *
236 * @param mixed $value Current database value of this field
237 * @param array $fieldConfig TCA field configuration
238 * @return string
239 */
240 protected function getRecordTitleForGroupType($value, $fieldConfig)
241 {
242 if ($fieldConfig['internal_type'] !== 'db') {
243 return implode(', ', GeneralUtility::trimExplode(',', $value, true));
244 }
245 $labelParts = array_map(
246 function ($rawLabelItem) {
247 return array_pop(GeneralUtility::trimExplode('|', $rawLabelItem, true, 2));
248 },
249 GeneralUtility::trimExplode(',', $value, true)
250 );
251 if (!empty($labelParts)) {
252 sort($labelParts);
253 return implode(', ', $labelParts);
254 }
255 return '';
256 }
257
258 /**
259 * Returns the record title for checkbox fields
260 *
261 * @param mixed $value Current database value of this field
262 * @param array $fieldConfig TCA field configuration
263 * @return string
264 */
265 protected function getRecordTitleForCheckboxType($value, $fieldConfig)
266 {
267 $languageService = $this->getLanguageService();
268 if (empty($fieldConfig['items']) || !is_array($fieldConfig['items'])) {
269 $title = (bool)$value
270 ? $languageService->sL('LLL:EXT:lang/locallang_common.xlf:yes')
271 : $languageService->sL('LLL:EXT:lang/locallang_common.xlf:no');
272 } else {
273 $labelParts = [];
274 foreach ($fieldConfig['items'] as $key => $val) {
275 if ($value & pow(2, $key)) {
276 $labelParts[] = $val[0];
277 }
278 }
279 $title = implode(', ', $labelParts);
280 }
281 return $title;
282 }
283
284 /**
285 * Returns the record title for input fields
286 *
287 * @param mixed $value Current database value of this field
288 * @param array $fieldConfig TCA field configuration
289 * @return string
290 */
291 protected function getRecordTitleForInputType($value, $fieldConfig)
292 {
293 if (!isset($value)) {
294 return '';
295 }
296 $title = $value;
297 if (GeneralUtility::inList($fieldConfig['eval'], 'date')) {
298 if (isset($fieldConfig['dbType']) && $fieldConfig['dbType'] === 'date') {
299 $value = $value === '0000-00-00' ? 0 : (int)strtotime($value);
300 } else {
301 $value = (int)$value;
302 }
303 if (!empty($value)) {
304 $ageSuffix = '';
305 // Generate age suffix as long as not explicitly suppressed
306 if (!isset($fieldConfig['disableAgeDisplay']) || (bool)$fieldConfig['disableAgeDisplay'] === false) {
307 $ageDelta = $GLOBALS['EXEC_TIME'] - $value;
308 $calculatedAge = BackendUtility::calcAge(
309 abs($ageDelta),
310 $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.minutesHoursDaysYears')
311 );
312 $ageSuffix = ' (' . ($ageDelta > 0 ? '-' : '') . $calculatedAge . ')';
313 }
314 $title = BackendUtility::date($value) . $ageSuffix;
315 }
316 } elseif (GeneralUtility::inList($fieldConfig['eval'], 'time')) {
317 if (!empty($value)) {
318 $title = BackendUtility::time((int)$value, false);
319 }
320 } elseif (GeneralUtility::inList($fieldConfig['eval'], 'timesec')) {
321 if (!empty($value)) {
322 $title = BackendUtility::time((int)$value);
323 }
324 } elseif (GeneralUtility::inList($fieldConfig['eval'], 'datetime')) {
325 // Handle native date/time field
326 if (isset($fieldConfig['dbType']) && $fieldConfig['dbType'] === 'datetime') {
327 $value = $value === '0000-00-00 00:00:00' ? 0 : (int)strtotime($value);
328 } else {
329 $value = (int)$value;
330 }
331 if (!empty($value)) {
332 $title = BackendUtility::datetime($value);
333 }
334 }
335 return $title;
336 }
337
338 /**
339 * Returns the record title for text fields
340 *
341 * @param mixed $value Current database value of this field
342 * @return string
343 */
344 protected function getRecordTitleForTextType($value)
345 {
346 return trim(strip_tags($value));
347 }
348
349 /**
350 * @return DatabaseConnection
351 */
352 protected function getDatabaseConnection()
353 {
354 return $GLOBALS['TYPO3_DB'];
355 }
356
357 /**
358 * @return LanguageService
359 */
360 protected function getLanguageService()
361 {
362 return $GLOBALS['LANG'];
363 }
364 }