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