Commit 72303b5e authored by Christian Kuhn's avatar Christian Kuhn Committed by Wouter Wolters
Browse files

[BUGFIX] FormEngine inline foreign_selector and foreign_unique

Resolves: #70434
Resolves: #70245
Releases: master
Change-Id: I14e187532b7f5eafa2e73c54ab8056a8033d0822
Reviewed-on: http://review.typo3.org/44126

Reviewed-by: default avatarMorton Jonuschat <m.jonuschat@mojocode.de>
Tested-by: default avatarMorton Jonuschat <m.jonuschat@mojocode.de>
Reviewed-by: Wouter Wolters's avatarWouter Wolters <typo3@wouterwolters.nl>
Tested-by: Wouter Wolters's avatarWouter Wolters <typo3@wouterwolters.nl>
parent f4d932dc
......@@ -370,8 +370,8 @@ class FormInlineAjaxController
$nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
$nameObjectForeignTable = $nameObject . '-' . $child['table'];
$oldItems = FormEngineUtility::getInlineRelatedRecordsUidArray($oldItemList);
$newItems = FormEngineUtility::getInlineRelatedRecordsUidArray($newItemList);
$oldItems = $this->getInlineRelatedRecordsUidArray($oldItemList);
$newItems = $this->getInlineRelatedRecordsUidArray($newItemList);
// Set the items that should be removed in the forms view:
$removedItems = array_diff($oldItems, $newItems);
......@@ -517,7 +517,6 @@ class FormInlineAjaxController
* @param array $intermediate Full data array of "mm" record
* @param array $parentConfig TCA configuration of "parent"
* @return array Full data array of child
* @todo: probably foreign_selector_fieldTcaOverride should be merged over here before
*/
protected function compileCombinationChild(array $intermediate, array $parentConfig)
{
......@@ -582,6 +581,26 @@ class FormInlineAjaxController
return $jsonResult;
}
/**
* Gets an array with the uids of related records out of a list of items.
* This list could contain more information than required. This methods just
* extracts the uids.
*
* @param string $itemList The list of related child records
* @return array An array with uids
*/
protected function getInlineRelatedRecordsUidArray($itemList)
{
$itemArray = GeneralUtility::trimExplode(',', $itemList, true);
// Perform modification of the selected items array:
foreach ($itemArray as &$value) {
$parts = explode('|', $value, 2);
$value = $parts[0];
}
unset($value);
return $itemArray;
}
/**
* Checks if a record selector may select a certain file type
*
......
......@@ -346,9 +346,12 @@ class InlineRecordContainer extends AbstractContainer
$recTitle = $params['title'];
} elseif ($hasForeignLabel || $hasSymmetricLabel) {
$titleCol = $hasForeignLabel ? $config['foreign_label'] : $config['symmetric_label'];
$foreignConfig = FormEngineUtility::getInlinePossibleRecordsSelectorConfig($config, $titleCol);
// Render title for everything else than group/db:
if ($foreignConfig['type'] !== 'groupdb') {
if (isset($this->data['processedTca']['columns'][$titleCol]['config']['type'])
&& $this->data['processedTca']['columns'][$titleCol]['config']['type'] === 'group'
&& isset($this->data['processedTca']['columns'][$titleCol]['config']['internal_type'])
&& $this->data['processedTca']['columns'][$titleCol]['config']['internal_type'] === 'db'
) {
$recTitle = BackendUtility::getProcessedValueExtra($foreign_table, $titleCol, $rec[$titleCol], 0, 0, false);
} else {
// $recTitle could be something like: "tx_table_123|...",
......
......@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Backend\Form\FormDataProvider;
*/
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\ArrayUtility;
/**
* Override some child TCA in an inline parent child relation.
......@@ -37,6 +38,20 @@ class InlineOverrideChildTca implements FormDataProviderInterface
}
}
// Override config section of foreign_selector field pointer if given
if (isset($result['inlineParentConfig']['foreign_selector'])
&& is_string($result['inlineParentConfig']['foreign_selector'])
&& isset($result['inlineParentConfig']['foreign_selector_fieldTcaOverride'])
&& is_array($result['inlineParentConfig']['foreign_selector_fieldTcaOverride'])
&& isset($result['processedTca']['columns'][$result['inlineParentConfig']['foreign_selector']])
&& is_array($result['processedTca']['columns'][$result['inlineParentConfig']['foreign_selector']])
) {
ArrayUtility::mergeRecursiveWithOverrule(
$result['processedTca']['columns'][$result['inlineParentConfig']['foreign_selector']],
$result['inlineParentConfig']['foreign_selector_fieldTcaOverride']
);
}
// Set default values for (new) child if foreign_record_defaults is defined in inlineParentConfig
if (isset($result['inlineParentConfig']['foreign_record_defaults']) && is_array($result['inlineParentConfig']['foreign_record_defaults'])) {
$foreignTableConfig = $GLOBALS['TCA'][$result['inlineParentConfig']['foreign_table']];
......@@ -70,7 +85,7 @@ class InlineOverrideChildTca implements FormDataProviderInterface
}
}
foreach ($result['inlineParentConfig']['foreign_record_defaults'] as $fieldName => $defaultValue) {
if (isset($foreignTableConfig['columns'][$fieldName]) && !in_array($fieldName, $notSetableFields, TRUE)) {
if (isset($foreignTableConfig['columns'][$fieldName]) && !in_array($fieldName, $notSetableFields, true)) {
$result['processedTca']['columns'][$fieldName]['config']['default'] = $defaultValue;
}
}
......
......@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Backend\Form\FormDataProvider;
*/
use TYPO3\CMS\Backend\Form\FormDataCompiler;
use TYPO3\CMS\Backend\Form\FormDataGroup\OnTheFly;
use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
......@@ -45,6 +46,7 @@ class TcaInline extends AbstractDatabaseRecordProvider implements FormDataProvid
$result['processedTca']['columns'][$fieldName]['children'] = [];
if ($result['inlineResolveExistingChildren']) {
$result = $this->resolveRelatedRecords($result, $fieldName);
$result = $this->addForeignSelectorAndUniquePossibleRecords($result, $fieldName);
}
}
......@@ -196,6 +198,57 @@ class TcaInline extends AbstractDatabaseRecordProvider implements FormDataProvid
return $result;
}
/**
* If there is a foreign_selector or foreign_unique configuration, fetch
* the list of possible records that can be connected and attach the to the
* inline configuration.
*
* @param array $result Result array
* @param string $fieldName Current handle field name
* @return array Modified item array
*/
protected function addForeignSelectorAndUniquePossibleRecords(array $result, $fieldName)
{
if (!is_array($result['processedTca']['columns'][$fieldName]['config']['selectorOrUniqueConfiguration'])) {
return $result;
}
$selectorOrUniqueConfiguration = $result['processedTca']['columns'][$fieldName]['config']['selectorOrUniqueConfiguration'];
$foreignFieldName = $selectorOrUniqueConfiguration['fieldName'];
$selectorOrUniquePossibleRecords = [];
if ($selectorOrUniqueConfiguration['config']['type'] === 'select') {
// Compile child table data for this field only
$selectDataInput = [
'tableName' => $result['processedTca']['columns'][$fieldName]['config']['foreign_table'],
'command' => 'new',
// Since there is no existing record that may have a type, it does not make sense to
// do extra handling of pageTsConfig merged here. Just provide "parent" pageTS as is
'pageTsConfig' => $result['pageTsConfig'],
'userTsConfig' => $result['userTsConfig'],
'processedTca' => [
'ctrl' => [],
'columns' => [
$foreignFieldName => [
'config' => $selectorOrUniqueConfiguration['config'],
],
],
],
];
/** @var OnTheFly $formDataGroup */
$formDataGroup = GeneralUtility::makeInstance(OnTheFly::class);
$formDataGroup->setProviderList([ TcaSelectItems::class ]);
/** @var FormDataCompiler $formDataCompiler */
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
$compilerResult = $formDataCompiler->compile($selectDataInput);
$selectorOrUniquePossibleRecords = $compilerResult['processedTca']['columns'][$foreignFieldName]['config']['items'];
}
$result['processedTca']['columns'][$fieldName]['config']['selectorOrUniquePossibleRecords'] = $selectorOrUniquePossibleRecords;
return $result;
}
/**
* Compile a full child record
*
......@@ -236,7 +289,6 @@ class TcaInline extends AbstractDatabaseRecordProvider implements FormDataProvid
* @param array $intermediate Full data array of "mm" record
* @param array $parentConfig TCA configuration of "parent"
* @return array Full data array of child
* @todo: probably foreign_selector_fieldTcaOverride should be merged over here before
*/
protected function compileCombinationChild(array $intermediate, array $parentConfig)
{
......
......@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Backend\Form\FormDataProvider;
*/
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
......@@ -47,6 +48,7 @@ class TcaInlineConfiguration extends AbstractItemProvider implements FormDataPro
$result = $this->initializeMinMaxItems($result, $fieldName);
$result = $this->initializeLocalizationMode($result, $fieldName);
$result = $this->initializeAppearance($result, $fieldName);
$result = $this->addInlineSelectorAndUniqueConfiguration($result, $fieldName);
}
return $result;
}
......@@ -151,7 +153,6 @@ class TcaInlineConfiguration extends AbstractItemProvider implements FormDataPro
$parentConfig = $result['processedTca']['columns'][$fieldName]['config'];
$isChildTableLocalizable = false;
// @todo: Direct $globals access here, but no good idea yet how to get rid of this
if (isset($GLOBALS['TCA'][$childTableName]['ctrl']) && is_array($GLOBALS['TCA'][$childTableName]['ctrl'])
&& isset($GLOBALS['TCA'][$childTableName]['ctrl']['languageField'])
&& $GLOBALS['TCA'][$childTableName]['ctrl']['languageField']
......@@ -192,4 +193,125 @@ class TcaInlineConfiguration extends AbstractItemProvider implements FormDataPro
$result['processedTca']['columns'][$fieldName]['config']['behaviour']['localizationMode'] = $mode;
return $result;
}
/**
* If foreign_selector or foreign_unique is set, this points to a field configuration of the child
* table. The InlineControlContainer may render a drop down field or an element browser later from this.
*
* Fetch configuration from child table configuration, sanitize and merge with
* foreign_selector_fieldTcaOverride that allows overriding this field definition again.
*
* Final configuration is written to selectorOrUniqueConfiguration of inline config section.
*
* @param array $result Result array
* @param string $fieldName Current handle field name
* @return array Modified item array
* @throws \UnexpectedValueException If configuration is broken
*/
protected function addInlineSelectorAndUniqueConfiguration(array $result, $fieldName)
{
$config = $result['processedTca']['columns'][$fieldName]['config'];
// Early return if neither foreign_unique nor foreign_selector are set
if (!isset($config['foreign_unique']) && !isset($config['foreign_selector'])) {
return $result;
}
// If both are set, they must point to the same field
if (isset($config['foreign_unique']) && isset($config['foreign_selector'])
&& $config['foreign_unique'] !== $config['foreign_selector']
) {
throw new \UnexpectedValueException(
'Table ' . $result['tableName'] . ' field ' . $fieldName . ': If both foreign_unique and'
. ' foreign_selector are set, they must point to the same field',
1444995464
);
}
if (isset($config['foreign_unique'])) {
$fieldNameInChildConfiguration = $config['foreign_unique'];
} else {
$fieldNameInChildConfiguration = $config['foreign_selector'];
}
// Throw if field name in globals does not exist or is not of type select or group
if (!isset($GLOBALS['TCA'][$config['foreign_table']]['columns'][$fieldNameInChildConfiguration]['config']['type'])
|| ($GLOBALS['TCA'][$config['foreign_table']]['columns'][$fieldNameInChildConfiguration]['config']['type'] !== 'select'
&& $GLOBALS['TCA'][$config['foreign_table']]['columns'][$fieldNameInChildConfiguration]['config']['type'] !== 'group')
) {
throw new \UnexpectedValueException(
'Table ' . $result['tableName'] . ' field ' . $fieldName . ' points in foreign_selector or foreign_unique'
. ' to field ' . $fieldNameInChildConfiguration . ' of table ' . $config['foreign_table'] . ', but this field'
. ' is either not defined or is not of type select or group',
1444996537
);
}
$selectorOrUniqueConfiguration = [
'config' => $GLOBALS['TCA'][$config['foreign_table']]['columns'][$fieldNameInChildConfiguration]['config'],
];
// Throw if field is type group, but not internal_type db
if ($selectorOrUniqueConfiguration['config']['type'] === 'group'
&& (!isset($selectorOrUniqueConfiguration['config']['internal_type']) || $selectorOrUniqueConfiguration['config']['internal_type'] !== 'db')) {
throw new \UnexpectedValueException(
'Table ' . $result['tableName'] . ' field ' . $fieldName . ' points in foreign_selector or foreign_unique'
. ' to field ' . $fieldNameInChildConfiguration . ' of table ' . $config['foreign_table'] . '. This field'
. ' is of type group and must be of internal_type db, which is not the case',
1444999130
);
}
// Merge foreign_selector_fieldTcaOverride if given
if (isset($config['foreign_selector'])
&& isset($config['foreign_selector_fieldTcaOverride']['config'])
&& is_array($config['foreign_selector_fieldTcaOverride']['config'])
) {
ArrayUtility::mergeRecursiveWithOverrule($selectorOrUniqueConfiguration['config'], $config['foreign_selector_fieldTcaOverride']['config']);
}
// Add field name to config for easy access later
$selectorOrUniqueConfiguration['fieldName'] = $fieldNameInChildConfiguration;
// Add remote table name for easy access later
if ($selectorOrUniqueConfiguration['config']['type'] === 'select') {
if (!isset($selectorOrUniqueConfiguration['config']['foreign_table'])) {
throw new \UnexpectedValueException(
'Table ' . $result['tableName'] . ' field ' . $fieldName . ' points in foreign_selector or foreign_unique'
. ' to field ' . $fieldNameInChildConfiguration . ' of table ' . $config['foreign_table'] . '. This field'
. ' is of type select and must define foreign_table',
1445078627
);
}
$foreignTable = $selectorOrUniqueConfiguration['config']['foreign_table'];
} else {
if (!isset($selectorOrUniqueConfiguration['config']['allowed'])) {
throw new \UnexpectedValueException(
'Table ' . $result['tableName'] . ' field ' . $fieldName . ' points in foreign_selector or foreign_unique'
. ' to field ' . $fieldNameInChildConfiguration . ' of table ' . $config['foreign_table'] . '. This field'
. ' is of type select and must define allowed',
1445078628
);
}
$foreignTable = $selectorOrUniqueConfiguration['config']['allowed'];
}
$selectorOrUniqueConfiguration['foreignTable'] = $foreignTable;
// If this is a foreign_selector field, mark it as such for data fetching later
$selectorOrUniqueConfiguration['isSelector'] = false;
if (isset($config['foreign_selector'])) {
$selectorOrUniqueConfiguration['isSelector'] = true;
}
// If this is a foreign_unique field, mark it a such for unique data fetching later
$selectorOrUniqueConfiguration['isUnique'] = false;
if (isset($config['foreign_unique'])) {
$selectorOrUniqueConfiguration['isUnique'] = true;
}
// Add field configuration to inline configuration
$result['processedTca']['columns'][$fieldName]['config']['selectorOrUniqueConfiguration'] = $selectorOrUniqueConfiguration;
return $result;
}
}
......@@ -158,66 +158,6 @@ class FormEngineUtility
. '</span>';
}
/**
* Determine the configuration and the type of a record selector.
* This is a helper method for inline / IRRE handling
*
* @param array $conf TCA configuration of the parent(!) field
* @param string $field Field name
* @return array Associative array with the keys 'PA' and 'type', both are FALSE if the selector was not valid.
* @internal
*/
public static function getInlinePossibleRecordsSelectorConfig($conf, $field = '')
{
$foreign_table = $conf['foreign_table'];
$foreign_selector = $conf['foreign_selector'];
$PA = false;
$type = false;
$table = false;
$selector = false;
if ($field) {
$PA = array();
$PA['fieldConf'] = $GLOBALS['TCA'][$foreign_table]['columns'][$field];
if ($PA['fieldConf'] && $conf['foreign_selector_fieldTcaOverride']) {
ArrayUtility::mergeRecursiveWithOverrule($PA['fieldConf'], $conf['foreign_selector_fieldTcaOverride']);
}
$PA['fieldTSConfig'] = FormEngineUtility::getTSconfigForTableRow($foreign_table, array(), $field);
$config = $PA['fieldConf']['config'];
// Determine type of Selector:
$type = static::getInlinePossibleRecordsSelectorType($config);
// Return table on this level:
$table = $type === 'select' ? $config['foreign_table'] : $config['allowed'];
// Return type of the selector if foreign_selector is defined and points to the same field as in $field:
if ($foreign_selector && $foreign_selector == $field && $type) {
$selector = $type;
}
}
return array(
'PA' => $PA,
'type' => $type,
'table' => $table,
'selector' => $selector
);
}
/**
* Determine the type of a record selector, e.g. select or group/db.
*
* @param array $config TCE configuration of the selector
* @return mixed The type of the selector, 'select' or 'groupdb' - FALSE not valid
* @internal
*/
protected static function getInlinePossibleRecordsSelectorType($config)
{
$type = false;
if ($config['type'] === 'select') {
$type = 'select';
} elseif ($config['type'] === 'group' && $config['internal_type'] === 'db') {
$type = 'groupdb';
}
return $type;
}
/**
* Update expanded/collapsed states on new inline records if any.
*
......@@ -261,27 +201,6 @@ class FormEngineUtility
}
}
/**
* Gets an array with the uids of related records out of a list of items.
* This list could contain more information than required. This methods just
* extracts the uids.
*
* @param string $itemList The list of related child records
* @return array An array with uids
* @internal
*/
public static function getInlineRelatedRecordsUidArray($itemList)
{
$itemArray = GeneralUtility::trimExplode(',', $itemList, true);
// Perform modification of the selected items array:
foreach ($itemArray as &$value) {
$parts = explode('|', $value, 2);
$value = $parts[0];
}
unset($value);
return $itemArray;
}
/**
* Compatibility layer for methods not in FormEngine scope.
*
......
......@@ -99,6 +99,59 @@ class InlineOverrrideChildTcaTest extends UnitTestCase
$this->assertSame($expected, $this->subject->addData($input));
}
/**
* @test
*/
public function addDataMergesForeignSelectorFieldTcaOverride()
{
$input = [
'inlineParentConfig' => [
'foreign_selector' => 'uid_local',
'foreign_selector_fieldTcaOverride' => [
'label' => 'aDifferentLabel',
'config' => [
'aGivenSetting' => 'overrideValue',
'aNewSetting' => 'anotherNewValue',
'appearance' => [
'elementBrowserType' => 'file',
'elementBrowserAllowed' => 'jpg,png'
],
],
],
],
'processedTca' => [
'columns' => [
'uid_local' => [
'label' => 'aLabel',
'config' => [
'aGivenSetting' => 'aValue',
'doNotChangeMe' => 'doNotChangeMe',
'appearance' => [
'elementBrowserType' => 'db',
],
],
]
],
],
];
$expected = $input;
$expected['processedTca']['columns']['uid_local'] = [
'label' => 'aDifferentLabel',
'config' => [
'aGivenSetting' => 'overrideValue',
'doNotChangeMe' => 'doNotChangeMe',
'appearance' => [
'elementBrowserType' => 'file',
'elementBrowserAllowed' => 'jpg,png',
],
'aNewSetting' => 'anotherNewValue',
],
];
$this->assertSame($expected, $this->subject->addData($input));
}
/**
* @test
*/
......
......@@ -395,7 +395,7 @@ class TcaInlineConfigurationTest extends UnitTestCase
'config' => [
'type' => 'inline',
'foreign_table' => 'aForeignTableName',
'foreign_selector' => 'foo',
'foreign_selector' => 'aField',
'appearance' => [
'levelLinksPosition' => 'both',
],
......@@ -404,8 +404,22 @@ class TcaInlineConfigurationTest extends UnitTestCase
],
],
];
$GLOBALS['TCA']['aForeignTableName']['columns']['aField']['config'] = [
'type' => 'select',
'foreign_table' => 'anotherForeignTableName',
];
$expected['processedTca']['columns']['aField']['config'] = $this->defaultConfig;
$expected['processedTca']['columns']['aField']['config']['foreign_selector'] = 'foo';
$expected['processedTca']['columns']['aField']['config']['foreign_selector'] = 'aField';
$expected['processedTca']['columns']['aField']['config']['selectorOrUniqueConfiguration'] = [
'fieldName' => 'aField',
'isSelector' => true,
'isUnique' => false,
'config' => [
'type' => 'select',
'foreign_table' => 'anotherForeignTableName',
],
'foreignTable' => 'anotherForeignTableName',
];
$expected['processedTca']['columns']['aField']['config']['appearance']['levelLinksPosition'] = 'none';
$this->assertEquals($expected, $this->subject->addData($input));
}
......@@ -422,7 +436,7 @@ class TcaInlineConfigurationTest extends UnitTestCase
'config' => [
'type' => 'inline',
'foreign_table' => 'aForeignTableName',
'foreign_selector' => 'foo',
'foreign_selector' => 'aField',
'appearance' => [
'useCombination' => true,
'levelLinksPosition' => 'both',
......@@ -432,8 +446,22 @@ class TcaInlineConfigurationTest extends UnitTestCase
],
],
];
$GLOBALS['TCA']['aForeignTableName']['columns']['aField']['config'] = [
'type' => 'select',
'foreign_table' => 'anotherForeignTableName',
];
$expected['processedTca']['columns']['aField']['config'] = $this->defaultConfig;
$expected['processedTca']['columns']['aField']['config']['foreign_selector'] = 'foo';
$expected['processedTca']['columns']['aField']['config']['foreign_selector'] = 'aField';
$expected['processedTca']['columns']['aField']['config']['selectorOrUniqueConfiguration'] = [
'fieldName' => 'aField',
'isSelector' => true,
'isUnique' => false,
'config' => [
'type' => 'select',
'foreign_table' => 'anotherForeignTableName',
],
'foreignTable' => 'anotherForeignTableName',
];
$expected['processedTca']['columns']['aField']['config']['appearance']['useCombination'] = true;
$expected['processedTca']['columns']['aField']['config']['appearance']['levelLinksPosition'] = 'both';
$this->assertEquals($expected, $this->subject->addData($input));
......@@ -538,4 +566,281 @@ class TcaInlineConfigurationTest extends UnitTestCase
$expected['processedTca']['columns']['aField']['config']['appearance']['showRemovedLocalizationRecords'] = false;
$this->assertEquals($expected, $this->subject->addData($input));
}
/**
* @test
*/
public function addDataThrowsExceptionIfForeignSelectorAndForeignUniquePointToDifferentFields()
{
$input = [
'processedTca' => [
'columns' => [
'aField' => [
'config' => [
'type' => 'inline',
'foreign_table' => 'aForeignTableName',
'foreign_selector' => 'aField',