[TASK] Deprecate inline localizationMode
[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 // @todo: localizationMode still needs handling!
83 // @deprecated: IRRE 'localizationMode' is deprecated and will be removed in TYPO3 CMS 9
84 || $isOverlay && $this->data['localizationMode'] && $this->data['localizationMode'] !== $parameterArray['fieldConf']['l10n_cat']
85 || $this->inlineFieldShouldBeSkipped()
86 ) {
87 return $resultArray;
88 }
89
90 $parameterArray['fieldTSConfig'] = [];
91 if (isset($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'])
92 && is_array($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'])
93 ) {
94 $parameterArray['fieldTSConfig'] = $this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'];
95 }
96 if ($parameterArray['fieldTSConfig']['disabled']) {
97 return $resultArray;
98 }
99
100 // Override fieldConf by fieldTSconfig:
101 $parameterArray['fieldConf']['config'] = FormEngineUtility::overrideFieldConf($parameterArray['fieldConf']['config'], $parameterArray['fieldTSConfig']);
102 $parameterArray['itemFormElName'] = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
103 $parameterArray['itemFormElID'] = 'data_' . $table . '_' . $row['uid'] . '_' . $fieldName;
104 $newElementBaseName = $this->data['elementBaseName'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
105
106 // The value to show in the form field.
107 $parameterArray['itemFormElValue'] = $row[$fieldName];
108 // Set field to read-only if configured for translated records to show default language content as readonly
109 if ($parameterArray['fieldConf']['l10n_display']
110 && GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'defaultAsReadonly')
111 && $isOverlay
112 ) {
113 $parameterArray['fieldConf']['config']['readOnly'] = true;
114 $parameterArray['itemFormElValue'] = $this->data['defaultLanguageRow'][$fieldName];
115 }
116
117 if (strpos($this->data['processedTca']['ctrl']['type'], ':') === false) {
118 $typeField = $this->data['processedTca']['ctrl']['type'];
119 } else {
120 $typeField = substr($this->data['processedTca']['ctrl']['type'], 0, strpos($this->data['processedTca']['ctrl']['type'], ':'));
121 }
122 // Create a JavaScript code line which will ask the user to save/update the form due to changing the element.
123 // This is used for eg. "type" fields and others configured with "onChange"
124 if (!empty($this->data['processedTca']['ctrl']['type']) && $fieldName === $typeField
125 || isset($parameterArray['fieldConf']['onChange']) && $parameterArray['fieldConf']['onChange'] === 'reload'
126 ) {
127 if ($backendUser->jsConfirmation(JsConfirmation::TYPE_CHANGE)) {
128 $alertMsgOnChange = 'top.TYPO3.Modal.confirm('
129 . 'TYPO3.lang["FormEngine.refreshRequiredTitle"],'
130 . ' TYPO3.lang["FormEngine.refreshRequiredContent"]'
131 . ')'
132 . '.on('
133 . '"button.clicked",'
134 . ' function(e) { if (e.target.name == "ok" && TBE_EDITOR.checkSubmit(-1)) { TBE_EDITOR.submitForm() } top.TYPO3.Modal.dismiss(); }'
135 . ');';
136 } else {
137 $alertMsgOnChange = 'if (TBE_EDITOR.checkSubmit(-1)){ TBE_EDITOR.submitForm() };';
138 }
139 } else {
140 $alertMsgOnChange = '';
141 }
142
143 // JavaScript code for event handlers:
144 $parameterArray['fieldChangeFunc'] = [];
145 $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = 'TBE_EDITOR.fieldChanged('
146 . GeneralUtility::quoteJSvalue($table) . ','
147 . GeneralUtility::quoteJSvalue($row['uid']) . ','
148 . GeneralUtility::quoteJSvalue($fieldName) . ','
149 . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName'])
150 . ');';
151 if ($alertMsgOnChange) {
152 $parameterArray['fieldChangeFunc']['alert'] = $alertMsgOnChange;
153 }
154
155 // If this is the child of an inline type and it is the field creating the label
156 if ($this->isInlineChildAndLabelField($table, $fieldName)) {
157 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
158 $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
159 $inlineDomObjectId = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
160 $inlineObjectId = implode(
161 '-',
162 [
163 $inlineDomObjectId,
164 $table,
165 $row['uid']
166 ]
167 );
168 $parameterArray['fieldChangeFunc']['inline'] = 'inline.handleChangedField('
169 . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ','
170 . GeneralUtility::quoteJSvalue($inlineObjectId)
171 . ');';
172 }
173
174 // Based on the type of the item, call a render function on a child element
175 $options = $this->data;
176 $options['parameterArray'] = $parameterArray;
177 $options['elementBaseName'] = $newElementBaseName;
178 if (!empty($parameterArray['fieldConf']['config']['renderType'])) {
179 $options['renderType'] = $parameterArray['fieldConf']['config']['renderType'];
180 } else {
181 // Fallback to type if no renderType is given
182 $options['renderType'] = $parameterArray['fieldConf']['config']['type'];
183 }
184 $resultArray = $this->nodeFactory->create($options)->render();
185 return $resultArray;
186 }
187
188 /**
189 * Checks if the $table is the child of an inline type AND the $field is the label field of this table.
190 * This function is used to dynamically update the label while editing. This has no effect on labels,
191 * that were processed by a FormEngine-hook on saving.
192 *
193 * @param string $table The table to check
194 * @param string $field The field on this table to check
195 * @return bool Is inline child and field is responsible for the label
196 */
197 protected function isInlineChildAndLabelField($table, $field)
198 {
199 /** @var InlineStackProcessor $inlineStackProcessor */
200 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
201 $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
202 $level = $inlineStackProcessor->getStructureLevel(-1);
203 if ($level['config']['foreign_label']) {
204 $label = $level['config']['foreign_label'];
205 } else {
206 $label = $this->data['processedTca']['ctrl']['label'];
207 }
208 return $level['config']['foreign_table'] === $table && $label === $field;
209 }
210
211 /**
212 * Rendering of inline fields should be skipped under certain circumstances
213 *
214 * @return bool TRUE if field should be skipped based on inline configuration
215 */
216 protected function inlineFieldShouldBeSkipped()
217 {
218 $table = $this->data['tableName'];
219 $fieldName = $this->data['fieldName'];
220 $fieldConfig = $this->data['processedTca']['columns'][$fieldName]['config'];
221
222 /** @var InlineStackProcessor $inlineStackProcessor */
223 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
224 $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
225 $structureDepth = $inlineStackProcessor->getStructureDepth();
226
227 $skipThisField = false;
228 if ($structureDepth > 0) {
229 $searchArray = [
230 '%OR' => [
231 'config' => [
232 0 => [
233 '%AND' => [
234 'foreign_table' => $table,
235 '%OR' => [
236 '%AND' => [
237 'appearance' => ['useCombination' => true],
238 'foreign_selector' => $fieldName
239 ],
240 'MM' => $fieldConfig['MM']
241 ]
242 ]
243 ],
244 1 => [
245 '%AND' => [
246 'foreign_table' => $fieldConfig['foreign_table'],
247 'foreign_selector' => $fieldConfig['foreign_field']
248 ]
249 ]
250 ]
251 ]
252 ];
253 // Get the parent record from structure stack
254 $level = $inlineStackProcessor->getStructureLevel(-1);
255 // If we have symmetric fields, check on which side we are and hide fields, that are set automatically:
256 if ($this->data['isOnSymmetricSide']) {
257 $searchArray['%OR']['config'][0]['%AND']['%OR']['symmetric_field'] = $fieldName;
258 $searchArray['%OR']['config'][0]['%AND']['%OR']['symmetric_sortby'] = $fieldName;
259 } else {
260 $searchArray['%OR']['config'][0]['%AND']['%OR']['foreign_field'] = $fieldName;
261 $searchArray['%OR']['config'][0]['%AND']['%OR']['foreign_sortby'] = $fieldName;
262 }
263 $skipThisField = $this->arrayCompareComplex($level, $searchArray);
264 }
265 return $skipThisField;
266 }
267
268 /**
269 * Handles complex comparison requests on an array.
270 * A request could look like the following:
271 *
272 * $searchArray = array(
273 * '%AND' => array(
274 * 'key1' => 'value1',
275 * 'key2' => 'value2',
276 * '%OR' => array(
277 * 'subarray' => array(
278 * 'subkey' => 'subvalue'
279 * ),
280 * 'key3' => 'value3',
281 * 'key4' => 'value4'
282 * )
283 * )
284 * );
285 *
286 * It is possible to use the array keys '%AND.1', '%AND.2', etc. to prevent
287 * overwriting the sub-array. It could be necessary, if you use complex comparisons.
288 *
289 * The example above means, key1 *AND* key2 (and their values) have to match with
290 * the $subjectArray and additional one *OR* key3 or key4 have to meet the same
291 * condition.
292 * It is also possible to compare parts of a sub-array (e.g. "subarray"), so this
293 * function recurses down one level in that sub-array.
294 *
295 * @param array $subjectArray The array to search in
296 * @param array $searchArray The array with keys and values to search for
297 * @param string $type Use '%AND' or '%OR' for comparison
298 * @return bool The result of the comparison
299 */
300 protected function arrayCompareComplex($subjectArray, $searchArray, $type = '')
301 {
302 $localMatches = 0;
303 $localEntries = 0;
304 if (is_array($searchArray) && !empty($searchArray)) {
305 // If no type was passed, try to determine
306 if (!$type) {
307 reset($searchArray);
308 $type = key($searchArray);
309 $searchArray = current($searchArray);
310 }
311 // We use '%AND' and '%OR' in uppercase
312 $type = strtoupper($type);
313 // Split regular elements from sub elements
314 foreach ($searchArray as $key => $value) {
315 $localEntries++;
316 // Process a sub-group of OR-conditions
317 if ($key === '%OR') {
318 $localMatches += $this->arrayCompareComplex($subjectArray, $value, '%OR') ? 1 : 0;
319 } elseif ($key === '%AND') {
320 $localMatches += $this->arrayCompareComplex($subjectArray, $value, '%AND') ? 1 : 0;
321 } elseif (is_array($value) && $this->isAssociativeArray($searchArray)) {
322 $localMatches += $this->arrayCompareComplex($subjectArray[$key], $value, $type) ? 1 : 0;
323 } elseif (is_array($value)) {
324 $localMatches += $this->arrayCompareComplex($subjectArray, $value, $type) ? 1 : 0;
325 } else {
326 if (isset($subjectArray[$key]) && isset($value)) {
327 // Boolean match:
328 if (is_bool($value)) {
329 $localMatches += !($subjectArray[$key] xor $value) ? 1 : 0;
330 } elseif (is_numeric($subjectArray[$key]) && is_numeric($value)) {
331 $localMatches += $subjectArray[$key] == $value ? 1 : 0;
332 } else {
333 $localMatches += $subjectArray[$key] === $value ? 1 : 0;
334 }
335 }
336 }
337 // If one or more matches are required ('OR'), return TRUE after the first successful match
338 if ($type === '%OR' && $localMatches > 0) {
339 return true;
340 }
341 // If all matches are required ('AND') and we have no result after the first run, return FALSE
342 if ($type === '%AND' && $localMatches == 0) {
343 return false;
344 }
345 }
346 }
347 // Return the result for '%AND' (if nothing was checked, TRUE is returned)
348 return $localEntries === $localMatches;
349 }
350
351 /**
352 * Checks whether an object is an associative array.
353 *
354 * @param mixed $object The object to be checked
355 * @return bool Returns TRUE, if the object is an associative array
356 */
357 protected function isAssociativeArray($object)
358 {
359 return is_array($object) && !empty($object) && array_keys($object) !== range(0, count($object) - 1);
360 }
361
362 /**
363 * @return BackendUserAuthentication
364 */
365 protected function getBackendUserAuthentication()
366 {
367 return $GLOBALS['BE_USER'];
368 }
369
370 /**
371 * @return LanguageService
372 */
373 protected function getLanguageService()
374 {
375 return $GLOBALS['LANG'];
376 }
377 }