Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
345 lines (319 sloc) 15.7 KB
<?php
namespace TYPO3\CMS\Backend\Form\Container;
/*
* 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!
*/
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
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
{
/**
* Entry method
*
* @throws \InvalidArgumentException
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
$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']])
) {
$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
// Check if this field is configured and editable according to exclude fields and other configuration
if (// Return if BE-user has no access rights to this field, @todo: another user access rights check!
$parameterArray['fieldConf']['exclude'] && !$backendUser->check('non_exclude_fields', $table . ':' . $fieldName)
// Return if field should not be rendered in translated records
|| $isOverlay && empty($parameterArray['fieldConf']['l10n_display']) && $parameterArray['fieldConf']['l10n_mode'] === 'exclude'
|| $this->inlineFieldShouldBeSkipped()
) {
return $resultArray;
}
$parameterArray['fieldTSConfig'] = [];
if (isset($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'])
&& is_array($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'])
) {
$parameterArray['fieldTSConfig'] = $this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'];
}
if ($parameterArray['fieldTSConfig']['disabled']) {
return $resultArray;
}
// Override fieldConf by fieldTSconfig:
$parameterArray['fieldConf']['config'] = FormEngineUtility::overrideFieldConf($parameterArray['fieldConf']['config'], $parameterArray['fieldTSConfig']);
$parameterArray['itemFormElName'] = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
$parameterArray['itemFormElID'] = 'data_' . $table . '_' . $row['uid'] . '_' . $fieldName;
$newElementBaseName = $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
if ($parameterArray['fieldConf']['l10n_display']
&& GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'defaultAsReadonly')
&& $isOverlay
) {
$parameterArray['fieldConf']['config']['readOnly'] = true;
$parameterArray['itemFormElValue'] = $this->data['defaultLanguageRow'][$fieldName];
}
// JavaScript code for event handlers:
$parameterArray['fieldChangeFunc'] = [];
$parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = 'TBE_EDITOR.fieldChanged('
. GeneralUtility::quoteJSvalue($table) . ','
. GeneralUtility::quoteJSvalue($row['uid']) . ','
. GeneralUtility::quoteJSvalue($fieldName) . ','
. GeneralUtility::quoteJSvalue($parameterArray['itemFormElName'])
. ');';
// 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'];
}
$resultArray = $this->nodeFactory->create($options)->render();
if (strpos($this->data['processedTca']['ctrl']['type'], ':') === false) {
$typeField = $this->data['processedTca']['ctrl']['type'];
} else {
$typeField = substr($this->data['processedTca']['ctrl']['type'], 0, strpos($this->data['processedTca']['ctrl']['type'], ':'));
}
// Create a JavaScript code line which will ask the user to save/update the form due to changing the element.
// This is used for eg. "type" fields and others configured with "onChange"
if ((!empty($this->data['processedTca']['ctrl']['type']) && $fieldName === $typeField)
|| (isset($parameterArray['fieldConf']['onChange']) && $parameterArray['fieldConf']['onChange'] === 'reload')
) {
$showConfirmation = $backendUser->jsConfirmation(JsConfirmation::TYPE_CHANGE) ? 'true' : 'false';
$resultArray['requireJsModules'][] = ['TYPO3/CMS/Backend/FormEngine' => 'function (FormEngine) {'
. 'FormEngine.requestConfirmationOnFieldChange(' . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ', ' . $showConfirmation . ');'
. '}'];
}
return $resultArray;
}
/**
* Checks if the $table is the child of an inline type AND the $field is the label field of this table.
* This function is used to dynamically update the label while editing. This has no effect on labels,
* that were processed by a FormEngine-hook on saving.
*
* @param string $table The table to check
* @param string $field The field on this table to check
* @return bool Is inline child and field is responsible for the label
*/
protected function isInlineChildAndLabelField($table, $field)
{
/** @var InlineStackProcessor $inlineStackProcessor */
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
$inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
$level = $inlineStackProcessor->getStructureLevel(-1);
if ($level['config']['foreign_label']) {
$label = $level['config']['foreign_label'];
} else {
$label = $this->data['processedTca']['ctrl']['label'];
}
return $level['config']['foreign_table'] === $table && $label === $field;
}
/**
* Rendering of inline fields should be skipped under certain circumstances
*
* @return bool TRUE if field should be skipped based on inline configuration
*/
protected function inlineFieldShouldBeSkipped()
{
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$fieldConfig = $this->data['processedTca']['columns'][$fieldName]['config'];
/** @var InlineStackProcessor $inlineStackProcessor */
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
$inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
$structureDepth = $inlineStackProcessor->getStructureDepth();
$skipThisField = false;
if ($structureDepth > 0) {
$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']
]
]
]
]
];
// Get the parent record from structure stack
$level = $inlineStackProcessor->getStructureLevel(-1);
// 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;
}
$skipThisField = $this->arrayCompareComplex($level, $searchArray);
}
return $skipThisField;
}
/**
* 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($subjectArray, $searchArray, $type = '')
{
$localMatches = 0;
$localEntries = 0;
if (is_array($searchArray) && !empty($searchArray)) {
// If no type was passed, try to determine
if (!$type) {
reset($searchArray);
$type = 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);
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUserAuthentication()
{
return $GLOBALS['BE_USER'];
}
/**
* @return LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
You can’t perform that action at this time.