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