Commit 9b7fd177 authored by Christian Kuhn's avatar Christian Kuhn Committed by Susanne Moog
Browse files

[!!!][TASK] Flex form data structure refactoring

Method BackendUtility::getFlexFormDS() does two things at once:
It finds a data structure by given data (TCA, row, ...) and then
parses it.
This construct gives tons of headaches, since the methods never
exposes where a specific data structure came from and the lookup
mechanism is complex. Especially if a flex form is used in
combination with ajax requests later, the core has massive issues
since the location can not be found out later again.

To solve that, the patch splits getFlexFormDS() into two methods:
First method "FlexFormTools->getDataStructureIdentifier()" gets
TCA and row and locates a specific structure. It returns an
"identifier" that points to that unique data structure. This
identifier can be later hand around easily.
The second method "FlexFormTools->parseDataStructureByIdentifier()"
then gets this identifier again, fetches the data structure the
identifier points to, and parses it.

Change-Id: I38264e8a4a6f956c12e9e50f6039d3d09af0f03a
Resolves: #78581
Releases: master
Reviewed-on: https://review.typo3.org/48212

Tested-by: default avatarTYPO3com <no-reply@typo3.com>
Tested-by: Claus Due's avatarClaus Due <claus@phpmind.net>
Reviewed-by: Wouter Wolters's avatarWouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: Susanne Moog's avatarSusanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog's avatarSusanne Moog <susanne.moog@typo3.org>
parent 5eb4024c
......@@ -18,6 +18,7 @@ use Doctrine\DBAL\DBALException;
use TYPO3\CMS\Backend\Module\ModuleLoader;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\Query\QueryHelper;
......@@ -715,10 +716,10 @@ abstract class AbstractItemProvider
if (!empty($GLOBALS['TCA'][$table]['columns'][$tableField]['label'])) {
$labelPrefix = $languageService->sL($GLOBALS['TCA'][$table]['columns'][$tableField]['label']);
}
// Get all sheets and title
// Get all sheets
foreach ($flexForms as $extIdent => $extConf) {
// Get all fields in sheet
foreach ($extConf['ds']['sheets'] as $sheetName => $sheet) {
foreach ($extConf['sheets'] as $sheetName => $sheet) {
if (empty($sheet['ROOT']['el']) || !is_array($sheet['ROOT']['el'])) {
continue;
}
......@@ -763,76 +764,40 @@ abstract class AbstractItemProvider
}
/**
* Returns all registered FlexForm definitions with title and fields
* Returns all registered FlexForm definitions
*
* Note: This only finds flex forms registered in 'ds' config sections.
* This does not resolve other sophisticated flex form data structure references.
*
* @param string $table Table to handle
* @return array Data structures with speaking extension title
* @return array Data structures
*/
protected function getRegisteredFlexForms($table)
{
if (empty($table) || empty($GLOBALS['TCA'][$table]['columns'])) {
return [];
}
$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
$flexForms = [];
foreach ($GLOBALS['TCA'][$table]['columns'] as $tableField => $fieldConf) {
if (!empty($fieldConf['config']['type']) && !empty($fieldConf['config']['ds']) && $fieldConf['config']['type'] == 'flex') {
$flexForms[$tableField] = [];
// Get pointer fields
$pointerFields = !empty($fieldConf['config']['ds_pointerField']) ? $fieldConf['config']['ds_pointerField'] : 'list_type,CType,default';
$pointerFields = GeneralUtility::trimExplode(',', $pointerFields);
// Get FlexForms
foreach ($fieldConf['config']['ds'] as $flexFormKey => $dataStructure) {
foreach (array_keys($fieldConf['config']['ds']) as $flexFormKey) {
// Get extension identifier (uses second value if it's not empty, "list" or "*", else first one)
$identFields = GeneralUtility::trimExplode(',', $flexFormKey);
$extIdent = $identFields[0];
// @todo: This approach is limited and doesn't find everything. It works for tt_content plugins, though.
if (!empty($identFields[1]) && $identFields[1] !== 'list' && $identFields[1] !== '*') {
$extIdent = $identFields[1];
}
// Load external file references
if (!is_array($dataStructure)) {
$file = GeneralUtility::getFileAbsFileName(str_ireplace('FILE:', '', $dataStructure));
if ($file && @is_file($file)) {
$dataStructure = file_get_contents($file);
}
$dataStructure = GeneralUtility::xml2array($dataStructure);
if (!is_array($dataStructure)) {
continue;
}
}
// Get flexform content
$dataStructure = GeneralUtility::resolveAllSheetsInDS($dataStructure);
if (empty($dataStructure['sheets']) || !is_array($dataStructure['sheets'])) {
continue;
}
// Use DS pointer to get extension title from TCA
// @todo: I don't understand this code ... does it make sense at all?
$title = $extIdent;
$keyFields = GeneralUtility::trimExplode(',', $flexFormKey);
foreach ($pointerFields as $pointerKey => $pointerName) {
if (empty($keyFields[$pointerKey])
|| $keyFields[$pointerKey] === '*'
|| $keyFields[$pointerKey] === 'list'
|| $keyFields[$pointerKey] === 'default'
) {
continue;
}
if (!empty($GLOBALS['TCA'][$table]['columns'][$pointerName]['config']['items'])) {
$items = $GLOBALS['TCA'][$table]['columns'][$pointerName]['config']['items'];
if (!is_array($items)) {
continue;
}
foreach ($items as $itemConf) {
if (!empty($itemConf[0]) && !empty($itemConf[1]) && $itemConf[1] == $keyFields[$pointerKey]) {
$title = $itemConf[0];
break 2;
}
}
}
}
$flexForms[$tableField][$extIdent] = [
'title' => $title,
'ds' => $dataStructure
];
$flexFormDataStructureIdentifier = json_encode([
'type' => 'tca',
'tableName' => $table,
'fieldName' => $tableField,
'dataStructureKey' => $flexFormKey,
]);
$dataStructure = $flexFormTools->parseDataStructureByIdentifier($flexFormDataStructureIdentifier);
$flexForms[$tableField][$extIdent] = $dataStructure;
}
}
}
......
<?php
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
/*
* 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\FormDataProviderInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Resolve and flex data structure and data values.
*
* This is the first data provider in the chain of flex form related providers.
*/
class TcaFlexFetch implements FormDataProviderInterface
{
/**
* Resolve ds pointer stuff and parse both ds and dv
*
* @param array $result
* @return array
*/
public function addData(array $result)
{
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'flex') {
continue;
}
$result = $this->initializeDataStructure($result, $fieldName);
$result = $this->initializeDataValues($result, $fieldName);
$result = $this->resolvePossibleExternalFileInDataStructure($result, $fieldName);
}
return $result;
}
/**
* Fetch / initialize data structure.
*
* The sub array with different possible data structures in ['config']['ds'] is
* resolved here, ds array contains only the one resolved data structure after this method.
*
* @param array $result Result array
* @param string $fieldName Currently handled field name
* @return array Modified result
* @throws \UnexpectedValueException
*/
protected function initializeDataStructure(array $result, $fieldName)
{
// Fetch / initialize data structure
$dataStructureArray = BackendUtility::getFlexFormDS(
$result['processedTca']['columns'][$fieldName]['config'],
$result['databaseRow'],
$result['tableName'],
$fieldName
);
// If data structure can't be parsed, this is a developer error, so throw a non catchable exception
if (!is_array($dataStructureArray)) {
throw new \UnexpectedValueException(
'Data structure error: ' . $dataStructureArray,
1440506893
);
}
if (!isset($dataStructureArray['meta']) || !is_array($dataStructureArray['meta'])) {
$dataStructureArray['meta'] = [];
}
// This kicks one array depth: config['ds']['matchingIdentifier'] becomes config['ds']
$result['processedTca']['columns'][$fieldName]['config']['ds'] = $dataStructureArray;
return $result;
}
/**
* Parse / initialize value from xml string to array
*
* @param array $result Result array
* @param string $fieldName Currently handled field name
* @return array Modified result
*/
protected function initializeDataValues(array $result, $fieldName)
{
if (!array_key_exists($fieldName, $result['databaseRow'])) {
$result['databaseRow'][$fieldName] = '';
}
$valueArray = [];
if (isset($result['databaseRow'][$fieldName])) {
$valueArray = $result['databaseRow'][$fieldName];
}
if (!is_array($result['databaseRow'][$fieldName])) {
$valueArray = GeneralUtility::xml2array($result['databaseRow'][$fieldName]);
}
if (!is_array($valueArray)) {
$valueArray = [];
}
if (!isset($valueArray['data'])) {
$valueArray['data'] = [];
}
if (!isset($valueArray['meta'])) {
$valueArray['meta'] = [];
}
$result['databaseRow'][$fieldName] = $valueArray;
return $result;
}
/**
* Single fields can be extracted to files again. This is resolved and parsed here.
*
* @todo: Why is this not done in BackendUtility::getFlexFormDS() directly? If done there, the two methods
* @todo: GeneralUtility::resolveSheetDefInDS() and GeneralUtility::resolveAllSheetsInDS() could be killed
* @todo: since this resolving is basically the only really useful thing they actually do.
*
* @param array $result Result array
* @param string $fieldName Current handle field name
* @return array Modified item array
*/
protected function resolvePossibleExternalFileInDataStructure(array $result, $fieldName)
{
$modifiedDataStructure = $result['processedTca']['columns'][$fieldName]['config']['ds'];
if (isset($modifiedDataStructure['sheets']) && is_array($modifiedDataStructure['sheets'])) {
foreach ($modifiedDataStructure['sheets'] as $sheetName => $sheetStructure) {
if (!is_array($sheetStructure)) {
$file = GeneralUtility::getFileAbsFileName($sheetStructure);
if ($file && @is_file($file)) {
$sheetStructure = GeneralUtility::xml2array(file_get_contents($file));
}
}
$modifiedDataStructure['sheets'][$sheetName] = $sheetStructure;
}
}
$result['processedTca']['columns'][$fieldName]['config']['ds'] = $modifiedDataStructure;
return $result;
}
}
......@@ -15,13 +15,14 @@ namespace TYPO3\CMS\Backend\Form\FormDataProvider;
*/
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Migrations\TcaMigration;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Prepare flex data structure and data values.
* Resolve flex data structure and data values, prepare and normalize.
*
* This data provider is typically executed directly after TcaFlexFetch
* This is the first data provider in the chain of flex form related providers.
*/
class TcaFlexPrepare implements FormDataProviderInterface
{
......@@ -40,7 +41,8 @@ class TcaFlexPrepare implements FormDataProviderInterface
if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'flex') {
continue;
}
$result = $this->createDefaultSheetInDataStructureIfNotGiven($result, $fieldName);
$result = $this->initializeDataStructure($result, $fieldName);
$result = $this->initializeDataValues($result, $fieldName);
$result = $this->removeTceFormsArrayKeyFromDataStructureElements($result, $fieldName);
$result = $this->migrateFlexformTcaDataStructureElements($result, $fieldName);
}
......@@ -49,31 +51,65 @@ class TcaFlexPrepare implements FormDataProviderInterface
}
/**
* Add a sheet structure if data structure has none yet to simplify further handling.
* Fetch / initialize data structure.
*
* Example TCA field config:
* ['config']['ds']['ROOT'] becomes
* ['config']['ds']['sheets']['sDEF']['ROOT']
* The sub array with different possible data structures in ['config']['ds'] is
* resolved here, ds array contains only the one resolved data structure after this method.
*
* @param array $result Result array
* @param string $fieldName Currently handled field name
* @return array Modified result
* @throws \UnexpectedValueException
*/
protected function createDefaultSheetInDataStructureIfNotGiven(array $result, $fieldName)
protected function initializeDataStructure(array $result, $fieldName)
{
$modifiedDataStructure = $result['processedTca']['columns'][$fieldName]['config']['ds'];
if (isset($modifiedDataStructure['ROOT']) && isset($modifiedDataStructure['sheets'])) {
throw new \UnexpectedValueException(
'Parsed data structure has both ROOT and sheets on top level',
1440676540
);
$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
$dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
$result['processedTca']['columns'][$fieldName],
$result['tableName'],
$fieldName,
$result['databaseRow']
);
$dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
if (!isset($dataStructureArray['meta']) || !is_array($dataStructureArray['meta'])) {
$dataStructureArray['meta'] = [];
}
if (isset($modifiedDataStructure['ROOT']) && is_array($modifiedDataStructure['ROOT'])) {
$modifiedDataStructure['sheets']['sDEF']['ROOT'] = $modifiedDataStructure['ROOT'];
unset($modifiedDataStructure['ROOT']);
// This kicks one array depth: config['ds']['listOfDataStructures'] becomes config['ds']
// This also ensures the final ds can be found in 'ds', even if the DS was fetch from
// a record, see FlexFormTools->getDataStructureIdentifier() for details.
$result['processedTca']['columns'][$fieldName]['config']['ds'] = $dataStructureArray;
return $result;
}
/**
* Parse / initialize value from xml string to array
*
* @param array $result Result array
* @param string $fieldName Currently handled field name
* @return array Modified result
*/
protected function initializeDataValues(array $result, $fieldName)
{
if (!array_key_exists($fieldName, $result['databaseRow'])) {
$result['databaseRow'][$fieldName] = '';
}
$result['processedTca']['columns'][$fieldName]['config']['ds'] = $modifiedDataStructure;
$valueArray = [];
if (isset($result['databaseRow'][$fieldName])) {
$valueArray = $result['databaseRow'][$fieldName];
}
if (!is_array($result['databaseRow'][$fieldName])) {
$valueArray = GeneralUtility::xml2array($result['databaseRow'][$fieldName]);
}
if (!is_array($valueArray)) {
$valueArray = [];
}
if (!isset($valueArray['data'])) {
$valueArray['data'] = [];
}
if (!isset($valueArray['meta'])) {
$valueArray['meta'] = [];
}
$result['databaseRow'][$fieldName] = $valueArray;
return $result;
}
......
......@@ -71,6 +71,7 @@ class TcaFlexProcess implements FormDataProviderInterface
*
* @todo: This method is only implemented half. It basically should do all the
* @todo: pointer handling that is done within BackendUtility::getFlexFormDS() to $srcPointer.
* @todo: This can be solved now by adding 'identifier' from TcaFlexPrepare to 'config' array
*
* @param array $result Result array
* @param string $fieldName Current handle field name
......
......@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Backend\Form\Wizard;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -263,8 +264,7 @@ class SuggestWizard
return;
}
$flexfieldTCAConfig = $GLOBALS['TCA'][$table]['columns'][$parts[0]]['config'];
// @todo: should be done via data preparation, resolveAllSheetsInDS() can be deprecated then
$flexfieldTCAConfig = $GLOBALS['TCA'][$table]['columns'][$parts[0]];
if (substr($row['uid'], 0, 3) === 'NEW') {
// We have to cleanup record information as they are coming from FormEngines DataProvider
$pointerFields = GeneralUtility::trimExplode(',', $flexfieldTCAConfig['ds_pointerField']);
......@@ -274,10 +274,17 @@ class SuggestWizard
}
}
}
$flexformDSArray = BackendUtility::getFlexFormDS($flexfieldTCAConfig, $row, $table, $parts[0]);
$flexformDSArray = GeneralUtility::resolveAllSheetsInDS($flexformDSArray);
$fieldConfig = $this->getFieldConfiguration($parts, $flexformDSArray);
// @todo: Better hand around the data structure identifier. This would free us from $row usage
// @todo: and getDataStructureIdentifier() would not have to be called anymore at all.
$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
$dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
$flexfieldTCAConfig,
$table,
$parts[0],
$row
);
$dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
$fieldConfig = $this->getFieldConfiguration($parts, $dataStructureArray);
// Flexform field name levels are separated with | instead of encapsulation in [];
// reverse this here to be compatible with regular field names.
$field = str_replace('|', '][', $field);
......
......@@ -954,10 +954,11 @@ class BackendUtility
* @param bool $WSOL If set, workspace overlay is applied to records. This is correct behaviour for all presentation and export, but NOT if you want a TRUE reflection of how things are in the live workspace.
* @param int $newRecordPidValue SPECIAL CASES: Use this, if the DataStructure may come from a parent record and the INPUT row doesn't have a uid yet (hence, the pid cannot be looked up). Then it is necessary to supply a PID value to search recursively in for the DS (used from DataHandler)
* @return mixed If array, the data structure was found and returned as an array. Otherwise (string) it is an error message.
* @todo: All those nasty details should be covered with tests, also it is very unfortunate the final $srcPointer is not exposed
* @deprecated since TYPO3 v8, will be removed in TYPO3 v9. This is now integrated as FlexFormTools->getDataStructureIdentifier()
*/
public static function getFlexFormDS($conf, $row, $table, $fieldName = '', $WSOL = true, $newRecordPidValue = 0)
{
GeneralUtility::logDeprecatedFunction();
// Get pointer field etc from TCA-config:
$ds_pointerField = $conf['ds_pointerField'];
$ds_array = $conf['ds'];
......@@ -1102,6 +1103,8 @@ class BackendUtility
$dataStructArray = 'No proper configuration!';
}
// Hook for post-processing the Flexform DS. Introduces the possibility to configure Flexforms via TSConfig
// This hook isn't called anymore from within the core, the whole method is deprecated.
// There are alternative hooks, see FlexFormTools->getDataStructureIdentifier() and ->parseDataStructureByIdentifier()
if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['getFlexFormDSClass'])) {
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['getFlexFormDSClass'] as $classRef) {
$hookObj = GeneralUtility::getUserObj($classRef);
......
<?php
namespace TYPO3\CMS\Backend\Tests\Unit\Form\FormDataProvider;
/*
* 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 Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use TYPO3\CMS\Backend\Form\FormDataProvider\TcaFlexFetch;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Tests\UnitTestCase;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Test case
*/
class TcaFlexFetchTest extends UnitTestCase
{
/**
* @var TcaFlexFetch
*/
protected $subject;
/**
* @var BackendUserAuthentication|ObjectProphecy
*/
protected $backendUserProphecy;
/**
* @var array A backup of registered singleton instances
*/
protected $singletonInstances = [];
protected function setUp()
{
$this->singletonInstances = GeneralUtility::getSingletonInstances();
// Suppress cache foo in xml helpers of GeneralUtility
/** @var CacheManager|ObjectProphecy $cacheManagerProphecy */
$cacheManagerProphecy = $this->prophesize(CacheManager::class);
GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerProphecy->reveal());
$cacheFrontendProphecy = $this->prophesize(FrontendInterface::class);
$cacheManagerProphecy->getCache(Argument::cetera())->willReturn($cacheFrontendProphecy->reveal());
$this->subject = new TcaFlexFetch();
}
protected function tearDown()
{
GeneralUtility::purgeInstances();
GeneralUtility::resetSingletonInstances($this->singletonInstances);
parent::tearDown();
}
/**
* @test
*/
public function addDataSetsParsedDataStructureArray()
{
$input = [
'systemLanguageRows' => [],
'databaseRow' => [
'aField' => [
'data' => [],
'meta' => [],
],
],
'processedTca' => [
'columns' => [
'aField' => [
'config' => [
'type' => 'flex',
'ds' => [
'default' => '
<T3DataStructure>
<ROOT>
<type>array</type>
<el>
<aFlexField>
<TCEforms>
<label>aFlexFieldLabel</label>
<config>
<type>input</type>
</config>
</TCEforms>
</aFlexField>
</el>
</ROOT>
</T3DataStructure>
',
],
],
],
],
],
];
$expected = $input;
$expected['processedTca']['columns']['aField']['config']['ds'] = [
'ROOT' => [
'type' => 'array',
'el' => [
'aFlexField' => [
'TCEforms' => [
'label' => 'aFlexFieldLabel',
'config' => [
'type' => 'input',
],
],
],
],
],
'meta' => [],
];
$this->assertEquals($expected, $this->subject->addData($input));
}
/**
* @test
*/
public function addDataSetsParsedDataStructureArrayWithSheets()
{
$input = [