[FEATURE] Enable IRRE fields in FlexForms
authorKai Vogel <kai.vogel@speedprogs.de>
Tue, 6 Nov 2012 16:45:07 +0000 (17:45 +0100)
committerHelmut Hummel <helmut.hummel@typo3.org>
Sat, 10 Nov 2012 12:08:23 +0000 (13:08 +0100)
This patch enables IRRE fields in flexform. They can be used
identically to the TCA.

FlexForm segments are separated by a new divider "---" to
easily have the possiblity to recognize and parse them when
handline the IRRE object identifiers.

Besides that a new remapping level for FlexForms has been
introduced to the DataHandler to update accordant FlexForm
references after all child records have been written.

Unit tests have been extended to check the new additions to
the accordant object identifiers and form names. This is
important to any processing of IRRE in the form view.

Change-Id: Icadb89c2e496a5f1ad7de298ebee06d144475a11
Resolves: #18957
Releases: 6.0
Reviewed-on: http://review.typo3.org/13968
Reviewed-by: Steffen Ritter
Tested-by: Steffen Ritter
Reviewed-by: Kai Vogel
Tested-by: Kai Vogel
Reviewed-by: Helmut Hummel
Tested-by: Helmut Hummel
t3lib/jsfunc.inline.js
typo3/sysext/backend/Classes/Form/Element/InlineElement.php
typo3/sysext/backend/Tests/Unit/Form/Element/InlineElementTest.php
typo3/sysext/core/Classes/DataHandling/DataHandler.php

