296 lines
14 KiB
PHP
296 lines
14 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the TYPO3 CMS project.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*
|
|
* The TYPO3 project - inspiring people to share!
|
|
*/
|
|
|
|
namespace TYPO3\CMS\Backend\Form\Container;
|
|
|
|
use TYPO3\CMS\Backend\Form\Behavior\ReloadOnFieldChange;
|
|
use TYPO3\CMS\Backend\Form\Behavior\UpdateValueOnFieldChange;
|
|
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
|
|
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
|
|
use TYPO3\CMS\Core\Authentication\JsConfirmation;
|
|
use TYPO3\CMS\Core\Localization\LanguageService;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Core\Utility\MathUtility;
|
|
|
|
/**
|
|
* Container around a "single field".
|
|
*
|
|
* This container is the last one in the chain before processing is handed over to single element classes.
|
|
* If a single field is of type flex or inline, it however creates FlexFormEntryContainer or InlineControlContainer.
|
|
*
|
|
* The container does various checks and processing for a given single fields.
|
|
*/
|
|
class SingleFieldContainer extends AbstractContainer
|
|
{
|
|
public function __construct(
|
|
private readonly InlineStackProcessor $inlineStackProcessor
|
|
) {}
|
|
|
|
public function render(): array
|
|
{
|
|
$backendUser = $this->getBackendUserAuthentication();
|
|
$resultArray = $this->initializeResultArray();
|
|
|
|
$table = $this->data['tableName'];
|
|
$row = $this->data['databaseRow'];
|
|
$fieldName = $this->data['fieldName'];
|
|
|
|
$parameterArray = [];
|
|
$parameterArray['fieldConf'] = $this->data['processedTca']['columns'][$fieldName];
|
|
|
|
$isOverlay = false;
|
|
|
|
// This field decides whether the current record is an overlay (as opposed to being a standalone record)
|
|
// Based on this decision we need to trigger field exclusion or special rendering (like readOnly)
|
|
if (isset($this->data['processedTca']['ctrl']['transOrigPointerField'])
|
|
&& is_array($this->data['processedTca']['columns'][$this->data['processedTca']['ctrl']['transOrigPointerField']] ?? null)
|
|
) {
|
|
$parentValue = $row[$this->data['processedTca']['ctrl']['transOrigPointerField']];
|
|
if (MathUtility::canBeInterpretedAsInteger($parentValue)) {
|
|
$isOverlay = (bool)$parentValue;
|
|
} elseif (is_array($parentValue)) {
|
|
// This case may apply if the value has been converted to an array by the select or group data provider
|
|
$isOverlay = !empty($parentValue) ? (bool)$parentValue[0] : false;
|
|
} else {
|
|
throw new \InvalidArgumentException(
|
|
'The given value "' . $parentValue . '" for the original language field ' . $this->data['processedTca']['ctrl']['transOrigPointerField']
|
|
. ' of table ' . $table . ' is invalid.',
|
|
1470742770
|
|
);
|
|
}
|
|
}
|
|
|
|
// A couple of early returns in case the field should not be rendered
|
|
$fieldIsExcluded = $parameterArray['fieldConf']['exclude'] ?? false;
|
|
$fieldNotExcludable = $backendUser->check('non_exclude_fields', $table . ':' . $fieldName);
|
|
$fieldExcludedFromTranslatedRecords = empty($parameterArray['fieldConf']['l10n_display']) && ($parameterArray['fieldConf']['l10n_mode'] ?? '') === 'exclude';
|
|
// Return if BE-user has no access rights to this field, @todo: another user access rights check!
|
|
if (($fieldIsExcluded && !$fieldNotExcludable) || ($isOverlay && $fieldExcludedFromTranslatedRecords) || $this->inlineFieldShouldBeSkipped()) {
|
|
return $resultArray;
|
|
}
|
|
|
|
$tsConfig = $this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'] ?? [];
|
|
$parameterArray['fieldTSConfig'] = is_array($tsConfig) ? $tsConfig : [];
|
|
|
|
if ($parameterArray['fieldTSConfig']['disabled'] ?? false) {
|
|
return $resultArray;
|
|
}
|
|
|
|
// Override fieldConf by fieldTSconfig:
|
|
$parameterArray['fieldConf']['config'] = FormEngineUtility::overrideFieldConf($parameterArray['fieldConf']['config'], $parameterArray['fieldTSConfig']);
|
|
$parameterArray['itemFormElName'] = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
|
|
$newElementBaseName = isset($this->data['elementBaseName']) ? $this->data['elementBaseName'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']' : '';
|
|
|
|
// The value to show in the form field.
|
|
$parameterArray['itemFormElValue'] = $row[$fieldName];
|
|
// Set field to read-only if configured for translated records to show default language content as readonly
|
|
// Note: In such case, the database value of this field was already overridden by DatabaseRowDefaultAsReadonly.
|
|
if (($parameterArray['fieldConf']['l10n_display'] ?? false)
|
|
&& GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'defaultAsReadonly')
|
|
&& $isOverlay
|
|
) {
|
|
$parameterArray['fieldConf']['config']['readOnly'] = true;
|
|
}
|
|
|
|
$processedTcaType = $this->data['processedTca']['ctrl']['type'] ?? '';
|
|
$typeField = !str_contains($processedTcaType, ':')
|
|
? $processedTcaType
|
|
: substr($processedTcaType, 0, (int)strpos($processedTcaType, ':'));
|
|
|
|
// JavaScript code for event handlers:
|
|
$parameterArray['fieldChangeFunc'] = [];
|
|
$parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = new UpdateValueOnFieldChange(
|
|
$table,
|
|
(string)$row['uid'],
|
|
$fieldName,
|
|
$parameterArray['itemFormElName']
|
|
);
|
|
|
|
$requestFormEngineUpdate
|
|
= (!empty($this->data['processedTca']['ctrl']['type']) && $fieldName === $typeField)
|
|
|| (isset($parameterArray['fieldConf']['onChange']) && $parameterArray['fieldConf']['onChange'] === 'reload');
|
|
if ($requestFormEngineUpdate) {
|
|
$askForUpdate = $backendUser->jsConfirmation(JsConfirmation::TYPE_CHANGE);
|
|
$parameterArray['fieldChangeFunc']['record_type_changed'] = new ReloadOnFieldChange($askForUpdate);
|
|
}
|
|
|
|
// Based on the type of the item, call a render function on a child element
|
|
$options = $this->data;
|
|
$options['parameterArray'] = $parameterArray;
|
|
$options['elementBaseName'] = $newElementBaseName;
|
|
if (!empty($parameterArray['fieldConf']['config']['renderType'])) {
|
|
$options['renderType'] = $parameterArray['fieldConf']['config']['renderType'];
|
|
} else {
|
|
// Fallback to type if no renderType is given
|
|
$options['renderType'] = $parameterArray['fieldConf']['config']['type'];
|
|
}
|
|
|
|
return $this->nodeFactory->create($options)->render();
|
|
}
|
|
|
|
/**
|
|
* Rendering of inline fields should be skipped under certain circumstances
|
|
*/
|
|
protected function inlineFieldShouldBeSkipped(): bool
|
|
{
|
|
$table = $this->data['tableName'];
|
|
$fieldName = $this->data['fieldName'];
|
|
$fieldConfig = $this->data['processedTca']['columns'][$fieldName]['config'];
|
|
$fieldConfig += [
|
|
'MM' => '',
|
|
'foreign_table' => '',
|
|
'foreign_selector' => '',
|
|
'foreign_field' => '',
|
|
];
|
|
if (($this->data['inlineStructure']['stable'] ?? []) !== []) {
|
|
$searchArray = [
|
|
'%OR' => [
|
|
'config' => [
|
|
0 => [
|
|
'%AND' => [
|
|
'foreign_table' => $table,
|
|
'%OR' => [
|
|
'%AND' => [
|
|
'appearance' => ['useCombination' => true],
|
|
'foreign_selector' => $fieldName,
|
|
],
|
|
'MM' => $fieldConfig['MM'],
|
|
],
|
|
],
|
|
],
|
|
1 => [
|
|
'%AND' => [
|
|
'foreign_table' => $fieldConfig['foreign_table'],
|
|
'foreign_selector' => $fieldConfig['foreign_field'],
|
|
],
|
|
],
|
|
],
|
|
],
|
|
];
|
|
// If we have symmetric fields, check on which side we are and hide fields, that are set automatically:
|
|
if ($this->data['isOnSymmetricSide']) {
|
|
$searchArray['%OR']['config'][0]['%AND']['%OR']['symmetric_field'] = $fieldName;
|
|
$searchArray['%OR']['config'][0]['%AND']['%OR']['symmetric_sortby'] = $fieldName;
|
|
} else {
|
|
$searchArray['%OR']['config'][0]['%AND']['%OR']['foreign_field'] = $fieldName;
|
|
$searchArray['%OR']['config'][0]['%AND']['%OR']['foreign_sortby'] = $fieldName;
|
|
}
|
|
// Parent record from structure stack
|
|
$parent = $this->inlineStackProcessor->getStructureLevelFromStructure($this->data['inlineStructure'], -1) ?? [];
|
|
return $this->arrayCompareComplex($parent, $searchArray);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Handles complex comparison requests on an array.
|
|
* A request could look like the following:
|
|
*
|
|
* $searchArray = array(
|
|
* '%AND' => array(
|
|
* 'key1' => 'value1',
|
|
* 'key2' => 'value2',
|
|
* '%OR' => array(
|
|
* 'subarray' => array(
|
|
* 'subkey' => 'subvalue'
|
|
* ),
|
|
* 'key3' => 'value3',
|
|
* 'key4' => 'value4'
|
|
* )
|
|
* )
|
|
* );
|
|
*
|
|
* It is possible to use the array keys '%AND.1', '%AND.2', etc. to prevent
|
|
* overwriting the sub-array. It could be necessary, if you use complex comparisons.
|
|
*
|
|
* The example above means, key1 *AND* key2 (and their values) have to match with
|
|
* the $subjectArray and additional one *OR* key3 or key4 have to meet the same
|
|
* condition.
|
|
* It is also possible to compare parts of a sub-array (e.g. "subarray"), so this
|
|
* function recurses down one level in that sub-array.
|
|
*
|
|
* @param array $subjectArray The array to search in
|
|
* @param array $searchArray The array with keys and values to search for
|
|
* @param string $type Use '%AND' or '%OR' for comparison
|
|
* @return bool The result of the comparison
|
|
*/
|
|
protected function arrayCompareComplex(array $subjectArray, array $searchArray, string $type = ''): bool
|
|
{
|
|
$localMatches = 0;
|
|
$localEntries = 0;
|
|
if ($searchArray !== []) {
|
|
// If no type was passed, try to determine
|
|
if (!$type) {
|
|
reset($searchArray);
|
|
$type = (string)key($searchArray);
|
|
$searchArray = current($searchArray);
|
|
}
|
|
// We use '%AND' and '%OR' in uppercase
|
|
$type = strtoupper($type);
|
|
// Split regular elements from sub elements
|
|
foreach ($searchArray as $key => $value) {
|
|
$localEntries++;
|
|
// Process a sub-group of OR-conditions
|
|
if ($key === '%OR') {
|
|
$localMatches += $this->arrayCompareComplex($subjectArray, $value, '%OR') ? 1 : 0;
|
|
} elseif ($key === '%AND') {
|
|
$localMatches += $this->arrayCompareComplex($subjectArray, $value, '%AND') ? 1 : 0;
|
|
} elseif (is_array($value) && $this->isAssociativeArray($searchArray)) {
|
|
$localMatches += $this->arrayCompareComplex($subjectArray[$key], $value, $type) ? 1 : 0;
|
|
} elseif (is_array($value)) {
|
|
$localMatches += $this->arrayCompareComplex($subjectArray, $value, $type) ? 1 : 0;
|
|
} else {
|
|
if (isset($subjectArray[$key]) && isset($value)) {
|
|
// Boolean match:
|
|
if (is_bool($value)) {
|
|
$localMatches += !($subjectArray[$key] xor $value) ? 1 : 0;
|
|
} elseif (is_numeric($subjectArray[$key]) && is_numeric($value)) {
|
|
$localMatches += $subjectArray[$key] == $value ? 1 : 0;
|
|
} else {
|
|
$localMatches += $subjectArray[$key] === $value ? 1 : 0;
|
|
}
|
|
}
|
|
}
|
|
// If one or more matches are required ('OR'), return TRUE after the first successful match
|
|
if ($type === '%OR' && $localMatches > 0) {
|
|
return true;
|
|
}
|
|
// If all matches are required ('AND') and we have no result after the first run, return FALSE
|
|
if ($type === '%AND' && $localMatches == 0) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
// Return the result for '%AND' (if nothing was checked, TRUE is returned)
|
|
return $localEntries === $localMatches;
|
|
}
|
|
|
|
/**
|
|
* Checks whether an object is an associative array.
|
|
*
|
|
* @param mixed $object The object to be checked
|
|
* @return bool Returns TRUE, if the object is an associative array
|
|
*/
|
|
protected function isAssociativeArray($object)
|
|
{
|
|
return is_array($object) && !empty($object) && array_keys($object) !== range(0, count($object) - 1);
|
|
}
|
|
|
|
protected function getLanguageService(): LanguageService
|
|
{
|
|
return $GLOBALS['LANG'];
|
|
}
|
|
}
|