[TASK] Remove l10n_cat leftover
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / Container / SingleFieldContainer.php
1 <?php
2 namespace TYPO3\CMS\Backend\Form\Container;
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\InlineStackProcessor;
18 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
19 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
20 use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
21 use TYPO3\CMS\Core\Utility\GeneralUtility;
22 use TYPO3\CMS\Core\Utility\MathUtility;
23 use TYPO3\CMS\Lang\LanguageService;
24
25 /**
26 * Container around a "single field".
27 *
28 * This container is the last one in the chain before processing is handed over to single element classes.
29 * If a single field is of type flex or inline, it however creates FlexFormEntryContainer or InlineControlContainer.
30 *
31 * The container does various checks and processing for a given single fields.
32 */
33 class SingleFieldContainer extends AbstractContainer
34 {
35 /**
36 * Entry method
37 *
38 * @throws \InvalidArgumentException
39 * @return array As defined in initializeResultArray() of AbstractNode
40 */
41 public function render()
42 {
43 $backendUser = $this->getBackendUserAuthentication();
44 $resultArray = $this->initializeResultArray();
45
46 $table = $this->data['tableName'];
47 $row = $this->data['databaseRow'];
48 $fieldName = $this->data['fieldName'];
49
50 $parameterArray = [];
51 $parameterArray['fieldConf'] = $this->data['processedTca']['columns'][$fieldName];
52
53 $isOverlay = false;
54
55 // This field decides whether the current record is an overlay (as opposed to being a standalone record)
56 // Based on this decision we need to trigger field exclusion or special rendering (like readOnly)
57 if (isset($this->data['processedTca']['ctrl']['transOrigPointerField'])
58 && is_array($this->data['processedTca']['columns'][$this->data['processedTca']['ctrl']['transOrigPointerField']])
59 ) {
60 $parentValue = $row[$this->data['processedTca']['ctrl']['transOrigPointerField']];
61 if (MathUtility::canBeInterpretedAsInteger($parentValue)) {
62 $isOverlay = (bool)$parentValue;
63 } elseif (is_array($parentValue)) {
64 // This case may apply if the value has been converted to an array by the select or group data provider
65 $isOverlay = !empty($parentValue) ? (bool)$parentValue[0] : false;
66 } else {
67 throw new \InvalidArgumentException(
68 'The given value for the original language field ' . $this->data['processedTca']['ctrl']['transOrigPointerField']
69 . ' of table ' . $table . ' contains an invalid value.',
70 1470742770
71 );
72 }
73 }
74
75 // A couple of early returns in case the field should not be rendered
76 // Check if this field is configured and editable according to exclude fields and other configuration
77 if (// Return if BE-user has no access rights to this field, @todo: another user access rights check!
78 $parameterArray['fieldConf']['exclude'] && !$backendUser->check('non_exclude_fields', $table . ':' . $fieldName)
79 || $parameterArray['fieldConf']['config']['type'] === 'passthrough'
80 // Return if field should not be rendered in translated records
81 || $isOverlay && empty($parameterArray['fieldConf']['l10n_display']) && $parameterArray['fieldConf']['l10n_mode'] === 'exclude'
82 || $this->inlineFieldShouldBeSkipped()
83 ) {
84 return $resultArray;
85 }
86
87 $parameterArray['fieldTSConfig'] = [];
88 if (isset($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'])
89 && is_array($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'])
90 ) {
91 $parameterArray['fieldTSConfig'] = $this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'];
92 }
93 if ($parameterArray['fieldTSConfig']['disabled']) {
94 return $resultArray;
95 }
96
97 // Override fieldConf by fieldTSconfig:
98 $parameterArray['fieldConf']['config'] = FormEngineUtility::overrideFieldConf($parameterArray['fieldConf']['config'], $parameterArray['fieldTSConfig']);
99 $parameterArray['itemFormElName'] = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
100 $parameterArray['itemFormElID'] = 'data_' . $table . '_' . $row['uid'] . '_' . $fieldName;
101 $newElementBaseName = $this->data['elementBaseName'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
102
103 // The value to show in the form field.
104 $parameterArray['itemFormElValue'] = $row[$fieldName];
105 // Set field to read-only if configured for translated records to show default language content as readonly
106 if ($parameterArray['fieldConf']['l10n_display']
107 && GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'defaultAsReadonly')
108 && $isOverlay
109 ) {
110 $parameterArray['fieldConf']['config']['readOnly'] = true;
111 $parameterArray['itemFormElValue'] = $this->data['defaultLanguageRow'][$fieldName];
112 }
113
114 if (strpos($this->data['processedTca']['ctrl']['type'], ':') === false) {
115 $typeField = $this->data['processedTca']['ctrl']['type'];
116 } else {
117 $typeField = substr($this->data['processedTca']['ctrl']['type'], 0, strpos($this->data['processedTca']['ctrl']['type'], ':'));
118 }
119 // Create a JavaScript code line which will ask the user to save/update the form due to changing the element.
120 // This is used for eg. "type" fields and others configured with "onChange"
121 if (!empty($this->data['processedTca']['ctrl']['type']) && $fieldName === $typeField
122 || isset($parameterArray['fieldConf']['onChange']) && $parameterArray['fieldConf']['onChange'] === 'reload'
123 ) {
124 if ($backendUser->jsConfirmation(JsConfirmation::TYPE_CHANGE)) {
125 $alertMsgOnChange = 'top.TYPO3.Modal.confirm('
126 . 'TYPO3.lang["FormEngine.refreshRequiredTitle"],'
127 . ' TYPO3.lang["FormEngine.refreshRequiredContent"]'
128 . ')'
129 . '.on('
130 . '"button.clicked",'
131 . ' function(e) { if (e.target.name == "ok" && TBE_EDITOR.checkSubmit(-1)) { TBE_EDITOR.submitForm() } top.TYPO3.Modal.dismiss(); }'
132 . ');';
133 } else {
134 $alertMsgOnChange = 'if (TBE_EDITOR.checkSubmit(-1)){ TBE_EDITOR.submitForm() };';
135 }
136 } else {
137 $alertMsgOnChange = '';
138 }
139
140 // JavaScript code for event handlers:
141 $parameterArray['fieldChangeFunc'] = [];
142 $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = 'TBE_EDITOR.fieldChanged('
143 . GeneralUtility::quoteJSvalue($table) . ','
144 . GeneralUtility::quoteJSvalue($row['uid']) . ','
145 . GeneralUtility::quoteJSvalue($fieldName) . ','
146 . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName'])
147 . ');';
148 if ($alertMsgOnChange) {
149 $parameterArray['fieldChangeFunc']['alert'] = $alertMsgOnChange;
150 }
151
152 // If this is the child of an inline type and it is the field creating the label
153 if ($this->isInlineChildAndLabelField($table, $fieldName)) {
154 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
155 $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
156 $inlineDomObjectId = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
157 $inlineObjectId = implode(
158 '-',
159 [
160 $inlineDomObjectId,
161 $table,
162 $row['uid']
163 ]
164 );
165 $parameterArray['fieldChangeFunc']['inline'] = 'inline.handleChangedField('
166 . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ','
167 . GeneralUtility::quoteJSvalue($inlineObjectId)
168 . ');';
169 }
170
171 // Based on the type of the item, call a render function on a child element
172 $options = $this->data;
173 $options['parameterArray'] = $parameterArray;
174 $options['elementBaseName'] = $newElementBaseName;
175 if (!empty($parameterArray['fieldConf']['config']['renderType'])) {
176 $options['renderType'] = $parameterArray['fieldConf']['config']['renderType'];
177 } else {
178 // Fallback to type if no renderType is given
179 $options['renderType'] = $parameterArray['fieldConf']['config']['type'];
180 }
181 $resultArray = $this->nodeFactory->create($options)->render();
182 return $resultArray;
183 }
184
185 /**
186 * Checks if the $table is the child of an inline type AND the $field is the label field of this table.
187 * This function is used to dynamically update the label while editing. This has no effect on labels,
188 * that were processed by a FormEngine-hook on saving.
189 *
190 * @param string $table The table to check
191 * @param string $field The field on this table to check
192 * @return bool Is inline child and field is responsible for the label
193 */
194 protected function isInlineChildAndLabelField($table, $field)
195 {
196 /** @var InlineStackProcessor $inlineStackProcessor */
197 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
198 $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
199 $level = $inlineStackProcessor->getStructureLevel(-1);
200 if ($level['config']['foreign_label']) {
201 $label = $level['config']['foreign_label'];
202 } else {
203 $label = $this->data['processedTca']['ctrl']['label'];
204 }
205 return $level['config']['foreign_table'] === $table && $label === $field;
206 }
207
208 /**
209 * Rendering of inline fields should be skipped under certain circumstances
210 *
211 * @return bool TRUE if field should be skipped based on inline configuration
212 */
213 protected function inlineFieldShouldBeSkipped()
214 {
215 $table = $this->data['tableName'];
216 $fieldName = $this->data['fieldName'];
217 $fieldConfig = $this->data['processedTca']['columns'][$fieldName]['config'];
218
219 /** @var InlineStackProcessor $inlineStackProcessor */
220 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
221 $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
222 $structureDepth = $inlineStackProcessor->getStructureDepth();
223
224 $skipThisField = false;
225 if ($structureDepth > 0) {
226 $searchArray = [
227 '%OR' => [
228 'config' => [
229 0 => [
230 '%AND' => [
231 'foreign_table' => $table,
232 '%OR' => [
233 '%AND' => [
234 'appearance' => ['useCombination' => true],
235 'foreign_selector' => $fieldName
236 ],
237 'MM' => $fieldConfig['MM']
238 ]
239 ]
240 ],
241 1 => [
242 '%AND' => [
243 'foreign_table' => $fieldConfig['foreign_table'],
244 'foreign_selector' => $fieldConfig['foreign_field']
245 ]
246 ]
247 ]
248 ]
249 ];
250 // Get the parent record from structure stack
251 $level = $inlineStackProcessor->getStructureLevel(-1);
252 // If we have symmetric fields, check on which side we are and hide fields, that are set automatically:
253 if ($this->data['isOnSymmetricSide']) {
254 $searchArray['%OR']['config'][0]['%AND']['%OR']['symmetric_field'] = $fieldName;
255 $searchArray['%OR']['config'][0]['%AND']['%OR']['symmetric_sortby'] = $fieldName;
256 } else {
257 $searchArray['%OR']['config'][0]['%AND']['%OR']['foreign_field'] = $fieldName;
258 $searchArray['%OR']['config'][0]['%AND']['%OR']['foreign_sortby'] = $fieldName;
259 }
260 $skipThisField = $this->arrayCompareComplex($level, $searchArray);
261 }
262 return $skipThisField;
263 }
264
265 /**
266 * Handles complex comparison requests on an array.
267 * A request could look like the following:
268 *
269 * $searchArray = array(
270 * '%AND' => array(
271 * 'key1' => 'value1',
272 * 'key2' => 'value2',
273 * '%OR' => array(
274 * 'subarray' => array(
275 * 'subkey' => 'subvalue'
276 * ),
277 * 'key3' => 'value3',
278 * 'key4' => 'value4'
279 * )
280 * )
281 * );
282 *
283 * It is possible to use the array keys '%AND.1', '%AND.2', etc. to prevent
284 * overwriting the sub-array. It could be necessary, if you use complex comparisons.
285 *
286 * The example above means, key1 *AND* key2 (and their values) have to match with
287 * the $subjectArray and additional one *OR* key3 or key4 have to meet the same
288 * condition.
289 * It is also possible to compare parts of a sub-array (e.g. "subarray"), so this
290 * function recurses down one level in that sub-array.
291 *
292 * @param array $subjectArray The array to search in
293 * @param array $searchArray The array with keys and values to search for
294 * @param string $type Use '%AND' or '%OR' for comparison
295 * @return bool The result of the comparison
296 */
297 protected function arrayCompareComplex($subjectArray, $searchArray, $type = '')
298 {
299 $localMatches = 0;
300 $localEntries = 0;
301 if (is_array($searchArray) && !empty($searchArray)) {
302 // If no type was passed, try to determine
303 if (!$type) {
304 reset($searchArray);
305 $type = key($searchArray);
306 $searchArray = current($searchArray);
307 }
308 // We use '%AND' and '%OR' in uppercase
309 $type = strtoupper($type);
310 // Split regular elements from sub elements
311 foreach ($searchArray as $key => $value) {
312 $localEntries++;
313 // Process a sub-group of OR-conditions
314 if ($key === '%OR') {
315 $localMatches += $this->arrayCompareComplex($subjectArray, $value, '%OR') ? 1 : 0;
316 } elseif ($key === '%AND') {
317 $localMatches += $this->arrayCompareComplex($subjectArray, $value, '%AND') ? 1 : 0;
318 } elseif (is_array($value) && $this->isAssociativeArray($searchArray)) {
319 $localMatches += $this->arrayCompareComplex($subjectArray[$key], $value, $type) ? 1 : 0;
320 } elseif (is_array($value)) {
321 $localMatches += $this->arrayCompareComplex($subjectArray, $value, $type) ? 1 : 0;
322 } else {
323 if (isset($subjectArray[$key]) && isset($value)) {
324 // Boolean match:
325 if (is_bool($value)) {
326 $localMatches += !($subjectArray[$key] xor $value) ? 1 : 0;
327 } elseif (is_numeric($subjectArray[$key]) && is_numeric($value)) {
328 $localMatches += $subjectArray[$key] == $value ? 1 : 0;
329 } else {
330 $localMatches += $subjectArray[$key] === $value ? 1 : 0;
331 }
332 }
333 }
334 // If one or more matches are required ('OR'), return TRUE after the first successful match
335 if ($type === '%OR' && $localMatches > 0) {
336 return true;
337 }
338 // If all matches are required ('AND') and we have no result after the first run, return FALSE
339 if ($type === '%AND' && $localMatches == 0) {
340 return false;
341 }
342 }
343 }
344 // Return the result for '%AND' (if nothing was checked, TRUE is returned)
345 return $localEntries === $localMatches;
346 }
347
348 /**
349 * Checks whether an object is an associative array.
350 *
351 * @param mixed $object The object to be checked
352 * @return bool Returns TRUE, if the object is an associative array
353 */
354 protected function isAssociativeArray($object)
355 {
356 return is_array($object) && !empty($object) && array_keys($object) !== range(0, count($object) - 1);
357 }
358
359 /**
360 * @return BackendUserAuthentication
361 */
362 protected function getBackendUserAuthentication()
363 {
364 return $GLOBALS['BE_USER'];
365 }
366
367 /**
368 * @return LanguageService
369 */
370 protected function getLanguageService()
371 {
372 return $GLOBALS['LANG'];
373 }
374 }