[TASK] FormEngine inline refactoring 55/43755/34
authorChristian Kuhn <lolli@schwarzbu.ch>
Sat, 3 Oct 2015 00:54:25 +0000 (02:54 +0200)
committerBenni Mack <benni@typo3.org>
Thu, 8 Oct 2015 15:40:12 +0000 (17:40 +0200)
FormEngine splits in two parts: Data compilation and preparation
and rendering.

The patch separates the inline related data preparation out
of the render containers and moves it into the data provider.
TcaInline provider now resolves and compiles children and adds them
to processedTca[columns][$field][children], so InlineControlContainer
can just loop over them to render children.

InlineRecordContainer, the second inline container that takes
care of rendering single children records now no longer receives
the full parent data, but only the specific child data array it should
render. This leads to better encapsulation and allows some future
performance improvements.

While the inline stuff is still a very complex thing, this last
main structural FormEngine change takes the opportunity to comment
further details and dependencies and it simplifies the structures
by better separation of concerns.

Change-Id: Ia0ed276d7fc6f541f8ae27eaac3e17e3b8714ddf
Resolves: #70490
Releases: master
Reviewed-on: http://review.typo3.org/43755
Reviewed-by: Morton Jonuschat <m.jonuschat@mojocode.de>
Tested-by: Morton Jonuschat <m.jonuschat@mojocode.de>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
20 files changed:
typo3/sysext/backend/Classes/Controller/FormInlineAjaxController.php
typo3/sysext/backend/Classes/Form/Container/FlexFormElementContainer.php
typo3/sysext/backend/Classes/Form/Container/InlineControlContainer.php
typo3/sysext/backend/Classes/Form/Container/InlineRecordContainer.php
typo3/sysext/backend/Classes/Form/FormDataCompiler.php
typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractDatabaseRecordProvider.php
typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseRowInitializeNew.php
typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseUserPermissionCheck.php
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInline.php
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInlineConfiguration.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInlineExpandCollapseState.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInputPlaceholders.php
typo3/sysext/backend/Classes/Form/InlineRelatedRecordResolver.php [deleted file]
typo3/sysext/backend/Classes/Form/InlineStackProcessor.php
typo3/sysext/backend/Classes/Form/Utility/FormEngineUtility.php
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaInlineConfigurationTest.php [new file with mode: 0644]
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaInlineExpandCollapseStateTest.php [new file with mode: 0644]
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaInputPlaceholdersTest.php
typo3/sysext/backend/Tests/Unit/Form/InlineStackProcessorTest.php
typo3/sysext/core/Configuration/DefaultConfiguration.php

index 36a547d..0eb421d 100644 (file)
@@ -19,7 +19,6 @@ use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Backend\Form\Exception\AccessDeniedException;
 use TYPO3\CMS\Backend\Form\FormDataCompiler;
 use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
-use TYPO3\CMS\Backend\Form\InlineRelatedRecordResolver;
 use TYPO3\CMS\Backend\Form\InlineStackProcessor;
 use TYPO3\CMS\Backend\Form\NodeFactory;
 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
