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