index 2b2e4e1..7656279 100644 (file)
@@ -31,6 +31,8 @@ var inline = {
        classVisible: 't3-form-field-container-inline-visible',
        classCollapsed: 't3-form-field-container-inline-collapsed',
        structureSeparator: '-',
+       flexFormSeparator: '---',
+       flexFormSubstitute: ':',
        prependFormFieldNames: 'data',
        noTitleString: '[No title]',
        lockedAjaxMethod: {},
@@ -60,17 +62,18 @@ var inline = {
        expandCollapseRecord: function(objectId, expandSingle, returnURL) {
                var currentUid = this.parseObjectId('none', objectId, 1);
                var objectPrefix = this.parseObjectId('full', objectId, 0, 1);
+               var escapedObjectId = this.escapeObjectId(objectId);
 
-               var currentObject = TYPO3.jQuery('#' + objectId + '_div');
+               var currentObject = TYPO3.jQuery('#' + escapedObjectId + '_div');
                        // if content is not loaded yet, get it now from server
-               if((TYPO3.jQuery('#' + objectId + '_fields') && $("irre-loading-indicator" + objectId)) || inline.isLoading) {
+               if((TYPO3.jQuery('#' + escapedObjectId + '_fields') && $("irre-loading-indicator" + objectId)) || inline.isLoading) {
                        return false;
                } else if ($(objectId + '_fields') && $(objectId + '_fields').innerHTML.substr(0,16) == '<!--notloaded-->') {
                        inline.isLoading = true;
                                // add loading-indicator
-                       if (TYPO3.jQuery('#' + objectId + '_icon')) {
-                               TYPO3.jQuery('#' + objectId + '_icon').hide();
-                               TYPO3.jQuery('#' + objectId + '_iconcontainer').addClass('loading-indicator');
+                       if (TYPO3.jQuery('#' + escapedObjectId + '_icon')) {
+                               TYPO3.jQuery('#' + escapedObjectId + '_icon').hide();
+                               TYPO3.jQuery('#' + escapedObjectId + '_iconcontainer').addClass('loading-indicator');
                        }
                        return this.getRecordDetails(objectId, returnURL);
                }
@@ -101,13 +104,15 @@ var inline = {
        },
 
        toggleElement: function(objectId) {
-               var jQueryObject = TYPO3.jQuery('#' + objectId + '_div');
+               var escapedObjectId = this.escapeObjectId(objectId);
+               var jQueryObject = TYPO3.jQuery('#' + escapedObjectId + '_div');
+
                if (jQueryObject.hasClass(this.classCollapsed)) {
                        jQueryObject.removeClass(this.classCollapsed).addClass(this.classVisible);
-                       jQueryObject.find('#' + objectId + '_header .t3-icon-irre-collapsed').removeClass('t3-icon-irre-collapsed').addClass('t3-icon-irre-expanded');
+                       jQueryObject.find('#' + escapedObjectId + '_header .t3-icon-irre-collapsed').removeClass('t3-icon-irre-collapsed').addClass('t3-icon-irre-expanded');
                } else {
                        jQueryObject.removeClass(this.classVisible).addClass(this.classCollapsed);
-                       jQueryObject.find('#' + objectId + '_header .t3-icon-irre-expanded').addClass('t3-icon-irre-collapsed').removeClass('t3-icon-irre-expanded');
+                       jQueryObject.find('#' + escapedObjectId + '_header .t3-icon-irre-expanded').addClass('t3-icon-irre-collapsed').removeClass('t3-icon-irre-expanded');
                }
        },
        collapseAllRecords: function(objectId, objectPrefix, callingUid) {
@@ -118,14 +123,16 @@ var inline = {
 
                if (formObj.length) {
                                // the uid of the calling object (last part in objectId)
-                       var recObjectId = '';
+                       var recObjectId = '', escapedRecordObjectId;
 
                        var records = formObj[0].value.split(',');
                        for (var i=0; i<records.length; i++) {
                                recObjectId = objectPrefix + this.structureSeparator + records[i];
-                               var recordEntry = TYPO3.jQuery('#' + recObjectId);
+                               escapedRecordObjectId = this.escapeObjectId(recObjectId);
+
+                               var recordEntry = TYPO3.jQuery('#' + escapedRecordObjectId);
                                if (records[i] != callingUid && recordEntry.hasClass(this.classVisible)) {
-                                       TYPO3.jQuery('#' + recObjectId + '_div').removeClass(this.classVisible).addClass(this.classCollapsed);
+                                       TYPO3.jQuery('#' + escapedRecordObjectId + '_div').removeClass(this.classVisible).addClass(this.classCollapsed);
                                        if (this.isNewRecord(recObjectId)) {
                                                this.updateExpandedCollapsedStateLocally(recObjectId, 0);
                                        } else {
@@ -147,16 +154,18 @@ var inline = {
        },
 
        getRecordDetails: function(objectId, returnURL) {
-               inline.makeAjaxCall('getRecordDetails', [inline.getNumberOfRTE(), objectId, returnURL], true);
+               var context = this.getContext(this.parseObjectId('full', objectId, 0, 1));
+               inline.makeAjaxCall('getRecordDetails', [inline.getNumberOfRTE(), objectId, returnURL], true, context);
                return false;
        },
 
        createNewRecord: function(objectId, recordUid) {
                if (this.isBelowMax(objectId)) {
+                       var context = this.getContext(objectId);
                        if (recordUid) {
                                objectId += this.structureSeparator + recordUid;
                        }
-                       this.makeAjaxCall('createNewRecord', [this.getNumberOfRTE(), objectId], true);
+                       this.makeAjaxCall('createNewRecord', [this.getNumberOfRTE(), objectId], true, context);
                } else {
                        alert('There are no more relations possible at this moment!');
                }
@@ -164,15 +173,17 @@ var inline = {
        },
 
        synchronizeLocalizeRecords: function(objectId, type) {
+               var context = this.getContext(objectId);
                var parameters = [this.getNumberOfRTE(), objectId, type];
-               this.makeAjaxCall('synchronizeLocalizeRecords', parameters, true);
+               this.makeAjaxCall('synchronizeLocalizeRecords', parameters, true, context);
        },
 
        setExpandedCollapsedState: function(objectId, expand, collapse) {
-               this.makeAjaxCall('setExpandedCollapsedState', [objectId, expand, collapse]);
+               var context = this.getContext(objectId);
+               this.makeAjaxCall('setExpandedCollapsedState', [objectId, expand, collapse], false, context);
        },
 
-       makeAjaxCall: function(method, params, lock) {
+       makeAjaxCall: function(method, params, lock, context) {
                var max, url='', urlParams='', options={};
                if (method && params && params.length && this.lockAjaxMethod(method, lock)) {
                        url = TBE_EDITOR.getBackendPath() + 'ajax.php';
@@ -180,6 +191,9 @@ var inline = {
                        for (var i=0, max=params.length; i<max; i++) {
                                urlParams += '&ajax['+i+']='+params[i];
                        }
+                       if (context) {
+                               urlParams += '&ajax[context]=' + Object.toJSON(context);
+                       }
                        options = {
                                method:         'post',
                                parameters:     urlParams,
@@ -207,7 +221,7 @@ var inline = {
        processAjaxResponse: function(method, xhr, json) {
                var addTag=null, restart=false, processedCount=0, element=null, errorCatch=[], sourcesWaiting=[];
                if (!json && xhr) {
-                       json = eval('('+xhr.responseText+')');
+                       json = xhr.responseJSON;
                }
                        // If there are elements the should be added to the <HEAD> tag (e.g. for RTEhtmlarea):
                if (json.headData) {
@@ -291,18 +305,20 @@ var inline = {
        importNewRecord: function(objectId) {
                var selector = $(objectId+'_selector');
                if (selector.selectedIndex != -1) {
+                       var context = this.getContext(objectId);
                        var selectedValue = selector.options[selector.selectedIndex].value;
                        if (!this.data.unique || !this.data.unique[objectId]) {
                                selector.options[selector.selectedIndex].selected = false;
                        }
-                       this.makeAjaxCall('createNewRecord', [this.getNumberOfRTE(), objectId, selectedValue], true);
+                       this.makeAjaxCall('createNewRecord', [this.getNumberOfRTE(), objectId, selectedValue], true, context);
                }
                return false;
        },
 
                // foreign_selector: used by element browser (type='group/db')
        importElement: function(objectId, table, uid, type) {
-               inline.makeAjaxCall('createNewRecord', [inline.getNumberOfRTE(), objectId, uid], true);
+               var context = this.getContext(objectId);
+               inline.makeAjaxCall('createNewRecord', [inline.getNumberOfRTE(), objectId, uid], true, context);
        },
 
        importElementMultiple: function(objectId, table, uidArray, type) {
@@ -611,7 +627,7 @@ var inline = {
                Sortable.create(
                        objectId,
                        {
-                               format: /^[^_\-](?:[A-Za-z0-9\-\_]*)-(.*)_div$/,
+                               format: /^[^_\-](?:[A-Za-z0-9\-\_\.]*)-(.*)_div$/,
                                onUpdate: inline.dragAndDropSorting,
                                tag: 'div',
                                handle: 'sortableHandle',
@@ -833,6 +849,7 @@ var inline = {
                        // Remove from TBE_EDITOR (required fields, required range, etc.):
                if (TBE_EDITOR && TBE_EDITOR.removeElement) {
                        var removeStack = [];
+                       // Iterate over all child records:
                        inlineRecords = Element.select(objectId+'_div', '.inlineRecord');
                                // Remove nested child records from TBE_EDITOR required/range checks:
                        for (i=inlineRecords.length-1; i>=0; i--) {
@@ -939,6 +956,7 @@ var inline = {
 
        splitObjectId: function(objectId) {
                objectId = objectId.substr(objectId.indexOf(this.structureSeparator)+1);
+               objectId = objectId.split(this.flexFormSeparator).join(this.flexFormSubstitute);
                var parts = objectId.split(this.structureSeparator);
 
                return parts;
@@ -949,8 +967,10 @@ var inline = {
 
                if (wrap == 'full') {
                        elReturn = this.prependFormFieldNames+'['+parts.join('][')+']';
+                       elReturn = elReturn.split(this.flexFormSubstitute).join('][');
                } else if (wrap == 'parts') {
                        elReturn = '['+parts.join('][')+']';
+                       elReturn = elReturn.split(this.flexFormSubstitute).join('][');
                } else if (wrap == 'none') {
                        elReturn = parts.length > 1 ? parts : parts.join('');
                }
@@ -963,8 +983,10 @@ var inline = {
 
                if (wrap == 'full') {
                        elReturn = this.prependFormFieldNames+this.structureSeparator+parts.join(this.structureSeparator);
+                       elReturn = elReturn.split(this.flexFormSubstitute).join(this.flexFormSeparator);
                } else if (wrap == 'parts') {
                        elReturn = this.structureSeparator+parts.join(this.structureSeparator);
+                       elReturn = elReturn.split(this.flexFormSubstitute).join(this.flexFormSeparator);
                } else if (wrap == 'none') {
                        elReturn = parts.length > 1 ? parts : parts.join('');
                }
@@ -1168,7 +1190,30 @@ var inline = {
                if ($(element)) {
                        new Effect.Fade(element, { afterFinish: function() { Element.remove(element); } });
                }
-       }
+       },
+
+       getContext: function(objectId) {
+               var result = null;
+
+               if (objectId !== '' && typeof this.data.config[objectId] !== 'undefined' && typeof this.data.config[objectId].context !== 'undefined') {
+                       result = this.data.config[objectId].context;
+               }
+
+               return result;
+       },
+
+       /**
+        * Escapes object identifiers to be used in jQuery.
+        *
+        * @param string objectId
+        * @return string
+        */
+       escapeObjectId: function(objectId) {
+               var escapedObjectId;
+               escapedObjectId = objectId.replace(/:/g, '\\:');
+               escapedObjectId = objectId.replace(/\./g, '\\.');
+               return escapedObjectId;
+       }
 }
 
 Object.extend(Array.prototype, {
index 2c48afa..0fc7dee 100644 (file)
@@ -34,6 +34,8 @@ namespace TYPO3\CMS\Backend\Form\Element;
 class InlineElement {
 
        const Structure_Separator = '-';
+       const FlexForm_Separator = '---';
+       const FlexForm_Substitute = ':';
        const Disposal_AttributeName = 'Disposal_AttributeName';
        const Disposal_AttributeId = 'Disposal_AttributeId';
        /**
@@ -211,7 +213,7 @@ class InlineElement {
                        }
                }
                // Add the current inline job to the structure stack
-               $this->pushStructure($table, $row['uid'], $field, $config);
+               $this->pushStructure($table, $row['uid'], $field, $config, $PA);
                // e.g. data[<table>][<uid>][<field>]
                $nameForm = $this->inlineNames['form'];
                // e.g. data-<pid>-<table1>-<uid1>-<field1>-<table2>-<uid2>-<field2>
@@ -235,7 +237,11 @@ class InlineElement {
                        'top' => array(
                                'table' => $top['table'],
                                'uid' => $top['uid']
-                       )
+                       ),
+                       'context' => array(
+                               'config' => $config,
+                               'hmac' => \TYPO3\CMS\Core\Utility\GeneralUtility::hmac(serialize($config)),
+                       ),
                );
                // Set a hint for nested IRRE and tab elements:
                $this->inlineData['nested'][$nameObject] = $this->fObj->getDynNestedStack(FALSE, $this->isAjaxCall);
@@ -918,6 +924,7 @@ class InlineElement {
                                $this->processAjaxRequestConstruct($ajaxArguments);
                                // Parse the DOM identifier (string), add the levels to the structure stack (array) and load the TCA config:
                                $this->parseStructureString($ajaxArguments[0], TRUE);
+                               $this->injectAjaxConfiguration($ajaxArguments);
                                // Render content:
                                $ajaxObj->setContentFormat('jsonbody');
                                $ajaxObj->setContent(call_user_func_array(array(&$this, $ajaxMethod), $ajaxArguments));
@@ -931,6 +938,34 @@ class InlineElement {
        }
 
        /**
+        * Injects configuration via AJAX calls.
+        * The configuration is validated using HMAC to avoid hijacking.
+        *
+        * @param array $ajaxArguments
+        * @return void
+        */
+       protected function injectAjaxConfiguration(array $ajaxArguments) {
+               $level = $this->calculateStructureLevel(-1);
+
+               if (empty($ajaxArguments['context']) || $level === FALSE) {
+                       return;
+               }
+
+               $current = &$this->inlineStructure['stable'][$level];
+               $context = json_decode($ajaxArguments['context'], TRUE);
+
+               if (\TYPO3\CMS\Core\Utility\GeneralUtility::hmac(serialize($context['config'])) !== $context['hmac']) {
+                       return;
+               }
+
+               $current['config'] = $context['config'];
+               $current['localizationMode'] = \TYPO3\CMS\Backend\Utility\BackendUtility::getInlineLocalizationMode(
+                       $current['table'],
+                       $current['config']
+               );
+       }
+
+       /**
         * Construct runtime environment for Inline Relational Record Editing.
         * - creates an anoymous SC_alt_doc in $GLOBALS['SOBE']
         * - creates a t3lib_TCEforms in $GLOBALS['SOBE']->tceforms
@@ -1565,17 +1600,30 @@ class InlineElement {
         * @param string $uid The uid of the record that embeds the inline data
         * @param string $field The field name which this element is supposed to edit
         * @param array $config The TCA-configuration of the inline field
+        * @param array $parameters The full parameter array (PA)
         * @return void
         * @todo Define visibility
         */
-       public function pushStructure($table, $uid, $field = '', $config = array()) {
-               $this->inlineStructure['stable'][] = array(
+       public function pushStructure($table, $uid, $field = '', $config = array(), array $parameters = array()) {
+               $structure = array(
                        'table' => $table,
                        'uid' => $uid,
                        'field' => $field,
                        'config' => $config,
-                       'localizationMode' => \TYPO3\CMS\Backend\Utility\BackendUtility::getInlineLocalizationMode($table, $config)
+                       'localizationMode' => \TYPO3\CMS\Backend\Utility\BackendUtility::getInlineLocalizationMode($table, $config),
                );
+
+               // Extract FlexForm parts (if any) from element name,
+               // e.g. array('vDEF', 'lDEF', 'FlexField', 'vDEF')
+               if (!empty($parameters['itemFormElName'])) {
+                       $flexFormParts = $this->extractFlexFormParts($parameters['itemFormElName']);
+
+                       if ($flexFormParts !== NULL) {
+                               $structure['flexform'] = $flexFormParts;
+                       }
+               }
+
+               $this->inlineStructure['stable'][] = $structure;
                $this->updateStructureNames();
        }
 
@@ -1586,6 +1634,8 @@ class InlineElement {
         * @todo Define visibility
         */
        public function popStructure() {
+               $popItem = NULL;
+
                if (count($this->inlineStructure['stable'])) {
                        $popItem = array_pop($this->inlineStructure['stable']);
                        $this->updateStructureNames();
@@ -1624,18 +1674,32 @@ class InlineElement {
         * @todo Define visibility
         */
        public function getStructureItemName($levelData, $disposal = self::Disposal_AttributeId) {
+               $name = NULL;
+
                if (is_array($levelData)) {
                        $parts = array($levelData['table'], $levelData['uid']);
-                       if (isset($levelData['field'])) {
+
+                       if (!empty($levelData['field'])) {
                                $parts[] = $levelData['field'];
                        }
+
                        // Use in name attributes:
                        if ($disposal === self::Disposal_AttributeName) {
+                               if (!empty($levelData['field']) && !empty($levelData['flexform']) && $this->getStructureLevel(-1) === $levelData) {
+                                       $parts[] = implode('][', $levelData['flexform']);
+                               }
                                $name = '[' . implode('][', $parts) . ']';
+                       // Use in object id attributes:
                        } else {
                                $name = implode(self::Structure_Separator, $parts);
+
+                               if (!empty($levelData['field']) && !empty($levelData['flexform'])) {
+                                       array_unshift($levelData['flexform'], $name);
+                                       $name = implode(self::FlexForm_Separator, $levelData['flexform']);
+                               }
                        }
                }
+
                return $name;
        }
 
@@ -1649,15 +1713,33 @@ class InlineElement {
         * @todo Define visibility
         */
        public function getStructureLevel($level) {
+               $level = $this->calculateStructureLevel($level);
+
+               if ($level !== FALSE) {
+                       return $this->inlineStructure['stable'][$level];
+               } else {
+                       return FALSE;
+               }
+       }
+
+       /**
+        * Calculates structure level.
+        *
+        * @param integer $level Which level to return
+        * @return boolean|integer
+        */
+       protected function calculateStructureLevel($level) {
+               $result = FALSE;
+
                $inlineStructureCount = count($this->inlineStructure['stable']);
                if ($level < 0) {
                        $level = $inlineStructureCount + $level;
                }
                if ($level >= 0 && $level < $inlineStructureCount) {
-                       return $this->inlineStructure['stable'][$level];
-               } else {
-                       return FALSE;
+                       $result = $level;
                }
+
+               return $result;
        }
 
        /**
@@ -1696,7 +1778,12 @@ class InlineElement {
        public function parseStructureString($string, $loadConfig = TRUE) {
                $unstable = array();
                $vector = array('table', 'uid', 'field');
+
+               // Substitute FlexForm additon and make parsing a bit easier
+               $string = str_replace(self::FlexForm_Separator, self::FlexForm_Substitute, $string);
+               // The starting pattern of an object identifer (e.g. "data-<firstPidValue>-<anything>)
                $pattern = '/^' . $this->prependNaming . self::Structure_Separator . '(.+?)' . self::Structure_Separator . '(.+)$/';
+
                if (preg_match($pattern, $string, $match)) {
                        $this->inlineFirstPid = $match[1];
                        $parts = explode(self::Structure_Separator, $match[2]);
@@ -1715,6 +1802,17 @@ class InlineElement {
                                                }
                                                $unstable['localizationMode'] = \TYPO3\CMS\Backend\Utility\BackendUtility::getInlineLocalizationMode($unstable['table'], $unstable['config']);
                                        }
+
+                                       // Extract FlexForm from field part (if any)
+                                       if (strpos($unstable['field'], self::FlexForm_Substitute) !== FALSE) {
+                                               $fieldParts = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(self::FlexForm_Substitute, $unstable['field']);
+                                               $unstable['field'] = array_shift($fieldParts);
+                                               // FlexForm parts start with data:
+                                               if (count($fieldParts) > 0 && $fieldParts[0] === 'data') {
+                                                       $unstable['flexform'] = $fieldParts;
+                                               }
+                                       }
+
                                        $this->inlineStructure['stable'][] = $unstable;
                                        $unstable = array();
                                }
@@ -2312,6 +2410,29 @@ class InlineElement {
                return $result;
        }
 
+       /**
+        * Extracts FlexForm parts of a form element name like
+        * data[table][uid][field][sDEF][lDEF][FlexForm][vDEF]
+        *
+        * @param string $formElementName The form element name
+        * @return array|NULL
+        */
+       protected function extractFlexFormParts($formElementName) {
+               $flexFormParts = NULL;
+
+               $matches = array();
+               $prefix = preg_quote($this->fObj->prependFormFieldNames, '#');
+
+               if (preg_match('#^' . $prefix . '(?:\[[^]]+\]){3}(\[data\](?:\[[^]]+\]){4,})$#', $formElementName, $matches)) {
+                       $flexFormParts = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(
+                               '][',
+                               trim($matches[1], '[]')
+                       );
+               }
+
+               return $flexFormParts;
+       }
+
 }
 
 
index 3e22e27..d7390f1 100644 (file)
@@ -41,6 +41,7 @@ class InlineElementTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
        protected function setUp() {
                // @todo Use $this->buildAccessibleProxy() if properties are protected
                $this->fixture = new \TYPO3\CMS\Backend\Form\Element\InlineElement();
+               $this->fixture->fObj = new \TYPO3\CMS\Backend\Form\FormEngine();
        }
 
        /**
@@ -51,18 +52,95 @@ class InlineElementTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
        }
 
        /**
+        * @param array $arguments
+        * @param array $expectedInlineStructure
+        * @param array $expectedInlineNames
+        * @dataProvider pushStructureFillsInlineStructureDataProvider
+        * @test
+        */
+       public function pushStructureFillsInlineStructure(array $arguments, array $expectedInlineStructure, array $expectedInlineNames) {
+               $this->fixture->inlineFirstPid = 'pageId';
+
+               call_user_func_array(array($this->fixture, 'pushStructure'), $arguments);
+
+               $this->assertEquals($expectedInlineStructure, $this->fixture->inlineStructure);
+               $this->assertEquals($expectedInlineNames, $this->fixture->inlineNames);
+       }
+
+       public function pushStructureFillsInlineStructureDataProvider() {
+               return array(
+                       'regular field' => array(
+                               array(
+                                       'parentTable',
+                                       'parentUid',
+                                       'parentField'
+                               ),
+                               array(
+                                       'stable' => array(
+                                               array(
+                                                       'table' => 'parentTable',
+                                                       'uid' => 'parentUid',
+                                                       'field' => 'parentField',
+                                                       'config' => array(),
+                                                       'localizationMode' => FALSE,
+                                               ),
+                                       ),
+                               ),
+                               array(
+                                       'form' => '[parentTable][parentUid][parentField]',
+                                       'object' => 'data-pageId-parentTable-parentUid-parentField',
+                               )
+                       ),
+                       'flexform field' => array(
+                               array(
+                                       'parentTable',
+                                       'parentUid',
+                                       'parentField',
+                                       array(),
+                                       array(
+                                               'itemFormElName' => 'data[parentTable][parentUid][parentField][data][sDEF][lDEF][grandParentFlexForm][vDEF]'
+                                       )
+                               ),
+                               array(
+                                       'stable' => array(
+                                               array(
+                                                       'table' => 'parentTable',
+                                                       'uid' => 'parentUid',
+                                                       'field' => 'parentField',
+                                                       'config' => array(),
+                                                       'localizationMode' => FALSE,
+                                                       'flexform' => array(
+                                                               'data', 'sDEF', 'lDEF', 'grandParentFlexForm', 'vDEF',
+                                                       ),
+                                               ),
+                                       ),
+                               ),
+                               array(
+                                       'form' => '[parentTable][parentUid][parentField][data][sDEF][lDEF][grandParentFlexForm][vDEF]',
+                                       'object' => 'data-pageId-parentTable-parentUid-parentField---data---sDEF---lDEF---grandParentFlexForm---vDEF',
+                               )
+                       ),
+               );
+       }
+
+       /**
         * @param string $string
         * @param array $expectedInlineStructure
+        * @param array $expectedInlineNames
         * @dataProvider structureStringIsParsedDataProvider
         * @test
         */
-       public function structureStringIsParsed($string, array $expectedInlineStructure) {
+       public function structureStringIsParsed($string, array $expectedInlineStructure, array $expectedInlineNames) {
                $this->fixture->parseStructureString($string, FALSE);
 
                $this->assertEquals('pageId', $this->fixture->inlineFirstPid);
                $this->assertEquals($expectedInlineStructure, $this->fixture->inlineStructure);
+               $this->assertEquals($expectedInlineNames, $this->fixture->inlineNames);
        }
 
+       /**
+        * @return array
+        */
        public function structureStringIsParsedDataProvider() {
                return array(
                        'simple 1-level table structure' => array(
@@ -72,6 +150,7 @@ class InlineElementTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                                                'table' => 'childTable',
                                        ),
                                ),
+                               array()
                        ),
                        'simple 1-level table-uid structure' => array(
                                'data-pageId-childTable-childUid',
@@ -81,6 +160,7 @@ class InlineElementTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                                                'uid' => 'childUid',
                                        ),
                                ),
+                               array()
                        ),
                        'simple 1-level table-uid-field structure' => array(
                                'data-pageId-childTable-childUid-childField',
@@ -91,6 +171,7 @@ class InlineElementTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                                                'field' => 'childField',
                                        ),
                                ),
+                               array(),
                        ),
                        'simple 2-level table structure' => array(
                                'data-pageId-parentTable-parentUid-parentField-childTable',
@@ -106,6 +187,10 @@ class InlineElementTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                                                'table' => 'childTable',
                                        ),
                                ),
+                               array(
+                                       'form' => '[parentTable][parentUid][parentField]',
+                                       'object' => 'data-pageId-parentTable-parentUid-parentField',
+                               ),
                        ),
                        'simple 2-level table-uid structure' => array(
                                'data-pageId-parentTable-parentUid-parentField-childTable-childUid',
@@ -122,6 +207,10 @@ class InlineElementTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                                                'uid' => 'childUid',
                                        ),
                                ),
+                               array(
+                                       'form' => '[parentTable][parentUid][parentField]',
+                                       'object' => 'data-pageId-parentTable-parentUid-parentField',
+                               ),
                        ),
                        'simple 2-level table-uid-field structure' => array(
                                'data-pageId-parentTable-parentUid-parentField-childTable-childUid-childField',
@@ -139,6 +228,10 @@ class InlineElementTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                                                'field' => 'childField',
                                        ),
                                ),
+                               array(
+                                       'form' => '[parentTable][parentUid][parentField]',
+                                       'object' => 'data-pageId-parentTable-parentUid-parentField',
+                               ),
                        ),
                        'simple 3-level table structure' => array(
                                'data-pageId-grandParentTable-grandParentUid-grandParentField-parentTable-parentUid-parentField-childTable',
@@ -159,6 +252,10 @@ class InlineElementTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                                                'table' => 'childTable',
                                        ),
                                ),
+                               array(
+                                       'form' => '[parentTable][parentUid][parentField]',
+                                       'object' => 'data-pageId-grandParentTable-grandParentUid-grandParentField-parentTable-parentUid-parentField',
+                               ),
                        ),
                        'simple 3-level table-uid structure' => array(
                                'data-pageId-grandParentTable-grandParentUid-grandParentField-parentTable-parentUid-parentField-childTable-childUid',
@@ -180,6 +277,10 @@ class InlineElementTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                                                'uid' => 'childUid',
                                        ),
                                ),
+                               array(
+                                       'form' => '[parentTable][parentUid][parentField]',
+                                       'object' => 'data-pageId-grandParentTable-grandParentUid-grandParentField-parentTable-parentUid-parentField',
+                               ),
                        ),
                        'simple 3-level table-uid-field structure' => array(
                                'data-pageId-grandParentTable-grandParentUid-grandParentField-parentTable-parentUid-parentField-childTable-childUid-childField',
@@ -202,6 +303,38 @@ class InlineElementTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                                                'field' => 'childField',
                                        ),
                                ),
+                               array(
+                                       'form' => '[parentTable][parentUid][parentField]',
+                                       'object' => 'data-pageId-grandParentTable-grandParentUid-grandParentField-parentTable-parentUid-parentField',
+                               ),
+                       ),
+                       'flexform 3-level table-uid structure' => array(
+                               'data-pageId-grandParentTable-grandParentUid-grandParentField---data---sDEF---lDEF---grandParentFlexForm---vDEF-parentTable-parentUid-parentField-childTable-childUid',
+                               array(
+                                       'stable' => array(
+                                               array(
+                                                       'table' => 'grandParentTable',
+                                                       'uid' => 'grandParentUid',
+                                                       'field' => 'grandParentField',
+                                                       'flexform' => array(
+                                                               'data', 'sDEF', 'lDEF', 'grandParentFlexForm', 'vDEF',
+                                                       ),
+                                               ),
+                                               array(
+                                                       'table' => 'parentTable',
+                                                       'uid' => 'parentUid',
+                                                       'field' => 'parentField',
+                                               ),
+                                       ),
+                                       'unstable' => array(
+                                               'table' => 'childTable',
+                                               'uid' => 'childUid',
+                                       ),
+                               ),
+                               array(
+                                       'form' => '[parentTable][parentUid][parentField]',
+                                       'object' => 'data-pageId-grandParentTable-grandParentUid-grandParentField---data---sDEF---lDEF---grandParentFlexForm---vDEF-parentTable-parentUid-parentField',
+                               ),
                        ),
                );
        }
index 26f6d18..48279d7 100644 (file)
@@ -1378,10 +1378,11 @@ class DataHandler {
         * @param string $field Field name. Must NOT be set if the call is for a flexform field (since flexforms are not allowed within flexforms).
         * @param [type] $uploadedFiles
         * @param [type] $tscPID
+        * @param array $additionalData Additional data to be forwarded to sub-processors
         * @return array Returns the evaluated $value as key "value" in this array.
         * @todo Define visibility
         */
-       public function checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, $uploadedFiles, $tscPID) {
+       public function checkValue_SW($res, $value, $tcaFieldConf, $table, $id, $curValue, $status, $realPid, $recFID, $field, $uploadedFiles, $tscPID, array $additionalData = NULL) {
                $PP = array($table, $id, $curValue, $status, $realPid, $recFID, $tscPID);
                switch ($tcaFieldConf['type']) {
                case 'text':
@@ -1407,7 +1408,7 @@ class DataHandler {
                        $res = $this->checkValue_group_select($res, $value, $tcaFieldConf, $PP, $uploadedFiles, $field);
                        break;
                case 'inline':
-                       $res = $this->checkValue_inline($res, $value, $tcaFieldConf, $PP, $field);
+                       $res = $this->checkValue_inline($res, $value, $tcaFieldConf, $PP, $field, $additionalData);
                        break;
                case 'flex':
                        // FlexForms are only allowed for real fields.
@@ -1961,6 +1962,7 @@ class DataHandler {
         */
        public function checkValue_flex($res, $value, $tcaFieldConf, $PP, $uploadedFiles, $field) {
                list($table, $id, $curValue, $status, $realPid, $recFID) = $PP;
+
                if (is_array($value)) {
                        // This value is necessary for flex form processing to happen on flexform fields in page records when they are copied.
                        // The problem is, that when copying a page, flexfrom XML comes along in the array for the new record - but since $this->checkValue_currentRecord does not have a uid or pid for that sake, the t3lib_BEfunc::getFlexFormDS() function returns no good DS. For new records we do know the expected PID so therefore we send that with this special parameter. Only active when larger than zero.
@@ -2005,6 +2007,7 @@ class DataHandler {
                        // Passthrough...:
                        $res['value'] = $value;
                }
+
                return $res;
        }
 
@@ -2066,10 +2069,11 @@ class DataHandler {
         * @param array $tcaFieldConf Field configuration from TCA
         * @param array $PP Additional parameters in a numeric array: $table,$id,$curValue,$status,$realPid,$recFID
         * @param string $field Field name
+        * @param array $additionalData Additional data to be forwarded to sub-processors
         * @return array Modified $res array
         * @todo Define visibility
         */
-       public function checkValue_inline($res, $value, $tcaFieldConf, $PP, $field) {
+       public function checkValue_inline($res, $value, $tcaFieldConf, $PP, $field, array $additionalData = NULL) {
                list($table, $id, $curValue, $status, $realPid, $recFID) = $PP;
                if (!$tcaFieldConf['foreign_table']) {
                        // Fatal error, inline fields should always have a foreign_table defined
@@ -2087,13 +2091,14 @@ class DataHandler {
                        $this->addNewValuesToRemapStackChildIds($valueArray);
                        $this->remapStack[] = array(
                                'func' => 'checkValue_inline_processDBdata',
-                               'args' => array($valueArray, $tcaFieldConf, $id, $status, $table, $field),
+                               'args' => array($valueArray, $tcaFieldConf, $id, $status, $table, $field, $additionalData),
                                'pos' => array('valueArray' => 0, 'tcaFieldConf' => 1, 'id' => 2, 'table' => 4),
-                               'field' => $field
+                               'additionalData' => $additionalData,
+                               'field' => $field,
                        );
                        unset($res['value']);
                } elseif ($value || \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($id)) {
-                       $res['value'] = $this->checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field);
+                       $res['value'] = $this->checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field, $additionalData);
                }
                return $res;
        }
@@ -2491,7 +2496,13 @@ class DataHandler {
                                                        } else {
                                                                // Default
                                                                list($CVtable, $CVid, $CVcurValue, $CVstatus, $CVrealPid, $CVrecFID, $CVtscPID) = $pParams;
-                                                               $res = $this->checkValue_SW(array(), $dataValues[$key][$vKey], $dsConf['TCEforms']['config'], $CVtable, $CVid, $dataValues_current[$key][$vKey], $CVstatus, $CVrealPid, $CVrecFID, '', $uploadedFiles[$key][$vKey], array(), $CVtscPID);
+
+                                                               $additionalData = array(
+                                                                       'flexFormId' => $CVrecFID,
+                                                                       'flexFormPath' => trim(rtrim($structurePath, '/') . '/' . $key . '/' . $vKey, '/'),
+                                                               );
+
+                                                               $res = $this->checkValue_SW(array(), $dataValues[$key][$vKey], $dsConf['TCEforms']['config'], $CVtable, $CVid, $dataValues_current[$key][$vKey], $CVstatus, $CVrealPid, $CVrecFID, '', $uploadedFiles[$key][$vKey], $CVtscPID, $additionalData);
                                                                // Look for RTE transformation of field:
                                                                if ($dataValues[$key]['_TRANSFORM_' . $vKey] == 'RTE' && !$this->dontProcessTransformations) {
                                                                        // Unsetting trigger field - we absolutely don't want that into the data storage!
@@ -2551,9 +2562,10 @@ class DataHandler {
         * @param string $status Status string ('update' or 'new')
         * @param string $table Table name, needs to be passed to t3lib_loadDBGroup
         * @param string $field The current field the values are modified for
+        * @param array $additionalData Additional data to be forwarded to sub-processors
         * @return string Modified values
         */
-       protected function checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field) {
+       protected function checkValue_inline_processDBdata($valueArray, $tcaFieldConf, $id, $status, $table, $field, array $additionalData = NULL) {
                $newValue = '';
                $foreignTable = $tcaFieldConf['foreign_table'];
                $valueArray = $this->applyFiltersToValues($tcaFieldConf, $valueArray);
@@ -4784,6 +4796,8 @@ class DataHandler {
        public function processRemapStack() {
                // Processes the remap stack:
                if (is_array($this->remapStack)) {
+                       $remapFlexForms = array();
+
                        foreach ($this->remapStack as $remapAction) {
                                // If no position index for the arguments was set, skip this remap action:
                                if (!is_array($remapAction['pos'])) {
@@ -4796,6 +4810,7 @@ class DataHandler {
                                $table = $remapAction['args'][$remapAction['pos']['table']];
                                $valueArray = $remapAction['args'][$remapAction['pos']['valueArray']];
                                $tcaFieldConf = $remapAction['args'][$remapAction['pos']['tcaFieldConf']];
+                               $additionalData = $remapAction['additionalData'];
                                // The record is new and has one or more new ids (in case of versioning/workspaces):
                                if (strpos($id, 'NEW') !== FALSE) {
                                        // Replace NEW...-ID with real uid:
@@ -4830,7 +4845,19 @@ class DataHandler {
                                        $newValue = implode(',', $this->checkValue_checkMax($tcaFieldConf, $newValue));
                                }
                                // Update in database (list of children (csv) or number of relations (foreign_field)):
-                               $this->updateDB($table, $id, array($field => $newValue));
+                               if (!empty($field)) {
+                                       $this->updateDB($table, $id, array($field => $newValue));
+                               // Collect data to update FlexForms
+                               } elseif (!empty($additionalData['flexFormId']) && !empty($additionalData['flexFormPath'])) {
+                                       $flexFormId = $additionalData['flexFormId'];
+                                       $flexFormPath = $additionalData['flexFormPath'];
+
+                                       if (!isset($remapFlexForms[$flexFormId])) {
+                                               $remapFlexForms[$flexFormId] = array();
+                                       }
+
+                                       $remapFlexForms[$flexFormId][$flexFormPath] = $newValue;
+                               }
                                // Process waiting Hook: processDatamap_afterDatabaseOperations:
                                if (isset($this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'])) {
                                        $hookArgs = $this->remapStackRecords[$table][$rawId]['processDatamap_afterDatabaseOperations'];
@@ -4845,6 +4872,12 @@ class DataHandler {
                                        }
                                }
                        }
+
+                       if ($remapFlexForms) {
+                               foreach ($remapFlexForms as $flexFormId => $modifications) {
+                                       $this->updateFlexFormData($flexFormId, $modifications);
+                               }
+                       }
                }
                // Processes the remap stack actions:
                if ($this->remapStackActions) {
@@ -4869,6 +4902,43 @@ class DataHandler {
        }
 
        /**
+        * Updates FlexForm data.
+        *
+        * @param string $flexFormId, e.g. <table>:<uid>:<field>
+        * @param array $modifications Modifications with paths and values (e.g. 'sDEF/lDEV/field/vDEF' => 'TYPO3')
+        * @return void
+        */
+       protected function updateFlexFormData($flexFormId, array $modifications) {
+               list ($table, $uid, $field) = explode(':', $flexFormId, 3);
+               $record = $this->recordInfo($table, $uid, '*');
+
+               if (!$table || !$uid || !$field || !is_array($record)) {
+                       return;
+               }
+
+               \TYPO3\CMS\Backend\Utility\BackendUtility::workspaceOL($table, $record);
+
+               // Get current data structure and value array:
+               $valueStructure = \TYPO3\CMS\Core\Utility\GeneralUtility::xml2array($record[$field]);
+
+               // Do recursive processing of the XML data:
+               foreach ($modifications as $path => $value) {
+                       $valueStructure['data'] = \TYPO3\CMS\Core\Utility\ArrayUtility::setValueByPath(
+                               $valueStructure['data'], $path, $value
+                       );
+               }
+
+               if (is_array($valueStructure['data'])) {
+                       // The return value should be compiled back into XML
+                       $values = array(
+                               $field => $this->checkValue_flexArray2Xml($valueStructure, TRUE),
+                       );
+
+                       $this->updateDB($table, $uid, $values);
+               }
+       }
+
+       /**
         * Triggers a remap action for a specific record.
         *
         * Some records are post-processed by the processRemapStack() method (e.g. IRRE children).