@@ -34,18 +33,6 @@ use TYPO3\CMS\Core\Utility\MathUtility;
 class FormInlineAjaxController {
 
        /**
-        * @var InlineStackProcessor
-        */
-       protected $inlineStackProcessor;
-
-       /**
-        * Create the inline stack processor
-        */
-       public function __construct() {
-               $this->inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
-       }
-
-       /**
         * Create a new inline child via AJAX.
         *
         * @param ServerRequestInterface $request
@@ -54,309 +41,350 @@ class FormInlineAjaxController {
         */
        public function createAction(ServerRequestInterface $request, ResponseInterface $response) {
                $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
-               $domObjectId = $ajaxArguments[0];
-               $createAfterUid = isset($ajaxArguments[1]) ? $ajaxArguments[1] : 0;
-               // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
-               $this->inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
-               $this->inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
-               $response->getBody()->write(json_encode($this->renderInlineNewChildRecord($domObjectId, $createAfterUid)));
-               return $response;
-       }
 
-       /**
-        * Show the details of a child record.
-        *
-        * @param ServerRequestInterface $request
-        * @param ResponseInterface $response
-        * @return ResponseInterface
-        */
-       public function detailsAction(ServerRequestInterface $request, ResponseInterface $response) {
-               $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
                $domObjectId = $ajaxArguments[0];
-               // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
-               $this->inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
-               $this->inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
-               $response->getBody()->write(json_encode($this->renderInlineChildRecord($domObjectId)));
-               return $response;
-       }
-
-       /**
-        * Adds localizations or synchronizes the locations of all child records.
-        *
-        * @param ServerRequestInterface $request the incoming request
-        * @param ResponseInterface $response the empty response
-        * @return ResponseInterface the filled response
-        */
-       public function synchronizeLocalizeAction(ServerRequestInterface $request, ResponseInterface $response) {
-               $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
-               $domObjectId = $ajaxArguments[0];
-               $type = $ajaxArguments[1];
-               // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
-               $this->inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
-               $this->inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
-               $inlineFirstPid = FormEngineUtility::getInlineFirstPidFromDomObjectId($domObjectId);
-               $response->getBody()->write(json_encode($this->renderInlineSynchronizeLocalizeRecords($type, $inlineFirstPid)));
-               return $response;
-       }
-
-       /**
-        * Adds localizations or synchronizes the locations of all child records.
-        *
-        * @param ServerRequestInterface $request the incoming request
-        * @param ResponseInterface $response the empty response
-        * @return ResponseInterface the filled response
-        */
-       public function expandOrCollapseAction(ServerRequestInterface $request, ResponseInterface $response) {
-               $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
-               $domObjectId = $ajaxArguments[0];
-               // Parse the DOM identifier (string), add the levels to the structure stack (array), don't load TCA config
-               $this->inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId, FALSE);
-               $expand = $ajaxArguments[1];
-               $collapse = $ajaxArguments[2];
-               $this->setInlineExpandedCollapsedState($expand, $collapse);
-               $response->getBody()->write(json_encode(array()));
-               return $response;
-       }
-
-       /**
-        * Handle AJAX calls to dynamically load the form fields of a given inline record.
-        *
-        * @param string $domObjectId The calling object in hierarchy, that requested a new record.
-        * @return array An array to be used for JSON
-        */
-       protected function renderInlineChildRecord($domObjectId) {
-               // The current table - for this table we should add/import records
-               $current = $this->inlineStackProcessor->getUnstableStructure();
-               // The parent table - this table embeds the current table
-               $parent = $this->inlineStackProcessor->getStructureLevel(-1);
-               $config = $parent['config'];
-
-               if (empty($config['foreign_table']) || !is_array($GLOBALS['TCA'][$config['foreign_table']])) {
-                       return $this->getErrorMessageForAJAX('Wrong configuration in table ' . $parent['table']);
+               $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
+               $childChildUid = NULL;
+               if (isset($ajaxArguments[1]) && MathUtility::canBeInterpretedAsInteger($ajaxArguments[1])) {
+                       $childChildUid = (int)$ajaxArguments[1];
                }
 
-               $config = FormEngineUtility::mergeInlineConfiguration($config);
+               // Parse the DOM identifier, add the levels to the structure stack
+               /** @var InlineStackProcessor $inlineStackProcessor */
+               $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
+               $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
+               $inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
 
-               // Set flag in config so that only the fields are rendered
-               $config['renderFieldsOnly'] = TRUE;
-               $collapseAll = isset($config['appearance']['collapseAll']) && $config['appearance']['collapseAll'];
-               $expandSingle = isset($config['appearance']['expandSingle']) && $config['appearance']['expandSingle'];
-
-               $inlineRelatedRecordResolver = GeneralUtility::makeInstance(InlineRelatedRecordResolver::class);
-               $record = $inlineRelatedRecordResolver->getRecord($current['table'], $current['uid']);
-
-               $inlineFirstPid = FormEngineUtility::getInlineFirstPidFromDomObjectId($domObjectId);
-               // The HTML-object-id's prefix of the dynamically created record
-               $objectPrefix = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid) . '-' . $current['table'];
-               $objectId = $objectPrefix . '-' . $record['uid'];
-
-               $formDataInput = [];
-               $formDataInput['vanillaUid'] = (int)$parent['uid'];
-               $formDataInput['command'] = 'edit';
-               $formDataInput['tableName'] = $parent['table'];
-               $formDataInput['inlineFirstPid'] = $inlineFirstPid;
-               $formDataInput['inlineStructure'] = $this->inlineStackProcessor->getStructure();
+               // Parent, this table embeds the child table
+               $parent = $inlineStackProcessor->getStructureLevel(-1);
+               $parentFieldName = $parent['field'];
 
+               if (MathUtility::canBeInterpretedAsInteger($parent['uid'])) {
+                       $command = 'edit';
+                       $vanillaUid = (int)$parent['uid'];
+               } else {
+                       $command = 'new';
+                       $vanillaUid = (int)$inlineFirstPid;
+               }
+               $formDataCompilerInputForParent = [
+                       'vanillaUid' => $vanillaUid,
+                       'command' => $command,
+                       'tableName' => $parent['table'],
+                       'inlineFirstPid' => $inlineFirstPid,
+                       // @todo: still needed?
+                       'inlineStructure' => $inlineStackProcessor->getStructure(),
+                       // Do not resolve existing children, we don't need them now
+                       'inlineResolveExistingChildren' => FALSE,
+               ];
+               // @todo: It would be enough to restrict parsing of parent to "inlineConfiguration" of according inline field only
+               // @todo: maybe, not even the database row is required?? We only need overruleTypesArray and sanitized configuration?
+               // @todo: Improving this area would significantly speed up this parsing!
                /** @var TcaDatabaseRecord $formDataGroup */
                $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
                /** @var FormDataCompiler $formDataCompiler */
                $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
+               $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
+               $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
+
+               // Child, a record from this table should be rendered
+               $child = $inlineStackProcessor->getUnstableStructure();
+               if (MathUtility::canBeInterpretedAsInteger($child['uid'])) {
+                       // If uid comes in, it is the id of the record neighbor record "create after"
+                       $childVanillaUid = -1 * abs((int)$child['uid']);
+               } else {
+                       // Else inline first Pid is the storage pid of new inline records
+                       $childVanillaUid = (int)$inlineFirstPid;
+               }
 
-               $formData = $formDataCompiler->compile($formDataInput);
-               $formData['renderType'] = 'inlineRecordContainer';
-               $formData['inlineRelatedRecordToRender'] = $record;
-               $formData['inlineRelatedRecordConfig'] = $config;
-
-               try {
-                       // Access to this record may be denied, create an according error message in this case
-                       $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
-                       $childArray = $nodeFactory->create($formData)->render();
-               } catch (AccessDeniedException $e) {
-                       return $this->getErrorMessageForAJAX('Access denied');
+               $childTableName = $parentConfig['foreign_table'];
+               $overruleTypesArray = [];
+               if (isset($parentConfig['foreign_types'])) {
+                       $overruleTypesArray = $parentConfig['foreign_types'];
+               }
+               /** @var TcaDatabaseRecord $formDataGroup */
+               $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
+               /** @var FormDataCompiler $formDataCompiler */
+               $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
+               $formDataCompilerInput = [
+                       'command' => 'new',
+                       'tableName' => $childTableName,
+                       'vanillaUid' => $childVanillaUid,
+                       'inlineFirstPid' => $inlineFirstPid,
+                       'overruleTypesArray' => $overruleTypesArray,
+               ];
+               $childData = $formDataCompiler->compile($formDataCompilerInput);
+
+               // Set default values for new created records
+               // @todo: This should be moved over to some data provider? foreign_record_defaults is currently not handled
+               // @todo: at all, but also not used in core itself. Bonus question: There is "overruleTypesArray", there is this
+               // @todo: default setting stuff ... why can't just "all" TCA be overwritten by parent similar to TCA type
+               // @todo: related columnsOverrides? Another gem: foreign_selector_fieldTcaOverride overwrites TCA of foreign_selector
+               // @todo: depending on parent ...
+               /**
+               if (isset($config['foreign_record_defaults']) && is_array($config['foreign_record_defaults'])) {
+                       $foreignTableConfig = $GLOBALS['TCA'][$child['table']];
+                       // The following system relevant fields can't be set by foreign_record_defaults
+                       $notSettableFields = [
+                               'uid', 'pid', 't3ver_oid', 't3ver_id', 't3ver_label', 't3ver_wsid', 't3ver_state', 't3ver_stage',
+                               't3ver_count', 't3ver_tstamp', 't3ver_move_id'
+                       ];
+                       $configurationKeysForNotSettableFields = [
+                               'crdate', 'cruser_id', 'delete', 'origUid', 'transOrigDiffSourceField', 'transOrigPointerField',
+                               'tstamp'
+                       ];
+                       foreach ($configurationKeysForNotSettableFields as $configurationKey) {
+                               if (isset($foreignTableConfig['ctrl'][$configurationKey])) {
+                                       $notSettableFields[] = $foreignTableConfig['ctrl'][$configurationKey];
+                               }
+                       }
+                       foreach ($config['foreign_record_defaults'] as $fieldName => $defaultValue) {
+                               if (isset($foreignTableConfig['columns'][$fieldName]) && !in_array($fieldName, $notSettableFields)) {
+                                       $record[$fieldName] = $defaultValue;
+                               }
+                       }
                }
+                */
+
+               // Set language of new child record to the language of the parent record:
+               // @todo: To my understanding, the below case can't happen: With localizationMode select, lang overlays
+               // @todo: of children are only created with the "synchronize" button that will trigger a different ajax action.
+               // @todo: The edge case of new page overlay together with localized media field, this code won't kick in either.
+               /**
+               if ($parent['localizationMode'] === 'select' && MathUtility::canBeInterpretedAsInteger($parent['uid'])) {
+                       $parentRecord = $inlineRelatedRecordResolver->getRecord($parent['table'], $parent['uid']);
+                       $parentLanguageField = $GLOBALS['TCA'][$parent['table']]['ctrl']['languageField'];
+                       $childLanguageField = $GLOBALS['TCA'][$child['table']]['ctrl']['languageField'];
+                       if ($parentRecord[$parentLanguageField] > 0) {
+                               $record[$childLanguageField] = $parentRecord[$parentLanguageField];
+                       }
+               }
+                */
+
+               if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
+                       // We have a foreign_selector. So, we just created a new record on an intermediate table in $mainChild.
+                       // Now, if a valid id is given as second ajax parameter, the intermediate row should be connected to an
+                       // existing record of the child-child table specified by the given uid. If there is no such id, user
+                       // clicked on "created new" and a new child-child should be created, too.
+                       if ($childChildUid) {
+                               // Fetch existing child child
+                               $childData['databaseRow'][$parentConfig['foreign_selector']] = [
+                                       $childChildUid,
+                               ];
+                               $childData['combinationChild'] = $this->compileCombinationChild($childData, $parentConfig);
+                       } else {
+                               /** @var TcaDatabaseRecord $formDataGroup */
+                               $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
+                               /** @var FormDataCompiler $formDataCompiler */
+                               $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
+                               $formDataCompilerInput = [
+                                       'command' => 'new',
+                                       'tableName' => $childData['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'],
+                                       'vanillaUid' => (int)$inlineFirstPid,
+                                       'inlineFirstPid' => (int)$inlineFirstPid,
+                               ];
+                               $childData['combinationChild'] = $formDataCompiler->compile($formDataCompilerInput);
+                       }
+               } elseif ($parentConfig['foreign_selector'] && $childChildUid) {
+                       // @todo: Setting these values here is too late, it should happen before single fields are
+                       // @todo: prepared in $childData. Otherwise fields that use this relation like for instance
+                       // @todo: the placeholder relation does not work for "fresh" children. This stuff here
+                       // @todo: probably needs to be moved somewhere after "initializeNew" data provider.
+                       // There is an existing child-child, but it should not be rendered directly. Still, the intermediate
+                       // record must point to the child table
+                       if ($childData['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['type'] === 'select') {
+                               $childData['databaseRow'][$parentConfig['foreign_selector']] = [
+                                       $childChildUid,
+                               ];
+                       }
+                       // This is the case for fal, uid_local is a group field in sys_file_reference
+                       if ($childData['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['type'] === 'group') {
+                               $childData['databaseRow'][$parentConfig['foreign_selector']]
+                                       = $childData['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['allowed'] . '_' . $childChildUid;
+                       }
+               }
+
+               $childData['inlineParentUid'] = (int)$parent['uid'];
+               $childData['inlineParentConfig'] = $parentConfig;
+               // @todo: needed?
+               $childData['inlineStructure'] = $inlineStackProcessor->getStructure();
+               // @todo: needed?
+               $childData['inlineExpandCollapseStateArray'] = $parentData['inlineExpandCollapseStateArray'];
+               $childData['renderType'] = 'inlineRecordContainer';
+               $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
+               $childResult = $nodeFactory->create($childData)->render();
 
                $jsonArray = [
                        'data' => '',
                        'stylesheetFiles' => [],
                        'scriptCall' => [],
                ];
-               $jsonArray['scriptCall'][] = 'inline.domAddRecordDetails(' . GeneralUtility::quoteJSvalue($domObjectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . ($expandSingle ? '1' : '0') . ',json.data);';
-               if ($config['foreign_unique']) {
-                       $jsonArray['scriptCall'][] = 'inline.removeUsed(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($record['uid']) . ');';
-               }
-               if (!empty($childArray['inlineData'])) {
-                       $jsonArray['scriptCall'][] = 'inline.addToDataArray(' . json_encode($childArray['inlineData']) . ');';
+
+               // The HTML-object-id's prefix of the dynamically created record
+               $objectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
+               $objectPrefix = $objectName . '-' . $child['table'];
+               $objectId = $objectPrefix . '-' . $childData['databaseRow']['uid'];
+               $expandSingle = $parentConfig['appearance']['expandSingle'];
+               if (!$child['uid']) {
+                       $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\',' . GeneralUtility::quoteJSvalue($objectName . '_records') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
+                       $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',null,' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
+               } else {
+                       $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'after\',' . GeneralUtility::quoteJSvalue($domObjectId . '_div') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
+                       $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',' . GeneralUtility::quoteJSvalue($child['uid']) . ',' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
                }
-               $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childArray);
-               if ($config['appearance']['useSortable']) {
-                       $inlineObjectName = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
+               $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
+               if ($parentConfig['appearance']['useSortable']) {
+                       $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
                        $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
                }
-               if (!$collapseAll && $expandSingle) {
-                       $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($record['uid']) . ');';
+               if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
+                       $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ');';
                }
+               // Fade out and fade in the new record in the browser view to catch the user's eye
+               $jsonArray['scriptCall'][] = 'inline.fadeOutFadeIn(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');';
+
+               $response->getBody()->write(json_encode($jsonArray));
 
-               return $jsonArray;
+               return $response;
        }
 
        /**
-        * Handle AJAX calls to show a new inline-record of the given table.
+        * Show the details of a child record.
         *
-        * @param string $domObjectId The calling object in hierarchy, that requested a new record.
-        * @param string|int $foreignUid If set, the new record should be inserted after that one.
-        * @return array An array to be used for JSON
+        * @param ServerRequestInterface $request
+        * @param ResponseInterface $response
+        * @return ResponseInterface
         */
-       protected function renderInlineNewChildRecord($domObjectId, $foreignUid) {
-               // The current table - for this table we should add/import records
-               $current = $this->inlineStackProcessor->getUnstableStructure();
-               // The parent table - this table embeds the current table
-               $parent = $this->inlineStackProcessor->getStructureLevel(-1);
-               $config = $parent['config'];
-
-               if (empty($config['foreign_table']) || !is_array($GLOBALS['TCA'][$config['foreign_table']])) {
-                       return $this->getErrorMessageForAJAX('Wrong configuration in table ' . $parent['table']);
-               }
-
-               /** @var InlineRelatedRecordResolver $inlineRelatedRecordResolver */
-               $inlineRelatedRecordResolver = GeneralUtility::makeInstance(InlineRelatedRecordResolver::class);
-
-               $config = FormEngineUtility::mergeInlineConfiguration($config);
-
-               $collapseAll = isset($config['appearance']['collapseAll']) && $config['appearance']['collapseAll'];
-               $expandSingle = isset($config['appearance']['expandSingle']) && $config['appearance']['expandSingle'];
-
-               $inlineFirstPid = FormEngineUtility::getInlineFirstPidFromDomObjectId($domObjectId);
-
-               // Dynamically create a new record
-               if (!$foreignUid || !MathUtility::canBeInterpretedAsInteger($foreignUid) || $config['foreign_selector']) {
-                       $record = $inlineRelatedRecordResolver->getNewRecord($inlineFirstPid, $current['table']);
-                       // Set default values for new created records
-                       if (isset($config['foreign_record_defaults']) && is_array($config['foreign_record_defaults'])) {
-                               $foreignTableConfig = $GLOBALS['TCA'][$current['table']];
-                               // The following system relevant fields can't be set by foreign_record_defaults
-                               $notSettableFields = array(
-                                       'uid', 'pid', 't3ver_oid', 't3ver_id', 't3ver_label', 't3ver_wsid', 't3ver_state', 't3ver_stage',
-                                       't3ver_count', 't3ver_tstamp', 't3ver_move_id'
-                               );
-                               $configurationKeysForNotSettableFields = array(
-                                       'crdate', 'cruser_id', 'delete', 'origUid', 'transOrigDiffSourceField', 'transOrigPointerField',
-                                       'tstamp'
-                               );
-                               foreach ($configurationKeysForNotSettableFields as $configurationKey) {
-                                       if (isset($foreignTableConfig['ctrl'][$configurationKey])) {
-                                               $notSettableFields[] = $foreignTableConfig['ctrl'][$configurationKey];
-                                       }
-                               }
-                               foreach ($config['foreign_record_defaults'] as $fieldName => $defaultValue) {
-                                       if (isset($foreignTableConfig['columns'][$fieldName]) && !in_array($fieldName, $notSettableFields)) {
-                                               $record[$fieldName] = $defaultValue;
-                                       }
-                               }
-                       }
-                       // Set language of new child record to the language of the parent record:
-                       if ($parent['localizationMode'] === 'select' && MathUtility::canBeInterpretedAsInteger($parent['uid'])) {
-                               $parentRecord = $inlineRelatedRecordResolver->getRecord($parent['table'], $parent['uid']);
-                               $parentLanguageField = $GLOBALS['TCA'][$parent['table']]['ctrl']['languageField'];
-                               $childLanguageField = $GLOBALS['TCA'][$current['table']]['ctrl']['languageField'];
-                               if ($parentRecord[$parentLanguageField] > 0) {
-                                       $record[$childLanguageField] = $parentRecord[$parentLanguageField];
-                               }
-                       }
-               } else {
-                       // @todo: Check this: Else also hits if $foreignUid = 0?
-                       $record = $inlineRelatedRecordResolver->getRecord($current['table'], $foreignUid);
-               }
-               // Now there is a foreign_selector, so there is a new record on the intermediate table, but
-               // this intermediate table holds a field, which is responsible for the foreign_selector, so
-               // we have to set this field to the uid we get - or if none, to a new uid
-               if ($config['foreign_selector'] && $foreignUid) {
-                       $selConfig = FormEngineUtility::getInlinePossibleRecordsSelectorConfig($config, $config['foreign_selector']);
-                       // For a selector of type group/db, prepend the tablename (<tablename>_<uid>):
-                       $record[$config['foreign_selector']] = $selConfig['type'] != 'groupdb' ? '' : $selConfig['table'] . '_';
-                       $record[$config['foreign_selector']] .= $foreignUid;
-                       if ($selConfig['table'] === 'sys_file') {
-                               $fileRecord = $inlineRelatedRecordResolver->getRecord($selConfig['table'], $foreignUid);
-                               if ($fileRecord !== FALSE && !$this->checkInlineFileTypeAccessForField($selConfig, $fileRecord)) {
-                                       return $this->getErrorMessageForAJAX('File extension ' . $fileRecord['extension'] . ' is not allowed here!');
-                               }
-                       }
-               }
-               // The HTML-object-id's prefix of the dynamically created record
-               $objectName = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
-               $objectPrefix = $objectName . '-' . $current['table'];
-               $objectId = $objectPrefix . '-' . $record['uid'];
-
-               $formDataInput = [];
-               $formDataInput['vanillaUid'] = (int)$parent['uid'];
-               $formDataInput['command'] = 'edit';
-               $formDataInput['tableName'] = $parent['table'];
-               $formDataInput['inlineFirstPid'] = $inlineFirstPid;
-               $formDataInput['inlineStructure'] = $this->inlineStackProcessor->getStructure();
-
-               if (!MathUtility::canBeInterpretedAsInteger($parent['uid']) && (int)$formDataInput['inlineFirstPid'] > 0) {
-                       $formDataInput['command'] = 'new';
-                       $formDataInput['vanillaUid'] = (int)$formDataInput['inlineFirstPid'];
-               }
+       public function detailsAction(ServerRequestInterface $request, ResponseInterface $response) {
+               $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
 
+               $domObjectId = $ajaxArguments[0];
+               $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
+
+               // Parse the DOM identifier, add the levels to the structure stack
+               /** @var InlineStackProcessor $inlineStackProcessor */
+               $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
+               $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
+               $inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
+
+               // Parent, this table embeds the child table
+               $parent = $inlineStackProcessor->getStructureLevel(-1);
+               $parentFieldName = $parent['field'];
+
+               $formDataCompilerInputForParent = [
+                       'vanillaUid' => (int)$parent['uid'],
+                       'command' => 'edit',
+                       'tableName' => $parent['table'],
+                       'inlineFirstPid' => $inlineFirstPid,
+                       // @todo: still needed?
+                       'inlineStructure' => $inlineStackProcessor->getStructure(),
+                       // Do not resolve existing children, we don't need them now
+                       'inlineResolveExistingChildren' => FALSE,
+               ];
+               // @todo: It would be enough to restrict parsing of parent to "inlineConfiguration" of according inline field only
+               // @todo: maybe, not even the database row is required?? We only need overruleTypesArray and sanitized configuration?
+               // @todo: Improving this area would significantly speed up this parsing!
                /** @var TcaDatabaseRecord $formDataGroup */
                $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
                /** @var FormDataCompiler $formDataCompiler */
                $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
+               $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
+               $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
+               // Set flag in config so that only the fields are rendered
+               // @todo: Solve differently / rename / whatever
+               $parentConfig['renderFieldsOnly'] = TRUE;
 
-               $formData = $formDataCompiler->compile($formDataInput);
-               $formData['renderType'] = 'inlineRecordContainer';
-               $formData['inlineRelatedRecordToRender'] = $record;
-               $formData['inlineRelatedRecordConfig'] = $config;
-
-               try {
-                       // Access to this record may be denied, create an according error message in this case
-                       $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
-                       $childArray = $nodeFactory->create($formData)->render();
-               } catch (AccessDeniedException $e) {
-                       return $this->getErrorMessageForAJAX('Access denied');
-               }
+               // Child, a record from this table should be rendered
+               $child = $inlineStackProcessor->getUnstableStructure();
+
+               $childData = $this->compileChild($parentData, $parentFieldName, (int)$child['uid']);
+
+               $childData['inlineParentUid'] = (int)$parent['uid'];
+               $childData['inlineParentConfig'] = $parentConfig;
+               // @todo: needed?
+               $childData['inlineStructure'] = $inlineStackProcessor->getStructure();
+               // @todo: needed?
+               $childData['inlineExpandCollapseStateArray'] = $parentData['inlineExpandCollapseStateArray'];
+               $childData['renderType'] = 'inlineRecordContainer';
+               $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
+               $childResult = $nodeFactory->create($childData)->render();
 
                $jsonArray = [
                        'data' => '',
                        'stylesheetFiles' => [],
                        'scriptCall' => [],
                ];
-               if (!$current['uid']) {
-                       $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\',' . GeneralUtility::quoteJSvalue($objectName . '_records') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
-                       $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($record['uid']) . ',null,' . GeneralUtility::quoteJSvalue($foreignUid) . ');';
-               } else {
-                       $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'after\',' . GeneralUtility::quoteJSvalue($domObjectId . '_div') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
-                       $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($record['uid']) . ',' . GeneralUtility::quoteJSvalue($current['uid']) . ',' . GeneralUtility::quoteJSvalue($foreignUid) . ');';
+
+               // The HTML-object-id's prefix of the dynamically created record
+               $objectPrefix = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid) . '-' . $child['table'];
+               $objectId = $objectPrefix . '-' . (int)$child['uid'];
+               $expandSingle = $parentConfig['appearance']['expandSingle'];
+               $jsonArray['scriptCall'][] = 'inline.domAddRecordDetails(' . GeneralUtility::quoteJSvalue($domObjectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . ($expandSingle ? '1' : '0') . ',json.data);';
+               if ($parentConfig['foreign_unique']) {
+                       $jsonArray['scriptCall'][] = 'inline.removeUsed(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
                }
-               $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childArray);
-               if ($config['appearance']['useSortable']) {
-                       $inlineObjectName = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
+               $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
+               if ($parentConfig['appearance']['useSortable']) {
+                       $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
                        $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
                }
-               if (!$collapseAll && $expandSingle) {
-                       $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($record['uid']) . ');';
+               if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
+                       $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
                }
-               // Fade out and fade in the new record in the browser view to catch the user's eye
-               $jsonArray['scriptCall'][] = 'inline.fadeOutFadeIn(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');';
 
-               return $jsonArray;
+               $response->getBody()->write(json_encode($jsonArray));
+
+               return $response;
        }
 
        /**
+        * Adds localizations or synchronizes the locations of all child records.
         * Handle AJAX calls to localize all records of a parent, localize a single record or to synchronize with the original language parent.
         *
-        * @param string $type Defines the type 'localize' or 'synchronize' (string) or a single uid to be localized (int)
-        * @param int $inlineFirstPid Inline first pid
-        * @return array An array to be used for JSON
+        * @param ServerRequestInterface $request the incoming request
+        * @param ResponseInterface $response the empty response
+        * @return ResponseInterface the filled response
         */
-       protected function renderInlineSynchronizeLocalizeRecords($type, $inlineFirstPid) {
+       public function synchronizeLocalizeAction(ServerRequestInterface $request, ResponseInterface $response) {
+               $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
+               $domObjectId = $ajaxArguments[0];
+               $type = $ajaxArguments[1];
+
+               /** @var InlineStackProcessor $inlineStackProcessor */
+               $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
+               // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
+               $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
+               $inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
+               $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
+
                $jsonArray = FALSE;
-               if (GeneralUtility::inList('localize,synchronize', $type) || MathUtility::canBeInterpretedAsInteger($type)) {
-                       $inlineRelatedRecordResolver = GeneralUtility::makeInstance(InlineRelatedRecordResolver::class);
-                       // The parent level:
-                       $parent = $this->inlineStackProcessor->getStructureLevel(-1);
-                       $current = $this->inlineStackProcessor->getUnstableStructure();
-                       $parentRecord = $inlineRelatedRecordResolver->getRecord($parent['table'], $parent['uid']);
+               if ($type === 'localize' || $type === 'synchronize' || MathUtility::canBeInterpretedAsInteger($type)) {
+                       // Parent, this table embeds the child table
+                       $parent = $inlineStackProcessor->getStructureLevel(-1);
+                       $parentFieldName = $parent['field'];
+
+                       // Child, a record from this table should be rendered
+                       $child = $inlineStackProcessor->getUnstableStructure();
+
+                       $formDataCompilerInputForParent = [
+                               'vanillaUid' => (int)$parent['uid'],
+                               'command' => 'edit',
+                               'tableName' => $parent['table'],
+                               'inlineFirstPid' => $inlineFirstPid,
+                               // @todo: still needed?
+                               'inlineStructure' => $inlineStackProcessor->getStructure(),
+                               // Do not compile existing children, we don't need them now
+                               'inlineCompileExistingChildren' => FALSE,
+                       ];
+                       // @todo: It would be enough to restrict parsing of parent to "inlineConfiguration" of according inline field only
+                       // @todo: maybe, not even the database row is required?? We only need overruleTypesArray and sanitized configuration?
+                       // @todo: Improving this area would significantly speed up this parsing!
+                       /** @var TcaDatabaseRecord $formDataGroup */
+                       $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
+                       /** @var FormDataCompiler $formDataCompiler */
+                       $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
+                       $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
+                       $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
+                       $oldItemList = $parentData['databaseRow'][$parentFieldName];
 
                        $cmd = array();
                        $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = $parent['field'] . ',' . $type;
@@ -366,77 +394,174 @@ class FormInlineAjaxController {
                        $tce->start(array(), $cmd);
                        $tce->process_cmdmap();
 
-                       $oldItemList = $parentRecord[$parent['field']];
-                       $newItemList = $tce->registerDBList[$parent['table']][$parent['uid']][$parent['field']];
+                       $newItemList = $tce->registerDBList[$parent['table']][$parent['uid']][$parentFieldName];
 
                        $jsonArray = array(
                                'data' => '',
                                'stylesheetFiles' => [],
                                'scriptCall' => [],
                        );
-                       $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
-                       $nameObjectForeignTable = $nameObject . '-' . $current['table'];
-                       // Get the name of the field pointing to the original record:
-                       $transOrigPointerField = $GLOBALS['TCA'][$current['table']]['ctrl']['transOrigPointerField'];
-                       // Get the name of the field used as foreign selector (if any):
-                       $foreignSelector = isset($parent['config']['foreign_selector']) && $parent['config']['foreign_selector'] ? $parent['config']['foreign_selector'] : FALSE;
-                       // Convert lists to array with uids of child records:
+                       $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
+                       $nameObjectForeignTable = $nameObject . '-' . $child['table'];
+
                        $oldItems = FormEngineUtility::getInlineRelatedRecordsUidArray($oldItemList);
                        $newItems = FormEngineUtility::getInlineRelatedRecordsUidArray($newItemList);
-                       // Determine the items that were localized or localized:
+
+                       // Set the items that should be removed in the forms view:
                        $removedItems = array_diff($oldItems, $newItems);
+                       foreach ($removedItems as $childUid) {
+                               $jsonArray['scriptCall'][] = 'inline.deleteRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $childUid) . ', {forceDirectRemoval: true});';
+                       }
+
                        $localizedItems = array_diff($newItems, $oldItems);
-                       // Set the items that should be removed in the forms view:
-                       foreach ($removedItems as $item) {
-                               $jsonArray['scriptCall'][] = 'inline.deleteRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $item) . ', {forceDirectRemoval: true});';
+                       foreach ($localizedItems as $childUid) {
+                               $childData = $this->compileChild($parentData, $parentFieldName, (int)$childUid);
+
+                               $childData['inlineParentUid'] = (int)$parent['uid'];
+                               $childData['inlineParentConfig'] = $parentConfig;
+                               // @todo: needed?
+                               $childData['inlineStructure'] = $inlineStackProcessor->getStructure();
+                               // @todo: needed?
+                               $childData['inlineExpandCollapseStateArray'] = $parentData['inlineExpandCollapseStateArray'];
+                               $childData['renderType'] = 'inlineRecordContainer';
+                               $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
+                               $childResult = $nodeFactory->create($childData)->render();
+
+                               $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
+
+                               // Get the name of the field used as foreign selector (if any):
+                               $foreignSelector = isset($parentConfig['foreign_selector']) && $parentConfig['foreign_selector'] ? $parentConfig['foreign_selector'] : FALSE;
+                               $selectedValue = $foreignSelector ? GeneralUtility::quoteJSvalue($childData['databaseRow'][$foreignSelector]) : 'null';
+                               if (is_array($selectedValue)) {
+                                       $selectedValue = $selectedValue[0];
+                               }
+                               $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable) . ', ' . GeneralUtility::quoteJSvalue($childUid) . ', null, ' . $selectedValue . ');';
+                               // Remove possible virtual records in the form which showed that a child records could be localized:
+                               $transOrigPointerFieldName = $GLOBALS['TCA'][$childData['table']]['ctrl']['transOrigPointerField'];
+                               $transOrigPointerField = FALSE;
+                               if (isset($childData['databaseRow'][$transOrigPointerFieldName]) && $childData['dataabaseRow'][$transOrigPointerFieldName]) {
+                                       $transOrigPointerField = $childData['databaseRow'][$transOrigPointerFieldName];
+                                       if (is_array($transOrigPointerField)) {
+                                               $transOrigPointerField = $transOrigPointerField[0];
+                                       }
+                                       $jsonArray['scriptCall'][] = 'inline.fadeAndRemove(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $transOrigPointerField . '_div') . ');';
+                               }
+                               if (!empty($childResult['html'])) {
+                                       array_unshift($jsonArray['scriptCall'], 'inline.domAddNewRecord(\'bottom\', ' . GeneralUtility::quoteJSvalue($nameObject . '_records') . ', ' . GeneralUtility::quoteJSvalue($nameObjectForeignTable) . ', json.data);');
+                               }
                        }
-                       foreach ($localizedItems as $item) {
-                               $row = $inlineRelatedRecordResolver->getRecord($current['table'], $item);
-                               $selectedValue = $foreignSelector ? GeneralUtility::quoteJSvalue($row[$foreignSelector]) : 'null';
+               }
 
-                               $formDataInput = [];
-                               $formDataInput['vanillaUid'] = (int)$parent['uid'];
-                               $formDataInput['command'] = 'edit';
-                               $formDataInput['tableName'] = $parent['table'];
-                               $formDataInput['inlineFirstPid'] = $inlineFirstPid;
-                               $formDataInput['inlineStructure'] = $this->inlineStackProcessor->getStructure();
+               $response->getBody()->write(json_encode($jsonArray));
 
-                               /** @var TcaDatabaseRecord $formDataGroup */
-                               $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
-                               /** @var FormDataCompiler $formDataCompiler */
-                               $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
+               return $response;
+       }
 
-                               $formData = $formDataCompiler->compile($formDataInput);
-                               $formData['renderType'] = 'inlineRecordContainer';
-                               $formData['inlineRelatedRecordToRender'] = $row;
-                               $formData['inlineRelatedRecordConfig'] = $parent['config'];
-
-                               try {
-                                       // Access to this record may be denied, create an according error message in this case
-                                       $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
-                                       $childArray = $nodeFactory->create($formData)->render();
-                               } catch (AccessDeniedException $e) {
-                                       return $this->getErrorMessageForAJAX('Access denied');
-                               }
+       /**
+        * Adds localizations or synchronizes the locations of all child records.
+        *
+        * @param ServerRequestInterface $request the incoming request
+        * @param ResponseInterface $response the empty response
+        * @return ResponseInterface the filled response
+        */
+       public function expandOrCollapseAction(ServerRequestInterface $request, ResponseInterface $response) {
+               $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
+               $domObjectId = $ajaxArguments[0];
 
-                               $jsonArray['html'] .= $childArray['html'];
-                               $jsonArray = [
-                                       'data' => '',
-                                       'stylesheetFiles' => [],
-                                       'scriptCall' => [],
-                               ];
-                               $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childArray);
-                               $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable) . ', ' . GeneralUtility::quoteJSvalue($item) . ', null, ' . $selectedValue . ');';
-                               // Remove possible virtual records in the form which showed that a child records could be localized:
-                               if (isset($row[$transOrigPointerField]) && $row[$transOrigPointerField]) {
-                                       $jsonArray['scriptCall'][] = 'inline.fadeAndRemove(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $row[$transOrigPointerField] . '_div') . ');';
-                               }
+               /** @var InlineStackProcessor $inlineStackProcessor */
+               $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
+               // Parse the DOM identifier (string), add the levels to the structure stack (array), don't load TCA config
+               $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
+               $expand = $ajaxArguments[1];
+               $collapse = $ajaxArguments[2];
+
+               $backendUser = $this->getBackendUserAuthentication();
+               // The current table - for this table we should add/import records
+               $currentTable = $inlineStackProcessor->getUnstableStructure();
+               $currentTable = $currentTable['table'];
+               // The top parent table - this table embeds the current table
+               $top = $inlineStackProcessor->getStructureLevel(0);
+               $topTable = $top['table'];
+               $topUid = $top['uid'];
+               $inlineView = $this->getInlineExpandCollapseStateArray();
+               // Only do some action if the top record and the current record were saved before
+               if (MathUtility::canBeInterpretedAsInteger($topUid)) {
+                       $expandUids = GeneralUtility::trimExplode(',', $expand);
+                       $collapseUids = GeneralUtility::trimExplode(',', $collapse);
+                       // Set records to be expanded
+                       foreach ($expandUids as $uid) {
+                               $inlineView[$topTable][$topUid][$currentTable][] = $uid;
                        }
-                       if (!empty($jsonArray['data'])) {
-                               array_unshift($jsonArray['scriptCall'], 'inline.domAddNewRecord(\'bottom\', ' . GeneralUtility::quoteJSvalue($nameObject . '_records') . ', ' . GeneralUtility::quoteJSvalue($nameObjectForeignTable) . ', json.data);');
+                       // Set records to be collapsed
+                       foreach ($collapseUids as $uid) {
+                               $inlineView[$topTable][$topUid][$currentTable] = $this->removeFromArray($uid, $inlineView[$topTable][$topUid][$currentTable]);
+                       }
+                       // Save states back to database
+                       if (is_array($inlineView[$topTable][$topUid][$currentTable])) {
+                               $inlineView[$topTable][$topUid][$currentTable] = array_unique($inlineView[$topTable][$topUid][$currentTable]);
+                               $backendUser->uc['inlineView'] = serialize($inlineView);
+                               $backendUser->writeUC();
                        }
                }
-               return $jsonArray;
+
+               $response->getBody()->write(json_encode(array()));
+               return $response;
+       }
+
+       /**
+        * Compile a full child record
+        *
+        * @param array $result Result array of parent
+        * @param string $parentFieldName Name of parent field
+        * @param int $childUid Uid of child to compile
+        * @return array Full result array
+        *
+        * @todo: This clones methods compileChild and compileCombinationChild from TcaInline Provider.
+        * @todo: Find something around that, eg. some option to force TcaInline provider to calculate a
+        * @todo: specific forced-open element only :)
+        */
+       protected function compileChild(array $result, $parentFieldName, $childUid) {
+               $parentConfig = $result['processedTca']['columns'][$parentFieldName]['config'];
+               $childTableName = $parentConfig['foreign_table'];
+               $overruleTypesArray = [];
+               if (isset($parentConfig['foreign_types'])) {
+                       $overruleTypesArray = $parentConfig['foreign_types'];
+               }
+               /** @var TcaDatabaseRecord $formDataGroup */
+               $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
+               /** @var FormDataCompiler $formDataCompiler */
+               $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
+               $formDataCompilerInput = [
+                       'command' => 'edit',
+                       'tableName' => $childTableName,
+                       'vanillaUid' => (int)$childUid,
+                       'inlineFirstPid' => $result['inlineFirstPid'],
+                       'overruleTypesArray' => $overruleTypesArray,
+               ];
+               // For foreign_selector with useCombination $mainChild is the mm record
+               // and $combinationChild is the child-child. For "normal" relations, $mainChild
+               // is just the normal child record and $combinationChild is empty.
+               $mainChild = $formDataCompiler->compile($formDataCompilerInput);
+               if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
+                       $mainChild['combinationChild'] = $this->compileCombinationChild($mainChild, $parentConfig);
+               }
+               return $mainChild;
+       }
+
+       /**
+        * With useCombination set, not only content of the intermediate table, but also
+        * the connected child should be rendered in one go. Prepare this here.
+        *
+        * @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) {
+               // foreign_selector on intermediate is probably type=select, so data provider of this table resolved that to the uid already
+               $intermediateUid = $intermediate['databaseRow'][$parentConfig['foreign_selector']][0];
+               $combinationChild = $this->compileChild($intermediate, $parentConfig['foreign_selector'], $intermediateUid);
+               return $combinationChild;
        }
 
        /**
@@ -494,49 +619,12 @@ class FormInlineAjaxController {
        }
 
        /**
-        * Save the expanded/collapsed state of a child record in the BE_USER->uc.
-        *
-        * @param string $expand Whether this record is expanded.
-        * @param string $collapse Whether this record is collapsed.
-        * @return void
-        */
-       protected function setInlineExpandedCollapsedState($expand, $collapse) {
-               $backendUser = $this->getBackendUserAuthentication();
-               // The current table - for this table we should add/import records
-               $currentTable = $this->inlineStackProcessor->getUnstableStructure();
-               $currentTable = $currentTable['table'];
-               // The top parent table - this table embeds the current table
-               $top = $this->inlineStackProcessor->getStructureLevel(0);
-               $topTable = $top['table'];
-               $topUid = $top['uid'];
-               $inlineView = $this->getInlineExpandCollapseStateArray();
-               // Only do some action if the top record and the current record were saved before
-               if (MathUtility::canBeInterpretedAsInteger($topUid)) {
-                       $expandUids = GeneralUtility::trimExplode(',', $expand);
-                       $collapseUids = GeneralUtility::trimExplode(',', $collapse);
-                       // Set records to be expanded
-                       foreach ($expandUids as $uid) {
-                               $inlineView[$topTable][$topUid][$currentTable][] = $uid;
-                       }
-                       // Set records to be collapsed
-                       foreach ($collapseUids as $uid) {
-                               $inlineView[$topTable][$topUid][$currentTable] = $this->removeFromArray($uid, $inlineView[$topTable][$topUid][$currentTable]);
-                       }
-                       // Save states back to database
-                       if (is_array($inlineView[$topTable][$topUid][$currentTable])) {
-                               $inlineView[$topTable][$topUid][$currentTable] = array_unique($inlineView[$topTable][$topUid][$currentTable]);
-                               $backendUser->uc['inlineView'] = serialize($inlineView);
-                               $backendUser->writeUC();
-                       }
-               }
-       }
-
-       /**
         * Checks if a record selector may select a certain file type
         *
         * @param array $selectorConfiguration
         * @param array $fileRecord
         * @return bool
+        * @todo: check this ...
         */
        protected function checkInlineFileTypeAccessForField(array $selectorConfiguration, array $fileRecord) {
                if (!empty($selectorConfiguration['PA']['fieldConf']['config']['appearance']['elementBrowserAllowed'])) {
@@ -616,6 +704,23 @@ class FormInlineAjaxController {
        }
 
        /**
+        * Get inlineFirstPid from a given objectId string
+        *
+        * @param string $domObjectId The id attribute of an element
+        * @return int|NULL Pid or null
+        */
+       protected function getInlineFirstPidFromDomObjectId($domObjectId) {
+               // Substitute FlexForm addition and make parsing a bit easier
+               $domObjectId = str_replace('---', ':', $domObjectId);
+               // The starting pattern of an object identifier (e.g. "data-<firstPidValue>-<anything>)
+               $pattern = '/^data' . '-' . '(.+?)' . '-' . '(.+)$/';
+               if (preg_match($pattern, $domObjectId, $match)) {
+                       return $match[1];
+               }
+               return NULL;
+       }
+
+       /**
         * @return BackendUserAuthentication
         */
        protected function getBackendUserAuthentication() {
index 6cbbb2b..41f68a9 100644 (file)
@@ -107,6 +107,9 @@ class FlexFormElementContainer extends AbstractContainer {
                                $originalFieldName = $parameterArray['itemFormElName'];
                                $fakeParameterArray['itemFormElName'] = $parameterArray['itemFormElName'] . $flexFormFormPrefix . '[' . $flexFormFieldName . '][vDEF]';
                                if ($fakeParameterArray['itemFormElName'] !== $originalFieldName) {
+                                       // If calculated itemFormElName is different from originalFieldName
+                                       // change the originalFieldName in TBE_EDITOR_fieldChanged. This is
+                                       // especially relevant for wizards writing their content back to hidden fields
                                        if (!empty($fakeParameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'])) {
                                                $fakeParameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = str_replace($originalFieldName, $fakeParameterArray['itemFormElName'], $fakeParameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged']);
                                        }
index 66be425..84ad912 100644 (file)
@@ -30,8 +30,6 @@ use TYPO3\CMS\Lang\LanguageService;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
 use TYPO3\CMS\Backend\Form\InlineStackProcessor;
-use TYPO3\CMS\Backend\Form\InlineRelatedRecordResolver;
-use TYPO3\CMS\Backend\Form\Exception\AccessDeniedException;
 
 /**
  * Inline element entry container.
@@ -95,28 +93,14 @@ class InlineControlContainer extends AbstractContainer {
                $parameterArray = $this->data['parameterArray'];
 
                $resultArray = $this->initializeResultArray();
-               $html = '';
-
-               // An inline field must have a foreign_table, if not, stop all further inline actions for this field
-               if (
-                       !$parameterArray['fieldConf']['config']['foreign_table']
-                       || !is_array($GLOBALS['TCA'][$parameterArray['fieldConf']['config']['foreign_table']])
-               ) {
-                       return $resultArray;
-               }
 
-               $config = FormEngineUtility::mergeInlineConfiguration($parameterArray['fieldConf']['config']);
+               $config = $parameterArray['fieldConf']['config'];
                $foreign_table = $config['foreign_table'];
 
                $language = 0;
                if (BackendUtility::isTableLocalizable($table)) {
                        $language = (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
                }
-               $minItems = MathUtility::forceIntegerInRange($config['minitems'], 0);
-               $maxItems = MathUtility::forceIntegerInRange($config['maxitems'], 0);
-               if (!$maxItems) {
-                       $maxItems = 100000;
-               }
 
                // Add the current inline job to the structure stack
                $newStructureItem = array(
@@ -128,7 +112,7 @@ class InlineControlContainer extends AbstractContainer {
                );
                // Extract FlexForm parts (if any) from element name, e.g. array('vDEF', 'lDEF', 'FlexField', 'vDEF')
                if (!empty($parameterArray['itemFormElName'])) {
-                       $flexFormParts = FormEngineUtility::extractFlexFormParts($parameterArray['itemFormElName']);
+                       $flexFormParts = $this->extractFlexFormParts($parameterArray['itemFormElName']);
                        if ($flexFormParts !== NULL) {
                                $newStructureItem['flexform'] = $flexFormParts;
                        }
@@ -140,15 +124,20 @@ class InlineControlContainer extends AbstractContainer {
                // e.g. data-<pid>-<table1>-<uid1>-<field1>-<table2>-<uid2>-<field2>
                $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
 
-               // Get the records related to this inline record
-               /** @var InlineRelatedRecordResolver $inlineRelatedRecordResolver */
-               $inlineRelatedRecordResolver = GeneralUtility::makeInstance(InlineRelatedRecordResolver::class);
-               $relatedRecords = $inlineRelatedRecordResolver->getRelatedRecords($table, $field, $row, $parameterArray, $config, $this->data['inlineFirstPid']);
-
-               // Set the first and last record to the config array
-               $relatedRecordsUids = array_keys($relatedRecords['records']);
-               $config['inline']['first'] = reset($relatedRecordsUids);
-               $config['inline']['last'] = end($relatedRecordsUids);
+               $config['inline']['first'] = FALSE;
+               // @todo: This initialization shouldn't be required data provider should take care this is set?
+               if (!is_array($this->data['parameterArray']['fieldConf']['children'])) {
+                       $this->data['parameterArray']['fieldConf']['children'] = array();
+               }
+               $firstChild = reset($this->data['parameterArray']['fieldConf']['children']);
+               if (isset($firstChild['databaseRow']['uid'])) {
+                       $config['inline']['first'] = $firstChild['databaseRow']['uid'];
+               }
+               $config['inline']['last'] = FALSE;
+               $lastChild = end($this->data['parameterArray']['fieldConf']['children']);
+               if (isset($lastChild['databaseRow']['uid'])) {
+                       $config['inline']['last'] = $lastChild['databaseRow']['uid'];
+               }
 
                $top = $inlineStackProcessor->getStructureLevel(0);
 
@@ -157,8 +146,8 @@ class InlineControlContainer extends AbstractContainer {
                        'md5' => md5($nameObject)
                );
                $this->inlineData['config'][$nameObject . '-' . $foreign_table] = array(
-                       'min' => $minItems,
-                       'max' => $maxItems,
+                       'min' => $config['minitems'],
+                       'max' => $config['maxitems'],
                        'sortable' => $config['appearance']['useSortable'],
                        'top' => array(
                                'table' => $top['table'],
@@ -179,7 +168,7 @@ class InlineControlContainer extends AbstractContainer {
                        // If uniqueness *and* selector are set, they should point to the same field - so, get the configuration of one:
                        $selConfig = FormEngineUtility::getInlinePossibleRecordsSelectorConfig($config, $config['foreign_unique']);
                        // Get the used unique ids:
-                       $uniqueIds = $this->getUniqueIds($relatedRecords['records'], $config, $selConfig['type'] == 'groupdb');
+                       $uniqueIds = $this->getUniqueIds($this->data['parameterArray']['fieldConf']['children'], $config, $selConfig['type'] == 'groupdb');
                        $possibleRecords = $this->getPossibleRecords($table, $field, $row, $config, 'foreign_unique');
                        $uniqueMax = $config['appearance']['useCombination'] || $possibleRecords === FALSE ? -1 : count($possibleRecords);
                        $this->inlineData['unique'][$nameObject . '-' . $foreign_table] = array(
@@ -210,8 +199,14 @@ class InlineControlContainer extends AbstractContainer {
                        }
                }
 
+               $numberOfFullChildren = 0;
+               foreach ($this->data['parameterArray']['fieldConf']['children'] as $child) {
+                       if (!$child['inlineIsDefaultLanguage']) {
+                               $numberOfFullChildren ++;
+                       }
+               }
                // Define how to show the "Create new record" link - if there are more than maxitems, hide it
-               if ($relatedRecords['count'] >= $maxItems || $uniqueMax > 0 && $relatedRecords['count'] >= $uniqueMax) {
+               if ($numberOfFullChildren >= $config['maxitems'] || $uniqueMax > 0 && $numberOfFullChildren >= $uniqueMax) {
                        $config['inline']['inlineNewButtonStyle'] = 'display: none;';
                        $config['inline']['inlineNewRelationButtonStyle'] = 'display: none;';
                }
@@ -220,11 +215,12 @@ class InlineControlContainer extends AbstractContainer {
                $levelLinks = $this->getLevelInteractionLink('newRecord', $nameObject . '-' . $foreign_table, $config);
 
                // Wrap all inline fields of a record with a <div> (like a container)
-               $html .= '<div class="form-group" id="' . $nameObject . '">';
+               $html = '<div class="form-group" id="' . $nameObject . '">';
                // Add the level links before all child records:
                if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'top') {
                        $html .= '<div class="form-group t3js-formengine-validation-marker">' . $levelLinks . $localizationLinks . '</div>';
                }
+
                // If it's required to select from possible child records (reusable children), add a selector box
                if ($config['foreign_selector'] && $config['appearance']['showPossibleRecordsSelector'] !== FALSE) {
                        // If not already set by the foreign_unique, set the possibleRecords here and the uniqueIds to an empty array
@@ -235,34 +231,31 @@ class InlineControlContainer extends AbstractContainer {
                        $selectorBox = $this->renderPossibleRecordsSelector($possibleRecords, $config, $uniqueIds);
                        $html .= $selectorBox . $localizationLinks;
                }
+
                $title = $languageService->sL($parameterArray['fieldConf']['label']);
                $html .= '<div class="panel-group panel-hover" data-title="' . htmlspecialchars($title) . '" id="' . $nameObject . '_records">';
 
-               $relationList = array();
-               if (!empty($relatedRecords['records'])) {
-                       foreach ($relatedRecords['records'] as $rec) {
-                               $options = $this->data;
-                               $options['inlineRelatedRecordToRender'] = $rec;
-                               $options['inlineRelatedRecordConfig'] = $config;
-                               $options['inlineData'] = $this->inlineData;
-                               $options['inlineStructure'] = $inlineStackProcessor->getStructure();
-                               $options['renderType'] = 'inlineRecordContainer';
-                               try {
-                                       // This container may raise an access denied exception, to not kill further processing,
-                                       // just a simple "empty" return is created here to ignore this field.
-                                       $childArray = $this->nodeFactory->create($options)->render();
-                               } catch (AccessDeniedException $e) {
-                                       $childArray = $this->initializeResultArray();
-                               }
-                               $html .= $childArray['html'];
-                               $childArray['html'] = '';
-                               $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
-                               if (!isset($rec['__virtual']) || !$rec['__virtual']) {
-                                       $relationList[] = $rec['uid'];
-                               }
+               $sortableRecordUids = [];
+               foreach ($this->data['parameterArray']['fieldConf']['children'] as $options) {
+                       $options['inlineParentUid'] = $row['uid'];
+                       $options['inlineParentConfig'] = $config;
+                       $options['inlineData'] = $this->inlineData;
+                       $options['inlineStructure'] = $inlineStackProcessor->getStructure();
+                       $options['inlineExpandCollapseStateArray'] = $this->data['inlineExpandCollapseStateArray'];
+                       $options['renderType'] = 'inlineRecordContainer';
+                       $childResult = $this->nodeFactory->create($options)->render();
+                       $html .= $childResult['html'];
+                       $childArray['html'] = '';
+                       $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult);
+                       if (!$options['inlineIsDefaultLanguage']) {
+                               // Don't add record to list of "valid" uids if it is only the default
+                               // language record of a not yet localized child
+                               $sortableRecordUids[] = $options['databaseRow']['uid'];
                        }
                }
+
                $html .= '</div>';
+
                // Add the level links after all child records:
                if ($config['appearance']['levelLinksPosition'] ===  'both' || $config['appearance']['levelLinksPosition'] === 'bottom') {
                        $html .= $levelLinks . $localizationLinks;
@@ -283,11 +276,13 @@ class InlineControlContainer extends AbstractContainer {
                        $html .= '</div>';
                }
                // Add Drag&Drop functions for sorting to FormEngine::$additionalJS_post
-               if (count($relationList) > 1 && $config['appearance']['useSortable']) {
+               if (count($sortableRecordUids) > 1 && $config['appearance']['useSortable']) {
                        $resultArray['additionalJavaScriptPost'][] = 'inline.createDragAndDropSorting("' . $nameObject . '_records' . '");';
                }
                // Publish the uids of the child records in the given order to the browser
-               $html .= '<input type="hidden" name="' . $nameForm . '" value="' . implode(',', $relationList) . '" ' . $this->getValidationDataAsDataAttribute(array('type' => 'inline', 'minitems' => $minItems, 'maxitems' => $maxItems)) . ' class="inlineRecord" />';
+               $html .= '<input type="hidden" name="' . $nameForm . '" value="' . implode(',', $sortableRecordUids) . '" '
+                       . $this->getValidationDataAsDataAttribute(array('type' => 'inline', 'minitems' => $config['minitems'], 'maxitems' => $config['maxitems']))
+                       . ' class="inlineRecord" />';
                // Close the wrap for all inline fields (container)
                $html .= '</div>';
 
@@ -298,18 +293,21 @@ class InlineControlContainer extends AbstractContainer {
        /**
         * Gets the uids of a select/selector that should be unique and have already been used.
         *
-        * @param array $records All inline records on this level
+        * @param array $children All inline records on this level
         * @param array $conf The TCA field configuration of the inline field to be rendered
         * @param bool $splitValue For usage with group/db, values come like "tx_table_123|Title%20abc", but we need "tx_table" and "123
         * @return array The uids, that have been used already and should be used unique
         */
-       protected function getUniqueIds($records, $conf = array(), $splitValue = FALSE) {
+       protected function getUniqueIds($children, $conf = array(), $splitValue = FALSE) {
                $uniqueIds = array();
-               if (isset($conf['foreign_unique']) && $conf['foreign_unique'] && !empty($records)) {
-                       foreach ($records as $rec) {
+               if (isset($conf['foreign_unique']) && $conf['foreign_unique'] && !empty($children)) {
+                       foreach ($children as $child) {
                                // Skip virtual records (e.g. shown in localization mode):
-                               if (!isset($rec['__virtual']) || !$rec['__virtual']) {
-                                       $value = $rec[$conf['foreign_unique']];
+                               if (!$child['inlineIsDefaultLanguage']) {
+                                       $value = $child[$conf['foreign_unique']];
+                                       if (is_array($value)) {
+                                               $value = $value['0'];
+                                       }
                                        // Split the value and extract the table and uid:
                                        if ($splitValue) {
                                                $valueParts = GeneralUtility::trimExplode('|', $value);
@@ -319,7 +317,7 @@ class InlineControlContainer extends AbstractContainer {
                                                        'table' => implode('_', $itemParts)
                                                );
                                        }
-                                       $uniqueIds[$rec['uid']] = $value;
+                                       $uniqueIds[$child['uid']] = $value;
                                }
                        }
                }
@@ -672,6 +670,26 @@ class InlineControlContainer extends AbstractContainer {
        }
 
        /**
+        * Extracts FlexForm parts of a form element name like
+        * data[table][uid][field][sDEF][lDEF][FlexForm][vDEF]
+        * Helper method used in inline
+        *
+        * @param string $formElementName The form element name
+        * @return array|NULL
+        */
+       protected function extractFlexFormParts($formElementName) {
+               $flexFormParts = NULL;
+               $matches = array();
+               if (preg_match('#^data(?:\[[^]]+\]){3}(\[data\](?:\[[^]]+\]){4,})$#', $formElementName, $matches)) {
+                       $flexFormParts = GeneralUtility::trimExplode(
+                               '][',
+                               trim($matches[1], '[]')
+                       );
+               }
+               return $flexFormParts;
+       }
+
+       /**
         * @return BackendUserAuthentication
         */
        protected function getBackendUserAuthentication() {
index f779fc2..bef1fd6 100644 (file)
@@ -16,9 +16,6 @@ namespace TYPO3\CMS\Backend\Form\Container;
 
 use TYPO3\CMS\Backend\Form\Element\InlineElementHookInterface;
 use TYPO3\CMS\Backend\Form\Exception\AccessDeniedContentEditException;
-use TYPO3\CMS\Backend\Form\FormDataCompiler;
-use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
-use TYPO3\CMS\Backend\Form\InlineRelatedRecordResolver;
 use TYPO3\CMS\Backend\Form\InlineStackProcessor;
 use TYPO3\CMS\Backend\Form\NodeFactory;
 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
@@ -34,7 +31,6 @@ use TYPO3\CMS\Core\Resource\ResourceFactory;
 use TYPO3\CMS\Core\Type\Bitmask\Permission;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
-use TYPO3\CMS\Core\Utility\StringUtility;
 use TYPO3\CMS\Lang\LanguageService;
 
 /**
@@ -100,10 +96,9 @@ class InlineRecordContainer extends AbstractContainer {
 
                $this->initHookObjects();
 
-               $row = $this->data['databaseRow'];
-               $parentUid = $row['uid'];
-               $record = $this->data['inlineRelatedRecordToRender'];
-               $config = $this->data['inlineRelatedRecordConfig'];
+               $parentUid = $this->data['inlineParentUid'];
+               $record = $this->data['databaseRow'];
+               $config = $this->data['inlineParentConfig'];
 
                $foreign_table = $config['foreign_table'];
                $foreign_selector = $config['foreign_selector'];
@@ -119,8 +114,8 @@ class InlineRecordContainer extends AbstractContainer {
 
                // Set this variable if we handle a brand new unsaved record:
                $isNewRecord = !MathUtility::canBeInterpretedAsInteger($record['uid']);
-               // Set this variable if the record is virtual and only show with header and not editable fields:
-               $isVirtualRecord = isset($record['__virtual']) && $record['__virtual'];
+               // Set this variable if the only the default language record and inline child has not been localized yet
+               $isDefaultLanguageRecord = $this->data['inlineIsDefaultLanguage'];
                // If there is a selector field, normalize it:
                if ($foreign_selector) {
                        $valueToNormalize = $record[$foreign_selector];
@@ -130,18 +125,13 @@ class InlineRecordContainer extends AbstractContainer {
                        }
                        $record[$foreign_selector] = $this->normalizeUid($valueToNormalize);
                }
-               if (!$this->checkAccess(($isNewRecord ? 'new' : 'edit'), $foreign_table, $record['uid'])) {
-                       // This is caught by InlineControlContainer or FormEngine, they need to handle this case differently
-                       // @todo: This is actually not the correct exception, but this code will vanish if inline data stuff is within provider
-                       throw new AccessDeniedContentEditException('Access denied', 1437081986);
-               }
                // Get the current naming scheme for DOM name/id attributes:
                $appendFormFieldNames = '[' . $foreign_table . '][' . $record['uid'] . ']';
                $objectId = $domObjectId . '-' . $foreign_table . '-' . $record['uid'];
                $class = '';
                $html = '';
                $combinationHtml = '';
-               if (!$isVirtualRecord) {
+               if (!$isDefaultLanguageRecord) {
                        // Get configuration:
                        $collapseAll = isset($config['appearance']['collapseAll']) && $config['appearance']['collapseAll'];
                        $expandAll = isset($config['appearance']['collapseAll']) && !$config['appearance']['collapseAll'];
@@ -154,12 +144,17 @@ class InlineRecordContainer extends AbstractContainer {
                        }
                        // Render full content ONLY IF this is an AJAX request, a new record, the record is not collapsed or AJAX loading is explicitly turned off
                        if ($isNewRecord || $isExpanded || !$ajaxLoad) {
-                               $combinationChildArray = $this->renderCombinationTable($record, $appendFormFieldNames, $config);
-                               $combinationHtml = $combinationChildArray['html'];
-                               $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $combinationChildArray);
 
-                               $overruleTypesArray = isset($config['foreign_types']) ? $config['foreign_types'] : array();
-                               $childArray = $this->renderRecord($foreign_table, $record, $overruleTypesArray);
+                               $combinationHtml = '';
+                               if (isset($this->data['combinationChild'])) {
+                                       $combinationChild = $this->renderCombinationChild($this->data, $appendFormFieldNames);
+                                       $combinationHtml = $combinationChild['html'];
+                                       $combinationChild['html'] = '';
+                                       $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $combinationChild);
+                               }
+
+                               $childArray = $this->renderChild($this->data);
+
                                $html = $childArray['html'];
                                $childArray['html'] = '';
                                $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
@@ -193,7 +188,7 @@ class InlineRecordContainer extends AbstractContainer {
                        $html = $html . $combinationHtml;
                } else {
                        // Set the record container with data for output
-                       if ($isVirtualRecord) {
+                       if ($isDefaultLanguageRecord) {
                                $class .= ' t3-form-field-container-inline-placeHolder';
                        }
                        if (isset($record['hidden']) && (int)$record['hidden']) {
@@ -207,7 +202,7 @@ class InlineRecordContainer extends AbstractContainer {
                                                        <div class="form-irre-header-cell form-irre-header-icon">
                                                                <span class="caret"></span>
                                                        </div>
-                                                       ' . $this->renderForeignRecordHeader($parentUid, $foreign_table, $record, $config, $isVirtualRecord) . '
+                                                       ' . $this->renderForeignRecordHeader($parentUid, $foreign_table, $this->data, $config, $isDefaultLanguageRecord) . '
                                                </div>
                                        </div>
                                        <div class="panel-collapse" id="' . $objectId . '_fields" data-expandSingle="' . ($config['appearance']['expandSingle'] ? 1 : 0) . '" data-returnURL="' . htmlspecialchars(GeneralUtility::getIndpEnv('REQUEST_URI')) . '">' . $html . $combinationHtml . '</div>
@@ -219,133 +214,86 @@ class InlineRecordContainer extends AbstractContainer {
        }
 
        /**
-        * Creates main container for foreign record and renders it
+        * Render inner child
         *
-        * @param string $table The table name
-        * @param array $row The record to be rendered
-        * @param array $overruleTypesArray Overrule TCA [types] array, e.g to override [showitem] configuration of a particular type
-        * @return string The rendered form
+        * @param array $options
+        * @return array Result array
         */
-       protected function renderRecord($table, array $row, array $overruleTypesArray = array()) {
-               $domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
-
-               $options = $this->data['tabAndInlineStack'];
-               $options['tabAndInlineStack'][] = array(
+       protected function renderChild(array $options) {
+               $domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($options['inlineFirstPid']);
+               $options['tabAndInlineStack'][] = [
                        'inline',
-                       $domObjectId . '-' . $table . '-' . $row['uid'],
-               );
-
-               $command = 'edit';
-               $vanillaUid = (int)$row['uid'];
-
-               // If dealing with a new record, take pid as vanillaUid and set command to new
-               if (!MathUtility::canBeInterpretedAsInteger($row['uid'])) {
-                       $command = 'new';
-                       $vanillaUid = (int)$row['pid'];
-               }
-
-               /** @var TcaDatabaseRecord $formDataGroup */
-               $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
-               /** @var FormDataCompiler $formDataCompiler */
-               $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
-               $formDataCompilerInput = [
-                       'command' => $command,
-                       'vanillaUid' => $vanillaUid,
-                       'tableName' => $table,
-                       'inlineData' => $this->inlineData,
-                       'tabAndInlineStack' => $options['tabAndInlineStack'],
-                       'overruleTypesArray' => $overruleTypesArray,
-                       'inlineStructure' => $this->data['inlineStructure'],
+                       $domObjectId . '-' . $options['tableName'] . '-' . $options['databaseRow']['uid'],
                ];
-               $options = $formDataCompiler->compile($formDataCompilerInput);
+               // @todo: ugly construct ...
+               $options['inlineData'] = $this->inlineData;
                $options['renderType'] = 'fullRecordContainer';
-
-               // @todo: This hack merges data from already prepared row over fresh row again.
-               // @todo: This really must fall ...
-               foreach ($row as $field => $value) {
-                       if ($command === 'new' && is_string($value) && $value !== '' && array_key_exists($field, $options['databaseRow'])) {
-                               $options['databaseRow'][$field] = $value;
-                       }
-               }
-
                return $this->nodeFactory->create($options)->render();
        }
 
        /**
+        * Render child child
+        *
         * Render a table with FormEngine, that occurs on an intermediate table but should be editable directly,
         * so two tables are combined (the intermediate table with attributes and the sub-embedded table).
         * -> This is a direct embedding over two levels!
         *
-        * @param array $record The table record of the child/embedded table
+        * @param array $options
         * @param string $appendFormFieldNames The [<table>][<uid>] of the parent record (the intermediate table)
-        * @param array $config content of $PA['fieldConf']['config']
-        * @return array As defined in initializeResultArray() of AbstractNode
-        * @todo: Maybe create another container from this?
+        * @return array Result array
         */
-       protected function renderCombinationTable($record, $appendFormFieldNames, $config = array()) {
-               $resultArray = $this->initializeResultArray();
+       protected function renderCombinationChild(array $options, $appendFormFieldNames) {
+               $childData = $options['combinationChild'];
+               $parentConfig = $options['inlineParentConfig'];
 
-               $foreign_table = $config['foreign_table'];
-               $foreign_selector = $config['foreign_selector'];
-
-               if ($foreign_selector && $config['appearance']['useCombination']) {
-                       $comboConfig = $GLOBALS['TCA'][$foreign_table]['columns'][$foreign_selector]['config'];
-                       // If record does already exist, load it:
-                       /** @var InlineRelatedRecordResolver $inlineRelatedRecordResolver */
-                       $inlineRelatedRecordResolver = GeneralUtility::makeInstance(InlineRelatedRecordResolver::class);
-                       if ($record[$foreign_selector] && MathUtility::canBeInterpretedAsInteger($record[$foreign_selector])) {
-                               $comboRecord = $inlineRelatedRecordResolver->getRecord($comboConfig['foreign_table'], $record[$foreign_selector]);
-                               $isNewRecord = FALSE;
-                       } else {
-                               $comboRecord = $inlineRelatedRecordResolver->getNewRecord($this->data['inlineFirstPid'], $comboConfig['foreign_table']);
-                               $isNewRecord = TRUE;
-                       }
+               $resultArray = $this->initializeResultArray();
 
-                       // Display Warning FlashMessage if it is not suppressed
-                       if (!isset($config['appearance']['suppressCombinationWarning']) || empty($config['appearance']['suppressCombinationWarning'])) {
-                               $combinationWarningMessage = 'LLL:EXT:lang/locallang_core.xlf:warning.inline_use_combination';
-                               if (!empty($config['appearance']['overwriteCombinationWarningMessage'])) {
-                                       $combinationWarningMessage = $config['appearance']['overwriteCombinationWarningMessage'];
-                               }
-                               $flashMessage = GeneralUtility::makeInstance(
-                                       FlashMessage::class,
-                                       $this->getLanguageService()->sL($combinationWarningMessage),
-                                       '',
-                                       FlashMessage::WARNING
-                               );
-                               $resultArray['html'] = $flashMessage->render();
+               // Display Warning FlashMessage if it is not suppressed
+               if (!isset($parentConfig['appearance']['suppressCombinationWarning']) || empty($parentConfig['appearance']['suppressCombinationWarning'])) {
+                       $combinationWarningMessage = 'LLL:EXT:lang/locallang_core.xlf:warning.inline_use_combination';
+                       if (!empty($parentConfig['appearance']['overwriteCombinationWarningMessage'])) {
+                               $combinationWarningMessage = $parentConfig['appearance']['overwriteCombinationWarningMessage'];
                        }
+                       $flashMessage = GeneralUtility::makeInstance(
+                               FlashMessage::class,
+                               $this->getLanguageService()->sL($combinationWarningMessage),
+                               '',
+                               FlashMessage::WARNING
+                       );
+                       $resultArray['html'] = $flashMessage->render();
+               }
 
-                       // Get the FormEngine interpretation of the TCA of the child table
-                       $childArray = $this->renderRecord($comboConfig['foreign_table'], $comboRecord);
-                       $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
+               $childArray = $this->renderChild($childData);
+               $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
 
-                       // If this is a new record, add a pid value to store this record and the pointer value for the intermediate table
-                       if ($isNewRecord) {
-                               $comboFormFieldName = 'data[' . $comboConfig['foreign_table'] . '][' . $comboRecord['uid'] . '][pid]';
-                               $resultArray['html'] .= '<input type="hidden" name="' . $comboFormFieldName . '" value="' . $comboRecord['pid'] . '" />';
-                       }
-                       // If the foreign_selector field is also responsible for uniqueness, tell the browser the uid of the "other" side of the relation
-                       if ($isNewRecord || $config['foreign_unique'] === $foreign_selector) {
-                               $parentFormFieldName = 'data' . $appendFormFieldNames . '[' . $foreign_selector . ']';
-                               $resultArray['html'] .= '<input type="hidden" name="' . $parentFormFieldName . '" value="' . $comboRecord['uid'] . '" />';
-                       }
+               // If this is a new record, add a pid value to store this record and the pointer value for the intermediate table
+               if ($childData['command'] === 'new') {
+                       $comboFormFieldName = 'data[' . $childData['tableName'] . '][' . $childData['databaseRow']['uid'] . '][pid]';
+                       $resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($comboFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['pid']) . '" />';
+               }
+               // If the foreign_selector field is also responsible for uniqueness, tell the browser the uid of the "other" side of the relation
+               if ($childData['command'] === 'new' || $parentConfig['foreign_unique'] === $parentConfig['foreign_selector']) {
+                       $parentFormFieldName = 'data' . $appendFormFieldNames . '[' . $parentConfig['foreign_selector'] . ']';
+                       $resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($parentFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['uid']) . '" />';
                }
+
                return $resultArray;
        }
 
+
        /**
         * Renders the HTML header for a foreign record, such as the title, toggle-function, drag'n'drop, etc.
         * Later on the command-icons are inserted here.
         *
         * @param string $parentUid The uid of the parent (embedding) record (uid or NEW...)
         * @param string $foreign_table The foreign_table we create a header for
-        * @param array $rec The current record of that foreign_table
+        * @param array $data Current data
         * @param array $config content of $PA['fieldConf']['config']
         * @param bool $isVirtualRecord
         * @return string The HTML code of the header
         */
-       protected function renderForeignRecordHeader($parentUid, $foreign_table, $rec, $config, $isVirtualRecord = FALSE) {
+       protected function renderForeignRecordHeader($parentUid, $foreign_table, $data, $config, $isVirtualRecord = FALSE) {
+               $rec = $data['databaseRow'];
                // Init:
                $domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
                $objectId = $domObjectId . '-' . $foreign_table . '-' . $rec['uid'];
@@ -417,13 +365,13 @@ class InlineRecordContainer extends AbstractContainer {
 
                $iconImg = '<span title="' . $altText . '" id="' . htmlspecialchars($objectId) . '_icon' . '">' . $this->iconFactory->getIconForRecord($foreign_table, $rec, Icon::SIZE_SMALL)->render() . '</span>';
                $label = '<span id="' . $objectId . '_label">' . $recTitle . '</span>';
-               $ctrl = $this->renderForeignRecordHeaderControl($parentUid, $foreign_table, $rec, $config, $isVirtualRecord);
+               $ctrl = $this->renderForeignRecordHeaderControl($parentUid, $foreign_table, $data, $config, $isVirtualRecord);
                $thumbnail = FALSE;
 
                // Renders a thumbnail for the header
                if (!empty($config['appearance']['headerThumbnail']['field'])) {
                        $fieldValue = $rec[$config['appearance']['headerThumbnail']['field']];
-                       $firstElement = array_shift(GeneralUtility::trimExplode(',', $fieldValue));
+                       $firstElement = array_shift(GeneralUtility::trimExplode('|', array_shift(GeneralUtility::trimExplode(',', $fieldValue))));
                        $fileUid = array_pop(BackendUtility::splitTable_Uid($firstElement));
 
                        if (!empty($fileUid)) {
@@ -471,12 +419,13 @@ class InlineRecordContainer extends AbstractContainer {
         *
         * @param string $parentUid The uid of the parent (embedding) record (uid or NEW...)
         * @param string $foreign_table The table (foreign_table) we create control-icons for
-        * @param array $rec The current record of that foreign_table
+        * @param array $data Current data
         * @param array $config (modified) TCA configuration of the field
         * @param bool $isVirtualRecord TRUE if the current record is virtual, FALSE otherwise
         * @return string The HTML code with the control-icons
         */
-       protected function renderForeignRecordHeaderControl($parentUid, $foreign_table, $rec, $config = array(), $isVirtualRecord = FALSE) {
+       protected function renderForeignRecordHeaderControl($parentUid, $foreign_table, $data, $config = array(), $isVirtualRecord = FALSE) {
+               $rec = $data['databaseRow'];
                $languageService = $this->getLanguageService();
                $backendUser = $this->getBackendUserAuthentication();
                // Initialize:
@@ -508,11 +457,11 @@ class InlineRecordContainer extends AbstractContainer {
                        /** @var InlineElementHookInterface $hookObj */
                        $hookObj->renderForeignRecordHeaderControl_preProcess($parentUid, $foreign_table, $rec, $config, $isVirtualRecord, $enabledControls);
                }
-               if (isset($rec['__create'])) {
+               if ($data['inlineIsDefaultLanguage']) {
                        $cells['localize.isLocalizable'] = '<span title="' . $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localize.isLocalizable', TRUE) . '">'
                                . $this->iconFactory->getIcon('actions-edit-localize-status-low', Icon::SIZE_SMALL)->render()
                                . '</span>';
-               } elseif (isset($rec['__remove'])) {
+               } elseif ($data['inlineIsDanglingLocalization']) {
                        $cells['localize.wasRemovedInOriginal'] = '<span title="' . $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localize.wasRemovedInOriginal', TRUE) . '">'
                                . $this->iconFactory->getIcon('actions-edit-localize-status-high', Icon::SIZE_SMALL)->render()
                                . '</span>';
@@ -631,7 +580,7 @@ class InlineRecordContainer extends AbstractContainer {
                                        </span>';
                        }
                } elseif ($isVirtualRecord && $isParentExisting) {
-                       if ($enabledControls['localize'] && isset($rec['__create'])) {
+                       if ($enabledControls['localize'] && $data['inlineIsDefaultLanguage']) {
                                $onClick = 'inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($nameObjectFt) . ', ' . GeneralUtility::quoteJSvalue($rec['uid']) . ');';
                                $cells['localize'] = '
                                        <a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . 'title="' . $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localize', TRUE) . '">
@@ -662,104 +611,6 @@ class InlineRecordContainer extends AbstractContainer {
        }
 
        /**
-        * Checks the page access rights (Code for access check mostly taken from alt_doc.php)
-        * as well as the table access rights of the user.
-        *
-        * @param string $cmd The command that should be performed ('new' or 'edit')
-        * @param string $table The table to check access for
-        * @param string $theUid The record uid of the table
-        * @return bool Returns TRUE is the user has access, or FALSE if not
-        */
-       protected function checkAccess($cmd, $table, $theUid) {
-               // @todo This should be within data provider
-               $backendUser = $this->getBackendUserAuthentication();
-               // Checking if the user has permissions? (Only working as a precaution, because the final permission check is always down in TCE. But it's good to notify the user on beforehand...)
-               // First, resetting flags.
-               $hasAccess = FALSE;
-               // Admin users always have access:
-               if ($backendUser->isAdmin()) {
-                       return TRUE;
-               }
-               // If the command is to create a NEW record...:
-               if ($cmd === 'new') {
-                       // If the pid is numerical, check if it's possible to write to this page:
-                       if (MathUtility::canBeInterpretedAsInteger($this->data['inlineFirstPid'])) {
-                               $calcPRec = BackendUtility::getRecord('pages', $this->data['inlineFirstPid']);
-                               if (!is_array($calcPRec)) {
-                                       return FALSE;
-                               }
-                               // Permissions for the parent page
-                               $CALC_PERMS = $backendUser->calcPerms($calcPRec);
-                               // If pages:
-                               if ($table === 'pages') {
-                                       // Are we allowed to create new subpages?
-                                       $hasAccess = (bool)($CALC_PERMS & Permission::PAGE_NEW);
-                               } else {
-                                       // Are we allowed to edit the page?
-                                       if ($table === 'sys_file_reference' && $this->isMediaOnPages($theUid)) {
-                                               $hasAccess = (bool)($CALC_PERMS & Permission::PAGE_EDIT);
-                                       }
-                                       if (!$hasAccess) {
-                                               // Are we allowed to edit content on this page?
-                                               $hasAccess = (bool)($CALC_PERMS & Permission::CONTENT_EDIT);
-                                       }
-                               }
-                       } else {
-                               $hasAccess = TRUE;
-                       }
-               } else {
-                       // Edit:
-                       $calcPRec = BackendUtility::getRecord($table, $theUid);
-                       BackendUtility::fixVersioningPid($table, $calcPRec);
-                       if (is_array($calcPRec)) {
-                               // If pages:
-                               if ($table === 'pages') {
-                                       $CALC_PERMS = $backendUser->calcPerms($calcPRec);
-                                       $hasAccess = (bool)($CALC_PERMS & Permission::PAGE_EDIT);
-                               } else {
-                                       // Fetching pid-record first.
-                                       $CALC_PERMS = $backendUser->calcPerms(BackendUtility::getRecord('pages', $calcPRec['pid']));
-                                       if ($table === 'sys_file_reference' && $this->isMediaOnPages($theUid)) {
-                                               $hasAccess = (bool)($CALC_PERMS & Permission::PAGE_EDIT);
-                                       }
-                                       if (!$hasAccess) {
-                                               $hasAccess = (bool)($CALC_PERMS & Permission::CONTENT_EDIT);
-                                       }
-                               }
-                               // Check internals regarding access
-                               $isRootLevelRestrictionIgnored = BackendUtility::isRootLevelRestrictionIgnored($table);
-                               if ($hasAccess || (int)$calcPRec['pid'] === 0 && $isRootLevelRestrictionIgnored) {
-                                       $hasAccess = (bool)$backendUser->recordEditAccessInternals($table, $calcPRec);
-                               }
-                       }
-               }
-               if (!$backendUser->check('tables_modify', $table)) {
-                       $hasAccess = FALSE;
-               }
-               if (
-                       !empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['checkAccess'])
-                       && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['checkAccess'])
-               ) {
-                       foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['checkAccess'] as $_funcRef) {
-                               $_params = array(
-                                       'table' => $table,
-                                       'uid' => $theUid,
-                                       'cmd' => $cmd,
-                                       'hasAccess' => $hasAccess
-                               );
-                               $hasAccess = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
-                       }
-               }
-               if (!$hasAccess) {
-                       $deniedAccessReason = $backendUser->errorMsg;
-                       if ($deniedAccessReason) {
-                               debug($deniedAccessReason);
-                       }
-               }
-               return $hasAccess;
-       }
-
-       /**
         * Checks if a uid of a child table is in the inline view settings.
         *
         * @param string $table Name of the child table
@@ -813,20 +664,6 @@ class InlineRecordContainer extends AbstractContainer {
        }
 
        /**
-        * Check if the record is a media element on a page.
-        *
-        * @param string $theUid Uid of the sys_file_reference record to be checked
-        * @return bool TRUE if the record has media in the column 'fieldname' and pages in the column 'tablenames'
-        */
-       protected function isMediaOnPages($theUid) {
-               if (StringUtility::beginsWith($theUid, 'NEW')) {
-                       return TRUE;
-               }
-               $row = BackendUtility::getRecord('sys_file_reference', $theUid);
-               return ($row['fieldname'] === 'media') && ($row['tablenames'] === 'pages');
-       }
-
-       /**
         * @return BackendUserAuthentication
         */
        protected function getBackendUserAuthentication() {
index 2c6c927..46566c1 100644 (file)
@@ -174,12 +174,27 @@ class FormDataCompiler {
                        // BackendUser->uc['inlineView'] - This array holds status of expand / collapsed inline items
                        'inlineExpandCollapseStateArray' => array(),
                        // The "entry" pid for inline records. Nested inline records can potentially hang around on different
-                       // pid's, but the entry pid is needed for AJAX calls, so that they would know where the action takes place on the page structure.
+                       // pid's, but the entry pid is needed for AJAX calls, so that they would know where the action takes place on the page structure.
                        'inlineFirstPid' => NULL,
                        // This array of fields will be set as hidden-fields instead of rendered normally!
                        // This is used by EditDocumentController to force some field values if set as "overrideVals" in _GP
                        'overrideValues' => [],
 
+                       // Inline scenario: A localized parent record is handled and localizationMode is set to "select", so inline
+                       // parents can have localized children. This value is set to TRUE if this array represents a localized child
+                       // overlay record that has no default language record.
+                       'inlineIsDanglingLocalization' => FALSE,
+                       // Inline scenario: A localized parent record is handled and localizationMode is set to "select", so inline
+                       // parents can have localized childen. This value is set to TRUE if this array represents a default language
+                       // child record that was not yet localized.
+                       'inlineIsDefaultLanguage' => FALSE,
+                       // If set, inline children will be resolved. This is set to FALSE in inline ajax context where new children
+                       // are created an existing children don't matter much.
+                       'inlineResolveExistingChildren' => TRUE,
+                       // @todo - for input placeholder inline to suppress an infinite loop, this *may* become obsolete if
+                       // @todo compilation of certain fields is possible
+                       'inlineCompileExistingChildren' => TRUE,
+
                        // @todo: must be handled / further defined
                        'elementBaseName' => '',
                        'flexFormFieldIdentifierPrefix' => 'ID',
index c88e3fd..ad8c1e7 100644 (file)
@@ -20,8 +20,6 @@ use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordException;
 
 /**
  * Extended by other provider that fetch records from database
- *
- * @todo: This abstract is rather semi useful and may get removed later again
  */
 abstract class AbstractDatabaseRecordProvider {
 
index f41c769..b465d66 100644 (file)
@@ -51,6 +51,8 @@ class DatabaseRowInitializeNew implements FormDataProviderInterface {
                }
 
                // Apply defaults from pageTsConfig
+               // @todo: For inline records it should be possible to set the pid here via TCAdefaults, but
+               // @todo: those values would be overwritten by 'pid' setter later below again.
                if (isset($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot])
                        && is_array($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot])
                ) {
index 9db807a..29221ef 100644 (file)
@@ -72,6 +72,8 @@ class DatabaseUserPermissionCheck implements FormDataProviderInterface {
                $userPermissionOnPage = Permission::NOTHING;
                if ($result['command'] === 'new') {
                        // A new record is created. Access rights of parent record are important here
+                       // @todo: In case of new inline child, parentPageRow should probably be the
+                       // @todo: "inlineFirstPid" page - Maybe effectivePid and parentPageRow should be calculated differently then?
                        if (is_array($result['parentPageRow'])) {
                                // Record is added below an existing page
                                $userPermissionOnPage = $backendUser->calcPerms($result['parentPageRow']);
index 819a1da..f5037cf 100644 (file)
@@ -14,20 +14,20 @@ namespace TYPO3\CMS\Backend\Form\FormDataProvider;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Backend\Form\FormDataCompiler;
+use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
 use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Database\RelationHandler;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
+use TYPO3\CMS\Core\Versioning\VersionState;
 
 /**
  * Resolve and prepare inline data.
- *
- * @todo: This class is currently only a stub and lots of data preparation is still done in render containers
  */
-class TcaInline extends AbstractItemProvider implements FormDataProviderInterface {
+class TcaInline extends AbstractDatabaseRecordProvider implements FormDataProviderInterface {
 
        /**
         * Resolve inline fields
@@ -36,39 +36,18 @@ class TcaInline extends AbstractItemProvider implements FormDataProviderInterfac
         * @return array
         */
        public function addData(array $result) {
-               $result = $this->addInlineExpandCollapseState($result);
                $result = $this->addInlineFirstPid($result);
 
                foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
                        if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'inline') {
                                continue;
                        }
-
-                       $result = $this->resolveConnectedRecordUids($result, $fieldName);
-               }
-
-               return $result;
-       }
-
-       /**
-        * Add expand / collapse state of inline items for this parent table / uid combination
-        *
-        * @param array $result Incoming result
-        * @return array Modified result
-        */
-       protected function addInlineExpandCollapseState(array $result) {
-               $inlineView = unserialize($this->getBackendUser()->uc['inlineView']);
-               if (!is_array($inlineView)) {
-                       $inlineView = [];
-               }
-               if ($result['command'] !== 'new') {
-                       $table = $result['tableName'];
-                       $uid = $result['databaseRow']['uid'];
-                       if (!empty($inlineView[$table][$uid])) {
-                               $inlineView = $inlineView[$table][$uid];
+                       $result['processedTca']['columns'][$fieldName]['children'] = [];
+                       if ($result['inlineResolveExistingChildren']) {
+                               $result = $this->resolveRelatedRecords($result, $fieldName);
                        }
                }
-               $result['inlineExpandCollapseStateArray'] = $inlineView;
+
                return $result;
        }
 
@@ -100,48 +79,234 @@ class TcaInline extends AbstractItemProvider implements FormDataProviderInterfac
        }
 
        /**
-        * Use RelationHandler to resolve connected uids
+        * Substitute the value in databaseRow of this inline field with an array
+        * that contains the databaseRows of currently connected records and some meta information.
         *
         * @param array $result Result array
         * @param string $fieldName Current handle field name
         * @return array Modified item array
         */
-       protected function resolveConnectedRecordUids(array $result, $fieldName) {
-               $localTable = $result['tableName'];
-               $localUid = $result['databaseRow']['uid'];
-               $localTca = $result['processedTca']['columns'][$fieldName];
-               $localFieldcontent = $result['databaseRow'][$fieldName];
-               $directlyConnectedIds = GeneralUtility::trimExplode(',', $localFieldcontent);
-
-               if (StringUtility::beginsWith((string)$localUid, 'NEW')) {
-                       return $result;
+       protected function resolveRelatedRecords(array $result, $fieldName) {
+               $childTableName = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'];
+
+               // localizationMode is either "none", "keep" or "select":
+               // * none:   Handled parent row is not a localized record, or if it is a localized row, this is ignored.
+               //           Default language records and overlays have distinct children that are not connected to each other.
+               // * keep:   Handled parent row is a localized record, but child table is either not localizable, or
+               //           "keep" is explicitly set. A localized parent and its default language row share the same
+               //           children records. Editing a child from a localized record will change this record for the
+               //           default language row, too.
+               // * select: Handled parent row is a localized record, child table is localizable. Children records are
+               //           localized overlays of a default language record. Three scenarios can happen:
+               //           ** Localized child overlay and its default language row exist - show localized overlay record
+               //           ** Default child language row exists but child overlay doesn't - show a "synchronize this record" button
+               //           ** Localized child overlay exists but default language row does not - this dangling child is a data inconsistency
+
+               // Mode was prepared by TcaInlineConfiguration provider
+               $mode = $result['processedTca']['columns'][$fieldName]['config']['behaviour']['localizationMode'];
+               if ($mode === 'none') {
+                       $connectedUids = [];
+                       // A new record that has distinct children can not have children yet, fetch connected uids for existing only
+                       if ($result['command'] === 'edit') {
+                               $connectedUids = $this->resolveConnectedRecordUids(
+                                       $result['processedTca']['columns'][$fieldName]['config'],
+                                       $result['tableName'],
+                                       $result['databaseRow']['uid'],
+                                       $result['databaseRow'][$fieldName]
+                               );
+                       }
+                       $result['databaseRow'][$fieldName] = implode(',', $connectedUids);
+                       $connectedUids = $this->getWorkspacedUids($connectedUids, $childTableName);
+                       // @todo: If inlineCompileExistingChildren must be kept, it might be better to change the data
+                       // @todo: format of databaseRow for this field and separate the child compilation to an own provider?
+                       if ($result['inlineCompileExistingChildren']) {
+                               foreach ($connectedUids as $childUid) {
+                                       $result['processedTca']['columns'][$fieldName]['children'][] = $this->compileChild($result, $fieldName, $childUid);
+                               }
+                       }
+               } elseif ($mode === 'keep') {
+                       // Fetch connected uids of default language record
+                       $connectedUids = $this->resolveConnectedRecordUids(
+                               $result['processedTca']['columns'][$fieldName]['config'],
+                               $result['tableName'],
+                               $result['defaultLanguageRow']['uid'],
+                               $result['defaultLanguageRow'][$fieldName]
+                       );
+                       $result['databaseRow'][$fieldName] = implode(',', $connectedUids);
+                       $connectedUids = $this->getWorkspacedUids($connectedUids, $childTableName);
+                       if ($result['inlineCompileExistingChildren']) {
+                               foreach ($connectedUids as $childUid) {
+                                       $result['processedTca']['columns'][$fieldName]['children'][] = $this->compileChild($result, $fieldName, $childUid);
+                               }
+                       }
+               } else {
+                       $connectedUidsOfLocalizedOverlay = [];
+                       if ($result['command'] === 'edit') {
+                               $connectedUidsOfLocalizedOverlay = $this->resolveConnectedRecordUids(
+                                       $result['processedTca']['columns'][$fieldName]['config'],
+                                       $result['tableName'],
+                                       $result['databaseRow']['uid'],
+                                       $result['databaseRow'][$fieldName]
+                               );
+                       }
+                       $result['databaseRow'][$fieldName] = implode(',', $connectedUidsOfLocalizedOverlay);
+                       if ($result['inlineCompileExistingChildren']) {
+                               $connectedUidsOfDefaultLanguageRecord = $this->resolveConnectedRecordUids(
+                                       $result['processedTca']['columns'][$fieldName]['config'],
+                                       $result['tableName'],
+                                       $result['defaultLanguageRow']['uid'],
+                                       $result['defaultLanguageRow'][$fieldName]
+                               );
+                               $showPossible = $result['processedTca']['columns'][$fieldName]['config']['appearance']['showPossibleLocalizationRecords'];
+                               $showRemoved = $result['processedTca']['columns'][$fieldName]['config']['appearance']['showRemovedLocalizationRecords'];
+                               if ($showPossible || $showRemoved) {
+                                       // Find which records are localized, which records are not localized and which are
+                                       // localized but miss default language record
+                                       $fieldNameWithDefaultLanguageUid = $GLOBALS['TCA'][$childTableName]['ctrl']['transOrigPointerField'];
+                                       foreach ($connectedUidsOfLocalizedOverlay as $localizedUid) {
+                                               $localizedRecord = $this->getRecordFromDatabase($childTableName, $localizedUid);
+                                               $uidOfDefaultLanguageRecord = $localizedRecord[$fieldNameWithDefaultLanguageUid];
+                                               if (in_array($uidOfDefaultLanguageRecord, $connectedUidsOfDefaultLanguageRecord, TRUE)) {
+                                                       // This localized child has a default language record. Remove this record from list of default language records
+                                                       $connectedUidsOfDefaultLanguageRecord = array_diff($connectedUidsOfDefaultLanguageRecord, array($uidOfDefaultLanguageRecord));
+                                                       // Compile localized record
+                                                       $result['processedTca']['columns'][$fieldName]['children'][] = $this->compileChild($result, $fieldName, $localizedUid);
+                                               } elseif ($showRemoved) {
+                                                       // This localized child has no default language record. Compile child and mark it as such
+                                                       $compiledChild = $this->compileChild($result, $fieldName, $localizedUid);
+                                                       $compiledChild['inlineIsDanglingLocalization'] = TRUE;
+                                                       $result['processedTca']['columns'][$fieldName]['children'][] = $compiledChild;
+                                               } // Discard child if default language is missing and no showRemoved is set
+                                       }
+                                       if ($showPossible) {
+                                               foreach ($connectedUidsOfDefaultLanguageRecord as $defaultLanguageUid) {
+                                                       // If there are still uids in $connectedUidsOfDefaultLanguageRecord, these are records that
+                                                       // exist in default language, but are not localized yet. Compile and mark those
+                                                       $compiledChild = $this->compileChild($result, $fieldName, $defaultLanguageUid);
+                                                       $compiledChild['inlineIsDefaultLanguage'] = TRUE;
+                                                       $result['processedTca']['columns'][$fieldName]['children'][] = $compiledChild;
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               return $result;
+       }
+
+       /**
+        * Compile a full child record
+        *
+        * @param array $result Result array of parent
+        * @param string $parentFieldName Name of parent field
+        * @param int $childUid Uid of child to compile
+        * @return array Full result array
+        */
+       protected function compileChild(array $result, $parentFieldName, $childUid) {
+               $parentConfig = $result['processedTca']['columns'][$parentFieldName]['config'];
+               $childTableName = $parentConfig['foreign_table'];
+               $overruleTypesArray = [];
+               if (isset($parentConfig['foreign_types'])) {
+                       $overruleTypesArray = $parentConfig['foreign_types'];
                }
+               /** @var TcaDatabaseRecord $formDataGroup */
+               $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
+               /** @var FormDataCompiler $formDataCompiler */
+               $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
+               $formDataCompilerInput = [
+                       'command' => 'edit',
+                       'tableName' => $childTableName,
+                       'vanillaUid' => (int)$childUid,
+                       'inlineFirstPid' => $result['inlineFirstPid'],
+                       'overruleTypesArray' => $overruleTypesArray,
+               ];
+               // For foreign_selector with useCombination $mainChild is the mm record
+               // and $combinationChild is the child-child. For "normal" relations, $mainChild
+               // is just the normal child record and $combinationChild is empty.
+               $mainChild = $formDataCompiler->compile($formDataCompilerInput);
+               if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
+                       $mainChild['combinationChild'] = $this->compileCombinationChild($mainChild, $parentConfig);
+               }
+               return $mainChild;
+       }
 
-               if (empty($localTca['config']['MM'])) {
-                       $localUid = $this->getLiveDefaultId($localTable, $localUid);
+       /**
+        * With useCombination set, not only content of the intermediate table, but also
+        * the connected child should be rendered in one go. Prepare this here.
+        *
+        * @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) {
+               // foreign_selector on intermediate is probably type=select, so data provider of this table resolved that to the uid already
+               $intermediateUid = $intermediate['databaseRow'][$parentConfig['foreign_selector']][0];
+               $combinationChild = $this->compileChild($intermediate, $parentConfig['foreign_selector'], $intermediateUid);
+               return $combinationChild;
+       }
+
+       /**
+        * Substitute given list of uids in child table with workspace uid if needed
+        *
+        * @param array $connectedUids List of connected uids
+        * @param string $childTableName Name of child table
+        * @return array List of uids in workspace
+        */
+       protected function getWorkspacedUids(array $connectedUids, $childTableName) {
+               $backendUser = $this->getBackendUser();
+               $newConnectedUids = [];
+               foreach ($connectedUids as $uid) {
+                       // Fetch workspace version of a record (if any):
+                       // @todo: Needs handling
+                       if ($backendUser->workspace !== 0 && BackendUtility::isTableWorkspaceEnabled($childTableName)) {
+                               $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($backendUser->workspace, $childTableName, $uid, 'uid,t3ver_state');
+                               if ($workspaceVersion !== FALSE) {
+                                       $versionState = VersionState::cast($workspaceVersion['t3ver_state']);
+                                       if ($versionState->equals(VersionState::DELETE_PLACEHOLDER)) {
+                                               return [];
+                                       }
+                                       $uid = $workspaceVersion['uid'];
+                               }
+                       }
+                       $newConnectedUids[] = $uid;
+               }
+               return $newConnectedUids;
+       }
+
+       /**
+        * Use RelationHandler to resolve connected uids.
+        *
+        * @param array $parentConfig TCA config section of parent
+        * @param string $parentTableName Name of parent table
+        * @param string $parentUid Uid of parent record
+        * @param string $parentFieldValue Database value of parent record of this inline field
+        * @return array Array with connected uids
+        * @todo: Cover with unit tests
+        */
+       protected function resolveConnectedRecordUids(array $parentConfig, $parentTableName, $parentUid, $parentFieldValue) {
+               $directlyConnectedIds = GeneralUtility::trimExplode(',', $parentFieldValue);
+               if (empty($parentConfig['MM'])) {
+                       $parentUid = $this->getLiveDefaultId($parentTableName, $parentUid);
                }
                /** @var RelationHandler $relationHandler */
                $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
-               $relationHandler->registerNonTableValues = (bool)$localTca['config']['allowedIdValues'];
-               $relationHandler->start($localFieldcontent, $localTca['config']['foreign_table'], $localTca['config']['MM'], $localUid, $localTable, $localTca['config']);
+               $relationHandler->registerNonTableValues = (bool)$parentConfig['allowedIdValues'];
+               $relationHandler->start($parentFieldValue, $parentConfig['foreign_table'], $parentConfig['MM'], $parentUid, $parentTableName, $parentConfig);
                $foreignRecordUids = $relationHandler->getValueArray();
-
                $resolvedForeignRecordUids = [];
                foreach ($foreignRecordUids as $aForeignRecordUid) {
-                       if ($localTca['config']['MM'] || $localTca['config']['foreign_field']) {
-                               $resolvedForeignRecordUids[] = $aForeignRecordUid;
+                       if ($parentConfig['MM'] || $parentConfig['foreign_field']) {
+                               $resolvedForeignRecordUids[] = (int)$aForeignRecordUid;
                        } else {
                                foreach ($directlyConnectedIds as $id) {
                                        if ((int)$aForeignRecordUid === (int)$id) {
-                                               $resolvedForeignRecordUids[] = $aForeignRecordUid;
+                                               $resolvedForeignRecordUids[] = (int)$aForeignRecordUid;
                                        }
                                }
                        }
                }
-
-               $result['databaseRow'][$fieldName] = implode(',', $resolvedForeignRecordUids);
-
-               return $result;
+               return $resolvedForeignRecordUids;
        }
 
        /**
@@ -151,6 +316,7 @@ class TcaInline extends AbstractItemProvider implements FormDataProviderInterfac
         * @param string $tableName
         * @param int $uid
         * @return int
+        * @todo: the workspace mess still must be resolved somehow
         */
        protected function getLiveDefaultId($tableName, $uid) {
                $liveDefaultId = BackendUtility::getLiveVersionIdOfRecord($tableName, $uid);
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInlineConfiguration.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInlineConfiguration.php
new file mode 100644 (file)
index 0000000..d624867
--- /dev/null
@@ -0,0 +1,192 @@
+<?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\Core\Utility\MathUtility;
+
+/**
+ * Set or initialize configuration for inline fields in TCA
+ */
+class TcaInlineConfiguration extends AbstractItemProvider implements FormDataProviderInterface {
+
+       /**
+        * Find all inline fields and force proper configuration
+        *
+        * @param array $result
+        * @return array
+        * @throws \UnexpectedValueException If inline configuration is broken
+        */
+       public function addData(array $result) {
+               foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
+                       if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'inline') {
+                               continue;
+                       }
+
+                       // Throw if an inline field without foreign_table is set
+                       if (!isset($fieldConfig['config']['foreign_table'])) {
+                               throw new \UnexpectedValueException(
+                                       'Inline field ' . $fieldName . ' of table ' . $result['tableName'] . ' must have a foreign_table config',
+                                       1443793404
+                               );
+                       }
+
+                       $result = $this->initializeMinMaxItems($result, $fieldName);
+                       $result = $this->initializeLocalizationMode($result, $fieldName);
+                       $result = $this->initializeAppearance($result, $fieldName);
+               }
+               return $result;
+       }
+
+       /**
+        * Set and validate minitems and maxitems in config
+        *
+        * @param array $result Result array
+        * @param string $fieldName Current handle field name
+        * @return array Modified item array
+        * @return array
+        */
+       protected function initializeMinMaxItems(array $result, $fieldName) {
+               $config = $result['processedTca']['columns'][$fieldName]['config'];
+
+               $minItems = 0;
+               if (isset($config['minitems'])) {
+                       $minItems = MathUtility::forceIntegerInRange($config['minitems'], 0);
+               }
+               $result['processedTca']['columns'][$fieldName]['config']['minitems'] = $minItems;
+
+               $maxItems = 100000;
+               if (isset($config['maxitems'])) {
+                       $maxItems = MathUtility::forceIntegerInRange($config['maxitems'], 1);
+               }
+               $result['processedTca']['columns'][$fieldName]['config']['maxitems'] = $maxItems;
+
+               return $result;
+       }
+
+       /**
+        * Set appearance configuration
+        *
+        * @param array $result Result array
+        * @param string $fieldName Current handle field name
+        * @return array Modified item array
+        * @return array
+        */
+       protected function initializeAppearance(array $result, $fieldName) {
+               $config = $result['processedTca']['columns'][$fieldName]['config'];
+               if (!isset($config['appearance']) || !is_array($config['appearance'])) {
+                       // Init appearance if not set
+                       $config['appearance'] = [];
+               }
+               // Set the position/appearance of the "Create new record" link
+               if (isset($config['foreign_selector']) && $config['foreign_selector']
+                       && (!isset($config['appearance']['useCombination']) || !$config['appearance']['useCombination'])
+               ) {
+                       $config['appearance']['levelLinksPosition'] = 'none';
+               } elseif (!isset($config['appearance']['levelLinksPosition'])
+                       || !in_array($config['appearance']['levelLinksPosition'], array('top', 'bottom', 'both', 'none'), TRUE)
+               ) {
+                       $config['appearance']['levelLinksPosition'] = 'top';
+               }
+               $config['appearance']['showPossibleLocalizationRecords']
+                       = isset($config['appearance']['showPossibleLocalizationRecords']) && $config['appearance']['showPossibleLocalizationRecords'];
+               $config['appearance']['showRemovedLocalizationRecords']
+                       = isset($config['appearance']['showRemovedLocalizationRecords']) && $config['appearance']['showRemovedLocalizationRecords'];
+               // Defines which controls should be shown in header of each record
+               $enabledControls = [
+                       'info' => TRUE,
+                       'new' => TRUE,
+                       'dragdrop' => TRUE,
+                       'sort' => TRUE,
+                       'hide' => TRUE,
+                       'delete' => TRUE,
+                       'localize' => TRUE
+               ];
+               if (isset($config['appearance']['enabledControls']) && is_array($config['appearance']['enabledControls'])) {
+                       $config['appearance']['enabledControls'] = array_merge($enabledControls, $config['appearance']['enabledControls']);
+               } else {
+                       $config['appearance']['enabledControls'] = $enabledControls;
+               }
+               $result['processedTca']['columns'][$fieldName]['config'] = $config;
+
+               return $result;
+       }
+
+       /**
+        * Set localization mode. This will end up with localizationMode to be set to either 'select', 'keep'
+        * or 'none' if the handled record is a localized record.
+        *
+        * @see TcaInline for a detailed explanation on the meaning of these modes.
+        *
+        * @param array $result Result array
+        * @param string $fieldName Current handle field name
+        * @return array Modified item array
+        * @throws \UnexpectedValueException If localizationMode configuration is broken
+        */
+       protected function initializeLocalizationMode(array $result, $fieldName) {
+               if ($result['defaultLanguageRow'] === NULL) {
+                       // Currently handled parent is a localized row if a former provider added the "default" row
+                       // If handled record is not localized, set localizationMode to 'none' and return
+                       $result['processedTca']['columns'][$fieldName]['config']['behaviour']['localizationMode'] = 'none';
+                       return $result;
+               }
+
+               $childTableName = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'];
+               $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']
+                       && isset($GLOBALS['TCA'][$childTableName]['ctrl']['transOrigPointerField'])
+                       && $GLOBALS['TCA'][$childTableName]['ctrl']['transOrigPointerField']
+               ) {
+                       $isChildTableLocalizable = TRUE;
+               }
+
+               $mode = NULL;
+
+               if (isset($parentConfig['behaviour']['localizationMode'])) {
+                       // Use explicit set mode, but validate before use
+                       // Use  mode if set, but throw if not set to either 'select' or 'keep'
+                       if ($parentConfig['behaviour']['localizationMode'] !== 'keep' && $parentConfig['behaviour']['localizationMode'] !== 'select') {
+                               throw new \UnexpectedValueException(
+                                       'localizationMode of table ' . $result['tableName'] . ' field ' . $fieldName . ' is not valid, set to either \'keep\' or \'select\'',
+                                       1443829370
+                               );
+                       }
+                       // Throw if is set to select, but child can not be localized
+                       if ($parentConfig['behaviour']['localizationMode'] === 'select' && !$isChildTableLocalizable) {
+                               throw new \UnexpectedValueException(
+                                       'Wrong configuration: localizationMode of table ' . $result['tableName'] . ' field ' . $fieldName . ' is set to \'select\', but table is not localizable.',
+                                       1443944274
+                               );
+                       }
+                       $mode = $parentConfig['behaviour']['localizationMode'];
+               } else {
+                       // Not set explicitly -> use "none"
+                       $mode = 'none';
+                       if ($isChildTableLocalizable) {
+                               // Except if child is localizable, then use "select"
+                               $mode = 'select';
+                       }
+               }
+
+               $result['processedTca']['columns'][$fieldName]['config']['behaviour']['localizationMode'] = $mode;
+               return $result;
+       }
+
+}
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInlineExpandCollapseState.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInlineExpandCollapseState.php
new file mode 100644 (file)
index 0000000..22ae25c
--- /dev/null
@@ -0,0 +1,56 @@
+<?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\Core\Authentication\BackendUserAuthentication;
+
+/**
+ * Fetch information of user specific inline record expanded / collapsed state
+ * from user->uc and put it into $result['inlineExpandCollapseStateArray']
+ */
+class TcaInlineExpandCollapseState extends AbstractItemProvider implements FormDataProviderInterface {
+
+       /**
+        * Add inline expand / collapse state
+        *
+        * @param array $result
+        * @return array
+        */
+       public function addData(array $result) {
+               $fullInlineState = unserialize($this->getBackendUser()->uc['inlineView']);
+               if (!is_array($fullInlineState)) {
+                       $fullInlineState = [];
+               }
+               $inlineStateForTable = [];
+               if ($result['command'] !== 'new') {
+                       $table = $result['tableName'];
+                       $uid = $result['databaseRow']['uid'];
+                       if (!empty($fullInlineState[$table][$uid])) {
+                               $inlineStateForTable = $fullInlineState[$table][$uid];
+                       }
+               }
+               $result['inlineExpandCollapseStateArray'] = $inlineStateForTable;
+               return $result;
+       }
+
+       /**
+        * @return BackendUserAuthentication
+        */
+       protected function getBackendUser() {
+               return $GLOBALS['BE_USER'];
+       }
+
+}
index 070c20d..2bdbfc9 100644 (file)
@@ -134,6 +134,7 @@ class TcaInputPlaceholders extends AbstractItemProvider implements FormDataProvi
                        'command' => 'edit',
                        'vanillaUid' => (int)$uid,
                        'tableName' => $tableName,
+                       'inlineCompileExistingChildren' => FALSE,
                ];
                /** @var TcaInputPlaceholderRecord $formDataGroup */
                $formDataGroup = GeneralUtility::makeInstance(TcaInputPlaceholderRecord::class);
diff --git a/typo3/sysext/backend/Classes/Form/InlineRelatedRecordResolver.php b/typo3/sysext/backend/Classes/Form/InlineRelatedRecordResolver.php
deleted file mode 100644 (file)
index 38eba68..0000000
+++ /dev/null
@@ -1,259 +0,0 @@
-<?php
-namespace TYPO3\CMS\Backend\Form;
-
-/*
- * 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\FormDataGroup\TcaDatabaseRecord;
-use TYPO3\CMS\Backend\Utility\BackendUtility;
-use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
-use TYPO3\CMS\Core\Utility\MathUtility;
-use TYPO3\CMS\Core\Utility\StringUtility;
-use TYPO3\CMS\Core\Versioning\VersionState;
-use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
-
-/**
- * Resolve inline relations.
- *
- * This class contains various methods to fetch inline child records based on configurations
- * and to prepare new child records.
- *
- * @internal This class will vanish with further refactoring without further notice
- */
-class InlineRelatedRecordResolver {
-
-       /**
-        * Get the related records of the embedding item, this could be 1:n, m:n.
-        * Returns an associative array with the keys records and count. 'count' contains only real existing records on the current parent record.
-        *
-        * @param string $table The table name of the record
-        * @param string $field The field name which this element is supposed to edit
-        * @param array $row The record data array where the value(s) for the field can be found
-        * @param array $PA An array with additional configuration options.
-        * @param array $config (Redundant) content of $PA['fieldConf']['config'] (for convenience)
-        * @param int $inlineFirstPid Inline first pid
-        * @return array The records related to the parent item as associative array.
-        */
-       public function getRelatedRecords($table, $field, $row, $PA, $config, $inlineFirstPid) {
-               $language = 0;
-               $elements = $PA['itemFormElValue'];
-               $foreignTable = $config['foreign_table'];
-               $localizationMode = BackendUtility::getInlineLocalizationMode($table, $config);
-               if ($localizationMode !== FALSE) {
-                       $language = $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
-                       if (is_array($language)) {
-                               $language = (int)$language[0];
-                       } else {
-                               $language = (int)$language;
-                       }
-                       if (isset($row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']])
-                               && is_array($row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']])
-                       ) {
-                               $transOrigPointer = (int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']][0];
-                       } else {
-                               $transOrigPointer = (int)$row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
-                       }
-                       $transOrigTable = BackendUtility::getOriginalTranslationTable($table);
-
-                       if ($language > 0 && $transOrigPointer) {
-                               // Localization in mode 'keep', isn't a real localization, but keeps the children of the original parent record:
-                               if ($localizationMode === 'keep') {
-                                       $transOrigRec = $this->getRecord($transOrigTable, $transOrigPointer);
-                                       $elements = $transOrigRec[$field];
-                               } elseif ($localizationMode === 'select') {
-                                       $transOrigRec = $this->getRecord($transOrigTable, $transOrigPointer);
-                                       $fieldValue = $transOrigRec[$field];
-
-                                       // Checks if it is a flexform field
-                                       if ($GLOBALS['TCA'][$table]['columns'][$field]['config']['type'] === 'flex') {
-                                               $flexFormParts = FormEngineUtility::extractFlexFormParts($PA['itemFormElName']);
-                                               $flexData = GeneralUtility::xml2array($fieldValue);
-                                               /** @var  $flexFormTools  FlexFormTools */
-                                               $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
-                                               $flexFormFieldValue = $flexFormTools->getArrayValueByPath($flexFormParts, $flexData);
-
-                                               if ($flexFormFieldValue !== NULL) {
-                                                       $fieldValue = $flexFormFieldValue;
-                                               }
-                                       }
-
-                                       $recordsOriginal = $this->getRelatedRecordsArray($foreignTable, $fieldValue);
-                               }
-                       }
-               }
-               $records = $this->getRelatedRecordsArray($foreignTable, $elements);
-               $relatedRecords = array('records' => $records, 'count' => count($records));
-               // Merge original language with current localization and show differences:
-               if (!empty($recordsOriginal)) {
-                       $options = array(
-                               'showPossible' => isset($config['appearance']['showPossibleLocalizationRecords']) && $config['appearance']['showPossibleLocalizationRecords'],
-                               'showRemoved' => isset($config['appearance']['showRemovedLocalizationRecords']) && $config['appearance']['showRemovedLocalizationRecords']
-                       );
-                       // Either show records that possibly can localized or removed
-                       if ($options['showPossible'] || $options['showRemoved']) {
-                               $relatedRecords['records'] = $this->getLocalizationDifferences($foreignTable, $options, $recordsOriginal, $records);
-                               // Otherwise simulate localizeChildrenAtParentLocalization behaviour when creating a new record
-                               // (which has language and translation pointer values set)
-                       } elseif (!empty($config['behaviour']['localizeChildrenAtParentLocalization']) && !MathUtility::canBeInterpretedAsInteger($row['uid'])) {
-                               if (!empty($GLOBALS['TCA'][$foreignTable]['ctrl']['transOrigPointerField'])) {
-                                       $foreignLanguageField = $GLOBALS['TCA'][$foreignTable]['ctrl']['languageField'];
-                               }
-                               if (!empty($GLOBALS['TCA'][$foreignTable]['ctrl']['transOrigPointerField'])) {
-                                       $foreignTranslationPointerField = $GLOBALS['TCA'][$foreignTable]['ctrl']['transOrigPointerField'];
-                               }
-                               // Duplicate child records of default language in form
-                               // this case becomes true for the pages translation, in case the default language
-                               // record has entries in media field.
-                               foreach ($recordsOriginal as $record) {
-                                       if (!empty($foreignLanguageField)) {
-                                               $record[$foreignLanguageField] = $language;
-                                       }
-                                       if (!empty($foreignTranslationPointerField)) {
-                                               $record[$foreignTranslationPointerField] = $record['uid'];
-                                       }
-                                       $newId = StringUtility::getUniqueId('NEW');
-                                       $record['uid'] = $newId;
-                                       $record['pid'] = $inlineFirstPid;
-                                       $relatedRecords['records'][$newId] = $record;
-                               }
-                       }
-               }
-               return $relatedRecords;
-       }
-
-       /**
-        * Wrapper. Calls getRecord in case of a new record should be created.
-        *
-        * @param int $pid The pid of the page the record should be stored (only relevant for NEW records)
-        * @param string $table The table to fetch data from (= foreign_table)
-        * @return array A record row from the database
-        */
-       public function getNewRecord($pid, $table) {
-               $record = $this->getRecord($table, $pid, 'new');
-               $record['uid'] = StringUtility::getUniqueId('NEW');
-               $newRecordPid = $pid;
-               $pageTS = BackendUtility::getPagesTSconfig($pid);
-               // @todo: this is to force new foreign records to a different pid via pageTS - should be handled elsewhere
-               if (isset($pageTS['TCAdefaults.'][$table . '.']['pid']) && MathUtility::canBeInterpretedAsInteger($pageTS['TCAdefaults.'][$table . '.']['pid'])) {
-                       $newRecordPid = $pageTS['TCAdefaults.'][$table . '.']['pid'];
-               }
-               $record['pid'] = $newRecordPid;
-               return $record;
-       }
-
-       /**
-        * Get a single record row for a TCA table from the database.
-        * Used in inline context
-        *
-        * @param string $table The table to fetch data from (= foreign_table)
-        * @param string $uid The uid of the record to fetch, or the pid if a new record should be created
-        * @param string $cmd The command to perform, empty or 'new'
-        * @return array A record row from the database
-        * @internal
-        */
-       public function getRecord($table, $uid, $cmd = '') {
-               $backendUser = $this->getBackendUserAuthentication();
-               // Fetch workspace version of a record (if any):
-               if ($cmd !== 'new' && $backendUser->workspace !== 0 && BackendUtility::isTableWorkspaceEnabled($table)) {
-                       $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($backendUser->workspace, $table, $uid, 'uid,t3ver_state');
-                       if ($workspaceVersion !== FALSE) {
-                               $versionState = VersionState::cast($workspaceVersion['t3ver_state']);
-                               if ($versionState->equals(VersionState::DELETE_PLACEHOLDER)) {
-                                       return FALSE;
-                               }
-                               $uid = $workspaceVersion['uid'];
-                       }
-               }
-
-               /** @var TcaDatabaseRecord $formDataGroup */
-               $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
-               /** @var FormDataCompiler $formDataCompiler */
-               $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
-               $command = $cmd === 'new' ? 'new' : 'edit';
-               $formDataCompilerInput = [
-                       'tableName' => $table,
-                       'vanillaUid' => (int)$uid,
-                       'command' => $command,
-               ];
-               $formData = $formDataCompiler->compile($formDataCompilerInput);
-               return $formData['databaseRow'];
-       }
-
-       /**
-        * Gets the related records of the embedding item, this could be 1:n, m:n.
-        *
-        * @param string $table The table name of the record
-        * @param string $itemList The list of related child records
-        * @return array The records related to the parent item
-        */
-       protected function getRelatedRecordsArray($table, $itemList) {
-               $records = array();
-               $itemArray = FormEngineUtility::getInlineRelatedRecordsUidArray($itemList);
-               // Perform modification of the selected items array:
-               foreach ($itemArray as $uid) {
-                       // Get the records for this uid
-                       if ($record = $this->getRecord($table, $uid)) {
-                               $records[$uid] = $record;
-                       }
-               }
-               return $records;
-       }
-
-       /**
-        * Gets the difference between current localized structure and the original language structure.
-        * If there are records which once were localized but don't exist in the original version anymore, the record row is marked with '__remove'.
-        * If there are records which can be localized and exist only in the original version, the record row is marked with '__create' and '__virtual'.
-        *
-        * @param string $table The table name of the parent records
-        * @param array $options Options defining what kind of records to display
-        * @param array $recordsOriginal The uids of the child records of the original language
-        * @param array $recordsLocalization The uids of the child records of the current localization
-        * @return array Merged array of uids of the child records of both versions
-        */
-       protected function getLocalizationDifferences($table, array $options, array $recordsOriginal, array $recordsLocalization) {
-               $records = array();
-               $transOrigPointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
-               // Compare original to localized version of the records:
-               foreach ($recordsLocalization as $uid => $row) {
-                       // If the record points to an original translation which doesn't exist anymore, it could be removed:
-                       if (isset($row[$transOrigPointerField]) && $row[$transOrigPointerField] > 0) {
-                               $transOrigPointer = $row[$transOrigPointerField];
-                               if (isset($recordsOriginal[$transOrigPointer])) {
-                                       unset($recordsOriginal[$transOrigPointer]);
-                               } elseif ($options['showRemoved']) {
-                                       $row['__remove'] = TRUE;
-                               }
-                       }
-                       $records[$uid] = $row;
-               }
-               // Process the remaining records in the original unlocalized parent:
-               if ($options['showPossible']) {
-                       foreach ($recordsOriginal as $uid => $row) {
-                               $row['__create'] = TRUE;
-                               $row['__virtual'] = TRUE;
-                               $records[$uid] = $row;
-                       }
-               }
-               return $records;
-       }
-
-       /**
-        * @return BackendUserAuthentication
-        */
-       protected function getBackendUserAuthentication() {
-               return $GLOBALS['BE_USER'];
-       }
-
-}
index 37fd48f..c6d8bc3 100644 (file)
@@ -23,6 +23,8 @@ use TYPO3\CMS\Backend\Utility\BackendUtility;
  *
  * Code related to inline elements need to know their nesting level. This class takes
  * care of the according handling and can return field prefixes to be used in DOM.
+ *
+ * @internal: This class may change any time or vanish altogether
  */
 class InlineStackProcessor {
 
@@ -51,10 +53,9 @@ class InlineStackProcessor {
         * - 'unstable': Containing partly filled data (e.g. only table and possibly field)
         *
         * @param string $domObjectId The DOM object-id
-        * @param bool $loadConfig Load the TCA configuration for that level (default: TRUE)
         * @return void
         */
-       public function initializeByParsingDomObjectIdString($domObjectId, $loadConfig = TRUE) {
+       public function initializeByParsingDomObjectIdString($domObjectId) {
                $unstable = array();
                $vector = array('table', 'uid', 'field');
 
@@ -70,16 +71,16 @@ class InlineStackProcessor {
                        for ($i = 0; $i < $partsCnt; $i++) {
                                if ($i > 0 && $i % 3 == 0) {
                                        // Load the TCA configuration of the table field and store it in the stack
-                                       if ($loadConfig) {
-                                               $unstable['config'] = $GLOBALS['TCA'][$unstable['table']]['columns'][$unstable['field']]['config'];
-                                               // Fetch TSconfig:
-                                               $TSconfig = FormEngineUtility::getTSconfigForTableRow($unstable['table'], array('uid' => $unstable['uid'], 'pid' => $inlineFirstPid), $unstable['field']);
-                                               // Override TCA field config by TSconfig:
-                                               if (!$TSconfig['disabled']) {
-                                                       $unstable['config'] = FormEngineUtility::overrideFieldConf($unstable['config'], $TSconfig);
-                                               }
-                                               $unstable['localizationMode'] = BackendUtility::getInlineLocalizationMode($unstable['table'], $unstable['config']);
+                                       // @todo: This TCA loading here must fall - config sub-array shouldn't exist at all!
+                                       $unstable['config'] = $GLOBALS['TCA'][$unstable['table']]['columns'][$unstable['field']]['config'];
+                                       // Fetch TSconfig:
+                                       // @todo: aaargs ;)
+                                       $TSconfig = FormEngineUtility::getTSconfigForTableRow($unstable['table'], array('uid' => $unstable['uid'], 'pid' => $inlineFirstPid), $unstable['field']);
+                                       // Override TCA field config by TSconfig:
+                                       if (!$TSconfig['disabled']) {
+                                               $unstable['config'] = FormEngineUtility::overrideFieldConf($unstable['config'], $TSconfig);
                                        }
+                                       $unstable['localizationMode'] = BackendUtility::getInlineLocalizationMode($unstable['table'], $unstable['config']);
 
                                        // Extract FlexForm from field part (if any)
                                        if (strpos($unstable['field'], ':') !== FALSE) {
index b156e5c..43a9a7b 100644 (file)
@@ -156,91 +156,6 @@ class FormEngineUtility {
        }
 
        /**
-        * Extracts FlexForm parts of a form element name like
-        * data[table][uid][field][sDEF][lDEF][FlexForm][vDEF]
-        * Helper method used in inline
-        *
-        * @param string $formElementName The form element name
-        * @return array|NULL
-        * @internal
-        */
-       static public function extractFlexFormParts($formElementName) {
-               $flexFormParts = NULL;
-
-               $matches = array();
-
-               if (preg_match('#^data(?:\[[^]]+\]){3}(\[data\](?:\[[^]]+\]){4,})$#', $formElementName, $matches)) {
-                       $flexFormParts = GeneralUtility::trimExplode(
-                               '][',
-                               trim($matches[1], '[]')
-                       );
-               }
-
-               return $flexFormParts;
-       }
-
-       /**
-        * Get inlineFirstPid from a given objectId string
-        *
-        * @param string $domObjectId The id attribute of an element
-        * @return int|NULL Pid or null
-        * @internal
-        */
-       static public function getInlineFirstPidFromDomObjectId($domObjectId) {
-               // Substitute FlexForm addition and make parsing a bit easier
-               $domObjectId = str_replace('---', ':', $domObjectId);
-               // The starting pattern of an object identifier (e.g. "data-<firstPidValue>-<anything>)
-               $pattern = '/^data' . '-' . '(.+?)' . '-' . '(.+)$/';
-               if (preg_match($pattern, $domObjectId, $match)) {
-                       return $match[1];
-               }
-               return NULL;
-       }
-
-       /**
-        * Adds / adapts some general options of main TCA config for inline usage
-        *
-        * @param array $config TCA field configuration
-        * @return array Modified configuration
-        * @internal
-        */
-       static public function mergeInlineConfiguration($config) {
-               // Init appearance if not set:
-               if (!isset($config['appearance']) || !is_array($config['appearance'])) {
-                       $config['appearance'] = array();
-               }
-               // Set the position/appearance of the "Create new record" link:
-               if (
-                       isset($config['foreign_selector'])
-                       && $config['foreign_selector']
-                       && (!isset($config['appearance']['useCombination']) || !$config['appearance']['useCombination'])
-               ) {
-                       $config['appearance']['levelLinksPosition'] = 'none';
-               } elseif (
-                       !isset($config['appearance']['levelLinksPosition'])
-                       || !in_array($config['appearance']['levelLinksPosition'], array('top', 'bottom', 'both', 'none'))
-               ) {
-                       $config['appearance']['levelLinksPosition'] = 'top';
-               }
-               // Defines which controls should be shown in header of each record:
-               $enabledControls = array(
-                       'info' => TRUE,
-                       'new' => TRUE,
-                       'dragdrop' => TRUE,
-                       'sort' => TRUE,
-                       'hide' => TRUE,
-                       'delete' => TRUE,
-                       'localize' => TRUE
-               );
-               if (isset($config['appearance']['enabledControls']) && is_array($config['appearance']['enabledControls'])) {
-                       $config['appearance']['enabledControls'] = array_merge($enabledControls, $config['appearance']['enabledControls']);
-               } else {
-                       $config['appearance']['enabledControls'] = $enabledControls;
-               }
-               return $config;
-       }
-
-       /**
         * Determine the configuration and the type of a record selector.
         * This is a helper method for inline / IRRE handling
         *
diff --git a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaInlineConfigurationTest.php b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaInlineConfigurationTest.php
new file mode 100644 (file)
index 0000000..19dfa96
--- /dev/null
@@ -0,0 +1,522 @@
+<?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 TYPO3\CMS\Core\Tests\UnitTestCase;
+use TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineConfiguration;
+
+/**
+ * Test case
+ */
+class TcaInlineConfigurationTest extends UnitTestCase {
+
+       /**
+        * @var TcaInlineConfiguration
+        */
+       protected $subject;
+
+       protected function setUp() {
+               $this->subject = new TcaInlineConfiguration();
+       }
+
+       /**
+        * @var array Set of default controls
+        */
+       protected $defaultConfig = [
+               'type' => 'inline',
+               'foreign_table' => 'aForeignTableName',
+               'minitems' => 0,
+               'maxitems' => 100000,
+               'behaviour' => [
+                       'localizationMode' => 'none',
+               ],
+               'appearance' => [
+                       'levelLinksPosition' => 'top',
+                       'showPossibleLocalizationRecords' => FALSE,
+                       'showRemovedLocalizationRecords' => FALSE,
+                       'enabledControls' => [
+                               'info' => TRUE,
+                               'new' => TRUE,
+                               'dragdrop' => TRUE,
+                               'sort' => TRUE,
+                               'hide' => TRUE,
+                               'delete' => TRUE,
+                               'localize' => TRUE,
+                       ],
+               ],
+       ];
+
+       /**
+        * @test
+        */
+       public function addDataThrowsExceptionForInlineFieldWithoutForeignTableConfig() {
+               $input = [
+                       'databaseRow' => [],
+                       'processedTca' => [
+                               'columns' => [
+                                       'aField' => [
+                                               'config' => [
+                                                       'type' => 'inline',
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               $this->setExpectedException(\UnexpectedValueException::class, $this->anything(), 1443793404);
+               $this->subject->addData($input);
+       }
+
+       /**
+        * @test
+        */
+       public function addDataSetsDefaults() {
+               $input = [
+                       'processedTca' => [
+                               'columns' => [
+                                       'aField' => [
+                                               'config' => [
+                                                       'type' => 'inline',
+                                                       'foreign_table' => 'aForeignTableName',
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               $expected['processedTca']['columns']['aField']['config'] = $this->defaultConfig;
+               $this->assertEquals($expected, $this->subject->addData($input));
+       }
+
+       /**
+        * @test
+        */
+       public function addDataKeepsGivenMinitems() {
+               $input = [
+                       'processedTca' => [
+                               'columns' => [
+                                       'aField' => [
+                                               'config' => [
+                                                       'type' => 'inline',
+                                                       'foreign_table' => 'aForeignTableName',
+                                                       'minitems' => 23,
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               $expected['processedTca']['columns']['aField']['config'] = $this->defaultConfig;
+               $expected['processedTca']['columns']['aField']['config']['minitems'] = 23;
+               $this->assertEquals($expected, $this->subject->addData($input));
+       }
+
+       /**
+        * @test
+        */
+       public function addDataForcesMinitemsPositive() {
+               $input = [
+                       'processedTca' => [
+                               'columns' => [
+                                       'aField' => [
+                                               'config' => [
+                                                       'type' => 'inline',
+                                                       'foreign_table' => 'aForeignTableName',
+                                                       'minitems' => '-23',
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               $expected['processedTca']['columns']['aField']['config'] = $this->defaultConfig;
+               $expected['processedTca']['columns']['aField']['config']['minitems'] = 0;
+               $this->assertEquals($expected, $this->subject->addData($input));
+       }
+
+       /**
+        * @test
+        */
+       public function addDataKeepsGivenMaxitems() {
+               $input = [
+                       'processedTca' => [
+                               'columns' => [
+                                       'aField' => [
+                                               'config' => [
+                                                       'type' => 'inline',
+                                                       'foreign_table' => 'aForeignTableName',
+                                                       'maxitems' => 23,
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               $expected['processedTca']['columns']['aField']['config'] = $this->defaultConfig;
+               $expected['processedTca']['columns']['aField']['config']['maxitems'] = 23;
+               $this->assertEquals($expected, $this->subject->addData($input));
+       }
+
+       /**
+        * @test
+        */
+       public function addDataForcesMaxitemsPositive() {
+               $input = [
+                       'processedTca' => [
+                               'columns' => [
+                                       'aField' => [
+                                               'config' => [
+                                                       'type' => 'inline',
+                                                       'foreign_table' => 'aForeignTableName',
+                                                       'maxitems' => '-23',
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               $expected['processedTca']['columns']['aField']['config'] = $this->defaultConfig;
+               $expected['processedTca']['columns']['aField']['config']['maxitems'] = 1;
+               $this->assertEquals($expected, $this->subject->addData($input));
+       }
+
+       /**
+        * @test
+        */
+       public function addDataThrowsExceptionIfLocalizationModeIsSetButNotToKeepOrSelect() {
+               $input = [
+                       'defaultLanguageRow' => [],
+                       'processedTca' => [
+                               'columns' => [
+                                       'aField' => [
+                                               'config' => [
+                                                       'type' => 'inline',
+                                                       'foreign_table' => 'aForeignTableName',
+                                                       'behaviour' => [
+                                                               'localizationMode' => 'foo',
+                                                       ]
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               $this->setExpectedException(\UnexpectedValueException::class, $this->anything(), 1443829370);
+               $this->subject->addData($input);
+       }
+
+       /**
+        * @test
+        */
+       public function addDataThrowsExceptionIfLocalizationModeIsSetToSelectAndChildIsNotLocalizable() {
+               $input = [
+                       'defaultLanguageRow' => [],
+                       'processedTca' => [
+                               'columns' => [
+                                       'aField' => [
+                                               'config' => [
+                                                       'type' => 'inline',
+                                                       'foreign_table' => 'aForeignTableName',
+                                                       'behaviour' => [
+                                                               'localizationMode' => 'select',
+                                                       ]
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               // not $globals definition for child here -> not localizable
+               $this->setExpectedException(\UnexpectedValueException::class, $this->anything(), 1443944274);
+               $this->subject->addData($input);
+       }
+
+       /**
+        * @test
+        */
+       public function addDataKeepsLocalizationModeSelectIfChildIsLocalizable() {
+               $input = [
+                       'defaultLanguageRow' => [],
+                       'processedTca' => [
+                               'columns' => [
+                                       'aField' => [
+                                               'config' => [
+                                                       'type' => 'inline',
+                                                       'foreign_table' => 'aForeignTableName',
+                                                       'behaviour' => [
+                                                               'localizationMode' => 'select',
+                                                       ]
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               $GLOBALS['TCA']['aForeignTableName']['ctrl'] = [
+                       'languageField' => 'theLanguageField',
+                       'transOrigPointerField' => 'theTransOrigPointerField',
+               ];
+               $expected = $input;
+               $expected['processedTca']['columns']['aField']['config'] = $this->defaultConfig;
+               $expected['processedTca']['columns']['aField']['config']['behaviour']['localizationMode'] = 'select';
+               $this->assertEquals($expected, $this->subject->addData($input));
+       }
+
+       /**
+        * @test
+        */
+       public function addDataKeepsLocalizationModeKeep() {
+               $input = [
+                       'defaultLanguageRow' => [],
+                       'processedTca' => [
+                               'columns' => [
+                                       'aField' => [
+                                               'config' => [
+                                                       'type' => 'inline',
+                                                       'foreign_table' => 'aForeignTableName',
+                                                       'behaviour' => [
+                                                               'localizationMode' => 'keep',
+                                                       ]
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               $expected = $input;
+               $expected['processedTca']['columns']['aField']['config'] = $this->defaultConfig;
+               $expected['processedTca']['columns']['aField']['config']['behaviour']['localizationMode'] = 'keep';
+               $this->assertEquals($expected, $this->subject->addData($input));
+       }
+
+       /**
+        * @test
+        */
+       public function addDataSetsLocalizationModeToNoneIfNotSetAndChildIsNotLocalizable() {
+               $input = [
+                       'defaultLanguageRow' => [],
+                       'processedTca' => [
+                               'columns' => [
+                                       'aField' => [
+                                               'config' => [
+                                                       'type' => 'inline',
+                                                       'foreign_table' => 'aForeignTableName',
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               $expected = $input;
+               $expected['processedTca']['columns']['aField']['config'] = $this->defaultConfig;
+               $expected['processedTca']['columns']['aField']['config']['behaviour']['localizationMode'] = 'none';
+               $this->assertEquals($expected, $this->subject->addData($input));
+       }
+
+       /**
+        * @test
+        */
+       public function addDataSetsLocalizationModeToSelectIfNotSetAndChildIsLocalizable() {
+               $input = [
+                       'defaultLanguageRow' => [],
+                       'processedTca' => [
+                               'columns' => [
+                                       'aField' => [
+                                               'config' => [
+                                                       'type' => 'inline',
+                                                       'foreign_table' => 'aForeignTableName',
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               $GLOBALS['TCA']['aForeignTableName']['ctrl'] = [
+                       'languageField' => 'theLanguageField',
+                       'transOrigPointerField' => 'theTransOrigPointerField',
+               ];
+               $expected = $input;
+               $expected['processedTca']['columns']['aField']['config'] = $this->defaultConfig;
+               $expected['processedTca']['columns']['aField']['config']['behaviour']['localizationMode'] = 'select';
+               $this->assertEquals($expected, $this->subject->addData($input));
+       }
+
+       /**
+        * @test
+        */
+       public function addDataMergesWithGivenAppearanceSettings() {
+               $input = [
+                       'processedTca' => [
+                               'columns' => [
+                                       'aField' => [
+                                               'config' => [
+                                                       'type' => 'inline',
+                                                       'foreign_table' => 'aForeignTableName',
+                                                       'appearance' => [
+                                                               'levelLinksPosition' => 'both',
+                                                               'enabledControls' => [
+                                                                       'dragdrop' => FALSE,
+                                                               ],
+                                                       ],
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               $expected['processedTca']['columns']['aField']['config'] = $this->defaultConfig;
+               $expected['processedTca']['columns']['aField']['config']['appearance']['levelLinksPosition'] = 'both';
+               $expected['processedTca']['columns']['aField']['config']['appearance']['enabledControls']['dragdrop'] = FALSE;
+               $this->assertEquals($expected, $this->subject->addData($input));
+       }
+
+       /**
+        * @test
+        */
+       public function addDataForcesLevelLinksPositionWithForeignSelector() {
+               $input = [
+                       'processedTca' => [
+                               'columns' => [
+                                       'aField' => [
+                                               'config' => [
+                                                       'type' => 'inline',
+                                                       'foreign_table' => 'aForeignTableName',
+                                                       'foreign_selector' => 'foo',
+                                                       'appearance' => [
+                                                               'levelLinksPosition' => 'both',
+                                                       ],
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               $expected['processedTca']['columns']['aField']['config'] = $this->defaultConfig;
+               $expected['processedTca']['columns']['aField']['config']['foreign_selector'] = 'foo';
+               $expected['processedTca']['columns']['aField']['config']['appearance']['levelLinksPosition'] = 'none';
+               $this->assertEquals($expected, $this->subject->addData($input));
+       }
+
+       /**
+        * @test
+        */
+       public function addDataKeepsLevelLinksPositionWithForeignSelectorAndUseCombination() {
+               $input = [
+                       'processedTca' => [
+                               'columns' => [
+                                       'aField' => [
+                                               'config' => [
+                                                       'type' => 'inline',
+                                                       'foreign_table' => 'aForeignTableName',
+                                                       'foreign_selector' => 'foo',
+                                                       'appearance' => [
+                                                               'useCombination' => TRUE,
+                                                               'levelLinksPosition' => 'both',
+                                                       ],
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               $expected['processedTca']['columns']['aField']['config'] = $this->defaultConfig;
+               $expected['processedTca']['columns']['aField']['config']['foreign_selector'] = 'foo';
+               $expected['processedTca']['columns']['aField']['config']['appearance']['useCombination'] = TRUE;
+               $expected['processedTca']['columns']['aField']['config']['appearance']['levelLinksPosition'] = 'both';
+               $this->assertEquals($expected, $this->subject->addData($input));
+       }
+
+       /**
+        * @test
+        */
+       public function addDataKeepsShowPossibleLocalizationRecordsButForcesBooleanTrue() {
+               $input = [
+                       'processedTca' => [
+                               'columns' => [
+                                       'aField' => [
+                                               'config' => [
+                                                       'type' => 'inline',
+                                                       'foreign_table' => 'aForeignTableName',
+                                                       'appearance' => [
+                                                               'showPossibleLocalizationRecords' => '1',
+                                                       ],
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               $expected['processedTca']['columns']['aField']['config'] = $this->defaultConfig;
+               $expected['processedTca']['columns']['aField']['config']['appearance']['showPossibleLocalizationRecords'] = TRUE;
+               $this->assertEquals($expected, $this->subject->addData($input));
+       }
+
+       /**
+        * @test
+        */
+       public function addDataKeepsShowPossibleLocalizationRecordsButForcesBooleanFalse() {
+               $input = [
+                       'processedTca' => [
+                               'columns' => [
+                                       'aField' => [
+                                               'config' => [
+                                                       'type' => 'inline',
+                                                       'foreign_table' => 'aForeignTableName',
+                                                       'appearance' => [
+                                                               'showPossibleLocalizationRecords' => 0,
+                                                       ],
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               $expected['processedTca']['columns']['aField']['config'] = $this->defaultConfig;
+               $expected['processedTca']['columns']['aField']['config']['appearance']['showPossibleLocalizationRecords'] = FALSE;
+               $this->assertEquals($expected, $this->subject->addData($input));
+       }
+
+       /**
+        * @test
+        */
+       public function addDataKeepshowRemovedLocalizationRecordsButForcesBooleanTrue() {
+               $input = [
+                       'processedTca' => [
+                               'columns' => [
+                                       'aField' => [
+                                               'config' => [
+                                                       'type' => 'inline',
+                                                       'foreign_table' => 'aForeignTableName',
+                                                       'appearance' => [
+                                                               'showRemovedLocalizationRecords' => 1,
+                                                       ],
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               $expected['processedTca']['columns']['aField']['config'] = $this->defaultConfig;
+               $expected['processedTca']['columns']['aField']['config']['appearance']['showRemovedLocalizationRecords'] = TRUE;
+               $this->assertEquals($expected, $this->subject->addData($input));
+       }
+
+       /**
+        * @test
+        */
+       public function addDataKeepsShowRemovedLocalizationRecordsButForcesBooleanFalse() {
+               $input = [
+                       'processedTca' => [
+                               'columns' => [
+                                       'aField' => [
+                                               'config' => [
+                                                       'type' => 'inline',
+                                                       'foreign_table' => 'aForeignTableName',
+                                                       'appearance' => [
+                                                               'showRemovedLocalizationRecords' => '',
+                                                       ],
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               $expected['processedTca']['columns']['aField']['config'] = $this->defaultConfig;
+               $expected['processedTca']['columns']['aField']['config']['appearance']['showRemovedLocalizationRecords'] = FALSE;
+               $this->assertEquals($expected, $this->subject->addData($input));
+       }
+
+}
diff --git a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaInlineExpandCollapseStateTest.php b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaInlineExpandCollapseStateTest.php
new file mode 100644 (file)
index 0000000..6fde0d6
--- /dev/null
@@ -0,0 +1,65 @@
+<?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 TYPO3\CMS\Core\Tests\UnitTestCase;
+use TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState;
+
+/**
+ * Test case
+ */
+class TcaInlineExpandCollapseStateTest extends UnitTestCase {
+
+       /**
+        * @var TcaInlineExpandCollapseState
+        */
+       protected $subject;
+
+       protected function setUp() {
+               $this->subject = new TcaInlineExpandCollapseState();
+       }
+
+       /**
+        * @test
+        */
+       public function addDataAddsInlineStatusForTableUid() {
+               $input = [
+                       'command' => 'edit',
+                       'tableName' => 'aParentTable',
+                       'databaseRow' => [
+                               'uid' => 5,
+                       ],
+               ];
+               $inlineState = [
+                       'aParentTable' => [
+                               5 => [
+                                       'aChildTable' => [
+                                               // Records 23 and 42 are expanded
+                                               23,
+                                               42,
+                                       ],
+                               ],
+                       ],
+               ];
+               $GLOBALS['BE_USER'] = new \stdClass();
+               $GLOBALS['BE_USER']->uc = [
+                       'inlineView' => serialize($inlineState),
+               ];
+               $expected = $input;
+               $expected['inlineExpandCollapseStateArray'] = $inlineState['aParentTable'][5];
+               $this->assertSame($expected, $this->subject->addData($input));
+       }
+
+}
index 1980023..311cdf2 100644 (file)
@@ -158,7 +158,7 @@ class TcaInputPlaceholdersTest extends UnitTestCase {
                /** @var TcaInputPlaceholderRecord $languageService */
                $formDataCompilerProphecy = $this->prophesize(FormDataCompiler::class);
                GeneralUtility::addInstance(FormDataCompiler::class, $formDataCompilerProphecy->reveal());
-               $formDataCompilerProphecy->compile(['command' => 'edit', 'vanillaUid' => 42, 'tableName' => 'aForeignTable'])
+               $formDataCompilerProphecy->compile(['command' => 'edit', 'vanillaUid' => 42, 'tableName' => 'aForeignTable', 'inlineCompileExistingChildren' => FALSE])
                        ->shouldBeCalled()
                        ->willReturn($aForeignTableInput);
 
@@ -216,7 +216,7 @@ class TcaInputPlaceholdersTest extends UnitTestCase {
                /** @var TcaInputPlaceholderRecord $languageService */
                $formDataCompilerProphecy = $this->prophesize(FormDataCompiler::class);
                GeneralUtility::addInstance(FormDataCompiler::class, $formDataCompilerProphecy->reveal());
-               $formDataCompilerProphecy->compile(['command' => 'edit', 'vanillaUid' => 3, 'tableName' => 'sys_file'])
+               $formDataCompilerProphecy->compile(['command' => 'edit', 'vanillaUid' => 3, 'tableName' => 'sys_file', 'inlineCompileExistingChildren' => FALSE])
                        ->shouldBeCalled()
                        ->willReturn($sysFileProphecyResult);
 
@@ -275,7 +275,7 @@ class TcaInputPlaceholdersTest extends UnitTestCase {
                /** @var TcaInputPlaceholderRecord $languageService */
                $formDataCompilerProphecy = $this->prophesize(FormDataCompiler::class);
                GeneralUtility::addInstance(FormDataCompiler::class, $formDataCompilerProphecy->reveal());
-               $formDataCompilerProphecy->compile(['command' => 'edit', 'vanillaUid' => 2, 'tableName' => 'sys_file_metadata'])
+               $formDataCompilerProphecy->compile(['command' => 'edit', 'vanillaUid' => 2, 'tableName' => 'sys_file_metadata', 'inlineCompileExistingChildren' => FALSE])
                        ->shouldBeCalled()
                        ->willReturn($sysFileMetadataProphecyResult);
 
@@ -351,13 +351,13 @@ class TcaInputPlaceholdersTest extends UnitTestCase {
 
                $sysFileFormDataCompilerProphecy = $this->prophesize(FormDataCompiler::class);
                GeneralUtility::addInstance(FormDataCompiler::class, $sysFileFormDataCompilerProphecy->reveal());
-               $sysFileFormDataCompilerProphecy->compile(['command' => 'edit', 'vanillaUid' => 3, 'tableName' => 'sys_file'])
+               $sysFileFormDataCompilerProphecy->compile(['command' => 'edit', 'vanillaUid' => 3, 'tableName' => 'sys_file', 'inlineCompileExistingChildren' => FALSE])
                        ->shouldBeCalled()
                        ->willReturn($sysFileProphecyResult);
 
                $sysFileMetaDataFormDataCompilerProphecy = $this->prophesize(FormDataCompiler::class);
                GeneralUtility::addInstance(FormDataCompiler::class, $sysFileMetaDataFormDataCompilerProphecy->reveal());
-               $sysFileMetaDataFormDataCompilerProphecy->compile(['command' => 'edit', 'vanillaUid' => 7, 'tableName' => 'sys_file_metadata'])
+               $sysFileMetaDataFormDataCompilerProphecy->compile(['command' => 'edit', 'vanillaUid' => 7, 'tableName' => 'sys_file_metadata', 'inlineCompileExistingChildren' => FALSE])
                        ->shouldBeCalled()
                        ->willReturn($sysFileMetadataProphecyResult);
 
index 2a3eb5a..46d2740 100644 (file)
@@ -14,6 +14,7 @@ namespace TYPO3\CMS\Backend\Tests\Unit\Form;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Database\DatabaseConnection;
 use TYPO3\CMS\Core\Tests\UnitTestCase;
 use TYPO3\CMS\Backend\Form\InlineStackProcessor;
 
@@ -22,6 +23,12 @@ use TYPO3\CMS\Backend\Form\InlineStackProcessor;
  */
 class InlineStackProcessorTest extends UnitTestCase {
 
+       protected function setUp() {
+               // @todo: Remove if stack processor does not fiddle with tsConfig anymore and no longer sets 'config'
+               $dbProphecy = $this->prophesize(DatabaseConnection::class);
+               $GLOBALS['TYPO3_DB'] = $dbProphecy->reveal();
+       }
+
        /**
         * @return array
         */
@@ -65,6 +72,8 @@ class InlineStackProcessorTest extends UnitTestCase {
                                                        'table' => 'parentTable',
                                                        'uid' => 'parentUid',
                                                        'field' => 'parentField',
+                                                       'config' => null,
+                                                       'localizationMode' => false,
                                                ),
                                        ),
                                        'unstable' => array(
@@ -84,6 +93,8 @@ class InlineStackProcessorTest extends UnitTestCase {
                                                        'table' => 'parentTable',
                                                        'uid' => 'parentUid',
                                                        'field' => 'parentField',
+                                                       'config' => null,
+                                                       'localizationMode' => false,
                                                ),
                                        ),
                                        'unstable' => array(
@@ -104,6 +115,8 @@ class InlineStackProcessorTest extends UnitTestCase {
                                                        'table' => 'parentTable',
                                                        'uid' => 'parentUid',
                                                        'field' => 'parentField',
+                                                       'config' => null,
+                                                       'localizationMode' => false,
                                                ),
                                        ),
                                        'unstable' => array(
@@ -125,11 +138,15 @@ class InlineStackProcessorTest extends UnitTestCase {
                                                        'table' => 'grandParentTable',
                                                        'uid' => 'grandParentUid',
                                                        'field' => 'grandParentField',
+                                                       'config' => null,
+                                                       'localizationMode' => false,
                                                ),
                                                array(
                                                        'table' => 'parentTable',
                                                        'uid' => 'parentUid',
                                                        'field' => 'parentField',
+                                                       'config' => null,
+                                                       'localizationMode' => false,
                                                ),
                                        ),
                                        'unstable' => array(
@@ -149,11 +166,15 @@ class InlineStackProcessorTest extends UnitTestCase {
                                                        'table' => 'grandParentTable',
                                                        'uid' => 'grandParentUid',
                                                        'field' => 'grandParentField',
+                                                       'config' => null,
+                                                       'localizationMode' => false,
                                                ),
                                                array(
                                                        'table' => 'parentTable',
                                                        'uid' => 'parentUid',
                                                        'field' => 'parentField',
+                                                       'config' => null,
+                                                       'localizationMode' => false,
                                                ),
                                        ),
                                        'unstable' => array(
@@ -174,11 +195,15 @@ class InlineStackProcessorTest extends UnitTestCase {
                                                        'table' => 'grandParentTable',
                                                        'uid' => 'grandParentUid',
                                                        'field' => 'grandParentField',
+                                                       'config' => null,
+                                                       'localizationMode' => false,
                                                ),
                                                array(
                                                        'table' => 'parentTable',
                                                        'uid' => 'parentUid',
                                                        'field' => 'parentField',
+                                                       'config' => null,
+                                                       'localizationMode' => false,
                                                ),
                                        ),
                                        'unstable' => array(
@@ -203,11 +228,15 @@ class InlineStackProcessorTest extends UnitTestCase {
                                                        'flexform' => array(
                                                                'data', 'sDEF', 'lDEF', 'grandParentFlexForm', 'vDEF',
                                                        ),
+                                                       'config' => null,
+                                                       'localizationMode' => false,
                                                ),
                                                array(
                                                        'table' => 'parentTable',
                                                        'uid' => 'parentUid',
                                                        'field' => 'parentField',
+                                                       'config' => null,
+                                                       'localizationMode' => false,
                                                ),
                                        ),
                                        'unstable' => array(
@@ -230,7 +259,7 @@ class InlineStackProcessorTest extends UnitTestCase {
        public function initializeByParsingDomObjectIdStringParsesStructureString($string, array $expectedInlineStructure, array $_) {
                /** @var InlineStackProcessor|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface $subject */
                $subject = $this->getAccessibleMock(InlineStackProcessor::class, array('dummy'));
-               $subject->initializeByParsingDomObjectIdString($string, FALSE);
+               $subject->initializeByParsingDomObjectIdString($string);
                $structure = $subject->_get('inlineStructure');
                $this->assertEquals($expectedInlineStructure, $structure);
        }
@@ -242,7 +271,7 @@ class InlineStackProcessorTest extends UnitTestCase {
        public function getCurrentStructureFormPrefixReturnsExceptedStringAfterInitializationByStructureString($string, array $_, array $expectedFormName) {
                /** @var InlineStackProcessor|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface $subject */
                $subject = new InlineStackProcessor;
-               $subject->initializeByParsingDomObjectIdString($string, FALSE);
+               $subject->initializeByParsingDomObjectIdString($string);
                $this->assertEquals($expectedFormName['form'], $subject->getCurrentStructureFormPrefix());
        }
 
@@ -253,7 +282,7 @@ class InlineStackProcessorTest extends UnitTestCase {
        public function getCurrentStructureDomObjectIdPrefixReturnsExceptedStringAfterInitializationByStructureString($string, array $_, array $expectedFormName) {
                /** @var InlineStackProcessor|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface $subject */
                $subject = new InlineStackProcessor;
-               $subject->initializeByParsingDomObjectIdString($string, FALSE);
+               $subject->initializeByParsingDomObjectIdString($string);
                $this->assertEquals($expectedFormName['object'], $subject->getCurrentStructureDomObjectIdPrefix('pageId'));
        }
 
index 83a3d47..217bb1b 100644 (file)
@@ -464,14 +464,24 @@ return array(
                                                        \TYPO3\CMS\Backend\Form\FormDataProvider\TcaFlexFetch::class,
                                                ),
                                        ),
-                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInline::class => array(
+                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class => array(
                                                'depends' => array(
                                                        \TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectItems::class,
                                                ),
                                        ),
+                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineConfiguration::class => array(
+                                               'depends' => array(
+                                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class,
+                                               ),
+                                       ),
+                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInline::class => array(
+                                               'depends' => array(
+                                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineConfiguration::class,
+                                               ),
+                                       ),
                                        \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInputPlaceholders::class => array(
                                                'depends' => array(
-                                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInline::class,
+                                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineConfiguration::class,
                                                ),
                                        ),
                                        \TYPO3\CMS\Backend\Form\FormDataProvider\TcaRecordTitle::class => array(
@@ -507,11 +517,21 @@ return array(
                                                        \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRowDefaultValues::class,
                                                ),
                                        ),
-                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInline::class => array(
+                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class => array(
                                                'depends' => array(
                                                        \TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectItems::class,
                                                ),
                                        ),
+                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineConfiguration::class => array(
+                                               'depends' => array(
+                                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class,
+                                               ),
+                                       ),
+                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInline::class => array(
+                                               'depends' => array(
+                                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineConfiguration::class,
+                                               ),
+                                       ),
                                ),
                                'tcaInputPlaceholderRecord' => array(
                                        \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseEditRow::class => array(),
@@ -558,11 +578,21 @@ return array(
                                                        \TYPO3\CMS\Backend\Form\FormDataProvider\TcaCheckboxItems::class,
                                                ),
                                        ),
-                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInline::class => array(
+                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class => array(
                                                'depends' => array(
                                                        \TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectItems::class,
                                                ),
                                        ),
+                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineConfiguration::class => array(
+                                               'depends' => array(
+                                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class,
+                                               ),
+                                       ),
+                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInline::class => array(
+                                               'depends' => array(
+                                                       \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineConfiguration::class,
+                                               ),
+                                       ),
                                ),
                        ),
                ),