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