[!!!][TASK] Improve flex and TCA handling in FormEngine
[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\Backend\Utility\BackendUtility;
20 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
21 use TYPO3\CMS\Core\Imaging\Icon;
22 use TYPO3\CMS\Core\Imaging\IconFactory;
23 use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
24 use TYPO3\CMS\Core\Utility\DiffUtility;
25 use TYPO3\CMS\Core\Utility\GeneralUtility;
26 use TYPO3\CMS\Core\Utility\MathUtility;
27 use TYPO3\CMS\Lang\LanguageService;
28
29 /**
30 * Container around a "single field".
31 *
32 * This container is the last one in the chain before processing is handed over to single element classes.
33 * If a single field is of type flex or inline, it however creates FlexFormEntryContainer or InlineControlContainer.
34 *
35 * The container does various checks and processing for a given single fields, for example it resolves
36 * display conditions and the HTML to compare different languages.
37 */
38 class SingleFieldContainer extends AbstractContainer
39 {
40 /**
41 * Entry method
42 *
43 * @throws \InvalidArgumentException
44 * @return array As defined in initializeResultArray() of AbstractNode
45 */
46 public function render()
47 {
48 $backendUser = $this->getBackendUserAuthentication();
49 $languageService = $this->getLanguageService();
50 $resultArray = $this->initializeResultArray();
51
52 $table = $this->data['tableName'];
53 $row = $this->data['databaseRow'];
54 $fieldName = $this->data['fieldName'];
55
56 // @todo: it should be safe at this point, this array exists ...
57 if (!is_array($this->data['processedTca']['columns'][$fieldName])) {
58 return $resultArray;
59 }
60
61 $parameterArray = [];
62 $parameterArray['fieldConf'] = $this->data['processedTca']['columns'][$fieldName];
63
64 $isOverlay = false;
65
66 // This field decides whether the current record is an overlay (as opposed to being a standalone record)
67 // Based on this decision we need to trigger field exclusion or special rendering (like readOnly)
68 if (isset($this->data['processedTca']['ctrl']['transOrigPointerField'])
69 && is_array($this->data['processedTca']['columns'][$this->data['processedTca']['ctrl']['transOrigPointerField']])
70 ) {
71 $parentValue = $row[$this->data['processedTca']['ctrl']['transOrigPointerField']];
72 if (MathUtility::canBeInterpretedAsInteger($parentValue)) {
73 $isOverlay = (bool)$parentValue;
74 } elseif (is_array($parentValue)) {
75 // This case may apply if the value has been converted to an array by the select or group data provider
76 $isOverlay = !empty($parentValue) ? (bool)$parentValue[0] : false;
77 } else {
78 throw new \InvalidArgumentException(
79 'The given value for the original language field ' . $this->data['processedTca']['ctrl']['transOrigPointerField']
80 . ' of table ' . $table . ' contains an invalid value.',
81 1470742770
82 );
83 }
84 }
85
86 // A couple of early returns in case the field should not be rendered
87 // Check if this field is configured and editable according to exclude fields and other configuration
88 if (// Return if BE-user has no access rights to this field, @todo: another user access rights check!
89 $parameterArray['fieldConf']['exclude'] && !$backendUser->check('non_exclude_fields', $table . ':' . $fieldName)
90 || $parameterArray['fieldConf']['config']['type'] === 'passthrough'
91 // Return if field should not be rendered in translated records
92 || $isOverlay && empty($parameterArray['fieldConf']['l10n_display']) && $parameterArray['fieldConf']['l10n_mode'] === 'exclude'
93 // @todo: localizationMode still needs handling!
94 || $isOverlay && $this->data['localizationMode'] && $this->data['localizationMode'] !== $parameterArray['fieldConf']['l10n_cat']
95 || $this->inlineFieldShouldBeSkipped()
96 ) {
97 return $resultArray;
98 }
99
100 $parameterArray['fieldTSConfig'] = [];
101 if (isset($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'])
102 && is_array($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'])
103 ) {
104 $parameterArray['fieldTSConfig'] = $this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'];
105 }
106 if ($parameterArray['fieldTSConfig']['disabled']) {
107 return $resultArray;
108 }
109
110 // Override fieldConf by fieldTSconfig:
111 $parameterArray['fieldConf']['config'] = FormEngineUtility::overrideFieldConf($parameterArray['fieldConf']['config'], $parameterArray['fieldTSConfig']);
112 $parameterArray['itemFormElName'] = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
113 $parameterArray['itemFormElID'] = 'data_' . $table . '_' . $row['uid'] . '_' . $fieldName;
114 $newElementBaseName = $this->data['elementBaseName'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
115
116 // The value to show in the form field.
117 $parameterArray['itemFormElValue'] = $row[$fieldName];
118 // Set field to read-only if configured for translated records to show default language content as readonly
119 if ($parameterArray['fieldConf']['l10n_display']
120 && GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'defaultAsReadonly')
121 && $isOverlay
122 ) {
123 $parameterArray['fieldConf']['config']['readOnly'] = true;
124 $parameterArray['itemFormElValue'] = $this->data['defaultLanguageRow'][$fieldName];
125 }
126
127 if (strpos($this->data['processedTca']['ctrl']['type'], ':') === false) {
128 $typeField = $this->data['processedTca']['ctrl']['type'];
129 } else {
130 $typeField = substr($this->data['processedTca']['ctrl']['type'], 0, strpos($this->data['processedTca']['ctrl']['type'], ':'));
131 }
132 // Create a JavaScript code line which will ask the user to save/update the form due to changing the element.
133 // This is used for eg. "type" fields and others configured with "onChange"
134 if (!empty($this->data['processedTca']['ctrl']['type']) && $fieldName === $typeField
135 || isset($parameterArray['fieldConf']['onChange']) && $parameterArray['fieldConf']['onChange'] === 'reload'
136 ) {
137 if ($backendUser->jsConfirmation(JsConfirmation::TYPE_CHANGE)) {
138 $alertMsgOnChange = 'top.TYPO3.Modal.confirm('
139 . 'TYPO3.lang["FormEngine.refreshRequiredTitle"],'
140 . ' TYPO3.lang["FormEngine.refreshRequiredContent"]'
141 . ')'
142 . '.on('
143 . '"button.clicked",'
144 . ' function(e) { if (e.target.name == "ok" && TBE_EDITOR.checkSubmit(-1)) { TBE_EDITOR.submitForm() } top.TYPO3.Modal.dismiss(); }'
145 . ');';
146 } else {
147 $alertMsgOnChange = 'if (TBE_EDITOR.checkSubmit(-1)){ TBE_EDITOR.submitForm() };';
148 }
149 } else {
150 $alertMsgOnChange = '';
151 }
152
153 // JavaScript code for event handlers:
154 $parameterArray['fieldChangeFunc'] = [];
155 $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = 'TBE_EDITOR.fieldChanged(' . GeneralUtility::quoteJSvalue($table) . ',' . GeneralUtility::quoteJSvalue($row['uid']) . ',' . GeneralUtility::quoteJSvalue($fieldName) . ',' . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ');';
156 if ($alertMsgOnChange) {
157 $parameterArray['fieldChangeFunc']['alert'] = $alertMsgOnChange;
158 }
159
160 // If this is the child of an inline type and it is the field creating the label
161 if ($this->isInlineChildAndLabelField($table, $fieldName)) {
162 /** @var InlineStackProcessor $inlineStackProcessor */
163 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
164 $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
165 $inlineDomObjectId = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
166 $inlineObjectId = implode(
167 '-',
168 [
169 $inlineDomObjectId,
170 $table,
171 $row['uid']
172 ]
173 );
174 $parameterArray['fieldChangeFunc']['inline'] = 'inline.handleChangedField(' . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ',' . GeneralUtility::quoteJSvalue($inlineObjectId) . ');';
175 }
176
177 // Based on the type of the item, call a render function on a child element
178 $options = $this->data;
179 $options['parameterArray'] = $parameterArray;
180 $options['elementBaseName'] = $newElementBaseName;
181 if (!empty($parameterArray['fieldConf']['config']['renderType'])) {
182 $options['renderType'] = $parameterArray['fieldConf']['config']['renderType'];
183 } else {
184 // Fallback to type if no renderType is given
185 $options['renderType'] = $parameterArray['fieldConf']['config']['type'];
186 }
187 $resultArray = $this->nodeFactory->create($options)->render();
188
189 // If output is empty stop further processing.
190 // This means there was internal processing only and we don't need to add additional information
191 if (empty($resultArray['html'])) {
192 return $resultArray;
193 }
194
195 $html = $resultArray['html'];
196
197 // @todo: the language handling, the null and the placeholder stuff should be embedded in the single
198 // @todo: element classes. Basically, this method should return here and have the element classes
199 // @todo: decide on language stuff and other wraps already.
200
201 // Add language + diff
202 $renderLanguageDiff = true;
203 if ($parameterArray['fieldConf']['l10n_display'] && (GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'hideDiff')
204 || GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'defaultAsReadonly'))
205 ) {
206 $renderLanguageDiff = false;
207 }
208 if ($renderLanguageDiff) {
209 $html = $this->renderDefaultLanguageContent($table, $fieldName, $row, $html);
210 $html = $this->renderDefaultLanguageDiff($table, $fieldName, $row, $html);
211 }
212
213 $fieldItemClasses = [
214 't3js-formengine-field-item'
215 ];
216
217 // NULL value and placeholder handling
218 $nullControlNameAttribute = ' name="' . htmlspecialchars('control[active][' . $table . '][' . $row['uid'] . '][' . $fieldName . ']') . '"';
219 if (!empty($parameterArray['fieldConf']['config']['eval']) && GeneralUtility::inList($parameterArray['fieldConf']['config']['eval'], 'null')
220 && (empty($parameterArray['fieldConf']['config']['mode']) || $parameterArray['fieldConf']['config']['mode'] !== 'useOrOverridePlaceholder')
221 ) {
222 // This field has eval=null set, but has no useOverridePlaceholder defined.
223 // Goal is to have a field that can distinct between NULL and empty string in the database.
224 // A checkbox and an additional hidden field will be created, both with the same name
225 // and prefixed with "control[active]". If the checkbox is set (value 1), the value from the casual
226 // input field will be written to the database. If the checkbox is not set, the hidden field
227 // transfers value=0 to DataHandler, the value of the input field will then be reset to NULL by the
228 // DataHandler at an early point in processing, so NULL will be written to DB as field value.
229
230 // If the value of the field *is* NULL at the moment, an additional class is set
231 // @todo: This does not work well at the moment, but is kept for now. see input_14 of ext:styleguide as example
232 $checked = ' checked="checked"';
233 if ($this->data['databaseRow'][$fieldName] === null) {
234 $fieldItemClasses[] = 'disabled';
235 $checked = '';
236 }
237
238 $formElementName = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
239 $onChange = htmlspecialchars(
240 'typo3form.fieldSetNull(' . GeneralUtility::quoteJSvalue($formElementName) . ', !this.checked)'
241 );
242
243 $nullValueWrap = [];
244 $nullValueWrap[] = '<div class="' . implode(' ', $fieldItemClasses) . '">';
245 $nullValueWrap[] = '<div class="t3-form-field-disable"></div>';
246 $nullValueWrap[] = '<div class="checkbox t3-form-field-eval-null-checkbox">';
247 $nullValueWrap[] = '<label>';
248 $nullValueWrap[] = '<input type="hidden"' . $nullControlNameAttribute . ' value="0" />';
249 $nullValueWrap[] = '<input type="checkbox"' . $nullControlNameAttribute . ' value="1" onchange="' . $onChange . '"' . $checked . ' /> &nbsp;';
250 $nullValueWrap[] = '</label>';
251 $nullValueWrap[] = '</div>';
252 $nullValueWrap[] = $html;
253 $nullValueWrap[] = '</div>';
254
255 $html = implode(LF, $nullValueWrap);
256 } elseif (isset($parameterArray['fieldConf']['config']['mode']) && $parameterArray['fieldConf']['config']['mode'] === 'useOrOverridePlaceholder') {
257 // This field has useOverridePlaceholder set.
258 // Here, a value from a deeper DB structure can be "fetched up" as value, and can also be overridden by a
259 // local value. This is used in FAL, where eg. the "title" field can have the default value from sys_file_metadata,
260 // the title field of sys_file_reference is then set to NULL. Or the "override" checkbox is set, and a string
261 // or an empty string is then written to the field of sys_file_reference.
262 // The situation is similar to the NULL handling above, but additionally a "default" value should be shown.
263 // To achieve this, again a hidden control[hidden] field is added together with a checkbox with the same name
264 // to transfer the information whether the default value should be used or not: Checkbox checked transfers 1 as
265 // value in control[active], meaning the overridden value should be used.
266 // Additionally to the casual input field, a second field is added containing the "placeholder" value. This
267 // field has no name attribute and is not transferred at all. Those two are then hidden / shown depending
268 // on the state of the above checkbox in via JS.
269
270 $placeholder = empty($parameterArray['fieldConf']['config']['placeholder']) ? '' : $parameterArray['fieldConf']['config']['placeholder'];
271 $onChange = 'typo3form.fieldTogglePlaceholder(' . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ', !this.checked)';
272 $checked = $parameterArray['itemFormElValue'] === null ? '' : ' checked="checked"';
273 $disabled = '';
274 $fallbackValue = 0;
275 if (strlen(BackendUtility::getRecordTitlePrep($placeholder, 20)) > 0) {
276 $overrideLabel = sprintf($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'), BackendUtility::getRecordTitlePrep($placeholder, 20));
277 } else {
278 $fallbackValue = 1;
279 $checked = ' checked="checked"';
280 $disabled = ' disabled="disabled"';
281 $overrideLabel = sprintf($languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'), BackendUtility::getRecordTitlePrep($placeholder, 20));
282 }
283
284 $resultArray['additionalJavaScriptPost'][] = 'typo3form.fieldTogglePlaceholder('
285 . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ', ' . ($checked ? 'false' : 'true') . ');';
286
287 // Renders an input or textarea field depending on type of "parent"
288 $options = [];
289 $options['databaseRow'] = [];
290 $options['table'] = '';
291 $options['parameterArray'] = $parameterArray;
292 $options['parameterArray']['itemFormElValue'] = GeneralUtility::fixed_lgd_cs($placeholder, 30);
293 $options['renderType'] = 'none';
294 $noneElementResult = $this->nodeFactory->create($options)->render();
295 $noneElementHtml = $noneElementResult['html'];
296
297 $placeholderWrap = [];
298 $placeholderWrap[] = '<div class="' . implode(' ', $fieldItemClasses) . '">';
299 $placeholderWrap[] = '<div class="t3-form-field-disable"></div>';
300 $placeholderWrap[] = '<div class="checkbox">';
301 $placeholderWrap[] = '<label>';
302 $placeholderWrap[] = '<input type="hidden"' . $nullControlNameAttribute . ' value="' . $fallbackValue . '" />';
303 $placeholderWrap[] = '<input type="checkbox"' . $nullControlNameAttribute . ' value="1" id="tce-forms-textfield-use-override-' . $fieldName . '-' . $row['uid'] . '" onchange="' . htmlspecialchars($onChange) . '"' . $checked . $disabled . ' />';
304 $placeholderWrap[] = $overrideLabel;
305 $placeholderWrap[] = '</label>';
306 $placeholderWrap[] = '</div>';
307 $placeholderWrap[] = '<div class="t3js-formengine-placeholder-placeholder">';
308 $placeholderWrap[] = $noneElementHtml;
309 $placeholderWrap[] = '</div>';
310 $placeholderWrap[] = '<div class="t3js-formengine-placeholder-formfield">';
311 $placeholderWrap[] = $html;
312 $placeholderWrap[] = '</div>';
313 $placeholderWrap[] = '</div>';
314
315 $html = implode(LF, $placeholderWrap);
316 } elseif ($parameterArray['fieldConf']['config']['type'] !== 'user' || empty($parameterArray['fieldConf']['config']['noTableWrapping'])) {
317 // Add a casual wrap if the field is not of type user with no wrap requested.
318 $standardWrap = [];
319 $standardWrap[] = '<div class="' . implode(' ', $fieldItemClasses) . '">';
320 $standardWrap[] = '<div class="t3-form-field-disable"></div>';
321 $standardWrap[] = $html;
322 $standardWrap[] = '</div>';
323
324 $html = implode(LF, $standardWrap);
325 }
326
327 $resultArray['html'] = $html;
328 return $resultArray;
329 }
330
331 /**
332 * Renders the display of default language record content around current field.
333 * Will render content if any is found in the internal array.
334 *
335 * @param string $table Table name of the record being edited
336 * @param string $field Field name represented by $item
337 * @param array $row Record array of the record being edited
338 * @param string $item HTML of the form field. This is what we add the content to.
339 * @return string Item string returned again, possibly with the original value added to.
340 */
341 protected function renderDefaultLanguageContent($table, $field, $row, $item)
342 {
343 if (is_array($this->data['defaultLanguageRow'])) {
344 $defaultLanguageValue = BackendUtility::getProcessedValue(
345 $table,
346 $field,
347 $this->data['defaultLanguageRow'][$field],
348 0,
349 true,
350 false,
351 $this->data['defaultLanguageRow']['uid'],
352 true,
353 $this->data['defaultLanguageRow']['pid']
354 );
355 $fieldConfig = $this->data['processedTca']['columns'][$field];
356 // Don't show content if it's for IRRE child records:
357 if ($fieldConfig['config']['type'] !== 'inline' && $fieldConfig['config']['type'] !== 'flex') {
358 /** @var IconFactory $iconFactory */
359 $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
360 if ($defaultLanguageValue !== '') {
361 $item .= '<div class="t3-form-original-language" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:localizeMergeIfNotBlank')) . '">'
362 . $iconFactory->getIcon($this->data['systemLanguageRows'][0]['flagIconIdentifier'], Icon::SIZE_SMALL)->render()
363 . $this->getMergeBehaviourIcon($fieldConfig['l10n_mode'])
364 . $this->previewFieldValue($defaultLanguageValue, $fieldConfig, $field) . '</div>';
365 }
366 $additionalPreviewLanguages = $this->data['additionalLanguageRows'];
367 foreach ($additionalPreviewLanguages as $previewLanguage) {
368 $defaultLanguageValue = BackendUtility::getProcessedValue(
369 $table,
370 $field,
371 $previewLanguage[$field],
372 0,
373 true
374 );
375 if ($defaultLanguageValue !== '') {
376 $item .= '<div class="t3-form-original-language" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:localizeMergeIfNotBlank')) . '">'
377 . $iconFactory->getIcon($this->data['systemLanguageRows'][$previewLanguage['sys_language_uid']]['flagIconIdentifier'], Icon::SIZE_SMALL)->render()
378 . $this->getMergeBehaviourIcon($fieldConfig['l10n_mode'])
379 . $this->previewFieldValue($defaultLanguageValue, $fieldConfig, $field) . '</div>';
380 }
381 }
382 }
383 }
384 return $item;
385 }
386
387 /**
388 * Renders an icon to indicate the way the translation and the original is merged (if this is relevant).
389 *
390 * If a field is defined as 'mergeIfNotBlank' this is useful information for an editor. He/she can leave the field blank and
391 * the original value will be used. Without this hint editors are likely to copy the contents even if it is not necessary.
392 *
393 * @param string $l10nMode Localization mode from TCA
394 * @return string
395 */
396 protected function getMergeBehaviourIcon($l10nMode)
397 {
398 $icon = '';
399 if ($l10nMode === 'mergeIfNotBlank') {
400 /** @var IconFactory $iconFactory */
401 $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
402 $icon = $iconFactory->getIcon('actions-edit-merge-localization', Icon::SIZE_SMALL)->render();
403 }
404 return $icon;
405 }
406
407 /**
408 * Renders the diff-view of default language record content compared with what the record was originally translated from.
409 * Will render content if any is found in the internal array
410 *
411 * @param string $table Table name of the record being edited
412 * @param string $field Field name represented by $item
413 * @param array $row Record array of the record being edited
414 * @param string $item HTML of the form field. This is what we add the content to.
415 * @return string Item string returned again, possibly with the original value added to.
416 */
417 protected function renderDefaultLanguageDiff($table, $field, $row, $item)
418 {
419 if (is_array($this->data['defaultLanguageDiffRow'][$table . ':' . $row['uid']])) {
420 // Initialize:
421 $dLVal = [
422 'old' => $this->data['defaultLanguageDiffRow'][$table . ':' . $row['uid']],
423 'new' => $this->data['defaultLanguageRow']
424 ];
425 // There must be diff-data:
426 if (isset($dLVal['old'][$field])) {
427 if ((string)$dLVal['old'][$field] !== (string)$dLVal['new'][$field]) {
428 // Create diff-result:
429 /** @var DiffUtility $diffUtility */
430 $diffUtility = GeneralUtility::makeInstance(DiffUtility::class);
431 $diffres = $diffUtility->makeDiffDisplay(
432 BackendUtility::getProcessedValue($table, $field, $dLVal['old'][$field], 0, 1),
433 BackendUtility::getProcessedValue($table, $field, $dLVal['new'][$field], 0, 1)
434 );
435 $item .= '
436 <div class="t3-form-original-language-diff">
437 <div class="t3-form-original-language-diffheader">'
438 . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.changeInOrig'))
439 . '</div>
440 <div class="t3-form-original-language-diffcontent">
441 <div class="diff">
442 <div class="diff-item">
443 <div class="diff-item-result diff-item-result-inline">' . $diffres . '</div>
444 </div>
445 </div>
446 </div>
447 </div>
448 ';
449 }
450 }
451 }
452 return $item;
453 }
454
455 /**
456 * Checks if the $table is the child of an inline type AND the $field is the label field of this table.
457 * This function is used to dynamically update the label while editing. This has no effect on labels,
458 * that were processed by a FormEngine-hook on saving.
459 *
460 * @param string $table The table to check
461 * @param string $field The field on this table to check
462 * @return bool Is inline child and field is responsible for the label
463 */
464 protected function isInlineChildAndLabelField($table, $field)
465 {
466 /** @var InlineStackProcessor $inlineStackProcessor */
467 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
468 $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
469 $level = $inlineStackProcessor->getStructureLevel(-1);
470 if ($level['config']['foreign_label']) {
471 $label = $level['config']['foreign_label'];
472 } else {
473 $label = $this->data['processedTca']['ctrl']['label'];
474 }
475 return $level['config']['foreign_table'] === $table && $label === $field;
476 }
477
478 /**
479 * Rendering of inline fields should be skipped under certain circumstances
480 *
481 * @return bool TRUE if field should be skipped based on inline configuration
482 */
483 protected function inlineFieldShouldBeSkipped()
484 {
485 $table = $this->data['tableName'];
486 $fieldName = $this->data['fieldName'];
487 $fieldConfig = $this->data['processedTca']['columns'][$fieldName]['config'];
488
489 /** @var InlineStackProcessor $inlineStackProcessor */
490 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
491 $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
492 $structureDepth = $inlineStackProcessor->getStructureDepth();
493
494 $skipThisField = false;
495 if ($structureDepth > 0) {
496 $searchArray = [
497 '%OR' => [
498 'config' => [
499 0 => [
500 '%AND' => [
501 'foreign_table' => $table,
502 '%OR' => [
503 '%AND' => [
504 'appearance' => ['useCombination' => true],
505 'foreign_selector' => $fieldName
506 ],
507 'MM' => $fieldConfig['MM']
508 ]
509 ]
510 ],
511 1 => [
512 '%AND' => [
513 'foreign_table' => $fieldConfig['foreign_table'],
514 'foreign_selector' => $fieldConfig['foreign_field']
515 ]
516 ]
517 ]
518 ]
519 ];
520 // Get the parent record from structure stack
521 $level = $inlineStackProcessor->getStructureLevel(-1);
522 // If we have symmetric fields, check on which side we are and hide fields, that are set automatically:
523 if ($this->data['isOnSymmetricSide']) {
524 $searchArray['%OR']['config'][0]['%AND']['%OR']['symmetric_field'] = $fieldName;
525 $searchArray['%OR']['config'][0]['%AND']['%OR']['symmetric_sortby'] = $fieldName;
526 } else {
527 $searchArray['%OR']['config'][0]['%AND']['%OR']['foreign_field'] = $fieldName;
528 $searchArray['%OR']['config'][0]['%AND']['%OR']['foreign_sortby'] = $fieldName;
529 }
530 $skipThisField = $this->arrayCompareComplex($level, $searchArray);
531 }
532 return $skipThisField;
533 }
534
535 /**
536 * Handles complex comparison requests on an array.
537 * A request could look like the following:
538 *
539 * $searchArray = array(
540 * '%AND' => array(
541 * 'key1' => 'value1',
542 * 'key2' => 'value2',
543 * '%OR' => array(
544 * 'subarray' => array(
545 * 'subkey' => 'subvalue'
546 * ),
547 * 'key3' => 'value3',
548 * 'key4' => 'value4'
549 * )
550 * )
551 * );
552 *
553 * It is possible to use the array keys '%AND.1', '%AND.2', etc. to prevent
554 * overwriting the sub-array. It could be necessary, if you use complex comparisons.
555 *
556 * The example above means, key1 *AND* key2 (and their values) have to match with
557 * the $subjectArray and additional one *OR* key3 or key4 have to meet the same
558 * condition.
559 * It is also possible to compare parts of a sub-array (e.g. "subarray"), so this
560 * function recurses down one level in that sub-array.
561 *
562 * @param array $subjectArray The array to search in
563 * @param array $searchArray The array with keys and values to search for
564 * @param string $type Use '%AND' or '%OR' for comparison
565 * @return bool The result of the comparison
566 */
567 protected function arrayCompareComplex($subjectArray, $searchArray, $type = '')
568 {
569 $localMatches = 0;
570 $localEntries = 0;
571 if (is_array($searchArray) && !empty($searchArray)) {
572 // If no type was passed, try to determine
573 if (!$type) {
574 reset($searchArray);
575 $type = key($searchArray);
576 $searchArray = current($searchArray);
577 }
578 // We use '%AND' and '%OR' in uppercase
579 $type = strtoupper($type);
580 // Split regular elements from sub elements
581 foreach ($searchArray as $key => $value) {
582 $localEntries++;
583 // Process a sub-group of OR-conditions
584 if ($key === '%OR') {
585 $localMatches += $this->arrayCompareComplex($subjectArray, $value, '%OR') ? 1 : 0;
586 } elseif ($key === '%AND') {
587 $localMatches += $this->arrayCompareComplex($subjectArray, $value, '%AND') ? 1 : 0;
588 } elseif (is_array($value) && $this->isAssociativeArray($searchArray)) {
589 $localMatches += $this->arrayCompareComplex($subjectArray[$key], $value, $type) ? 1 : 0;
590 } elseif (is_array($value)) {
591 $localMatches += $this->arrayCompareComplex($subjectArray, $value, $type) ? 1 : 0;
592 } else {
593 if (isset($subjectArray[$key]) && isset($value)) {
594 // Boolean match:
595 if (is_bool($value)) {
596 $localMatches += !($subjectArray[$key] xor $value) ? 1 : 0;
597 } elseif (is_numeric($subjectArray[$key]) && is_numeric($value)) {
598 $localMatches += $subjectArray[$key] == $value ? 1 : 0;
599 } else {
600 $localMatches += $subjectArray[$key] === $value ? 1 : 0;
601 }
602 }
603 }
604 // If one or more matches are required ('OR'), return TRUE after the first successful match
605 if ($type === '%OR' && $localMatches > 0) {
606 return true;
607 }
608 // If all matches are required ('AND') and we have no result after the first run, return FALSE
609 if ($type === '%AND' && $localMatches == 0) {
610 return false;
611 }
612 }
613 }
614 // Return the result for '%AND' (if nothing was checked, TRUE is returned)
615 return $localEntries === $localMatches;
616 }
617
618 /**
619 * Checks whether an object is an associative array.
620 *
621 * @param mixed $object The object to be checked
622 * @return bool Returns TRUE, if the object is an associative array
623 */
624 protected function isAssociativeArray($object)
625 {
626 return is_array($object) && !empty($object) && array_keys($object) !== range(0, sizeof($object) - 1);
627 }
628
629 /**
630 * @return BackendUserAuthentication
631 */
632 protected function getBackendUserAuthentication()
633 {
634 return $GLOBALS['BE_USER'];
635 }
636
637 /**
638 * @return LanguageService
639 */
640 protected function getLanguageService()
641 {
642 return $GLOBALS['LANG'];
643 }
644 }