From 6ab0f6aa36cade26e25bea7e9d89b552318a5062 Mon Sep 17 00:00:00 2001 From: Kai Vogel Date: Tue, 6 Nov 2012 17:45:07 +0100 Subject: [PATCH] [FEATURE] Enable IRRE fields in FlexForms 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 | 87 ++++++++--- .../Classes/Form/Element/InlineElement.php | 139 ++++++++++++++++-- .../Unit/Form/Element/InlineElementTest.php | 135 ++++++++++++++++- .../core/Classes/DataHandling/DataHandler.php | 88 +++++++++-- 4 files changed, 409 insertions(+), 40 deletions(-) diff --git a/t3lib/jsfunc.inline.js b/t3lib/jsfunc.inline.js index 2b2e4e143174..76562799b641 100644 --- a/t3lib/jsfunc.inline.js +++ b/t3lib/jsfunc.inline.js @@ -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) == '') { 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 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, { diff --git a/typo3/sysext/backend/Classes/Form/Element/InlineElement.php b/typo3/sysext/backend/Classes/Form/Element/InlineElement.php index 2c48afa90114..0fc7deea4cf3 100644 --- a/typo3/sysext/backend/Classes/Form/Element/InlineElement.php +++ b/typo3/sysext/backend/Classes/Form/Element/InlineElement.php @@ -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[][][] $nameForm = $this->inlineNames['form']; // e.g. data------- @@ -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)); @@ -930,6 +937,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'] @@ -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--) $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; + } + } diff --git a/typo3/sysext/backend/Tests/Unit/Form/Element/InlineElementTest.php b/typo3/sysext/backend/Tests/Unit/Form/Element/InlineElementTest.php index 3e22e27890e7..d7390f1f70f9 100644 --- a/typo3/sysext/backend/Tests/Unit/Form/Element/InlineElementTest.php +++ b/typo3/sysext/backend/Tests/Unit/Form/Element/InlineElementTest.php @@ -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(); } /** @@ -50,19 +51,96 @@ class InlineElementTest extends \TYPO3\CMS\Core\Tests\UnitTestCase { unset($this->fixture); } + /** + * @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', + ), ), ); } diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php index 26f6d18045ba..48279d7e9239 100644 --- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php +++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php @@ -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) { @@ -4868,6 +4901,43 @@ class DataHandler { $this->remapStackRefIndex = array(); } + /** + * Updates FlexForm data. + * + * @param string $flexFormId, e.g.
:: + * @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. * -- 2.20.1