TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
<?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'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user