[!!!][TASK] House of forms 33/38433/40
authorChristian Kuhn <lolli@schwarzbu.ch>
Fri, 13 Mar 2015 13:43:53 +0000 (14:43 +0100)
committerChristian Kuhn <lolli@schwarzbu.ch>
Tue, 28 Apr 2015 13:25:11 +0000 (15:25 +0200)
This patch introduces a structural code refactoring to the
FormEngine class and its related friend classes.

FormEngine used to call itself over and over again with itself
and sub classes writing to public properties of FormEngine keeping
a global state that is then magically merged to sometimes working
output.

The patch introduces a tree approach with lots of small containers
doing an encapsulated part of the rendering process and calling
sub containers for inner details.
As main construct a "globalOptions" array is modified in containers
and given down to sub containers (tree knots) or elements (leaves),
while sub structures always return a defined array that is
merged by the parent and accumulates the full result.
Goal is to have a better encapsulated code structure with better
visible impact on changes done to this system.

The patch creates this main structure. There is still a lot of
mess around and additional patches can further improve the overall
situation with smaller changes.

Change-Id: I56b898dc0eaae8de4d31016997cfefe8d14ec53e
Releases: master
Resolves: #63846
Resolves: #63854
Resolves: #63856
Resolves: #63858
Resolves: #63859
Resolves: #63860
Resolves: #63861
Resolves: #63862
Resolves: #63863
Resolves: #63865
Resolves: #63876
Resolves: #63881
Resolves: #63882
Resolves: #63883
Resolves: #63893
Reviewed-on: http://review.typo3.org/38433
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Markus Klein <klein.t3@reelworx.at>
Tested-by: Markus Klein <klein.t3@reelworx.at>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
74 files changed:
typo3/sysext/backend/Classes/Controller/EditDocumentController.php
typo3/sysext/backend/Classes/Controller/PageLayoutController.php
typo3/sysext/backend/Classes/Controller/Wizard/RteController.php
typo3/sysext/backend/Classes/Form/AbstractNode.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Container/AbstractContainer.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Container/FlexFormContainer.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Container/FlexFormContainerContainer.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Container/FlexFormElementContainer.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Container/FlexFormLanguageContainer.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Container/FlexFormNoTabsContainer.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Container/FlexFormSectionContainer.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Container/FlexFormTabsContainer.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Container/FullRecordContainer.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Container/InlineControlContainer.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Container/InlineRecordContainer.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Container/ListOfFieldsContainer.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Container/NoTabsContainer.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Container/PaletteAndSingleContainer.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Container/SingleFieldContainer.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Container/SoloFieldContainer.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Container/TabsContainer.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/DataPreprocessor.php
typo3/sysext/backend/Classes/Form/Element/AbstractFormElement.php
typo3/sysext/backend/Classes/Form/Element/CheckboxElement.php
typo3/sysext/backend/Classes/Form/Element/FlexElement.php [deleted file]
typo3/sysext/backend/Classes/Form/Element/GroupElement.php
typo3/sysext/backend/Classes/Form/Element/ImageManipulationElement.php
typo3/sysext/backend/Classes/Form/Element/InlineElement.php [deleted file]
typo3/sysext/backend/Classes/Form/Element/InlineElementHookInterface.php
typo3/sysext/backend/Classes/Form/Element/InputElement.php
typo3/sysext/backend/Classes/Form/Element/NoneElement.php
typo3/sysext/backend/Classes/Form/Element/RadioElement.php
typo3/sysext/backend/Classes/Form/Element/SelectElement.php
typo3/sysext/backend/Classes/Form/Element/TextElement.php
typo3/sysext/backend/Classes/Form/Element/TreeElement.php
typo3/sysext/backend/Classes/Form/Element/UnknownElement.php
typo3/sysext/backend/Classes/Form/Element/UserElement.php
typo3/sysext/backend/Classes/Form/FlexFormsHelper.php
typo3/sysext/backend/Classes/Form/FormDataTraverser.php
typo3/sysext/backend/Classes/Form/FormEngine.php
typo3/sysext/backend/Classes/Form/FrontendFormEngine.php
typo3/sysext/backend/Classes/Form/InlineRelatedRecordResolver.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/InlineStackProcessor.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/NodeFactory.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/Utility/FormEngineUtility.php
typo3/sysext/backend/Classes/Form/Wizard/SuggestWizardDefaultReceiver.php
typo3/sysext/backend/Classes/Rte/AbstractRte.php
typo3/sysext/backend/Classes/Utility/BackendUtility.php
typo3/sysext/backend/Tests/Unit/Form/Element/InlineElementTest.php [deleted file]
typo3/sysext/backend/Tests/Unit/Form/FormEngineTest.php [new file with mode: 0644]
typo3/sysext/backend/Tests/Unit/Form/InlineStackProcessorTest.php [new file with mode: 0644]
typo3/sysext/compatibility6/Migrations/Code/ClassAliasMap.php
typo3/sysext/compatibility6/Migrations/Code/LegacyClassesForIde.php
typo3/sysext/core/Configuration/DefaultConfiguration.php
typo3/sysext/core/Configuration/TCA/be_users.php
typo3/sysext/core/Documentation/Changelog/7.0/Breaking-59659-DeprecatedCodeRemovalInBackendSysext.rst [deleted file]
typo3/sysext/core/Documentation/Changelog/7.0/Breaking-61828-RemoveIsDisplayCondition.rst [deleted file]
typo3/sysext/core/Documentation/Changelog/7.0/Deprecation-61958-MoveTcaRenderings.rst [deleted file]
typo3/sysext/core/Documentation/Changelog/7.0/Deprecation-62667-WrapBorderTable-In-FormEngine.rst [deleted file]
typo3/sysext/core/Documentation/Changelog/7.1/Breaking-64762-FormEngineWizards.rst [deleted file]
typo3/sysext/core/Documentation/Changelog/7.1/Deprecation-63847-FormEngine-renderReadonly.rst [deleted file]
typo3/sysext/core/Documentation/Changelog/7.1/Deprecation-63850-FormEngine-insertDefStyle.rst [deleted file]
typo3/sysext/core/Documentation/Changelog/7.1/Deprecation-63852-FormEngine-getAvailableLanguages.rst [deleted file]
typo3/sysext/core/Documentation/Changelog/7.1/Deprecation-63855-FormEngine-sL.rst [deleted file]
typo3/sysext/core/Documentation/Changelog/7.1/Deprecation-63864-FormEngine-renderVDEFDiff.rst [deleted file]
typo3/sysext/core/Documentation/Changelog/7.1/Deprecation-63878-FormEngine-getLL.rst [deleted file]
typo3/sysext/core/Documentation/Changelog/7.1/Deprecation-63889-FormEngine-getTSCpid.rst [deleted file]
typo3/sysext/core/Documentation/Changelog/7.1/Deprecation-63912-FormEngine-unusedMethods.rst [deleted file]
typo3/sysext/core/Documentation/Changelog/7.2/Breaking-65357-DependenciesToFormEngine.rst [deleted file]
typo3/sysext/core/Documentation/Changelog/7.2/Deprecation-65357-DependenciesToFormEngine.rst [deleted file]
typo3/sysext/core/Documentation/Changelog/master/Breaking-63846-FormEngineRefactoring.rst [new file with mode: 0644]
typo3/sysext/frontend/Configuration/TCA/pages_language_overlay.php
typo3/sysext/rtehtmlarea/Classes/Controller/FrontendRteController.php
typo3/sysext/rtehtmlarea/Classes/RteHtmlAreaBase.php

index 93c3476..08a0bd4 100644 (file)
@@ -24,6 +24,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Frontend\Page\PageRepository;
+use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
 
 /**
  * Script Class: Drawing the editing form for editing records in TYPO3.
@@ -174,14 +175,6 @@ class EditDocumentController {
        public $recTitle;
 
        /**
-        * Disable help... ?
-        *
-        * @var bool
-        * @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8
-        */
-       public $disHelp;
-
-       /**
         * If set, then no SAVE/VIEW button is printed
         *
         * @var bool
@@ -557,7 +550,7 @@ class EditDocumentController {
                        // If there was saved any new items, load them:
                        if (count($tce->substNEWwithIDs_table)) {
                                // save the expanded/collapsed states for new inline records, if any
-                               \TYPO3\CMS\Backend\Form\Element\InlineElement::updateInlineView($this->uc, $tce);
+                               FormEngineUtility::updateInlineView($this->uc, $tce);
                                $newEditConf = array();
                                foreach ($this->editconf as $tableName => $tableCmds) {
                                        $keys = array_keys($tce->substNEWwithIDs_table, $tableName);
@@ -845,7 +838,6 @@ class EditDocumentController {
                if (is_array($this->editconf)) {
                        // Initialize TCEforms (rendering the forms)
                        $this->tceforms = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Form\FormEngine::class);
-                       $this->tceforms->initDefaultBEMode();
                        $this->tceforms->doSaveFieldName = 'doSave';
                        $this->tceforms->localizationMode = GeneralUtility::inList('text,media', $this->localizationMode) ? $this->localizationMode : '';
                        // text,media is keywords defined in TYPO3 Core API..., see "l10n_cat"
@@ -1044,8 +1036,6 @@ class EditDocumentController {
                                                                        if (is_array($this->overrideVals) && is_array($this->overrideVals[$table])) {
                                                                                $this->tceforms->hiddenFieldListArr = array_keys($this->overrideVals[$table]);
                                                                        }
-                                                                       // Register default language labels, if any:
-                                                                       $this->tceforms->registerDefaultLanguageData($table, $rec);
                                                                        // Create form for the record (either specific list of fields or the whole record):
                                                                        $panel = '';
                                                                        if ($this->columnsOnly) {
@@ -1284,17 +1274,6 @@ class EditDocumentController {
                return '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xlf:labels.openInNewWindow', TRUE) . '">' . IconUtility::getSpriteIcon('actions-window-open') . '</a>';
        }
 
-       /**
-        * Reads comment messages from TCEforms and prints them in a HTML comment in the bottom of the page.
-        *
-        * @return string
-        * @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8
-        */
-       public function tceformMessages() {
-               GeneralUtility::logDeprecatedFunction();
-               return '';
-       }
-
        /***************************
         *
         * Localization stuff
index fc9da06..1e32a27 100644 (file)
@@ -811,8 +811,6 @@ class PageLayoutController {
                                // If the record is an array (which it will always be... :-)
                                // Create instance of TCEforms, setting defaults:
                                $tceforms = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Form\FormEngine::class);
-                               $tceforms->initDefaultBEMode();
-                               $tceforms->fieldOrder = $this->modTSconfig['properties']['tt_content.']['fieldOrder'];
                                $tceforms->palettesCollapsed = !$this->MOD_SETTINGS['showPalettes'];
                                // Render form, wrap it:
                                $panel = '';
index 645d00a..ccf3e1e 100644 (file)
@@ -130,8 +130,6 @@ class RteController extends AbstractWizardController {
                        }
                        // Initialize TCeforms - for rendering the field:
                        $tceforms = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Form\FormEngine::class);
-                       // Init...
-                       $tceforms->initDefaultBEMode();
                        // SPECIAL: Disables all wizards - we are NOT going to need them.
                        $tceforms->disableWizards = 1;
                        // Initialize style for RTE object:
diff --git a/typo3/sysext/backend/Classes/Form/AbstractNode.php b/typo3/sysext/backend/Classes/Form/AbstractNode.php
new file mode 100644 (file)
index 0000000..826f4e0
--- /dev/null
@@ -0,0 +1,140 @@
+<?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\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Base class for container and single elements - their abstracts extend from here.
+ */
+abstract class AbstractNode {
+
+       /**
+        * A list of global options given from parent to child elements
+        *
+        * @var array
+        */
+       protected $globalOptions = array();
+
+       /**
+        * Handler for single nodes
+        *
+        * @return array As defined in initializeResultArray() of AbstractNode
+        */
+       abstract public function render();
+
+       /**
+        * Set global options from parent instance
+        *
+        * @param array $globalOptions Global options like 'readonly' for all elements
+        * @return $this
+        */
+       public function setGlobalOptions(array $globalOptions) {
+               $this->globalOptions = $globalOptions;
+               return $this;
+       }
+
+       /**
+        * Initialize the array that is returned to parent after calling. This structure
+        * is identical for *all* nodes. Parent will merge the return of a child with its
+        * own stuff and in itself return an array of the same structure.
+        *
+        * @return array
+        */
+       protected function initializeResultArray() {
+               return array(
+                       'requiredElements' => array(), // name => value
+                       'requiredFields' => array(), // value => name
+                       'requiredAdditional' => array(), // name => array
+                       'requiredNested' => array(),
+                       'additionalJavaScriptPost' => array(),
+                       'additionalJavaScriptSubmit' => array(),
+                       'additionalHiddenFields' => array(),
+                       'additionalHeadTags' => array(),
+                       'extJSCODE' => '',
+                       'inlineData' => array(),
+                       'html' => '',
+               );
+       }
+
+       /**
+        * Merge existing data with a child return array
+        *
+        * @param array $existing Currently merged array
+        * @param array $childReturn Array returned by child
+        * @return array Result array
+        */
+       protected function mergeChildReturnIntoExistingResult(array $existing, array $childReturn) {
+               if (!empty($childReturn['html'])) {
+                       $existing['html'] .= LF . $childReturn['html'];
+               }
+               if (!empty($childReturn['extJSCODE'])) {
+                       $existing['extJSCODE'] .= LF . $childReturn['extJSCODE'];
+               }
+               foreach ($childReturn['requiredElements'] as $name => $value) {
+                       $existing['requiredElements'][$name] = $value;
+               }
+               foreach ($childReturn['requiredFields'] as $value => $name) { // Params swapped ?!
+                       $existing['requiredFields'][$value] = $name;
+               }
+               foreach ($childReturn['requiredAdditional'] as $name => $subArray) {
+                       $existing['requiredAdditional'][$name] = $subArray;
+               }
+               foreach ($childReturn['requiredNested'] as $value => $name) {
+                       $existing['requiredNested'][$value] = $name;
+               }
+               foreach ($childReturn['additionalJavaScriptPost'] as $value) {
+                       $existing['additionalJavaScriptPost'][] = $value;
+               }
+               foreach ($childReturn['additionalJavaScriptSubmit'] as $value) {
+                       $existing['additionalJavaScriptSubmit'][] = $value;
+               }
+               if (!empty($childReturn['inlineData'])) {
+                       $existingInlineData = $existing['inlineData'];
+                       $childInlineData = $childReturn['inlineData'];
+                       ArrayUtility::mergeRecursiveWithOverrule($existingInlineData, $childInlineData);
+                       $existing['inlineData'] = $existingInlineData;
+               }
+               return $existing;
+       }
+
+       /**
+        * Determine and get the value for the placeholder for an input field.
+        * Typically used in an inline relation where values from fields down the record chain
+        * are used as "default" values for fields.
+        *
+        * @param string $table
+        * @param array $config
+        * @param array $row
+        * @return mixed
+        */
+       protected function getPlaceholderValue($table, array $config, array $row) {
+               $value = trim($config['placeholder']);
+               if (!$value) {
+                       return '';
+               }
+               // Check if we have a reference to another field value from the current record
+               if (substr($value, 0, 6) === '__row|') {
+                       /** @var FormDataTraverser $traverser */
+                       $traverseFields = GeneralUtility::trimExplode('|', substr($value, 6));
+                       $traverser = GeneralUtility::makeInstance(FormDataTraverser::class);
+                       $value = $traverser->getTraversedFieldValue($traverseFields, $table, $row, $this->globalOptions['inlineFirstPid'], $this->globalOptions['prependFormFieldNames']);
+               }
+
+               return $value;
+       }
+
+}
diff --git a/typo3/sysext/backend/Classes/Form/Container/AbstractContainer.php b/typo3/sysext/backend/Classes/Form/Container/AbstractContainer.php
new file mode 100644 (file)
index 0000000..5afd4a0
--- /dev/null
@@ -0,0 +1,367 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Container;
+
+/*
+ * 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\Utility\GeneralUtility;
+use TYPO3\CMS\Backend\Form\AbstractNode;
+use TYPO3\CMS\Backend\Form\ElementConditionMatcher;
+use TYPO3\CMS\Backend\Utility\IconUtility;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Backend\Template\DocumentTemplate;
+use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
+use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
+use TYPO3\CMS\Core\Utility\MathUtility;
+
+/**
+ * Abstract container has various methods used by the container classes
+ */
+abstract class AbstractContainer extends AbstractNode {
+
+       /**
+        * Array where records in the default language are stored. (processed by transferdata)
+        *
+        * @var array
+        */
+       protected $defaultLanguageData = array();
+
+       /**
+        * Array where records in the default language are stored (raw without any processing. used for making diff).
+        * This is the unserialized content of configured TCA ['ctrl']['transOrigDiffSourceField'] field, typically l18n_diffsource
+        *
+        * @var array
+        */
+       protected $defaultLanguageDataDiff = array();
+
+       /**
+        * Contains row data of "additional" language overlays
+        * array(
+        *   $table:$uid => array(
+        *     $additionalPreviewLanguageUid => $rowData
+        *   )
+        * )
+        *
+        * @var array
+        */
+       protected $additionalPreviewLanguageData = array();
+
+       /**
+        * Calculate and return the current type value of a record
+        *
+        * @param string $table The table name. MUST be in $GLOBALS['TCA']
+        * @param array $row The row from the table, should contain at least the "type" field, if applicable.
+        * @return string Return the "type" value for this record, ready to pick a "types" configuration from the $GLOBALS['TCA'] array.
+        * @throws \RuntimeException
+        */
+       protected function getRecordTypeValue($table, array $row) {
+               $typeNum = 0;
+               $field = $GLOBALS['TCA'][$table]['ctrl']['type'];
+               if ($field) {
+                       if (strpos($field, ':') !== FALSE) {
+                               list($pointerField, $foreignTypeField) = explode(':', $field);
+                               $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$pointerField]['config'];
+                               $relationType = $fieldConfig['type'];
+                               if ($relationType === 'select') {
+                                       $foreignUid = $row[$pointerField];
+                                       $foreignTable = $fieldConfig['foreign_table'];
+                               } elseif ($relationType === 'group') {
+                                       $values = FormEngineUtility::extractValuesOnlyFromValueLabelList($row[$pointerField]);
+                                       list(, $foreignUid) = GeneralUtility::revExplode('_', $values[0], 2);
+                                       $allowedTables = explode(',', $fieldConfig['allowed']);
+                                       // Always take the first configured table.
+                                       $foreignTable = $allowedTables[0];
+                               } else {
+                                       throw new \RuntimeException('TCA Foreign field pointer fields are only allowed to be used with group or select field types.', 1325861239);
+                               }
+                               if ($foreignUid) {
+                                       $foreignRow = BackendUtility::getRecord($foreignTable, $foreignUid, $foreignTypeField);
+                                       $this->registerDefaultLanguageData($foreignTable, $foreignRow);
+                                       if ($foreignRow[$foreignTypeField]) {
+                                               $foreignTypeFieldConfig = $GLOBALS['TCA'][$table]['columns'][$field];
+                                               $typeNum = $this->overrideTypeWithValueFromDefaultLanguageRecord($foreignTable, $foreignRow, $foreignTypeField, $foreignTypeFieldConfig);
+                                       }
+                               }
+                       } else {
+                               $typeFieldConfig = $GLOBALS['TCA'][$table]['columns'][$field];
+                               $typeNum = $this->overrideTypeWithValueFromDefaultLanguageRecord($table, $row, $field, $typeFieldConfig);
+                       }
+               }
+               if (empty($typeNum)) {
+                       // If that value is an empty string, set it to "0" (zero)
+                       $typeNum = 0;
+               }
+               // If current typeNum doesn't exist, set it to 0 (or to 1 for historical reasons, if 0 doesn't exist)
+               if (!$GLOBALS['TCA'][$table]['types'][$typeNum]) {
+                       $typeNum = $GLOBALS['TCA'][$table]['types']['0'] ? 0 : 1;
+               }
+               // Force to string. Necessary for eg '-1' to be recognized as a type value.
+               return (string)$typeNum;
+       }
+
+       /**
+        * Producing an array of field names NOT to display in the form,
+        * based on settings from subtype_value_field, bitmask_excludelist_bits etc.
+        * Notice, this list is in NO way related to the "excludeField" flag
+        *
+        * @param string $table Table name, MUST be in $GLOBALS['TCA']
+        * @param array $row A record from table.
+        * @param string $typeNum A "type" pointer value, probably the one calculated based on the record array.
+        * @return array Array with field names as values. The field names are those which should NOT be displayed "anyways
+        */
+       protected function getExcludeElements($table, $row, $typeNum) {
+               $excludeElements = array();
+               // If a subtype field is defined for the type
+               if ($GLOBALS['TCA'][$table]['types'][$typeNum]['subtype_value_field']) {
+                       $subTypeField = $GLOBALS['TCA'][$table]['types'][$typeNum]['subtype_value_field'];
+                       if (trim($GLOBALS['TCA'][$table]['types'][$typeNum]['subtypes_excludelist'][$row[$subTypeField]])) {
+                               $excludeElements = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['types'][$typeNum]['subtypes_excludelist'][$row[$subTypeField]], TRUE);
+                       }
+               }
+               // If a bitmask-value field has been configured, then find possible fields to exclude based on that:
+               if ($GLOBALS['TCA'][$table]['types'][$typeNum]['bitmask_value_field']) {
+                       $subTypeField = $GLOBALS['TCA'][$table]['types'][$typeNum]['bitmask_value_field'];
+                       $sTValue = MathUtility::forceIntegerInRange($row[$subTypeField], 0);
+                       if (is_array($GLOBALS['TCA'][$table]['types'][$typeNum]['bitmask_excludelist_bits'])) {
+                               foreach ($GLOBALS['TCA'][$table]['types'][$typeNum]['bitmask_excludelist_bits'] as $bitKey => $eList) {
+                                       $bit = substr($bitKey, 1);
+                                       if (MathUtility::canBeInterpretedAsInteger($bit)) {
+                                               $bit = MathUtility::forceIntegerInRange($bit, 0, 30);
+                                               if ($bitKey[0] === '-' && !($sTValue & pow(2, $bit)) || $bitKey[0] === '+' && $sTValue & pow(2, $bit)) {
+                                                       $excludeElements = array_merge($excludeElements, GeneralUtility::trimExplode(',', $eList, TRUE));
+                                               }
+                                       }
+                               }
+                       }
+               }
+               return $excludeElements;
+       }
+
+       /**
+        * The requested field value will be overridden with the data from the default
+        * language if the field is configured accordingly.
+        *
+        * @param string $table Table name of the record being edited
+        * @param array $row Record array of the record being edited in current language
+        * @param string $field Field name represented by $item
+        * @param array $fieldConf Content of $PA['fieldConf']
+        * @return string Unprocessed field value merged with default language data if needed
+        */
+       protected function overrideTypeWithValueFromDefaultLanguageRecord($table, array $row, $field, $fieldConf) {
+               $value = $row[$field];
+               if (is_array($this->defaultLanguageData[$table . ':' . $row['uid']])) {
+                       // @todo: Is this a bug? Currently the field from default lang is picked in mergeIfNotBlank mode if the
+                       // @todo: default value is not empty, but imho it should only be picked if the language overlay record *is* empty?!
+                       if (
+                               $fieldConf['l10n_mode'] === 'exclude'
+                               || $fieldConf['l10n_mode'] === 'mergeIfNotBlank' && trim($this->defaultLanguageData[$table . ':' . $row['uid']][$field]) !== ''
+                       ) {
+                               $value = $this->defaultLanguageData[$table . ':' . $row['uid']][$field];
+                       }
+               }
+               return $value;
+       }
+
+       /**
+        * Return a list without excluded elements.
+        *
+        * @param array $fieldsArray Typically coming from types show item
+        * @param array $excludeElements Field names to be excluded
+        * @return array $fieldsArray without excluded elements
+        */
+       protected function removeExcludeElementsFromFieldArray(array $fieldsArray, array $excludeElements) {
+               $newFieldArray = array();
+               foreach ($fieldsArray as $fieldString) {
+                       $fieldArray = $this->explodeSingleFieldShowItemConfiguration($fieldString);
+                       $fieldName = $fieldArray['fieldName'];
+                       // It doesn't make sense to exclude palettes and tabs
+                       if (!in_array($fieldName, $excludeElements, TRUE) || $fieldName === '--palette--' || $fieldName === '--div--') {
+                               $newFieldArray[] = $fieldString;
+                       }
+               }
+               return $newFieldArray;
+       }
+
+
+       /**
+        * A single field of TCA 'types' 'showitem' can have four semicolon separated configuration options:
+        *   fieldName: Name of the field to be found in TCA 'columns' section
+        *   fieldLabel: An alternative field label
+        *   paletteName: Name of a palette to be found in TCA 'palettes' section that is rendered after this field
+        *   extra: Special configuration options of this field
+        *
+        * @param string $field Semicolon separated field configuration
+        * @throws \RuntimeException
+        * @return array
+        */
+       protected function explodeSingleFieldShowItemConfiguration($field) {
+               $fieldArray = GeneralUtility::trimExplode(';', $field);
+               if (empty($fieldArray[0])) {
+                       throw new \RuntimeException('Field must not be empty', 1426448465);
+               }
+               return array(
+                       'fieldName' => $fieldArray[0],
+                       'fieldLabel' => $fieldArray[1] ?: NULL,
+                       'paletteName' => $fieldArray[2] ?: NULL,
+                       'fieldExtra' => $fieldArray[3] ?: NULL,
+               );
+       }
+
+       /**
+        * Will register data from original language records if the current record is a translation of another.
+        * The original data is shown with the edited record in the form.
+        * The information also includes possibly diff-views of what changed in the original record.
+        * Function called from outside (see alt_doc.php + quick edit) before rendering a form for a record
+        *
+        * @param string $table Table name of the record being edited
+        * @param array $rec Record array of the record being edited
+        * @return void
+        */
+       protected function registerDefaultLanguageData($table, $rec) {
+               // @todo: early return here if the arrays are already filled?
+
+               // Add default language:
+               if (
+                       $GLOBALS['TCA'][$table]['ctrl']['languageField'] && $rec[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0
+                       && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
+                       && (int)$rec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0
+               ) {
+                       $lookUpTable = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable']
+                               ? $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable']
+                               : $table;
+                       // Get data formatted:
+                       $this->defaultLanguageData[$table . ':' . $rec['uid']] = BackendUtility::getRecordWSOL(
+                               $lookUpTable,
+                               (int)$rec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
+                       );
+                       // Get data for diff:
+                       if ($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']) {
+                               $this->defaultLanguageDataDiff[$table . ':' . $rec['uid']] = unserialize($rec[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']]);
+                       }
+                       // If there are additional preview languages, load information for them also:
+                       foreach ($this->globalOptions['additionalPreviewLanguages'] as $prL) {
+                               /** @var $translationConfigurationProvider TranslationConfigurationProvider */
+                               $translationConfigurationProvider = GeneralUtility::makeInstance(TranslationConfigurationProvider::class);
+                               $translationInfo = $translationConfigurationProvider->translationInfo($lookUpTable, (int)$rec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']], $prL['uid']);
+                               if (is_array($translationInfo['translations']) && is_array($translationInfo['translations'][$prL['uid']])) {
+                                       $this->additionalPreviewLanguageData[$table . ':' . $rec['uid']][$prL['uid']] = BackendUtility::getRecordWSOL($table, (int)$translationInfo['translations'][$prL['uid']]['uid']);
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Evaluate condition of flex forms
+        *
+        * @param string $displayCondition The condition to evaluate
+        * @param array $flexFormData Given data the condition is based on
+        * @return bool TRUE if condition matched
+        */
+       protected function evaluateFlexFormDisplayCondition($displayCondition, $flexFormData) {
+               $elementConditionMatcher = GeneralUtility::makeInstance(ElementConditionMatcher::class);
+
+               $splitCondition = GeneralUtility::trimExplode(':', $displayCondition);
+               $skipCondition = FALSE;
+               $fakeRow = array();
+               switch ($splitCondition[0]) {
+                       case 'FIELD':
+                               // @todo: Not 100% sure if that is correct this way
+                               list($_sheetName, $fieldName) = GeneralUtility::trimExplode('.', $splitCondition[1]);
+                               $fieldValue = $flexFormData[$fieldName];
+                               $splitCondition[1] = $fieldName;
+                               $dataStructure['ROOT']['TCEforms']['displayCond'] = join(':', $splitCondition);
+                               $fakeRow = array($fieldName => $fieldValue);
+                               break;
+                       case 'HIDE_FOR_NON_ADMINS':
+
+                       case 'VERSION':
+
+                       case 'HIDE_L10N_SIBLINGS':
+
+                       case 'EXT':
+                               break;
+                       case 'REC':
+                               $fakeRow = array('uid' => $this->globalOptions['databaseRow']['uid']);
+                               break;
+                       default:
+                               $skipCondition = TRUE;
+               }
+               if ($skipCondition) {
+                       return TRUE;
+               } else {
+                       return $elementConditionMatcher->match($displayCondition, $fakeRow, 'vDEF');
+               }
+       }
+
+       /**
+        * Rendering preview output of a field value which is not shown as a form field but just outputted.
+        *
+        * @param string $value The value to output
+        * @param array $config Configuration for field.
+        * @param string $field Name of field.
+        * @return string HTML formatted output
+        */
+       protected function previewFieldValue($value, $config, $field = '') {
+               if ($config['config']['type'] === 'group' && ($config['config']['internal_type'] === 'file' || $config['config']['internal_type'] === 'file_reference')) {
+                       // Ignore upload folder if internal_type is file_reference
+                       if ($config['config']['internal_type'] === 'file_reference') {
+                               $config['config']['uploadfolder'] = '';
+                       }
+                       $table = 'tt_content';
+                       // Making the array of file items:
+                       $itemArray = GeneralUtility::trimExplode(',', $value, TRUE);
+                       // Showing thumbnails:
+                       $thumbnail = '';
+                       $imgs = array();
+                       foreach ($itemArray as $imgRead) {
+                               $imgParts = explode('|', $imgRead);
+                               $imgPath = rawurldecode($imgParts[0]);
+                               $rowCopy = array();
+                               $rowCopy[$field] = $imgPath;
+                               // Icon + click menu:
+                               $absFilePath = GeneralUtility::getFileAbsFileName($config['config']['uploadfolder'] ? $config['config']['uploadfolder'] . '/' . $imgPath : $imgPath);
+                               $fileInformation = pathinfo($imgPath);
+                               $fileIcon = IconUtility::getSpriteIconForFile(
+                                       $imgPath,
+                                       array(
+                                               'title' => htmlspecialchars($fileInformation['basename'] . ($absFilePath && @is_file($absFilePath) ? ' (' . GeneralUtility::formatSize(filesize($absFilePath)) . 'bytes)' : ' - FILE NOT FOUND!'))
+                                       )
+                               );
+                               $imgs[] =
+                                       '<span class="text-nowrap">' .
+                                       BackendUtility::thumbCode(
+                                               $rowCopy,
+                                               $table,
+                                               $field,
+                                               '',
+                                               'thumbs.php',
+                                               $config['config']['uploadfolder'], 0, ' align="middle"'
+                                       ) .
+                                       ($absFilePath ? $this->getControllerDocumentTemplate()->wrapClickMenuOnIcon($fileIcon, $absFilePath, 0, 1, '', '+copy,info,edit,view') : $fileIcon) .
+                                       $imgPath .
+                                       '</span>';
+                       }
+                       return implode('<br />', $imgs);
+               } else {
+                       return nl2br(htmlspecialchars($value));
+               }
+       }
+
+       /**
+        * @return DocumentTemplate
+        */
+       protected function getControllerDocumentTemplate() {
+               return $GLOBALS['SOBE']->doc;
+       }
+
+}
diff --git a/typo3/sysext/backend/Classes/Form/Container/FlexFormContainer.php b/typo3/sysext/backend/Classes/Form/Container/FlexFormContainer.php
new file mode 100644 (file)
index 0000000..a865017
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Container;
+
+/*
+ * 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\Utility\GeneralUtility;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Lang\LanguageService;
+use TYPO3\CMS\Backend\Form\FlexFormsHelper;
+
+/**
+ * Entry container to a flex form element. This container is created by
+ * SingleFieldContainer if a type='flexform' field is rendered.
+ * The container prepares the flex form data and structure and hands
+ * over to FlexFormLanguageContainer for further processing.
+ */
+class FlexFormContainer extends AbstractContainer {
+
+       /**
+        * Entry method
+        *
+        * @return array As defined in initializeResultArray() of AbstractNode
+        */
+       public function render() {
+               $languageService = $this->getLanguageService();
+
+               $table = $this->globalOptions['table'];
+               $row = $this->globalOptions['databaseRow'];
+               $field = $this->globalOptions['field'];
+               $parameterArray = $this->globalOptions['parameterArray'];
+
+               // Data Structure
+               $flexFormDataStructureArray = BackendUtility::getFlexFormDS($parameterArray['fieldConf']['config'], $row, $table, $field);
+
+               // Early return if no data structure was found at all
+               if (!is_array($flexFormDataStructureArray)) {
+                       $resultArray = $this->initializeResultArray();
+                       $resultArray['html'] = 'Data Structure ERROR: ' . $flexFormDataStructureArray;
+                       return $resultArray;
+               }
+
+               // Manipulate Flex form DS via TSConfig and group access lists
+               if (is_array($flexFormDataStructureArray)) {
+                       $flexFormHelper = GeneralUtility::makeInstance(FlexFormsHelper::class);
+                       $flexFormDataStructureArray = $flexFormHelper->modifyFlexFormDS($flexFormDataStructureArray, $table, $field, $row, $parameterArray['fieldConf']);
+               }
+
+               // Get data
+               $xmlData = $parameterArray['itemFormElValue'];
+               $xmlHeaderAttributes = GeneralUtility::xmlGetHeaderAttribs($xmlData);
+               $storeInCharset = strtolower($xmlHeaderAttributes['encoding']);
+               if ($storeInCharset) {
+                       $currentCharset = $languageService->charSet;
+                       $xmlData = $languageService->csConvObj->conv($xmlData, $storeInCharset, $currentCharset, 1);
+               }
+               $flexFormRowData = GeneralUtility::xml2array($xmlData);
+
+               // Must be XML parsing error...
+               if (!is_array($flexFormRowData)) {
+                       $flexFormRowData = array();
+               } elseif (!isset($flexFormRowData['meta']) || !is_array($flexFormRowData['meta'])) {
+                       $flexFormRowData['meta'] = array();
+               }
+
+               $options = $this->globalOptions;
+               $options['flexFormDataStructureArray'] = $flexFormDataStructureArray;
+               $options['flexFormRowData'] = $flexFormRowData;
+               /** @var FlexFormLanguageContainer $flexFormLanguageContainer */
+               $flexFormLanguageContainer = GeneralUtility::makeInstance(FlexFormLanguageContainer::class);
+               return $flexFormLanguageContainer->setGlobalOptions($options)->render();
+       }
+
+       /**
+        * @return LanguageService
+        */
+       protected function getLanguageService() {
+               return $GLOBALS['LANG'];
+       }
+
+}
\ No newline at end of file
diff --git a/typo3/sysext/backend/Classes/Form/Container/FlexFormContainerContainer.php b/typo3/sysext/backend/Classes/Form/Container/FlexFormContainerContainer.php
new file mode 100644 (file)
index 0000000..11f841c
--- /dev/null
@@ -0,0 +1,138 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Container;
+
+/*
+ * 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\Utility\IconUtility;
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Flex form container implementation
+ * This one is called by FlexFormSectionContainer and renders HTML for a single container.
+ * For processing of single elements FlexFormElementContainer is called
+ */
+class FlexFormContainerContainer extends AbstractContainer {
+
+       /**
+        * Entry method
+        *
+        * @return array As defined in initializeResultArray() of AbstractNode
+        */
+       public function render() {
+               $table = $this->globalOptions['table'];
+               $row = $this->globalOptions['databaseRow'];
+               $fieldName = $this->globalOptions['fieldName'];
+               $flexFormFormPrefix = $this->globalOptions['flexFormFormPrefix'];
+               $flexFormContainerElementCollapsed = $this->globalOptions['flexFormContainerElementCollapsed'];
+               $flexFormContainerTitle = $this->globalOptions['flexFormContainerTitle'];
+               $flexFormFieldIdentifierPrefix = $this->globalOptions['flexFormFieldIdentifierPrefix'];
+               $parameterArray = $this->globalOptions['parameterArray'];
+
+               // Every container adds its own part to the id prefix
+               $flexFormFieldIdentifierPrefix = $flexFormFieldIdentifierPrefix . '-' . GeneralUtility::shortMd5(uniqid('id', TRUE));
+
+               $toggleIcons = IconUtility::getSpriteIcon(
+                       'actions-move-down',
+                       array(
+                               'class' => 't3-flex-control-toggle-icon-open',
+                               'style' => $flexFormContainerElementCollapsed ? 'display: none;' : '',
+                       )
+               );
+               $toggleIcons .= IconUtility::getSpriteIcon(
+                       'actions-move-right',
+                       array(
+                               'class' => 't3-flex-control-toggle-icon-close',
+                               'style' => $flexFormContainerElementCollapsed ? '' : 'display: none;',
+                       )
+               );
+
+               $flexFormContainerCounter = $this->globalOptions['flexFormContainerCounter'];
+               $actionFieldName = '_ACTION_FLEX_FORM'
+                       . $parameterArray['itemFormElName']
+                       . $this->globalOptions['flexFormFormPrefix']
+                       . '[_ACTION]'
+                       . '[' . $flexFormContainerCounter . ']';
+               $toggleFieldName = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']'
+                       . $flexFormFormPrefix
+                       . '[' . $flexFormContainerCounter . ']'
+                       . '[_TOGGLE]';
+
+               $moveAndDeleteContent = array();
+               $userHasAccessToDefaultLanguage = $this->getBackendUserAuthentication()->checkLanguageAccess(0);
+               if ($userHasAccessToDefaultLanguage) {
+                       $moveAndDeleteContent[] = '<div class="pull-right">';
+                       $moveAndDeleteContent[] = IconUtility::getSpriteIcon(
+                               'actions-move-move',
+                               array(
+                                       'title' => 'Drag to Move', // @todo: hardcoded title ...
+                                       'class' => 't3-js-sortable-handle'
+                               )
+                       );
+                       $moveAndDeleteContent[] = IconUtility::getSpriteIcon(
+                               'actions-edit-delete',
+                               array(
+                                       'title' => 'Delete', // @todo: hardcoded title ...
+                                       'class' => 't3-delete'
+                               )
+                       );
+                       $moveAndDeleteContent[] = '</div>';
+               }
+
+               $options = $this->globalOptions;
+               $options['flexFormFieldIdentifierPrefix'] = $flexFormFieldIdentifierPrefix;
+               // Append container specific stuff to field prefix
+               $options['flexFormFormPrefix'] =  $flexFormFormPrefix . '[' . $flexFormContainerCounter . '][' .  $this->globalOptions['flexFormContainerName'] . '][el]';
+               /** @var FlexFormElementContainer $containerContent */
+               $containerContent = GeneralUtility::makeInstance(FlexFormElementContainer::class);
+               $containerContentResult = $containerContent->setGlobalOptions($options)->render();
+
+               $html = array();
+               $html[] = '<div id="' . $flexFormFieldIdentifierPrefix . '" class="t3-form-field-container-flexsections t3-flex-section">';
+               $html[] =       '<input class="t3-flex-control t3-flex-control-action" type="hidden" name="' . htmlspecialchars($actionFieldName) . '" value="" />';
+               $html[] =       '<div class="t3-form-field-header-flexsection t3-flex-section-header">';
+               $html[] =               '<div class="pull-left">';
+               $html[] =                       '<a href="#" class="t3-flex-control-toggle-button">' . $toggleIcons . '</a>';
+               $html[] =                       '<span class="t3-record-title">' . $flexFormContainerTitle . '</span>';
+               $html[] =               '</div>';
+               $html[] =               implode(LF, $moveAndDeleteContent);
+               $html[] =       '</div>';
+               $html[] =       '<div class="t3-form-field-record-flexsection t3-flex-section-content"' . ($flexFormContainerElementCollapsed ? ' style="display:none;"' : '') . '>';
+               $html[] =               $containerContentResult['html'];
+               $html[] =       '</div>';
+               $html[] =       '<input';
+               $html[] =               'class="t3-flex-control t3-flex-control-toggle"';
+               $html[] =               'id="' . $flexFormFieldIdentifierPrefix . '-toggleClosed"';
+               $html[] =               'type="hidden"';
+               $html[] =               'name="' . htmlspecialchars($toggleFieldName) . '"';
+               $html[] =               'value="' . ($flexFormContainerElementCollapsed ? '1' : '0') . '"';
+               $html[] =       '/>';
+               $html[] = '</div>';
+
+               $containerContentResult['html'] = '';
+               $resultArray = $this->initializeResultArray();
+               $resultArray['html'] = implode(LF, $html);
+               $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $containerContentResult);
+
+               return $resultArray;
+       }
+
+       /**
+        * @return BackendUserAuthentication
+        */
+       protected function getBackendUserAuthentication() {
+               return $GLOBALS['BE_USER'];
+       }
+
+}
diff --git a/typo3/sysext/backend/Classes/Form/Container/FlexFormElementContainer.php b/typo3/sysext/backend/Classes/Form/Container/FlexFormElementContainer.php
new file mode 100644 (file)
index 0000000..a2b4dc5
--- /dev/null
@@ -0,0 +1,233 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Container;
+
+/*
+ * 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\ElementConditionMatcher;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Lang\LanguageService;
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
+use TYPO3\CMS\Backend\Form\NodeFactory;
+use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+
+/**
+ * The container handles single elements.
+ *
+ * This one is called by FlexFormTabsContainer, FlexFormNoTabsContainer or FlexFormContainerContainer.
+ * For single fields, the code is similar to SingleFieldContainer, processing will end up in single
+ * element classes depending on specific type of an element. Additionally, it determines if a
+ * section is handled and hands over to FlexFormSectionContainer in this case.
+ */
+class FlexFormElementContainer extends AbstractContainer {
+
+       /**
+        * Entry method
+        *
+        * @return array As defined in initializeResultArray() of AbstractNode
+        */
+       public function render() {
+               $table = $this->globalOptions['table'];
+               $row = $this->globalOptions['databaseRow'];
+               $fieldName = $this->globalOptions['fieldName'];
+               $flexFormDataStructureArray = $this->globalOptions['flexFormDataStructureArray'];
+               $flexFormRowData = $this->globalOptions['flexFormRowData'];
+               $flexFormCurrentLanguage = $this->globalOptions['flexFormCurrentLanguage'];
+               $flexFormNoEditDefaultLanguage = $this->globalOptions['flexFormNoEditDefaultLanguage'];
+               $flexFormFormPrefix = $this->globalOptions['flexFormFormPrefix'];
+               $parameterArray = $this->globalOptions['parameterArray'];
+
+               $languageService = $this->getLanguageService();
+               $resultArray = $this->initializeResultArray();
+               foreach ($flexFormDataStructureArray as $flexFormFieldName => $flexFormFieldArray) {
+                       if (
+                               // No item array found at all
+                               !is_array($flexFormFieldArray)
+                               // Not a section or container and not a list of single items
+                               || (!isset($flexFormFieldArray['type']) && !is_array($flexFormFieldArray['TCEforms']['config']))
+                       ) {
+                               continue;
+                       }
+
+                       if ($flexFormFieldArray['type'] === 'array') {
+                               // Section
+                               if (empty($flexFormFieldArray['section'])) {
+                                       $resultArray['html'] = LF . 'Section expected at ' . $flexFormFieldName . ' but not found';
+                                       continue;
+                               }
+
+                               $sectionTitle = '';
+                               if (!empty($flexFormFieldArray['title'])) {
+                                       $sectionTitle = $languageService->sL($flexFormFieldArray['title']);
+                               }
+
+                               $options = $this->globalOptions;
+                               $options['flexFormDataStructureArray'] = $flexFormFieldArray['el'];
+                               $options['flexFormRowData'] = $flexFormRowData[$flexFormFieldName]['el'];
+                               $options['flexFormSectionType'] = $flexFormFieldName;
+                               $options['flexFormSectionTitle'] = $sectionTitle;
+                               /** @var FlexFormSectionContainer $sectionContainer */
+                               $sectionContainer = GeneralUtility::makeInstance(FlexFormSectionContainer::class);
+                               $sectionContainerResult = $sectionContainer->setGlobalOptions($options)->render();
+                               $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $sectionContainerResult);
+                       } else {
+                               // Single element
+                               $vDEFkey = 'vDEF';
+
+                               $displayConditionResult = TRUE;
+                               if (!empty($flexFormFieldArray['TCEforms']['displayCond'])) {
+                                       $conditionData = is_array($flexFormRowData) ? $flexFormRowData : array();
+                                       $conditionData['parentRec'] = $row;
+                                       /** @var $elementConditionMatcher ElementConditionMatcher */
+                                       $elementConditionMatcher = GeneralUtility::makeInstance(ElementConditionMatcher::class);
+                                       $displayConditionResult = $elementConditionMatcher->match($flexFormFieldArray['TCEforms']['displayCond'], $conditionData, $vDEFkey);
+                               }
+                               if (!$displayConditionResult) {
+                                       continue;
+                               }
+
+                               // Set up options for single element
+                               $fakeParameterArray = array(
+                                       'fieldConf' => array(
+                                               'label' => $languageService->sL(trim($flexFormFieldArray['TCEforms']['label'])),
+                                               'config' => $flexFormFieldArray['TCEforms']['config'],
+                                               'defaultExtras' => $flexFormFieldArray['TCEforms']['defaultExtras'],
+                                               'onChange' => $flexFormFieldArray['TCEforms']['onChange'],
+                                       ),
+                               );
+
+                               // Force a none field if default language can not be edited
+                               if ($flexFormNoEditDefaultLanguage && $flexFormCurrentLanguage === 'lDEF') {
+                                       $fakeParameterArray['fieldConf']['config'] = array(
+                                               'type' => 'none',
+                                               'rows' => 2
+                                       );
+                               }
+
+                               $alertMsgOnChange = '';
+                               if (
+                                       $fakeParameterArray['fieldConf']['onChange'] === 'reload'
+                                       || !empty($GLOBALS['TCA'][$table]['ctrl']['type']) && $GLOBALS['TCA'][$table]['ctrl']['type'] === $flexFormFieldName
+                                       || !empty($GLOBALS['TCA'][$table]['ctrl']['requestUpdate']) && GeneralUtility::inList($GLOBALS['TCA'][$table]['ctrl']['requestUpdate'], $flexFormFieldName)
+                               ) {
+                                       if ($this->getBackendUserAuthentication()->jsConfirmation(JsConfirmation::TYPE_CHANGE)) {
+                                               $alertMsgOnChange = 'if (confirm(TBE_EDITOR.labels.onChangeAlert) && TBE_EDITOR.checkSubmit(-1)){ TBE_EDITOR.submitForm() };';
+                                       } else {
+                                               $alertMsgOnChange = 'if (TBE_EDITOR.checkSubmit(-1)){ TBE_EDITOR.submitForm();}';
+                                       }
+                               }
+                               $fakeParameterArray['fieldChangeFunc'] = $parameterArray['fieldChangeFunc'];
+                               if ($alertMsgOnChange) {
+                                       $fakeParameterArray['fieldChangeFunc']['alert'] = $alertMsgOnChange;
+                               }
+
+                               $fakeParameterArray['onFocus'] = $parameterArray['onFocus'];
+                               $fakeParameterArray['label'] = $parameterArray['label'];
+                               $fakeParameterArray['itemFormElName'] = $parameterArray['itemFormElName'] . $flexFormFormPrefix . '[' . $flexFormFieldName . '][' . $vDEFkey . ']';
+                               $fakeParameterArray['itemFormElName_file'] = $parameterArray['itemFormElName_file'] . $flexFormFormPrefix . '[' . $flexFormFieldName . '][' . $vDEFkey . ']';
+                               $fakeParameterArray['itemFormElID'] = $fakeParameterArray['itemFormElName'];
+                               if (isset($flexFormRowData[$flexFormFieldName][$vDEFkey])) {
+                                       $fakeParameterArray['itemFormElValue'] = $flexFormRowData[$flexFormFieldName][$vDEFkey];
+                               } else {
+                                       $fakeParameterArray['itemFormElValue'] = $fakeParameterArray['fieldConf']['config']['default'];
+                               }
+
+                               $options = $this->globalOptions;
+                               $options['parameterArray'] = $fakeParameterArray;
+                               /** @var NodeFactory $nodeFactory */
+                               $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
+                               $child = $nodeFactory->create($flexFormFieldArray['TCEforms']['config']['type']);
+                               $childResult = $child->setGlobalOptions($options)->render();
+
+                               $theTitle = htmlspecialchars($fakeParameterArray['fieldConf']['label']);
+                               $defInfo = array();
+                               if (!$flexFormNoEditDefaultLanguage) {
+                                       $previewLanguages = $this->globalOptions['additionalPreviewLanguages'];
+                                       foreach ($previewLanguages as $previewLanguage) {
+                                               $defInfo[] = '<div class="t3-form-original-language">';
+                                               $defInfo[] =    FormEngineUtility::getLanguageIcon($table, $row, ('v' . $previewLanguage['ISOcode']));
+                                               $defInfo[] =    $this->previewFieldValue($flexFormRowData[$flexFormFieldName][('v' . $previewLanguage['ISOcode'])], $fakeParameterArray['fieldConf'], $fieldName);
+                                               $defInfo[] = '</div>';
+                                       }
+                               }
+
+                               $languageIcon = '';
+                               if ($vDEFkey !== 'vDEF') {
+                                       $languageIcon = FormEngineUtility::getLanguageIcon($table, $row, $vDEFkey);
+                               }
+                               // Possible line breaks in the label through xml: \n => <br/>, usage of nl2br() not possible, so it's done through str_replace (?!)
+                               $processedTitle = str_replace('\\n', '<br />', $theTitle);
+                               // @todo: Similar to the processing within SingleElementContainer ... use it from there?!
+                               $html = array();
+                               $html[] = '<div class="form-section">';
+                               $html[] =       '<div class="form-group t3js-formengine-palette-field">';
+                               $html[] =               '<label class="t3js-formengine-label">';
+                               $html[] =                       $languageIcon;
+                               $html[] =                       BackendUtility::wrapInHelp($parameterArray['_cshKey'], $flexFormFieldName, $processedTitle);
+                               $html[] =               '</label>';
+                               $html[] =               '<div class="t3js-formengine-field-item">';
+                               $html[] =                       $childResult['html'];
+                               $html[] =                       implode(LF, $defInfo);
+                               $html[] =                       $this->renderVDEFDiff($flexFormRowData[$flexFormFieldName], $vDEFkey);
+                               $html[] =               '</div>';
+                               $html[] =       '</div>';
+                               $html[] = '</div>';
+
+                               $resultArray['html'] .= implode(LF, $html);
+                               $childResult['html'] = '';
+                               $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult);
+                       }
+               }
+
+               return $resultArray;
+       }
+
+       /**
+        * Renders the diff-view of vDEF fields in flex forms
+        *
+        * @param array $vArray Record array of the record being edited
+        * @param string $vDEFkey HTML of the form field. This is what we add the content to.
+        * @return string Item string returned again, possibly with the original value added to.
+        */
+       protected function renderVDEFDiff($vArray, $vDEFkey) {
+               $item = NULL;
+               if (
+                       $GLOBALS['TYPO3_CONF_VARS']['BE']['flexFormXMLincludeDiffBase'] && isset($vArray[$vDEFkey . '.vDEFbase'])
+                       && (string)$vArray[$vDEFkey . '.vDEFbase'] !== (string)$vArray['vDEF']
+               ) {
+                       // Create diff-result:
+                       $diffUtility = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Utility\DiffUtility::class);
+                       $diffres = $diffUtility->makeDiffDisplay($vArray[$vDEFkey . '.vDEFbase'], $vArray['vDEF']);
+                       $item = '<div class="typo3-TCEforms-diffBox">' . '<div class="typo3-TCEforms-diffBox-header">'
+                               . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.changeInOrig')) . ':</div>' . $diffres . '</div>';
+               }
+               return $item;
+       }
+
+       /**
+        * @return LanguageService
+        */
+       protected function getLanguageService() {
+               return $GLOBALS['LANG'];
+       }
+
+       /**
+        * @return BackendUserAuthentication
+        */
+       protected function getBackendUserAuthentication() {
+               return $GLOBALS['BE_USER'];
+       }
+
+}
diff --git a/typo3/sysext/backend/Classes/Form/Container/FlexFormLanguageContainer.php b/typo3/sysext/backend/Classes/Form/Container/FlexFormLanguageContainer.php
new file mode 100644 (file)
index 0000000..41af724
--- /dev/null
@@ -0,0 +1,177 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Container;
+
+/*
+ * 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\Utility\GeneralUtility;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Database\DatabaseConnection;
+use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
+
+/**
+ * Handle flex form language overlays.
+ *
+ * This container is falled from the entry FlexFormContainer. For each existing language overlay
+ * it forks a FlexFormTabsContainer or a FlexFormNoTabsContainer for rendering a full flex form
+ * record of the specific language.
+ */
+class FlexFormLanguageContainer extends AbstractContainer {
+
+       /**
+        * Entry method
+        *
+        * @return array As defined in initializeResultArray() of AbstractNode
+        */
+       public function render() {
+               $table = $this->globalOptions['table'];
+               $row = $this->globalOptions['databaseRow'];
+               $flexFormDataStructureArray = $this->globalOptions['flexFormDataStructureArray'];
+               $flexFormRowData = $this->globalOptions['flexFormRowData'];
+
+               // Determine available languages
+               $langChildren = (bool)$flexFormDataStructureArray['meta']['langChildren'];
+               $langDisabled = (bool)$flexFormDataStructureArray['meta']['langDisable'];
+               $flexFormRowData['meta']['currentLangId'] = array();
+               // Look up page language overlays
+               $checkPageLanguageOverlay = $this->getBackendUserAuthentication()->getTSConfigVal('options.checkPageLanguageOverlay') ? TRUE : FALSE;
+               $pageOverlays = array();
+               if ($checkPageLanguageOverlay) {
+                       $whereClause = 'pid=' . (int)$row['pid'] . BackendUtility::deleteClause('pages_language_overlay')
+                               . BackendUtility::versioningPlaceholderClause('pages_language_overlay');
+                       $pageOverlays = $this->getDatabaseConnection()->exec_SELECTgetRows('*', 'pages_language_overlay', $whereClause, '', '', '', 'sys_language_uid');
+               }
+               $languages = $this->getAvailableLanguages();
+               foreach ($languages as $langInfo) {
+                       if (
+                               $this->getBackendUserAuthentication()->checkLanguageAccess($langInfo['uid'])
+                               && (!$checkPageLanguageOverlay || $langInfo['uid'] <= 0 || is_array($pageOverlays[$langInfo['uid']]))
+                       ) {
+                               $flexFormRowData['meta']['currentLangId'][] = $langInfo['ISOcode'];
+                       }
+               }
+               if (!is_array($flexFormRowData['meta']['currentLangId']) || !count($flexFormRowData['meta']['currentLangId'])) {
+                       $flexFormRowData['meta']['currentLangId'] = array('DEF');
+               }
+               $flexFormRowData['meta']['currentLangId'] = array_unique($flexFormRowData['meta']['currentLangId']);
+               $flexFormNoEditDefaultLanguage = FALSE;
+               if ($langChildren || $langDisabled) {
+                       $availableLanguages = array('DEF');
+               } else {
+                       if (!in_array('DEF', $flexFormRowData['meta']['currentLangId'])) {
+                               array_unshift($flexFormRowData['meta']['currentLangId'], 'DEF');
+                               $flexFormNoEditDefaultLanguage = TRUE;
+                       }
+                       $availableLanguages = $flexFormRowData['meta']['currentLangId'];
+               }
+
+               // Tabs or no tabs - that's the question
+               $hasTabs = FALSE;
+               if (is_array($flexFormDataStructureArray['sheets'])) {
+                       $hasTabs = TRUE;
+               }
+
+               $resultArray = $this->initializeResultArray();
+
+               foreach ($availableLanguages as $lKey) {
+                       // Add language as header
+                       if (!$langChildren && !$langDisabled) {
+                               $resultArray['html'] .= LF . '<strong>' . FormEngineUtility::getLanguageIcon($table, $row, ('v' . $lKey)) . $lKey . ':</strong>';
+                       }
+
+                       // Default language "lDEF", other options are "lUK" or whatever country code
+                       $flexFormCurrentLanguage = 'l' . $lKey;
+
+                       $options = $this->globalOptions;
+                       $options['flexFormCurrentLanguage'] = $flexFormCurrentLanguage;
+                       $options['flexFormNoEditDefaultLanguage'] = $flexFormNoEditDefaultLanguage;
+                       if (!$hasTabs) {
+                               /** @var FlexFormNoTabsContainer $flexFormNoTabsContainer */
+                               $flexFormNoTabsContainer = GeneralUtility::makeInstance(FlexFormNoTabsContainer::class);
+                               $flexFormNoTabsResult = $flexFormNoTabsContainer->setGlobalOptions($options)->render();
+                               $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $flexFormNoTabsResult);
+                       } else {
+                               /** @var FlexFormTabsContainer $flexFormTabsContainer */
+                               $flexFormTabsContainer = GeneralUtility::makeInstance(FlexFormTabsContainer::class);
+                               $flexFormTabsContainerResult = $flexFormTabsContainer->setGlobalOptions($options)->render();
+                               $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $flexFormTabsContainerResult);
+                       }
+               }
+
+               return $resultArray;
+       }
+
+       /**
+        * Returns an array of available languages (to use for FlexForms)
+        *
+        * @return array
+        */
+       protected function getAvailableLanguages() {
+               $isLoaded = ExtensionManagementUtility::isLoaded('static_info_tables');
+
+               // Find all language records in the system
+               $db = $this->getDatabaseConnection();
+               $res = $db->exec_SELECTquery(
+                       'language_isocode,static_lang_isocode,title,uid',
+                       'sys_language',
+                       'pid=0 AND hidden=0' . BackendUtility::deleteClause('sys_language'),
+                       '',
+                       'title'
+               );
+
+               // Traverse them
+               $output = array(
+                       0 => array(
+                               'uid' => 0,
+                               'title' => 'Default language',
+                               'ISOcode' => 'DEF',
+                       )
+               );
+
+               while ($row = $db->sql_fetch_assoc($res)) {
+                       $output[$row['uid']] = $row;
+                       if (!empty($row['language_isocode'])) {
+                               $output[$row['uid']]['ISOcode'] = $row['language_isocode'];
+                       } elseif ($isLoaded && $row['static_lang_isocode']) {
+                               GeneralUtility::deprecationLog('Usage of the field "static_lang_isocode" is discouraged, and will stop working with CMS 8. Use the built-in language field "language_isocode" in your sys_language records.');
+                               $rr = BackendUtility::getRecord('static_languages', $row['static_lang_isocode'], 'lg_iso_2');
+                               if ($rr['lg_iso_2']) {
+                                       $output[$row['uid']]['ISOcode'] = $rr['lg_iso_2'];
+                               }
+                       }
+                       if (!$output[$row['uid']]['ISOcode']) {
+                               unset($output[$row['uid']]);
+                       }
+               }
+               $db->sql_free_result($res);
+
+               return $output;
+       }
+
+       /**
+        * @return BackendUserAuthentication
+        */
+       protected function getBackendUserAuthentication() {
+               return $GLOBALS['BE_USER'];
+       }
+
+       /**
+        * @return DatabaseConnection
+        */
+       protected function getDatabaseConnection() {
+               return $GLOBALS['TYPO3_DB'];
+       }
+
+}
diff --git a/typo3/sysext/backend/Classes/Form/Container/FlexFormNoTabsContainer.php b/typo3/sysext/backend/Classes/Form/Container/FlexFormNoTabsContainer.php
new file mode 100644 (file)
index 0000000..b293440
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Container;
+
+/*
+ * 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\Utility\GeneralUtility;
+
+/**
+ * Handle a flex form that has no tabs.
+ *
+ * This container is called by FlexFormLanguageContainer if only a default sheet
+ * exists. It evaluates the display condition and hands over rendering of single
+ * fields to FlexFormElementContainer.
+ */
+class FlexFormNoTabsContainer extends AbstractContainer {
+
+       /**
+        * Entry method
+        *
+        * @return array As defined in initializeResultArray() of AbstractNode
+        */
+       public function render() {
+               $table = $this->globalOptions['table'];
+               $row = $this->globalOptions['databaseRow'];
+               $fieldName = $this->globalOptions['fieldName']; // field name of the flex form field in DB
+               $parameterArray = $this->globalOptions['parameterArray'];
+               $flexFormDataStructureArray = $this->globalOptions['flexFormDataStructureArray'];
+               $flexFormSheetNameInRowData = 'sDEF';
+               $flexFormCurrentLanguage = $this->globalOptions['flexFormCurrentLanguage'];
+               $flexFormRowData = $this->globalOptions['flexFormRowData'];
+               $flexFormRowDataSubPart = $flexFormRowData['data'][$flexFormSheetNameInRowData][$flexFormCurrentLanguage];
+               $resultArray = $this->initializeResultArray();
+
+               // That was taken from GeneralUtility::resolveSheetDefInDS - no idea if it is important
+               unset($flexFormDataStructureArray['meta']);
+
+               // Evaluate display condition for this "sheet" if there is one
+               $displayConditionResult = TRUE;
+               if (!empty($flexFormDataStructureArray['ROOT']['TCEforms']['displayCond'])) {
+                       $displayConditionDefinition = $flexFormDataStructureArray['ROOT']['TCEforms']['displayCond'];
+                       $displayConditionResult = $this->evaluateFlexFormDisplayCondition(
+                               $displayConditionDefinition,
+                               $flexFormRowDataSubPart
+                       );
+               }
+               if (!$displayConditionResult) {
+                       return $resultArray;
+               }
+
+               if (!is_array($flexFormDataStructureArray['ROOT']['el'])) {
+                       $resultArray['html'] = 'Data Structure ERROR: No [\'ROOT\'][\'el\'] element found in flex form definition.';
+                       return $resultArray;
+               }
+
+               // Assemble key for loading the correct CSH file
+               // @todo: what is that good for? That is for the title of single elements ... see FlexFormElementContainer!
+               $dsPointerFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['ds_pointerField'], TRUE);
+               $parameterArray['_cshKey'] = $table . '.' . $fieldName;
+               foreach ($dsPointerFields as $key) {
+                       $parameterArray['_cshKey'] .= '.' . $row[$key];
+               }
+
+               $options = $this->globalOptions;
+               $options['flexFormDataStructureArray'] = $flexFormDataStructureArray['ROOT']['el'];
+               $options['flexFormRowData'] = $flexFormRowDataSubPart;
+               $options['flexFormFormPrefix'] = '[data][' . $flexFormSheetNameInRowData . '][' . $flexFormCurrentLanguage . ']';
+               $options['parameterArray'] = $parameterArray;
+
+               /** @var FlexFormElementContainer $flexFormElementContainer */
+               $flexFormElementContainer = GeneralUtility::makeInstance(FlexFormElementContainer::class);
+               return $flexFormElementContainer->setGlobalOptions($options)->render();
+       }
+
+}
diff --git a/typo3/sysext/backend/Classes/Form/Container/FlexFormSectionContainer.php b/typo3/sysext/backend/Classes/Form/Container/FlexFormSectionContainer.php
new file mode 100644 (file)
index 0000000..dee578b
--- /dev/null
@@ -0,0 +1,190 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Container;
+
+/*
+ * 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\Utility\IconUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Lang\LanguageService;
+
+/**
+ * Handle flex form sections.
+ *
+ * This container is created by FlexFormElementContainer if a "single" element is in
+ * fact a sections. For each existing section container it creates as FlexFormContainerContainer
+ * to render its inner fields, additionally for each possible container a "template" of this
+ * container type is rendered and added - to be added by JS to DOM on click on "new xy container".
+ */
+class FlexFormSectionContainer extends AbstractContainer {
+
+       /**
+        * Entry method
+        *
+        * @return array As defined in initializeResultArray() of AbstractNode
+        */
+       public function render() {
+               $languageService = $this->getLanguageService();
+
+               $flexFormFieldsArray = $this->globalOptions['flexFormDataStructureArray'];
+               $flexFormRowData = $this->globalOptions['flexFormRowData'];
+               $flexFormFieldIdentifierPrefix = $this->globalOptions['flexFormFieldIdentifierPrefix'];
+               $flexFormSectionType = $this->globalOptions['flexFormSectionType'];
+               $flexFormSectionTitle = $this->globalOptions['flexFormSectionTitle'];
+
+               $userHasAccessToDefaultLanguage = $this->getBackendUserAuthentication()->checkLanguageAccess(0);
+
+               $resultArray = $this->initializeResultArray();
+
+               // Creating IDs for form fields:
+               // It's important that the IDs "cascade" - otherwise we can't dynamically expand the flex form
+               // because this relies on simple string substitution of the first parts of the id values.
+               $flexFormFieldIdentifierPrefix = $flexFormFieldIdentifierPrefix . '-' . GeneralUtility::shortMd5(uniqid('id', TRUE));
+
+               // Render each existing container
+               foreach ($flexFormRowData as $flexFormContainerCounter => $existingSectionContainerData) {
+                       // @todo: This relies on the fact that "_TOGGLE" is *below* the real data in the saved xml structure
+                       if (is_array($existingSectionContainerData)) {
+                               $existingSectionContainerDataStructureType = key($existingSectionContainerData);
+                               $existingSectionContainerData = $existingSectionContainerData[$existingSectionContainerDataStructureType];
+                               $containerDataStructure = $flexFormFieldsArray[$existingSectionContainerDataStructureType];
+                               // There may be cases where a field is still in DB but does not exist in definition
+                               if (is_array($containerDataStructure)) {
+                                       $sectionTitle = '';
+                                       if (!empty($containerDataStructure['title'])) {
+                                               $sectionTitle = $languageService->sL($containerDataStructure['title']);
+                                       }
+
+                                       $options = $this->globalOptions;
+                                       $options['flexFormRowData'] = $existingSectionContainerData['el'];
+                                       $options['flexFormDataStructureArray'] = $containerDataStructure['el'];
+                                       $options['flexFormFieldIdentifierPrefix'] = $flexFormFieldIdentifierPrefix;
+                                       $options['flexFormFormPrefix'] = $this->globalOptions['flexFormFormPrefix'] . '[' . $flexFormSectionType . ']' . '[el]';
+                                       $options['flexFormContainerName'] = $existingSectionContainerDataStructureType;
+                                       $options['flexFormContainerCounter'] = $flexFormContainerCounter;
+                                       $options['flexFormContainerTitle'] = $sectionTitle;
+                                       $options['flexFormContainerElementCollapsed'] = (bool)$existingSectionContainerData['el']['_TOGGLE'];
+                                       /** @var FlexFormContainerContainer $flexFormContainerContainer */
+                                       $flexFormContainerContainer = GeneralUtility::makeInstance(FlexFormContainerContainer::class);
+                                       $flexFormContainerContainerResult = $flexFormContainerContainer->setGlobalOptions($options)->render();
+                                       $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $flexFormContainerContainerResult);
+                               }
+                       }
+               }
+
+               // "New container" handling: Creates a "template" of each possible container and stuffs it
+               // somewhere into DOM to be handled with JS magic.
+               // Fun part: Handle the fact that requiredElements and such may be set for children
+               $containerTemplatesHtml = array();
+               foreach ($flexFormFieldsArray as $flexFormContainerName => $flexFormFieldDefinition) {
+                       $containerTemplateHtml = array();
+                       $sectionTitle = '';
+                       if (!empty($flexFormFieldDefinition['title'])) {
+                               $sectionTitle = $languageService->sL($flexFormFieldDefinition['title']);
+                       }
+
+                       $options = $this->globalOptions;
+                       $options['flexFormRowData'] = array();
+                       $options['flexFormDataStructureArray'] = $flexFormFieldDefinition['el'];
+                       $options['flexFormFieldIdentifierPrefix'] = $flexFormFieldIdentifierPrefix;
+                       // @todo: something wrong here ... where is it used - needed for the templates??
+                       $flexFormContainerCounter = 0;
+                       $options['flexFormContainerCounter'] = $flexFormContainerCounter;
+                       $options['flexFormContainerTitle'] = $sectionTitle;
+                       $options['flexFormContainerElementCollapsed'] = FALSE;
+                       /** @var FlexFormContainerContainer $flexFormContainerContainer */
+                       $flexFormContainerContainerTemplate = GeneralUtility::makeInstance(FlexFormContainerContainer::class);
+                       $flexFormContainerContainerTemplateResult = $flexFormContainerContainerTemplate->setGlobalOptions($options)->render();
+
+                       $uniqueId = str_replace('.', '', uniqid('idvar', TRUE));
+                       $identifierPrefixJs = 'replace(/' . $flexFormFieldIdentifierPrefix . '-/g,"' . $flexFormFieldIdentifierPrefix . '-"+' . $uniqueId . '+"-")';
+                       $identifierPrefixJs .= '.replace(/(tceforms-(datetime|date)field-)/g,"$1" + (new Date()).getTime())';
+
+                       $onClickInsert = array();
+                       $onClickInsert[] = 'var ' . $uniqueId . ' = "' . 'idx"+(new Date()).getTime();';
+                       $onClickInsert[] = 'new Insertion.Bottom($("' . $flexFormFieldIdentifierPrefix . '"), ' . json_encode($flexFormContainerContainerTemplateResult['html']) . '.' . $identifierPrefixJs . ');';
+                       $onClickInsert[] = 'TYPO3.jQuery("#' . $flexFormFieldIdentifierPrefix . '").t3FormEngineFlexFormElement();';
+                       $onClickInsert[] = 'eval(unescape("' . rawurlencode(implode(';', $flexFormContainerContainerTemplateResult['additionalJavaScriptPost'])) . '").' . $identifierPrefixJs . ');';
+                       $onClickInsert[] = 'TBE_EDITOR.addActionChecks("submit", unescape("' . rawurlencode(implode(';', $flexFormContainerContainerTemplateResult['additionalJavaScriptSubmit'])) . '").' . $identifierPrefixJs . ');';
+                       $onClickInsert[] = 'TYPO3.FormEngine.reinitialize();';
+                       $onClickInsert[] = 'return false;';
+
+                       $containerTemplateHtml[] = '<a href="#" onclick="' . htmlspecialchars(implode(LF, $onClickInsert)) . '">';
+                       $containerTemplateHtml[] =      IconUtility::getSpriteIcon('actions-document-new');
+                       $containerTemplateHtml[] =      htmlspecialchars(GeneralUtility::fixed_lgd_cs($sectionTitle, 30));
+                       $containerTemplateHtml[] = '</a>';
+                       $containerTemplatesHtml[] = implode(LF, $containerTemplateHtml);
+
+                       $flexFormContainerContainerTemplateResult['html'] = '';
+                       $flexFormContainerContainerTemplateResult['additionalJavaScriptPost'] = array();
+                       $flexFormContainerContainerTemplateResult['additionalJavaScriptSubmit'] = array();
+
+                       $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $flexFormContainerContainerTemplateResult);
+               }
+
+               // Create new elements links
+               $createElementsHtml = array();
+               if ($userHasAccessToDefaultLanguage) {
+                       $createElementsHtml[] = '<div class="t3-form-field-add-flexsection">';
+                       $createElementsHtml[] =         '<strong>';
+                       $createElementsHtml[] =                 $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.addnew', TRUE) . ':';
+                       $createElementsHtml[] =         '</strong>';
+                       $createElementsHtml[] =         implode('|', $containerTemplatesHtml);
+                       $createElementsHtml[] = '</div>';
+               }
+
+               // Wrap child stuff
+               $toggleAll = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.toggleall', TRUE);
+               $html = array();
+               $html[] = '<div class="t3-form-field-container t3-form-flex">';
+               $html[] =       '<div class="t3-form-field-label-flexsection">';
+               $html[] =               '<strong>';
+               $html[] =                       htmlspecialchars($flexFormSectionTitle);
+               $html[] =               '</strong>';
+               $html[] =       '</div>';
+               $html[] =       '<div class="t3-form-field-toggle-flexsection t3-form-flexsection-toggle">';
+               $html[] =               '<a href="#">';
+               $html[] =                       IconUtility::getSpriteIcon('actions-move-right', array('title' => $toggleAll)) . $toggleAll;
+               $html[] =               '</a>';
+               $html[] =       '</div>';
+               $html[] =       '<div';
+               $html[] =               'id="' . $flexFormFieldIdentifierPrefix . '"';
+               $html[] =               'class="t3-form-field-container-flexsection t3-flex-container"';
+               $html[] =               'data-t3-flex-allow-restructure="' . ($userHasAccessToDefaultLanguage ? '1' : '0') . '"';
+               $html[] =       '>';
+               $html[] =               $resultArray['html'];
+               $html[] =       '</div>';
+               $html[] =       implode(LF, $createElementsHtml);
+               $html[] = '</div>';
+
+               $resultArray['html'] = implode(LF, $html);
+
+               return $resultArray;
+       }
+
+       /**
+        * @return BackendUserAuthentication
+        */
+       protected function getBackendUserAuthentication() {
+               return $GLOBALS['BE_USER'];
+       }
+
+       /**
+        * @return LanguageService
+        */
+       protected function getLanguageService() {
+               return $GLOBALS['LANG'];
+       }
+
+}
\ No newline at end of file
diff --git a/typo3/sysext/backend/Classes/Form/Container/FlexFormTabsContainer.php b/typo3/sysext/backend/Classes/Form/Container/FlexFormTabsContainer.php
new file mode 100644 (file)
index 0000000..79117a6
--- /dev/null
@@ -0,0 +1,133 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Container;
+
+/*
+ * 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\Template\DocumentTemplate;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Lang\LanguageService;
+
+/**
+ * Handle flex forms that have tabs (multiple "sheets").
+ *
+ * This container is called by FlexFormLanguageContainer. It resolves each
+ * sheet and hands rendering of single sheet content over to FlexFormElementContainer.
+ */
+class FlexFormTabsContainer extends AbstractContainer {
+
+       /**
+        * Entry method
+        *
+        * @return array As defined in initializeResultArray() of AbstractNode
+        */
+       public function render() {
+               $languageService = $this->getLanguageService();
+               $docTemplate = $this->getDocumentTemplate();
+
+               $table = $this->globalOptions['table'];
+               $row = $this->globalOptions['databaseRow'];
+               $fieldName = $this->globalOptions['fieldName']; // field name of the flex form field in DB
+               $parameterArray = $this->globalOptions['parameterArray'];
+               $flexFormDataStructureArray = $this->globalOptions['flexFormDataStructureArray'];
+               $flexFormCurrentLanguage = $this->globalOptions['flexFormCurrentLanguage'];
+               $flexFormRowData = $this->globalOptions['flexFormRowData'];
+
+               $tabId = 'TCEFORMS:flexform:' . $this->globalOptions['parameterArray']['itemFormElName'] . $flexFormCurrentLanguage;
+               $tabIdString = $docTemplate->getDynTabMenuId($tabId);
+               $tabCounter = 0;
+
+               $resultArray = $this->initializeResultArray();
+               $tabsContent = array();
+               foreach ($flexFormDataStructureArray['sheets'] as $sheetName => $sheetDataStructure) {
+                       $flexFormRowSheetDataSubPart = $flexFormRowData['data'][$sheetName][$flexFormCurrentLanguage];
+
+                       // Evaluate display condition for this sheet if there is one
+                       $displayConditionResult = TRUE;
+                       if (!empty($sheetDataStructure['ROOT']['TCEforms']['displayCond'])) {
+                               $displayConditionDefinition = $sheetDataStructure['ROOT']['TCEforms']['displayCond'];
+                               $displayConditionResult = $this->evaluateFlexFormDisplayCondition(
+                                       $displayConditionDefinition,
+                                       $flexFormRowSheetDataSubPart
+                               );
+                       }
+                       if (!$displayConditionResult) {
+                               continue;
+                       }
+
+                       if (!is_array($sheetDataStructure['ROOT']['el'])) {
+                               $resultArray['html'] .= LF . 'No Data Structure ERROR: No [\'ROOT\'][\'el\'] found for sheet "' . $sheetName . '".';
+                               continue;
+                       }
+
+                       $tabCounter ++;
+
+                       // Assemble key for loading the correct CSH file
+                       // @todo: what is that good for? That is for the title of single elements ... see FlexFormElementContainer!
+                       $dsPointerFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['ds_pointerField'], TRUE);
+                       $parameterArray['_cshKey'] = $table . '.' . $fieldName;
+                       foreach ($dsPointerFields as $key) {
+                               $parameterArray['_cshKey'] .= '.' . $row[$key];
+                       }
+
+                       $options = $this->globalOptions;
+                       $options['flexFormDataStructureArray'] = $sheetDataStructure['ROOT']['el'];
+                       $options['flexFormRowData'] = $flexFormRowSheetDataSubPart;
+                       $options['flexFormFormPrefix'] = '[data][' . $sheetName . '][' . $flexFormCurrentLanguage . ']';
+                       $options['parameterArray'] = $parameterArray;
+                       // Merge elements of this tab into a single list again and hand over to
+                       // palette and single field container to render this group
+                       $options['tabAndInlineStack'][] = array(
+                               'tab',
+                               $tabIdString . '-' . $tabCounter,
+                       );
+                       /** @var FlexFormElementContainer $flexFormElementContainer */
+                       $flexFormElementContainer = GeneralUtility::makeInstance(FlexFormElementContainer::class);
+                       $childReturn = $flexFormElementContainer->setGlobalOptions($options)->render();
+
+                       $tabsContent[] = array(
+                               'label' => !empty($sheetDataStructure['ROOT']['TCEforms']['sheetTitle']) ? $languageService->sL($sheetDataStructure['ROOT']['TCEforms']['sheetTitle']) : $sheetName,
+                               'content' => $childReturn['html'],
+                               'description' => $sheetDataStructure['ROOT']['TCEforms']['sheetDescription'] ? $languageService->sL($sheetDataStructure['ROOT']['TCEforms']['sheetDescription']) : '',
+                               'linkTitle' => $sheetDataStructure['ROOT']['TCEforms']['sheetShortDescr'] ? $languageService->sL($sheetDataStructure['ROOT']['TCEforms']['sheetShortDescr']) : '',
+                       );
+
+                       $childReturn['html'] = '';
+                       $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childReturn);
+               }
+
+               // Feed everything to document template for tab rendering
+               $resultArray['html'] = $docTemplate->getDynamicTabMenu($tabsContent, $tabId, 1, FALSE, FALSE);
+               return $resultArray;
+       }
+
+       /**
+        * @throws \RuntimeException
+        * @return DocumentTemplate
+        */
+       protected function getDocumentTemplate() {
+               $docTemplate = $GLOBALS['TBE_TEMPLATE'];
+               if (!is_object($docTemplate)) {
+                       throw new \RuntimeException('No instance of DocumentTemplate found', 1427143328);
+               }
+               return $docTemplate;
+       }
+
+       /**
+        * @return LanguageService
+        */
+       protected function getLanguageService() {
+               return $GLOBALS['LANG'];
+       }
+
+}
diff --git a/typo3/sysext/backend/Classes/Form/Container/FullRecordContainer.php b/typo3/sysext/backend/Classes/Form/Container/FullRecordContainer.php
new file mode 100644 (file)
index 0000000..d747840
--- /dev/null
@@ -0,0 +1,190 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Container;
+
+/*
+ * 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\Lang\LanguageService;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * A container rendering a "full record". This is an entry container used as first
+ * step into the rendering tree..
+ *
+ * This container determines the to be rendered fields depending on the record type,
+ * initializes possible language base data, finds out if tabs should be rendered and
+ * then calls either TabsContainer or a NoTabsContainer for further processing.
+ */
+class FullRecordContainer extends AbstractContainer {
+
+       /**
+        * Entry method
+        *
+        * @return array As defined in initializeResultArray() of AbstractNode
+        */
+       public function render() {
+               $table = $this->globalOptions['table'];
+               $row = $this->globalOptions['databaseRow'];
+
+               if (!$GLOBALS['TCA'][$table]) {
+                       return $this->initializeResultArray();
+               }
+
+               $languageService = $this->getLanguageService();
+
+               // Load the description content for the table if requested
+               if ($GLOBALS['TCA'][$table]['interface']['always_description']) {
+                       $languageService->loadSingleTableDescription($table);
+               }
+
+               // If this is a localized record, stuff data from original record to local registry, will then be given to child elements
+               $this->registerDefaultLanguageData($table, $row);
+
+               // Current type value of the record.
+               $recordTypeValue = $this->getRecordTypeValue($table, $row);
+
+               // List of items to be rendered
+               $itemList = '';
+               if ($GLOBALS['TCA'][$table]['types'][$recordTypeValue]) {
+                       $itemList = $GLOBALS['TCA'][$table]['types'][$recordTypeValue]['showitem'];
+                       // Inline may override the type value - setting is given down from InlineRecordContainer if so - used primarily for FAL
+                       $overruleTypesArray = $this->globalOptions['overruleTypesArray'];
+                       if (isset($overruleTypesArray[$recordTypeValue]['showitem'])) {
+                               $itemList = $overruleTypesArray[$recordTypeValue]['showitem'];
+                       }
+               }
+
+               $fieldsArray = GeneralUtility::trimExplode(',', $itemList, TRUE);
+               // Add fields and remove excluded fields
+               $fieldsArray = $this->mergeFieldsWithAddedFields($fieldsArray, $this->getFieldsToAdd($table, $row, $recordTypeValue), $table);
+               $excludeElements = $this->getExcludeElements($table, $row, $recordTypeValue);
+               $fieldsArray = $this->removeExcludeElementsFromFieldArray($fieldsArray, $excludeElements);
+
+               // Streamline the fields array
+               // First, make sure there is always a --div-- definition for the first element
+               if (substr($fieldsArray[0], 0, 7) !== '--div--') {
+                       array_unshift($fieldsArray, '--div--;LLL:EXT:lang/locallang_core.xlf:labels.generalTab');
+               }
+               // If first tab has no label definition, add "general" label
+               $firstTabHasLabel = count(GeneralUtility::trimExplode(';',  $fieldsArray[0])) > 1 ? TRUE : FALSE;
+               if (!$firstTabHasLabel) {
+                       $fieldsArray[0] = '--div--;LLL:EXT:lang/locallang_core.xlf:labels.generalTab';
+               }
+               // If there are at least two --div-- definitions, inner container will be a TabContainer, else a NoTabContainer
+               $tabCount = 0;
+               foreach ($fieldsArray as $field) {
+                       if (substr($field, 0, 7) === '--div--') {
+                               $tabCount++;
+                       }
+               }
+               $hasTabs = TRUE;
+               if ($tabCount < 2) {
+                       // Remove first tab definition again if there is only one tab defined
+                       array_shift($fieldsArray);
+                       $hasTabs = FALSE;
+               }
+
+               $options = $this->globalOptions;
+               $options['fieldsArray'] = $fieldsArray;
+               // Palettes may contain elements that should be excluded, resolved in PaletteContainer
+               $options['excludeElements'] = $excludeElements;
+               $options['defaultLanguageData'] = $this->defaultLanguageData;
+               $options['defaultLanguageDataDiff'] = $this->defaultLanguageDataDiff;
+               $options['additionalPreviewLanguageData'] = $this->additionalPreviewLanguageData;
+
+               if ($hasTabs) {
+                       /** @var TabsContainer $TabsContainer */
+                       $container = GeneralUtility::makeInstance(TabsContainer::class);
+                       $container->setGlobalOptions($options);
+                       $resultArray = $container->render();
+               } else {
+                       /** @var NoTabsContainer $NoTabsContainer */
+                       $container = GeneralUtility::makeInstance(NoTabsContainer::class);
+                       $container->setGlobalOptions($options);
+                       $resultArray = $container->render();
+               }
+
+               return $resultArray;
+       }
+
+       /**
+        * Finds possible field to add to the form, based on subtype fields.
+        *
+        * @param string $table Table name, MUST be in $GLOBALS['TCA']
+        * @param array $row A record from table.
+        * @param string $typeNum A "type" pointer value, probably the one calculated based on the record array.
+        * @return array An array containing two values: 1) Another array containing field names to add and 2) the subtype value field.
+        */
+       protected function getFieldsToAdd($table, $row, $typeNum) {
+               $addElements = array();
+               $subTypeField = '';
+               if ($GLOBALS['TCA'][$table]['types'][$typeNum]['subtype_value_field']) {
+                       $subTypeField = $GLOBALS['TCA'][$table]['types'][$typeNum]['subtype_value_field'];
+                       if (trim($GLOBALS['TCA'][$table]['types'][$typeNum]['subtypes_addlist'][$row[$subTypeField]])) {
+                               $addElements = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['types'][$typeNum]['subtypes_addlist'][$row[$subTypeField]], TRUE);
+                       }
+               }
+               return array($addElements, $subTypeField);
+       }
+
+       /**
+        * Merges the current [types][showitem] array with the array of fields to add for the current subtype field of the "type" value.
+        *
+        * @param array $fields A [types][showitem] list of fields, exploded by ",
+        * @param array $fieldsToAdd The output from getFieldsToAdd()
+        * @param string $table The table name, if we want to consider its palettes when positioning the new elements
+        * @return array Return the modified $fields array.
+        */
+       protected function mergeFieldsWithAddedFields($fields, $fieldsToAdd, $table = '') {
+               if (!empty($fieldsToAdd[0])) {
+                       $c = 0;
+                       $found = FALSE;
+                       foreach ($fields as $fieldInfo) {
+                               list($fieldName, $label, $paletteName) = GeneralUtility::trimExplode(';', $fieldInfo);
+                               if ($fieldName === $fieldsToAdd[1]) {
+                                       $found = TRUE;
+                               } elseif ($fieldName === '--palette--' && $paletteName && $table !== '') {
+                                       // Look inside the palette
+                                       if (is_array($GLOBALS['TCA'][$table]['palettes'][$paletteName])) {
+                                               $itemList = $GLOBALS['TCA'][$table]['palettes'][$paletteName]['showitem'];
+                                               if ($itemList) {
+                                                       $paletteFields = GeneralUtility::trimExplode(',', $itemList, TRUE);
+                                                       foreach ($paletteFields as $info) {
+                                                               $fieldParts = GeneralUtility::trimExplode(';', $info);
+                                                               $theField = $fieldParts[0];
+                                                               if ($theField === $fieldsToAdd[1]) {
+                                                                       $found = TRUE;
+                                                                       break 1;
+                                                               }
+                                                       }
+                                               }
+                                       }
+                               }
+                               if ($found) {
+                                       array_splice($fields, $c + 1, 0, $fieldsToAdd[0]);
+                                       break;
+                               }
+                               $c++;
+                       }
+               }
+               return $fields;
+       }
+
+       /**
+        * @return LanguageService
+        */
+       protected function getLanguageService() {
+               return $GLOBALS['LANG'];
+       }
+
+}
diff --git a/typo3/sysext/backend/Classes/Form/Container/InlineControlContainer.php b/typo3/sysext/backend/Classes/Form/Container/InlineControlContainer.php
new file mode 100644 (file)
index 0000000..f5aa5e1
--- /dev/null
@@ -0,0 +1,657 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Container;
+
+/*
+ * 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\Utility\BackendUtility;
+use TYPO3\CMS\Core\Resource\Folder;
+use TYPO3\CMS\Core\Utility\MathUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Backend\Form\DataPreprocessor;
+use TYPO3\CMS\Lang\LanguageService;
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
+use TYPO3\CMS\Backend\Utility\IconUtility;
+use TYPO3\CMS\Backend\Form\InlineStackProcessor;
+use TYPO3\CMS\Backend\Form\InlineRelatedRecordResolver;
+
+/**
+ * Inline element entry container.
+ *
+ * This container is the entry step to rendering an inline element. It is created by SingleFieldContainer.
+ *
+ * The code creates the main structure for the single inline elements, initializes
+ * the inlineData array, that is manipulated and also returned back in its manipulated state.
+ * The "control" stuff of inline elements is rendered here, for example the "create new" button.
+ *
+ * For each existing inline relation an InlineRecordContainer is called for further processing.
+ */
+class InlineControlContainer extends AbstractContainer {
+
+       /**
+        * Inline data array used in JS, returned as JSON object to frontend
+        *
+        * @var array
+        */
+       protected $inlineData = array();
+
+       /**
+        * @var InlineStackProcessor
+        */
+       protected $inlineStackProcessor;
+
+       /**
+        * Entry method
+        *
+        * @return array As defined in initializeResultArray() of AbstractNode
+        */
+       public function render() {
+               $languageService = $this->getLanguageService();
+
+               $this->inlineData = $this->globalOptions['inlineData'];
+
+               /** @var InlineStackProcessor $inlineStackProcessor */
+               $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
+               $this->inlineStackProcessor = $inlineStackProcessor;
+               $inlineStackProcessor->initializeByGivenStructure($this->globalOptions['inlineStructure']);
+
+               $table = $this->globalOptions['table'];
+               $row = $this->globalOptions['databaseRow'];
+               $field = $this->globalOptions['fieldName'];
+               $parameterArray = $this->globalOptions['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']);
+               $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;
+               }
+               $resultArray['requiredElements'][$parameterArray['itemFormElName']] = array(
+                       $minItems,
+                       $maxItems,
+                       'imgName' => $table . '_' . $row['uid'] . '_' . $field
+               );
+
+               // Add the current inline job to the structure stack
+               $newStructureItem = array(
+                       'table' => $table,
+                       'uid' => $row['uid'],
+                       'field' => $field,
+                       'config' => $config,
+                       'localizationMode' => BackendUtility::getInlineLocalizationMode($table, $config),
+               );
+               // Extract FlexForm parts (if any) from element name, e.g. array('vDEF', 'lDEF', 'FlexField', 'vDEF')
+               if (!empty($parameterArray['itemFormElName'])) {
+                       $flexFormParts = FormEngineUtility::extractFlexFormParts($parameterArray['itemFormElName'], $this->globalOptions['prependFormFieldNames']);
+                       if ($flexFormParts !== NULL) {
+                               $newStructureItem['flexform'] = $flexFormParts;
+                       }
+               }
+               $inlineStackProcessor->pushStableStructureItem($newStructureItem);
+
+               // e.g. data[<table>][<uid>][<field>]
+               $nameForm = $inlineStackProcessor->getCurrentStructureFormPrefix($this->globalOptions['prependFormFieldNames']);
+               // e.g. data-<pid>-<table1>-<uid1>-<field1>-<table2>-<uid2>-<field2>
+               $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->globalOptions['inlineFirstPid']);
+
+               // Get the records related to this inline record
+               $inlineRelatedRecordResolver = GeneralUtility::makeInstance(InlineRelatedRecordResolver::class);
+               $relatedRecords = $inlineRelatedRecordResolver->getRelatedRecords($table, $field, $row, $parameterArray, $config, $this->globalOptions['inlineFirstPid'], $this->globalOptions['prependFormFieldNames']);
+
+               // 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);
+
+               $top = $inlineStackProcessor->getStructureLevel(0);
+
+               $this->inlineData['config'][$nameObject] = array(
+                       'table' => $foreign_table,
+                       'md5' => md5($nameObject)
+               );
+               $this->inlineData['config'][$nameObject . '-' . $foreign_table] = array(
+                       'min' => $minItems,
+                       'max' => $maxItems,
+                       'sortable' => $config['appearance']['useSortable'],
+                       'top' => array(
+                               'table' => $top['table'],
+                               'uid' => $top['uid']
+                       ),
+                       'context' => array(
+                               'config' => $config,
+                               'hmac' => GeneralUtility::hmac(serialize($config)),
+                       ),
+               );
+               $this->inlineData['nested'][$nameObject] = $this->globalOptions['tabAndInlineStack'];
+
+               // If relations are required to be unique, get the uids that have already been used on the foreign side of the relation
+               if ($config['foreign_unique']) {
+                       // 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');
+                       $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(
+                               'max' => $uniqueMax,
+                               'used' => $uniqueIds,
+                               'type' => $selConfig['type'],
+                               'table' => $config['foreign_table'],
+                               'elTable' => $selConfig['table'],
+                               // element/record table (one step down in hierarchy)
+                               'field' => $config['foreign_unique'],
+                               'selector' => $selConfig['selector'],
+                               'possible' => $this->getPossibleRecordsFlat($possibleRecords)
+                       );
+               }
+
+               $resultArray['inlineData'] = $this->inlineData;
+
+               // Render the localization links
+               $localizationLinks = '';
+               if ($language > 0 && $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0 && MathUtility::canBeInterpretedAsInteger($row['uid'])) {
+                       // Add the "Localize all records" link before all child records:
+                       if (isset($config['appearance']['showAllLocalizationLink']) && $config['appearance']['showAllLocalizationLink']) {
+                               $localizationLinks .= ' ' . $this->getLevelInteractionLink('localize', $nameObject . '-' . $foreign_table, $config);
+                       }
+                       // Add the "Synchronize with default language" link before all child records:
+                       if (isset($config['appearance']['showSynchronizationLink']) && $config['appearance']['showSynchronizationLink']) {
+                               $localizationLinks .= ' ' . $this->getLevelInteractionLink('synchronize', $nameObject . '-' . $foreign_table, $config);
+                       }
+               }
+               // 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
+                       if (!$config['foreign_unique']) {
+                               $possibleRecords = $this->getPossibleRecords($table, $field, $row, $config);
+                               $uniqueIds = array();
+                       }
+                       $selectorBox = $this->renderPossibleRecordsSelector($possibleRecords, $config, $uniqueIds);
+                       $html .= $selectorBox . $localizationLinks;
+               }
+               // Render the level links (create new record):
+               $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 . '">';
+               // 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) {
+                       $config['inline']['inlineNewButtonStyle'] = 'display: none;';
+               }
+               // Add the level links before all child records:
+               if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'top') {
+                       $html .= '<div class="form-group">' . $levelLinks . $localizationLinks . '</div>';
+               }
+               $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->globalOptions;
+                               $options['inlineRelatedRecordToRender'] = $rec;
+                               $options['inlineRelatedRecordConfig'] = $config;
+                               $options['inlineData'] = $this->inlineData;
+                               $options['inlineStructure'] = $inlineStackProcessor->getStructure();
+                               /** @var InlineRecordContainer $inlineRecordContainer */
+                               $inlineRecordContainer = GeneralUtility::makeInstance(InlineRecordContainer::class);
+                               $childArray = $inlineRecordContainer->setGlobalOptions($options)->render();
+                               $html .= $childArray['html'];
+                               $childArray['html'] = '';
+                               $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
+                               if (!isset($rec['__virtual']) || !$rec['__virtual']) {
+                                       $relationList[] = $rec['uid'];
+                               }
+                       }
+               }
+               $html .= '</div>';
+               // Add the level links after all child records:
+               if ($config['appearance']['levelLinksPosition'] ===  'both' || $config['appearance']['levelLinksPosition'] === 'bottom') {
+                       $html .= $levelLinks . $localizationLinks;
+               }
+               if (is_array($config['customControls'])) {
+                       $html .= '<div id="' . $nameObject . '_customControls">';
+                       foreach ($config['customControls'] as $customControlConfig) {
+                               $parameters = array(
+                                       'table' => $table,
+                                       'field' => $field,
+                                       'row' => $row,
+                                       'nameObject' => $nameObject,
+                                       'nameForm' => $nameForm,
+                                       'config' => $config
+                               );
+                               $html .= GeneralUtility::callUserFunction($customControlConfig, $parameters, $this);
+                       }
+                       $html .= '</div>';
+               }
+               // Add Drag&Drop functions for sorting to FormEngine::$additionalJS_post
+               if (count($relationList) > 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) . '" class="inlineRecord" />';
+               // Close the wrap for all inline fields (container)
+               $html .= '</div>';
+
+               $resultArray['html'] = $html;
+               return $resultArray;
+       }
+
+       /**
+        * 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 $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) {
+               $uniqueIds = array();
+               if (isset($conf['foreign_unique']) && $conf['foreign_unique'] && count($records)) {
+                       foreach ($records as $rec) {
+                               // Skip virtual records (e.g. shown in localization mode):
+                               if (!isset($rec['__virtual']) || !$rec['__virtual']) {
+                                       $value = $rec[$conf['foreign_unique']];
+                                       // Split the value and extract the table and uid:
+                                       if ($splitValue) {
+                                               $valueParts = GeneralUtility::trimExplode('|', $value);
+                                               $itemParts = explode('_', $valueParts[0]);
+                                               $value = array(
+                                                       'uid' => array_pop($itemParts),
+                                                       'table' => implode('_', $itemParts)
+                                               );
+                                       }
+                                       $uniqueIds[$rec['uid']] = $value;
+                               }
+                       }
+               }
+               return $uniqueIds;
+       }
+
+       /**
+        * Get possible records.
+        * Copied from FormEngine and modified.
+        *
+        * @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 $conf An array with additional configuration options.
+        * @param string $checkForConfField For which field in the foreign_table the possible records should be fetched
+        * @return mixed Array of possible record items; FALSE if type is "group/db", then everything could be "possible
+        */
+       protected function getPossibleRecords($table, $field, $row, $conf, $checkForConfField = 'foreign_selector') {
+               $backendUser = $this->getBackendUserAuthentication();
+               $languageService = $this->getLanguageService();
+
+               // ctrl configuration from TCA:
+               $tcaTableCtrl = $GLOBALS['TCA'][$table]['ctrl'];
+               // Field configuration from TCA:
+               $foreign_check = $conf[$checkForConfField];
+               $foreignConfig = FormEngineUtility::getInlinePossibleRecordsSelectorConfig($conf, $foreign_check);
+               $PA = $foreignConfig['PA'];
+               $config = $PA['fieldConf']['config'];
+               if ($foreignConfig['type'] == 'select') {
+                       // Getting the selector box items from the system
+                       $selItems = FormEngineUtility::addSelectOptionsToItemArray(
+                               FormEngineUtility::initItemArray($PA['fieldConf']),
+                               $PA['fieldConf'],
+                               FormEngineUtility::getTSconfigForTableRow($table, $row),
+                               $field
+                       );
+
+                       // Possibly filter some items:
+                       $selItems = ArrayUtility::keepItemsInArray(
+                               $selItems,
+                               $PA['fieldTSConfig']['keepItems'],
+                               function ($value) {
+                                       return $value[1];
+                               }
+                       );
+
+                       // Possibly add some items:
+                       $selItems = FormEngineUtility::addItems($selItems, $PA['fieldTSConfig']['addItems.']);
+                       if (isset($config['itemsProcFunc']) && $config['itemsProcFunc']) {
+                               $dataPreprocessor = GeneralUtility::makeInstance(DataPreprocessor::class);
+                               $selItems = $dataPreprocessor->procItems($selItems, $PA['fieldTSConfig']['itemsProcFunc.'], $config, $table, $row, $field);
+                       }
+                       // Possibly remove some items:
+                       $removeItems = GeneralUtility::trimExplode(',', $PA['fieldTSConfig']['removeItems'], TRUE);
+                       foreach ($selItems as $tk => $p) {
+                               // Checking languages and authMode:
+                               $languageDeny = $tcaTableCtrl['languageField'] && (string)$tcaTableCtrl['languageField'] === $field && !$backendUser->checkLanguageAccess($p[1]);
+                               $authModeDeny = $config['type'] == 'select' && $config['authMode'] && !$backendUser->checkAuthMode($table, $field, $p[1], $config['authMode']);
+                               if (in_array($p[1], $removeItems) || $languageDeny || $authModeDeny) {
+                                       unset($selItems[$tk]);
+                               } else {
+                                       if (isset($PA['fieldTSConfig']['altLabels.'][$p[1]])) {
+                                               $selItems[$tk][0] = htmlspecialchars($languageService->sL($PA['fieldTSConfig']['altLabels.'][$p[1]]));
+                                       }
+                                       if (isset($PA['fieldTSConfig']['altIcons.'][$p[1]])) {
+                                               $selItems[$tk][2] = $PA['fieldTSConfig']['altIcons.'][$p[1]];
+                                       }
+                               }
+                               // Removing doktypes with no access:
+                               if (($table === 'pages' || $table === 'pages_language_overlay') && $field === 'doktype') {
+                                       if (!($backendUser->isAdmin() || GeneralUtility::inList($backendUser->groupData['pagetypes_select'], $p[1]))) {
+                                               unset($selItems[$tk]);
+                                       }
+                               }
+                       }
+               } else {
+                       $selItems = FALSE;
+               }
+               return $selItems;
+       }
+
+       /**
+        * Makes a flat array from the $possibleRecords array.
+        * The key of the flat array is the value of the record,
+        * the value of the flat array is the label of the record.
+        *
+        * @param array $possibleRecords The possibleRecords array (for select fields)
+        * @return mixed A flat array with key=uid, value=label; if $possibleRecords isn't an array, FALSE is returned.
+        */
+       protected function getPossibleRecordsFlat($possibleRecords) {
+               $flat = FALSE;
+               if (is_array($possibleRecords)) {
+                       $flat = array();
+                       foreach ($possibleRecords as $record) {
+                               $flat[$record[1]] = $record[0];
+                       }
+               }
+               return $flat;
+       }
+
+       /**
+        * Creates the HTML code of a general link to be used on a level of inline children.
+        * The possible keys for the parameter $type are 'newRecord', 'localize' and 'synchronize'.
+        *
+        * @param string $type The link type, values are 'newRecord', 'localize' and 'synchronize'.
+        * @param string $objectPrefix The "path" to the child record to create (e.g. 'data-parentPageId-partenTable-parentUid-parentField-childTable]')
+        * @param array $conf TCA configuration of the parent(!) field
+        * @return string The HTML code of the new link, wrapped in a div
+        */
+       protected function getLevelInteractionLink($type, $objectPrefix, $conf = array()) {
+               $languageService = $this->getLanguageService();
+               $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->globalOptions['inlineFirstPid']);
+               $attributes = array();
+               switch ($type) {
+                       case 'newRecord':
+                               $title = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.createnew', TRUE);
+                               $icon = 'actions-document-new';
+                               $className = 'typo3-newRecordLink';
+                               $attributes['class'] = 'btn btn-default inlineNewButton ' . $this->inlineData['config'][$nameObject]['md5'];
+                               $attributes['onclick'] = 'return inline.createNewRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ')';
+                               if (!empty($conf['inline']['inlineNewButtonStyle'])) {
+                                       $attributes['style'] = $conf['inline']['inlineNewButtonStyle'];
+                               }
+                               if (!empty($conf['appearance']['newRecordLinkAddTitle'])) {
+                                       $title = sprintf(
+                                               $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.createnew.link', TRUE),
+                                               $languageService->sL($GLOBALS['TCA'][$conf['foreign_table']]['ctrl']['title'], TRUE)
+                                       );
+                               } elseif (isset($conf['appearance']['newRecordLinkTitle']) && $conf['appearance']['newRecordLinkTitle'] !== '') {
+                                       $title = $languageService->sL($conf['appearance']['newRecordLinkTitle'], TRUE);
+                               }
+                               break;
+                       case 'localize':
+                               $title = $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localizeAllRecords', 1);
+                               $icon = 'actions-document-localize';
+                               $className = 'typo3-localizationLink';
+                               $attributes['class'] = 'btn btn-default';
+                               $attributes['onclick'] = 'return inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($objectPrefix) . ', \'localize\')';
+                               break;
+                       case 'synchronize':
+                               $title = $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:synchronizeWithOriginalLanguage', TRUE);
+                               $icon = 'actions-document-synchronize';
+                               $className = 'typo3-synchronizationLink';
+                               $attributes['class'] = 'btn btn-default inlineNewButton ' . $this->inlineData['config'][$nameObject]['md5'];
+                               $attributes['onclick'] = 'return inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($objectPrefix) . ', \'synchronize\')';
+                               break;
+                       default:
+                               $title = '';
+                               $icon = '';
+                               $className = '';
+               }
+               // Create the link:
+               $icon = $icon ? IconUtility::getSpriteIcon($icon, array('title' => htmlspecialchars($title))) : '';
+               $link = $this->wrapWithAnchor($icon . $title, '#', $attributes);
+               return '<div' . ($className ? ' class="' . $className . '"' : '') . '>' . $link . '</div>';
+       }
+
+       /**
+        * Wraps a text with an anchor and returns the HTML representation.
+        *
+        * @param string $text The text to be wrapped by an anchor
+        * @param string $link  The link to be used in the anchor
+        * @param array $attributes Array of attributes to be used in the anchor
+        * @return string The wrapped text as HTML representation
+        */
+       protected function wrapWithAnchor($text, $link, $attributes = array()) {
+               $link = trim($link);
+               $result = '<a href="' . ($link ?: '#') . '"';
+               foreach ($attributes as $key => $value) {
+                       $result .= ' ' . $key . '="' . htmlspecialchars(trim($value)) . '"';
+               }
+               $result .= '>' . $text . '</a>';
+               return $result;
+       }
+
+       /**
+        * Get a selector as used for the select type, to select from all available
+        * records and to create a relation to the embedding record (e.g. like MM).
+        *
+        * @param array $selItems Array of all possible records
+        * @param array $conf TCA configuration of the parent(!) field
+        * @param array $uniqueIds The uids that have already been used and should be unique
+        * @return string A HTML <select> box with all possible records
+        */
+       protected function renderPossibleRecordsSelector($selItems, $conf, $uniqueIds = array()) {
+               $foreign_selector = $conf['foreign_selector'];
+               $selConfig = FormEngineUtility::getInlinePossibleRecordsSelectorConfig($conf, $foreign_selector);
+               $item  = '';
+               if ($selConfig['type'] === 'select') {
+                       $item = $this->renderPossibleRecordsSelectorTypeSelect($selItems, $conf, $selConfig['PA'], $uniqueIds);
+               } elseif ($selConfig['type'] === 'groupdb') {
+                       $item = $this->renderPossibleRecordsSelectorTypeGroupDB($conf, $selConfig['PA']);
+               }
+               return $item;
+       }
+
+       /**
+        * Generate a link that opens an element browser in a new window.
+        * For group/db there is no way to use a "selector" like a <select>|</select>-box.
+        *
+        * @param array $conf TCA configuration of the parent(!) field
+        * @param array $PA An array with additional configuration options
+        * @return string A HTML link that opens an element browser in a new window
+        */
+       protected function renderPossibleRecordsSelectorTypeGroupDB($conf, &$PA) {
+               $backendUser = $this->getBackendUserAuthentication();
+
+               $config = $PA['fieldConf']['config'];
+               ArrayUtility::mergeRecursiveWithOverrule($config, $conf);
+               $foreign_table = $config['foreign_table'];
+               $allowed = $config['allowed'];
+               $objectPrefix = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->globalOptions['inlineFirstPid']) . '-' . $foreign_table;
+               $mode = 'db';
+               $showUpload = FALSE;
+               if (!empty($config['appearance']['createNewRelationLinkTitle'])) {
+                       $createNewRelationText = $this->getLanguageService()->sL($config['appearance']['createNewRelationLinkTitle'], TRUE);
+               } else {
+                       $createNewRelationText = $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:cm.createNewRelation', TRUE);
+               }
+               if (is_array($config['appearance'])) {
+                       if (isset($config['appearance']['elementBrowserType'])) {
+                               $mode = $config['appearance']['elementBrowserType'];
+                       }
+                       if ($mode === 'file') {
+                               $showUpload = TRUE;
+                       }
+                       if (isset($config['appearance']['fileUploadAllowed'])) {
+                               $showUpload = (bool)$config['appearance']['fileUploadAllowed'];
+                       }
+                       if (isset($config['appearance']['elementBrowserAllowed'])) {
+                               $allowed = $config['appearance']['elementBrowserAllowed'];
+                       }
+               }
+               $browserParams = '|||' . $allowed . '|' . $objectPrefix . '|inline.checkUniqueElement||inline.importElement';
+               $onClick = 'setFormValueOpenBrowser(' . GeneralUtility::quoteJSvalue($mode) . ', ' . GeneralUtility::quoteJSvalue($browserParams) . '); return false;';
+
+               $item = '
+                       <a href="#" class="btn btn-default" onclick="' . htmlspecialchars($onClick) . '">
+                               ' . IconUtility::getSpriteIcon('actions-insert-record', array('title' => $createNewRelationText)) . '
+                               ' . $createNewRelationText . '
+                       </a>';
+
+               $isDirectFileUploadEnabled = (bool)$this->getBackendUserAuthentication()->uc['edit_docModuleUpload'];
+               if ($showUpload && $isDirectFileUploadEnabled) {
+                       $folder = $backendUser->getDefaultUploadFolder();
+                       if (
+                               $folder instanceof Folder
+                               && $folder->checkActionPermission('add')
+                       ) {
+                               $maxFileSize = GeneralUtility::getMaxUploadFileSize() * 1024;
+                               $item .= ' <a href="#" class="btn btn-default t3-drag-uploader"
+                                       style="display:none"
+                                       data-dropzone-target="#' . htmlspecialchars($this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->globalOptions['inlineFirstPid'])) . '"
+                                       data-insert-dropzone-before="1"
+                                       data-file-irre-object="' . htmlspecialchars($objectPrefix) . '"
+                                       data-file-allowed="' . htmlspecialchars($allowed) . '"
+                                       data-target-folder="' . htmlspecialchars($folder->getCombinedIdentifier()) . '"
+                                       data-max-file-size="' . htmlspecialchars($maxFileSize) . '"
+                                       ><span class="t3-icon t3-icon-actions t3-icon-actions-edit t3-icon-edit-upload">&nbsp;</span>';
+                               $item .= $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:file_upload.select-and-submit', TRUE);
+                               $item .= '</a>';
+                       }
+               }
+
+               $item = '<div class="form-control-wrap">' . $item . '</div>';
+               $allowedList = '';
+               $allowedArray = GeneralUtility::trimExplode(',', $allowed, TRUE);
+               $allowedLabel = $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:cm.allowedFileExtensions', TRUE);
+               foreach ($allowedArray as $allowedItem) {
+                       $allowedList .= '<span class="label label-success">' . strtoupper($allowedItem) . '</span> ';
+               }
+               if (!empty($allowedList)) {
+                       $item .= '<div class="help-block">' . $allowedLabel . '<br>' . $allowedList . '</div>';
+               }
+               $item = '<div class="form-group">' . $item . '</div>';
+               return $item;
+       }
+
+       /**
+        * Get a selector as used for the select type, to select from all available
+        * records and to create a relation to the embedding record (e.g. like MM).
+        *
+        * @param array $selItems Array of all possible records
+        * @param array $conf TCA configuration of the parent(!) field
+        * @param array $PA An array with additional configuration options
+        * @param array $uniqueIds The uids that have already been used and should be unique
+        * @return string A HTML <select> box with all possible records
+        */
+       protected function renderPossibleRecordsSelectorTypeSelect($selItems, $conf, &$PA, $uniqueIds = array()) {
+               $foreign_table = $conf['foreign_table'];
+               $foreign_selector = $conf['foreign_selector'];
+               $PA = array();
+               $PA['fieldConf'] = $GLOBALS['TCA'][$foreign_table]['columns'][$foreign_selector];
+               $PA['fieldTSConfig'] = FormEngineUtility::getTSconfigForTableRow($foreign_table, array(), $foreign_selector);
+               $config = $PA['fieldConf']['config'];
+               $item = '';
+               // @todo $disabled is not present - should be read from config?
+               $disabled = FALSE;
+               if (!$disabled) {
+                       $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->globalOptions['inlineFirstPid']);;
+                       // Create option tags:
+                       $opt = array();
+                       $styleAttrValue = '';
+                       foreach ($selItems as $p) {
+                               if ($config['iconsInOptionTags']) {
+                                       $styleAttrValue = FormEngineUtility::optionTagStyle($p[2]);
+                               }
+                               if (!in_array($p[1], $uniqueIds)) {
+                                       $opt[] = '<option value="' . htmlspecialchars($p[1]) . '"' . ($styleAttrValue ? ' style="' . htmlspecialchars($styleAttrValue) . '"' : '') . '>' . htmlspecialchars($p[0]) . '</option>';
+                               }
+                       }
+                       // Put together the selector box:
+                       $itemListStyle = isset($config['itemListStyle']) ? ' style="' . htmlspecialchars($config['itemListStyle']) . '"' : '';
+                       $size = (int)$conf['size'];
+                       $size = $conf['autoSizeMax'] ? MathUtility::forceIntegerInRange(count($selItems) + 1, MathUtility::forceIntegerInRange($size, 1), $conf['autoSizeMax']) : $size;
+                       $onChange = 'return inline.importNewRecord(' . GeneralUtility::quoteJSvalue($nameObject . '-' . $conf['foreign_table']) . ')';
+                       $item = '
+                               <select id="' . $nameObject . '-' . $conf['foreign_table'] . '_selector" class="form-control"' . ($size ? ' size="' . $size . '"' : '') . ' onchange="' . htmlspecialchars($onChange) . '"' . $PA['onFocus'] . $itemListStyle . ($conf['foreign_unique'] ? ' isunique="isunique"' : '') . '>
+                                       ' . implode('', $opt) . '
+                               </select>';
+
+                       if ($size <= 1) {
+                               // Add a "Create new relation" link for adding new relations
+                               // This is necessary, if the size of the selector is "1" or if
+                               // there is only one record item in the select-box, that is selected by default
+                               // The selector-box creates a new relation on using a onChange event (see some line above)
+                               if (!empty($conf['appearance']['createNewRelationLinkTitle'])) {
+                                       $createNewRelationText = $this->getLanguageService()->sL($conf['appearance']['createNewRelationLinkTitle'], TRUE);
+                               } else {
+                                       $createNewRelationText = $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:cm.createNewRelation', TRUE);
+                               }
+                               $item .= '
+                               <span class="input-group-btn">
+                                       <a href="#" class="btn btn-default" onclick="' . htmlspecialchars($onChange) . '">
+                                               ' . IconUtility::getSpriteIcon('actions-document-new', array('title' => $createNewRelationText)) . $createNewRelationText . '
+                                       </a>
+                               </span>';
+                       } else {
+                               $item .= '
+                               <span class="input-group-btn btn"></span>';
+                       }
+
+                       // Wrap the selector and add a spacer to the bottom
+
+                       $item = '<div class="input-group form-group ' . $this->inlineData['config'][$nameObject]['md5'] . '">' . $item . '</div>';
+               }
+               return $item;
+       }
+
+       /**
+        * @return BackendUserAuthentication
+        */
+       protected function getBackendUserAuthentication() {
+               return $GLOBALS['BE_USER'];
+       }
+
+       /**
+        * @return LanguageService
+        */
+       protected function getLanguageService() {
+               return $GLOBALS['LANG'];
+       }
+
+}
\ No newline at end of file
diff --git a/typo3/sysext/backend/Classes/Form/Container/InlineRecordContainer.php b/typo3/sysext/backend/Classes/Form/Container/InlineRecordContainer.php
new file mode 100644 (file)
index 0000000..76d5900
--- /dev/null
@@ -0,0 +1,737 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Container;
+
+/*
+ * 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\Element\InlineElementHookInterface;
+use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\Resource\ProcessedFile;
+use TYPO3\CMS\Core\Resource\ResourceFactory;
+use TYPO3\CMS\Core\Utility\MathUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Lang\LanguageService;
+use TYPO3\CMS\Backend\Utility\IconUtility;
+use TYPO3\CMS\Core\Type\Bitmask\Permission;
+use TYPO3\CMS\Core\Messaging\FlashMessage;
+use TYPO3\CMS\Core\Database\RelationHandler;
+use TYPO3\CMS\Core\Database\DatabaseConnection;
+use TYPO3\CMS\Backend\Form\InlineStackProcessor;
+use TYPO3\CMS\Backend\Form\InlineRelatedRecordResolver;
+
+/**
+ * Render a single inline record relation.
+ *
+ * This container is called by InlineControlContainer to render single existing records.
+ * Furthermore it is called by FormEngine for an incoming ajax request to expand an existing record
+ * or to create a new one.
+ *
+ * This container creates the outer HTML of single inline records - eg. drag and drop and delete buttons.
+ * For rendering of the record itself processing is handed over to FullRecordContainer.
+ */
+class InlineRecordContainer extends AbstractContainer {
+
+       /**
+        * Inline data array used for JSON output
+        *
+        * @var array
+        */
+       protected $inlineData = array();
+
+       /**
+        * @var InlineStackProcessor
+        */
+       protected $inlineStackProcessor;
+
+       /**
+        * Array containing instances of hook classes called once for IRRE objects
+        *
+        * @var array
+        */
+       protected $hookObjects = array();
+
+       /**
+        * Entry method
+        *
+        * @return array As defined in initializeResultArray() of AbstractNode
+        */
+       public function render() {
+               $this->inlineData = $this->globalOptions['inlineData'];
+
+               /** @var InlineStackProcessor $inlineStackProcessor */
+               $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
+               $this->inlineStackProcessor = $inlineStackProcessor;
+               $inlineStackProcessor->initializeByGivenStructure($this->globalOptions['inlineStructure']);
+
+               $this->initHookObjects();
+
+               $row = $this->globalOptions['databaseRow'];
+               $parentUid = $row['uid'];
+               $record = $this->globalOptions['inlineRelatedRecordToRender'];
+               $config = $this->globalOptions['inlineRelatedRecordConfig'];
+
+               $foreign_table = $config['foreign_table'];
+               $foreign_selector = $config['foreign_selector'];
+               $resultArray = $this->initializeResultArray();
+               $html = '';
+
+               // Send a mapping information to the browser via JSON:
+               // e.g. data[<curTable>][<curId>][<curField>] => data-<pid>-<parentTable>-<parentId>-<parentField>-<curTable>-<curId>-<curField>
+               $formPrefix = $inlineStackProcessor->getCurrentStructureFormPrefix($this->globalOptions['prependFormFieldNames']);
+               $domObjectId = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->globalOptions['inlineFirstPid']);
+               $this->inlineData['map'][$formPrefix] = $domObjectId;
+
+               $resultArray['inlineData'] = $this->inlineData;
+
+               // 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'];
+               // If there is a selector field, normalize it:
+               if ($foreign_selector) {
+                       $record[$foreign_selector] = $this->normalizeUid($record[$foreign_selector]);
+               }
+               if (!$this->checkAccess(($isNewRecord ? 'new' : 'edit'), $foreign_table, $record['uid'])) {
+                       // @todo: Suddenly returning something different than the usual return array is not a cool thing ...
+                       // @todo: Inline ajax relies on this at the moment, though.
+                       return FALSE;
+               }
+               // 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) {
+                       // Get configuration:
+                       $collapseAll = isset($config['appearance']['collapseAll']) && $config['appearance']['collapseAll'];
+                       $expandAll = isset($config['appearance']['collapseAll']) && !$config['appearance']['collapseAll'];
+                       $ajaxLoad = isset($config['appearance']['ajaxLoad']) && !$config['appearance']['ajaxLoad'] ? FALSE : TRUE;
+                       if ($isNewRecord) {
+                               // Show this record expanded or collapsed
+                               $isExpanded = $expandAll || (!$collapseAll ? 1 : 0);
+                       } else {
+                               $isExpanded = $config['renderFieldsOnly'] || !$collapseAll && $this->getExpandedCollapsedState($foreign_table, $record['uid']) || $expandAll;
+                       }
+                       // Render full content ONLY IF this is a 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);
+                               $html = $childArray['html'];
+                               $childArray['html'] = '';
+                               $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
+
+                               // Replace returnUrl in Wizard-Code, if this is an AJAX call
+                               $ajaxArguments = GeneralUtility::_GP('ajax');
+                               if (isset($ajaxArguments[2]) && trim($ajaxArguments[2]) != '') {
+                                       $html = str_replace('P[returnUrl]=%2F' . rawurlencode(TYPO3_mainDir) . 'ajax.php', 'P[returnUrl]=' . rawurlencode($ajaxArguments[2]), $html);
+                               }
+                       } else {
+                               // This string is the marker for the JS-function to check if the full content has already been loaded
+                               $html = '<!--notloaded-->';
+                       }
+                       if ($isNewRecord) {
+                               // Get the top parent table
+                               $top = $this->inlineStackProcessor->getStructureLevel(0);
+                               $ucFieldName = 'uc[inlineView][' . $top['table'] . '][' . $top['uid'] . ']' . $appendFormFieldNames;
+                               // Set additional fields for processing for saving
+                               $html .= '<input type="hidden" name="' . $this->globalOptions['prependFormFieldNames'] . $appendFormFieldNames . '[pid]" value="' . $record['pid'] . '"/>';
+                               $html .= '<input type="hidden" name="' . $ucFieldName . '" value="' . $isExpanded . '" />';
+                       } else {
+                               // Set additional field for processing for saving
+                               $html .= '<input type="hidden" name="' . $this->globalOptions['prependCmdFieldNames'] . $appendFormFieldNames . '[delete]" value="1" disabled="disabled" />';
+                               if (!$isExpanded
+                                       && !empty($GLOBALS['TCA'][$foreign_table]['ctrl']['enablecolumns']['disabled'])
+                                       && $ajaxLoad
+                               ) {
+                                       $checked = !empty($record['hidden']) ? ' checked="checked"' : '';
+                                       $html .= '<input type="checkbox" name="' . $this->globalOptions['prependFormFieldNames'] . $appendFormFieldNames . '[hidden]_0" value="1"' . $checked . ' />';
+                                       $html .= '<input type="input" name="' . $this->globalOptions['prependFormFieldNames'] . $appendFormFieldNames . '[hidden]" value="' . $record['hidden'] . '" />';
+                               }
+                       }
+                       // If this record should be shown collapsed
+                       $class = $isExpanded ? 'panel-visible' : 'panel-collapsed';
+               }
+               if ($config['renderFieldsOnly']) {
+                       $html = $html . $combinationHtml;
+               } else {
+                       // Set the record container with data for output
+                       if ($isVirtualRecord) {
+                               $class .= ' t3-form-field-container-inline-placeHolder';
+                       }
+                       if (isset($record['hidden']) && (int)$record['hidden']) {
+                               $class .= ' t3-form-field-container-inline-hidden';
+                       }
+                       $class .= ($isNewRecord ? ' inlineIsNewRecord' : '');
+                       $html = '
+                               <div class="panel panel-default panel-condensed ' . trim($class) . '" id="' . $objectId . '_div">
+                                       <div class="panel-heading" data-toggle="formengine-inline" id="' . $objectId . '_header">
+                                               <div class="form-irre-header">
+                                                       <div class="form-irre-header-cell form-irre-header-icon">
+                                                               <span class="caret"></span>
+                                                       </div>
+                                                       ' . $this->renderForeignRecordHeader($parentUid, $foreign_table, $record, $config, $isVirtualRecord) . '
+                                               </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>
+                               </div>';
+               }
+
+               $resultArray['html'] = $html;
+               return $resultArray;
+       }
+
+       /**
+        * Creates main container for foreign record and renders it
+        *
+        * @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
+        */
+       protected function renderRecord($table, array $row, array $overruleTypesArray = array()) {
+               $domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->globalOptions['inlineFirstPid']);
+               $options = $this->globalOptions;
+               $options['inlineData'] = $this->inlineData;
+               $options['databaseRow'] = $row;
+               $options['table'] = $table;
+               $options['tabAndInlineStack'][] = array(
+                       'inline',
+                       $domObjectId . '-' . $table . '-' . $row['uid'],
+               );
+               $options['overruleTypesArray'] = $overruleTypesArray;
+               /** @var FullRecordContainer $entryContainer */
+               $entryContainer = GeneralUtility::makeInstance(FullRecordContainer::class);
+               return $entryContainer->setGlobalOptions($options)->render();
+       }
+
+       /**
+        * Render a table with FormEngine, that occurs on a 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 (normaly post-processed by \TYPO3\CMS\Backend\Form\DataPreprocessor)
+        * @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?
+        */
+       protected function renderCombinationTable($record, $appendFormFieldNames, $config = array()) {
+               $resultArray = $this->initializeResultArray();
+
+               $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:
+                       if ($record[$foreign_selector] && MathUtility::canBeInterpretedAsInteger($record[$foreign_selector])) {
+                               $inlineRelatedRecordResolver = GeneralUtility::makeInstance(InlineRelatedRecordResolver::class);
+                               $comboRecord = $inlineRelatedRecordResolver->getRecord($comboConfig['foreign_table'], $record[$foreign_selector]);
+                               $isNewRecord = FALSE;
+                       } else {
+                               $inlineRelatedRecordResolver = GeneralUtility::makeInstance(InlineRelatedRecordResolver::class);
+                               $comboRecord = $inlineRelatedRecordResolver->getNewRecord($this->globalOptions['inlineFirstPid'], $comboConfig['foreign_table']);
+                               $isNewRecord = TRUE;
+                       }
+                       $flashMessage = GeneralUtility::makeInstance(
+                               FlashMessage::class,
+                               $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:warning.inline_use_combination'),
+                               '',
+                               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);
+
+                       // 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 = $this->globalOptions['prependFormFieldNames'] . '[' . $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 = $this->globalOptions['prependFormFieldNames'] . $appendFormFieldNames . '[' . $foreign_selector . ']';
+                               $resultArray['html'] .= '<input type="hidden" name="' . $parentFormFieldName . '" value="' . $comboRecord['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 $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) {
+               // Init:
+               $domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->globalOptions['inlineFirstPid']);
+               $objectId = $domObjectId . '-' . $foreign_table . '-' . $rec['uid'];
+               // We need the returnUrl of the main script when loading the fields via AJAX-call (to correct wizard code, so include it as 3rd parameter)
+               // Pre-Processing:
+               $isOnSymmetricSide = RelationHandler::isOnSymmetricSide($parentUid, $config, $rec);
+               $hasForeignLabel = (bool)(!$isOnSymmetricSide && $config['foreign_label']);
+               $hasSymmetricLabel = (bool)$isOnSymmetricSide && $config['symmetric_label'];
+
+               // Get the record title/label for a record:
+               // Try using a self-defined user function only for formatted labels
+               if (isset($GLOBALS['TCA'][$foreign_table]['ctrl']['formattedLabel_userFunc'])) {
+                       $params = array(
+                               'table' => $foreign_table,
+                               'row' => $rec,
+                               'title' => '',
+                               'isOnSymmetricSide' => $isOnSymmetricSide,
+                               'options' => isset($GLOBALS['TCA'][$foreign_table]['ctrl']['formattedLabel_userFunc_options'])
+                                       ? $GLOBALS['TCA'][$foreign_table]['ctrl']['formattedLabel_userFunc_options']
+                                       : array(),
+                               'parent' => array(
+                                       'uid' => $parentUid,
+                                       'config' => $config
+                               )
+                       );
+                       // callUserFunction requires a third parameter, but we don't want to give $this as reference!
+                       $null = NULL;
+                       GeneralUtility::callUserFunction($GLOBALS['TCA'][$foreign_table]['ctrl']['formattedLabel_userFunc'], $params, $null);
+                       $recTitle = $params['title'];
+
+                       // Try using a normal self-defined user function
+               } elseif (isset($GLOBALS['TCA'][$foreign_table]['ctrl']['label_userFunc'])) {
+                       $params = array(
+                               'table' => $foreign_table,
+                               'row' => $rec,
+                               'title' => '',
+                               'isOnSymmetricSide' => $isOnSymmetricSide,
+                               'parent' => array(
+                                       'uid' => $parentUid,
+                                       'config' => $config
+                               )
+                       );
+                       // callUserFunction requires a third parameter, but we don't want to give $this as reference!
+                       $null = NULL;
+                       GeneralUtility::callUserFunction($GLOBALS['TCA'][$foreign_table]['ctrl']['label_userFunc'], $params, $null);
+                       $recTitle = $params['title'];
+               } elseif ($hasForeignLabel || $hasSymmetricLabel) {
+                       $titleCol = $hasForeignLabel ? $config['foreign_label'] : $config['symmetric_label'];
+                       $foreignConfig = FormEngineUtility::getInlinePossibleRecordsSelectorConfig($config, $titleCol);
+                       // Render title for everything else than group/db:
+                       if ($foreignConfig['type'] !== 'groupdb') {
+                               $recTitle = BackendUtility::getProcessedValueExtra($foreign_table, $titleCol, $rec[$titleCol], 0, 0, FALSE);
+                       } else {
+                               // $recTitle could be something like: "tx_table_123|...",
+                               $valueParts = GeneralUtility::trimExplode('|', $rec[$titleCol]);
+                               $itemParts = GeneralUtility::revExplode('_', $valueParts[0], 2);
+                               $recTemp = BackendUtility::getRecordWSOL($itemParts[0], $itemParts[1]);
+                               $recTitle = BackendUtility::getRecordTitle($itemParts[0], $recTemp, FALSE);
+                       }
+                       $recTitle = BackendUtility::getRecordTitlePrep($recTitle);
+                       if (trim($recTitle) === '') {
+                               $recTitle = BackendUtility::getNoRecordTitle(TRUE);
+                       }
+               } else {
+                       $recTitle = BackendUtility::getRecordTitle($foreign_table, $rec, TRUE);
+               }
+
+               $altText = BackendUtility::getRecordIconAltText($rec, $foreign_table);
+               $iconImg = IconUtility::getSpriteIconForRecord($foreign_table, $rec, array('title' => htmlspecialchars($altText), 'id' => $objectId . '_icon'));
+               $label = '<span id="' . $objectId . '_label">' . $recTitle . '</span>';
+               $ctrl = $this->renderForeignRecordHeaderControl($parentUid, $foreign_table, $rec, $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));
+                       $fileUid = array_pop(BackendUtility::splitTable_Uid($firstElement));
+
+                       if (!empty($fileUid)) {
+                               $fileObject = ResourceFactory::getInstance()->getFileObject($fileUid);
+                               if ($fileObject && $fileObject->isMissing()) {
+                                       $flashMessage = \TYPO3\CMS\Core\Resource\Utility\BackendUtility::getFlashMessageForMissingFile($fileObject);
+                                       $thumbnail = $flashMessage->render();
+                               } elseif ($fileObject) {
+                                       $imageSetup = $config['appearance']['headerThumbnail'];
+                                       unset($imageSetup['field']);
+                                       if (!empty($rec['crop'])) {
+                                               $imageSetup['crop'] = $rec['crop'];
+                                       }
+                                       $imageSetup = array_merge(array('width' => '45', 'height' => '45c'), $imageSetup);
+                                       $processedImage = $fileObject->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $imageSetup);
+                                       // Only use a thumbnail if the processing process was successful by checking if image width is set
+                                       if ($processedImage->getProperty('width')) {
+                                               $imageUrl = $processedImage->getPublicUrl(TRUE);
+                                               $thumbnail = '<img src="' . $imageUrl . '" ' .
+                                                                        'width="' . $processedImage->getProperty('width') . '" ' .
+                                                                        'height="' . $processedImage->getProperty('height') . '" ' .
+                                                                        'alt="' . htmlspecialchars($altText) . '" ' .
+                                                                        'title="' . htmlspecialchars($altText) . '">';
+                                       }
+                               }
+                       }
+               }
+
+               if (!empty($config['appearance']['headerThumbnail']['field']) && $thumbnail) {
+                       $mediaContainer = '<div class="form-irre-header-cell form-irre-header-thumbnail" id="' . $objectId . '_thumbnailcontainer">' . $thumbnail . '</div>';
+               } else {
+                       $mediaContainer = '<div class="form-irre-header-cell form-irre-header-icon" id="' . $objectId . '_iconcontainer">' . $iconImg . '</div>';
+               }
+               $header = $mediaContainer . '
+                               <div class="form-irre-header-cell form-irre-header-body">' . $label . '</div>
+                               <div class="form-irre-header-cell form-irre-header-control t3js-formengine-irre-control">' . $ctrl . '</div>';
+
+               return $header;
+       }
+
+       /**
+        * Render the control-icons for a record header (create new, sorting, delete, disable/enable).
+        * Most of the parts are copy&paste from TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList and
+        * modified for the JavaScript calls here
+        *
+        * @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 $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) {
+               $languageService = $this->getLanguageService();
+               $backendUser = $this->getBackendUserAuthentication();
+               // Initialize:
+               $cells = array();
+               $additionalCells = array();
+               $isNewItem = substr($rec['uid'], 0, 3) == 'NEW';
+               $isParentExisting = MathUtility::canBeInterpretedAsInteger($parentUid);
+               $tcaTableCtrl = &$GLOBALS['TCA'][$foreign_table]['ctrl'];
+               $tcaTableCols = &$GLOBALS['TCA'][$foreign_table]['columns'];
+               $isPagesTable = $foreign_table == 'pages' ? TRUE : FALSE;
+               $isOnSymmetricSide = RelationHandler::isOnSymmetricSide($parentUid, $config, $rec);
+               $enableManualSorting = $tcaTableCtrl['sortby'] || $config['MM'] || !$isOnSymmetricSide && $config['foreign_sortby'] || $isOnSymmetricSide && $config['symmetric_sortby'] ? TRUE : FALSE;
+               $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->globalOptions['inlineFirstPid']);
+               $nameObjectFt = $nameObject . '-' . $foreign_table;
+               $nameObjectFtId = $nameObjectFt . '-' . $rec['uid'];
+               $calcPerms = $backendUser->calcPerms(BackendUtility::readPageAccess($rec['pid'], $backendUser->getPagePermsClause(1)));
+               // If the listed table is 'pages' we have to request the permission settings for each page:
+               $localCalcPerms = FALSE;
+               if ($isPagesTable) {
+                       $localCalcPerms = $backendUser->calcPerms(BackendUtility::getRecord('pages', $rec['uid']));
+               }
+               // This expresses the edit permissions for this particular element:
+               $permsEdit = $isPagesTable && $localCalcPerms & Permission::PAGE_EDIT || !$isPagesTable && $calcPerms & Permission::CONTENT_EDIT;
+               // Controls: Defines which controls should be shown
+               $enabledControls = $config['appearance']['enabledControls'];
+               // Hook: Can disable/enable single controls for specific child records:
+               foreach ($this->hookObjects as $hookObj) {
+                       /** @var InlineElementHookInterface $hookObj */
+                       $hookObj->renderForeignRecordHeaderControl_preProcess($parentUid, $foreign_table, $rec, $config, $isVirtualRecord, $enabledControls);
+               }
+               if (isset($rec['__create'])) {
+                       $cells['localize.isLocalizable'] = IconUtility::getSpriteIcon('actions-edit-localize-status-low', array('title' => $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localize.isLocalizable', TRUE)));
+               } elseif (isset($rec['__remove'])) {
+                       $cells['localize.wasRemovedInOriginal'] = IconUtility::getSpriteIcon('actions-edit-localize-status-high', array('title' => $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localize.wasRemovedInOriginal', TRUE)));
+               }
+               // "Info": (All records)
+               if ($enabledControls['info'] && !$isNewItem) {
+                       if ($rec['table_local'] === 'sys_file') {
+                               $uid = (int)substr($rec['uid_local'], 9);
+                               $table = '_FILE';
+                       } else {
+                               $uid = $rec['uid'];
+                               $table = $foreign_table;
+                       }
+                       $cells['info'] = '
+                               <a class="btn btn-default" href="#" onclick="' . htmlspecialchars(('top.launchView(' . GeneralUtility::quoteJSvalue($table) . ', ' . GeneralUtility::quoteJSvalue($uid) . '); return false;')) . '">
+                                       ' . IconUtility::getSpriteIcon('status-dialog-information', array('title' => $languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:showInfo', TRUE))) . '
+                               </a>';
+               }
+               // If the table is NOT a read-only table, then show these links:
+               if (!$tcaTableCtrl['readOnly'] && !$isVirtualRecord) {
+                       // "New record after" link (ONLY if the records in the table are sorted by a "sortby"-row or if default values can depend on previous record):
+                       if ($enabledControls['new'] && ($enableManualSorting || $tcaTableCtrl['useColumnsForDefaultValues'])) {
+                               if (!$isPagesTable && $calcPerms & Permission::CONTENT_EDIT || $isPagesTable && $calcPerms & Permission::PAGE_NEW) {
+                                       $onClick = 'return inline.createNewRecord(' . GeneralUtility::quoteJSvalue($nameObjectFt) . ',' . GeneralUtility::quoteJSvalue($rec['uid']) . ')';
+                                       $style = '';
+                                       if ($config['inline']['inlineNewButtonStyle']) {
+                                               $style = ' style="' . $config['inline']['inlineNewButtonStyle'] . '"';
+                                       }
+                                       $cells['new'] = '
+                                               <a class="btn btn-default inlineNewButton ' . $this->inlineData['config'][$nameObject]['md5'] . '" href="#" onclick="' . htmlspecialchars($onClick) . '"' . $style . '>
+                                                       ' . IconUtility::getSpriteIcon(('actions-' . ($isPagesTable ? 'page' : 'document') . '-new'), array('title' => $languageService->sL(('LLL:EXT:lang/locallang_mod_web_list.xlf:new' . ($isPagesTable ? 'Page' : 'Record')), TRUE))) . '
+                                               </a>';
+                               }
+                       }
+                       // "Up/Down" links
+                       if ($enabledControls['sort'] && $permsEdit && $enableManualSorting) {
+                               // Up
+                               $onClick = 'return inline.changeSorting(\'' . $nameObjectFtId . '\', \'1\')';
+                               $style = $config['inline']['first'] == $rec['uid'] ? 'style="visibility: hidden;"' : '';
+                               $cells['sort.up'] = '
+                                       <a class="btn btn-default sortingUp" href="#" onclick="' . htmlspecialchars($onClick) . '" ' . $style . '>
+                                               ' . IconUtility::getSpriteIcon('actions-move-up', array('title' => $languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:moveUp', TRUE))) . '
+                                       </a>';
+                               // Down
+                               $onClick = 'return inline.changeSorting(\'' . $nameObjectFtId . '\', \'-1\')';
+                               $style = $config['inline']['last'] == $rec['uid'] ? 'style="visibility: hidden;"' : '';
+                               $cells['sort.down'] = '
+                                       <a class="btn btn-default sortingDown" href="#" onclick="' . htmlspecialchars($onClick) . '" ' . $style . '>
+                                               ' . IconUtility::getSpriteIcon('actions-move-down', array('title' => $languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:moveDown', TRUE))) . '
+                                       </a>';
+                       }
+                       // "Edit" link:
+                       if (($rec['table_local'] === 'sys_file') && !$isNewItem) {
+                               $recordInDatabase = $this->getDatabaseConnection()->exec_SELECTgetSingleRow(
+                                       'uid',
+                                       'sys_file_metadata',
+                                       'file = ' . (int)substr($rec['uid_local'], 9) . ' AND sys_language_uid = ' . $rec['sys_language_uid']
+                               );
+                               if ($backendUser->check('tables_modify', 'sys_file_metadata')) {
+                                       $url = BackendUtility::getModuleUrl('record_edit', array(
+                                               'edit[sys_file_metadata][' . (int)$recordInDatabase['uid'] . ']' => 'edit'
+                                       ));
+                                       $editOnClick = 'if (top.content.list_frame) {' .
+                                               'top.content.list_frame.location.href=' .
+                                                       GeneralUtility::quoteJSvalue($url . '&returnUrl=') .
+                                                       '+top.rawurlencode(top.content.list_frame.document.location.pathname+top.content.list_frame.document.location.search)' .
+                                               ';' .
+                                       '}';
+                                       $title = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.editMetadata');
+                                       $cells['editmetadata'] = '
+                                               <a class="btn btn-default" href="#" class="btn" onclick="' . htmlspecialchars($editOnClick) . '" title="' . htmlspecialchars($title) . '">
+                                                       ' . IconUtility::getSpriteIcon('actions-document-open') . '
+                                               </a>';
+                               }
+                       }
+                       // "Delete" link:
+                       if ($enabledControls['delete'] && ($isPagesTable && $localCalcPerms & Permission::PAGE_DELETE || !$isPagesTable && $calcPerms & Permission::CONTENT_EDIT)) {
+                               $onClick = 'inline.deleteRecord(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ');';
+                               $cells['delete'] = '
+                                       <a class="btn btn-default" href="#" onclick="' . htmlspecialchars(('if (confirm(' . GeneralUtility::quoteJSvalue($languageService->getLL('deleteWarning')) . ')) {      ' . $onClick . ' } return false;')) . '">
+                                               ' . IconUtility::getSpriteIcon('actions-edit-delete', array('title' => $languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:delete', TRUE))) . '
+                                       </a>';
+                       }
+
+                       // "Hide/Unhide" links:
+                       $hiddenField = $tcaTableCtrl['enablecolumns']['disabled'];
+                       if ($enabledControls['hide'] && $permsEdit && $hiddenField && $tcaTableCols[$hiddenField] && (!$tcaTableCols[$hiddenField]['exclude'] || $backendUser->check('non_exclude_fields', $foreign_table . ':' . $hiddenField))) {
+                               $onClick = 'return inline.enableDisableRecord(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ')';
+                               if ($rec[$hiddenField]) {
+                                       $cells['hide.unhide'] = '
+                                               <a class="btn btn-default hiddenHandle" href="#" onclick="' . htmlspecialchars($onClick) . '">
+                                                       ' . IconUtility::getSpriteIcon('actions-edit-unhide', array('title' => $languageService->sL(('LLL:EXT:lang/locallang_mod_web_list.xlf:unHide' . ($isPagesTable ? 'Page' : '')), TRUE), 'id' => ($nameObjectFtId . '_disabled'))) . '
+                                               </a>';
+                               } else {
+                                       $cells['hide.hide'] = '
+                                               <a class="btn btn-default hiddenHandle" href="#" onclick="' . htmlspecialchars($onClick) . '">
+                                                       ' . IconUtility::getSpriteIcon('actions-edit-hide', array('title' => $languageService->sL(('LLL:EXT:lang/locallang_mod_web_list.xlf:hide' . ($isPagesTable ? 'Page' : '')), TRUE), 'id' => ($nameObjectFtId . '_disabled'))) . '
+                                               </a>';
+                               }
+                       }
+                       // Drag&Drop Sorting: Sortable handler for script.aculo.us
+                       if ($enabledControls['dragdrop'] && $permsEdit && $enableManualSorting && $config['appearance']['useSortable']) {
+                               $additionalCells['dragdrop'] = '
+                                       <span class="btn btn-default">
+                                               ' . IconUtility::getSpriteIcon('actions-move-move', array('data-id' => $rec['uid'], 'class' => 'sortableHandle', 'title' => $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.move', TRUE))) . '
+                                       </span>';
+                       }
+               } elseif ($isVirtualRecord && $isParentExisting) {
+                       if ($enabledControls['localize'] && isset($rec['__create'])) {
+                               $onClick = 'inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($nameObjectFt) . ', ' . GeneralUtility::quoteJSvalue($rec['uid']) . ');';
+                               $cells['localize'] = '
+                                       <a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . '">
+                                               ' . IconUtility::getSpriteIcon('actions-document-localize', array('title' => $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localize', TRUE))) . '
+                                       </a>';
+                       }
+               }
+               // If the record is edit-locked by another user, we will show a little warning sign:
+               if ($lockInfo = BackendUtility::isRecordLocked($foreign_table, $rec['uid'])) {
+                       $cells['locked'] = '
+                               <a class="btn btn-default" href="#" onclick="alert(' . GeneralUtility::quoteJSvalue($lockInfo['msg']) . ');return false;">
+                                       ' . IconUtility::getSpriteIcon('status-warning-in-use', array('title' => $lockInfo['msg'])) . '
+                               </a>';
+               }
+               // Hook: Post-processing of single controls for specific child records:
+               foreach ($this->hookObjects as $hookObj) {
+                       $hookObj->renderForeignRecordHeaderControl_postProcess($parentUid, $foreign_table, $rec, $config, $isVirtualRecord, $cells);
+               }
+
+               $out = '
+                       <!-- CONTROL PANEL: ' . $foreign_table . ':' . $rec['uid'] . ' -->
+                       <img name="' . $nameObjectFtId . '_req" src="clear.gif" alt="" />';
+               if (!empty($cells)) {
+                       $out .= ' <div class="btn-group btn-group-sm" role="group">' . implode('', $cells) . '</div>';
+               }
+               if (!empty($additionalCells)) {
+                       $out .= ' <div class="btn-group btn-group-sm" role="group">' . implode('', $additionalCells) . '</div>';
+               }
+               return $out;
+       }
+
+       /**
+        * 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) {
+               $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->globalOptions['inlineFirstPid'])) {
+                               $calcPRec = BackendUtility::getRecord('pages', $this->globalOptions['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 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']));
+                                       $hasAccess = (bool)($CALC_PERMS & Permission::CONTENT_EDIT);
+                               }
+                               // Check internals regarding access:
+                               if ($hasAccess) {
+                                       $hasAccess = (bool)$backendUser->recordEditAccessInternals($table, $calcPRec);
+                               }
+                       }
+               }
+               if (!$backendUser->check('tables_modify', $table)) {
+                       $hasAccess = FALSE;
+               }
+               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
+        * @param int $uid uid of the the child record
+        * @return bool TRUE=expand, FALSE=collapse
+        */
+       protected function getExpandedCollapsedState($table, $uid) {
+               $inlineView = $this->globalOptions['inlineExpandCollapseStateArray'];
+               // @todo Add checking/cleaning for unused tables, records, etc. to save space in uc-field
+               if (isset($inlineView[$table]) && is_array($inlineView[$table])) {
+                       if (in_array($uid, $inlineView[$table]) !== FALSE) {
+                               return TRUE;
+                       }
+               }
+               return FALSE;
+       }
+
+       /**
+        * Normalize a relation "uid" published by transferData, like "1|Company%201"
+        *
+        * @param string $string A transferData reference string, containing the uid
+        * @return string The normalized uid
+        */
+       protected function normalizeUid($string) {
+               $parts = explode('|', $string);
+               return $parts[0];
+       }
+
+       /**
+        * Initialized the hook objects for this class.
+        * Each hook object has to implement the interface
+        * \TYPO3\CMS\Backend\Form\Element\InlineElementHookInterface
+        *
+        * @throws \UnexpectedValueException
+        * @return void
+        */
+       protected function initHookObjects() {
+               $this->hookObjects = array();
+               if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['tceformsInlineHook'])) {
+                       $tceformsInlineHook = &$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['tceformsInlineHook'];
+                       if (is_array($tceformsInlineHook)) {
+                               foreach ($tceformsInlineHook as $classData) {
+                                       $processObject = GeneralUtility::getUserObj($classData);
+                                       if (!$processObject instanceof InlineElementHookInterface) {
+                                               throw new \UnexpectedValueException('$processObject must implement interface ' . InlineElementHookInterface::class, 1202072000);
+                                       }
+                                       $this->hookObjects[] = $processObject;
+                               }
+                       }
+               }
+       }
+
+       /**
+        * @return BackendUserAuthentication
+        */
+       protected function getBackendUserAuthentication() {
+               return $GLOBALS['BE_USER'];
+       }
+
+       /**
+        * @return DatabaseConnection
+        */
+       protected function getDatabaseConnection() {
+               return $GLOBALS['TYPO3_DB'];
+       }
+
+       /**
+        * @return LanguageService
+        */
+       protected function getLanguageService() {
+               return $GLOBALS['LANG'];
+       }
+
+}
diff --git a/typo3/sysext/backend/Classes/Form/Container/ListOfFieldsContainer.php b/typo3/sysext/backend/Classes/Form/Container/ListOfFieldsContainer.php
new file mode 100644 (file)
index 0000000..76672f5
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Container;
+
+/*
+ * 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\Utility\GeneralUtility;
+use TYPO3\CMS\Lang\LanguageService;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+
+/**
+ * Render a given list of field of a TCA table.
+ *
+ * This is an entry container called from FormEngine to handle a
+ * list of specific fields. Access rights are checked here and globalOption array
+ * is prepared for further processing of single fields by PaletteAndSingleContainer.
+ */
+class ListOfFieldsContainer extends AbstractContainer {
+
+       /**
+        * Entry method
+        *
+        * @return array As defined in initializeResultArray() of AbstractNode
+        */
+       public function render() {
+               $table = $this->globalOptions['table'];
+               $row = $this->globalOptions['databaseRow'];
+               $list = $this->globalOptions['fieldListToRender'];
+
+               if (!$GLOBALS['TCA'][$table]) {
+                       return $this->initializeResultArray();
+               }
+
+               $languageService = $this->getLanguageService();
+               // Load the description content for the table if requested
+               if ($GLOBALS['TCA'][$table]['interface']['always_description']) {
+                       $languageService->loadSingleTableDescription($table);
+               }
+
+               // If this is a localized record, stuff data from original record to local registry, will then be given to child elements
+               $this->registerDefaultLanguageData($table, $row);
+
+               $list = array_unique(GeneralUtility::trimExplode(',', $list, TRUE));
+               $typesFieldConfig = BackendUtility::getTCAtypes($table, $row, 1);
+               $finalFieldsConfiguration = array();
+               foreach ($list as $singleField) {
+                       if (!is_array($GLOBALS['TCA'][$table]['columns'][$singleField])) {
+                               continue;
+                       }
+                       if (isset($typesFieldConfig[$singleField]['origString'])) {
+                               $fieldConfiguration = $this->explodeSingleFieldShowItemConfiguration($typesFieldConfig[$singleField]['origString']);
+                               // Fields of sub palettes should not be rendered
+                               $fieldConfiguration['paletteName'] = '';
+                       } else {
+                               $fieldConfiguration = array(
+                                       'fieldName' => $singleField,
+                               );
+                       }
+                       $finalFieldsConfiguration[] = implode(';', $fieldConfiguration);
+               }
+
+               $options = $this->globalOptions;
+               $options['fieldsArray'] = $finalFieldsConfiguration;
+               /** @var PaletteAndSingleContainer $paletteAndSingleContainer */
+               $paletteAndSingleContainer = GeneralUtility::makeInstance(PaletteAndSingleContainer::class);
+               $paletteAndSingleContainer->setGlobalOptions($options);
+               return $paletteAndSingleContainer->render();
+       }
+
+       /**
+        * @return LanguageService
+        */
+       protected function getLanguageService() {
+               return $GLOBALS['LANG'];
+       }
+
+}
\ No newline at end of file
diff --git a/typo3/sysext/backend/Classes/Form/Container/NoTabsContainer.php b/typo3/sysext/backend/Classes/Form/Container/NoTabsContainer.php
new file mode 100644 (file)
index 0000000..bc62976
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Container;
+
+/*
+ * 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\Utility\GeneralUtility;
+
+/**
+ * Handle a record that has no tabs.
+ *
+ * This container is called by FullRecordContainer and just wraps the output
+ * of PaletteAndSingleContainer in some HTML.
+ */
+class NoTabsContainer extends AbstractContainer {
+
+       /**
+        * Entry method
+        *
+        * @return array As defined in initializeResultArray() of AbstractNode
+        */
+       public function render() {
+               /** @var PaletteAndSingleContainer $paletteAndSingleContainer */
+               $paletteAndSingleContainer = GeneralUtility::makeInstance(PaletteAndSingleContainer::class);
+               $paletteAndSingleContainer->setGlobalOptions($this->globalOptions);
+               $resultArray = $paletteAndSingleContainer->render();
+               $resultArray['html'] = '<div class="tab-content">' . $resultArray['html'] . '</div>';
+               return $resultArray;
+       }
+
+}
\ No newline at end of file
diff --git a/typo3/sysext/backend/Classes/Form/Container/PaletteAndSingleContainer.php b/typo3/sysext/backend/Classes/Form/Container/PaletteAndSingleContainer.php
new file mode 100644 (file)
index 0000000..d09c408
--- /dev/null
@@ -0,0 +1,577 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Container;
+
+/*
+ * 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\Utility\GeneralUtility;
+use TYPO3\CMS\Lang\LanguageService;
+use TYPO3\CMS\Backend\Utility\IconUtility;
+use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+
+/**
+ * Handle palettes and single fields.
+ *
+ * This container is called by TabsContainer, NoTabsContainer and ListOfFieldsContainer.
+ *
+ * This container mostly operates on TCA showItem of a specific type - the value is
+ * coming in from upper containers as "fieldArray". It handles palettes with all its
+ * different options and prepares rendering of single fields for the SingleFieldContainer.
+ */
+class PaletteAndSingleContainer extends AbstractContainer {
+
+       /**
+        * Final result array accumulating results from children and final HTML
+        *
+        * @var array
+        */
+       protected $resultArray = array();
+
+       /**
+        * Entry method
+        *
+        * @return array As defined in initializeResultArray() of AbstractNode
+        */
+       public function render() {
+               $languageService = $this->getLanguageService();
+               $table = $this->globalOptions['table'];
+
+               /**
+                * The first code block creates a target structure array to later create the final
+                * HTML string. The single fields and sub containers are rendered here already and
+                * other parts of the return array from children except html are accumulated in
+                * $this->resultArray
+                *
+               $targetStructure = array(
+                       0 => array(
+                               'type' => 'palette',
+                               'fieldName' => 'palette1',
+                               'fieldLabel' => 'palette1',
+                               'elements' => array(
+                                       0 => array(
+                                               'type' => 'single',
+                                               'fieldName' => 'palettenName',
+                                               'fieldLabel' => 'element1',
+                                               'fieldHtml' => 'element1',
+                                       ),
+                                       1 => array(
+                                               'type' => 'linebreak',
+                                       ),
+                                       2 => array(
+                                               'type' => 'single',
+                                               'fieldName' => 'palettenName',
+                                               'fieldLabel' => 'element2',
+                                               'fieldHtml' => 'element2',
+                                       ),
+                               ),
+                       ),
+                       1 => array( // has 2 as "additional palette"
+                               'type' => 'single',
+                               'fieldName' => 'element3',
+                               'fieldLabel' => 'element3',
+                               'fieldHtml' => 'element3',
+                       ),
+                       2 => array( // do only if 1 had result
+                               'type' => 'palette2',
+                               'fieldName' => 'palette2',
+                               'fieldLabel' => '', // label missing because label of 1 is displayed only
+                               'canNotCollapse' => TRUE, // An "additional palette" can not be collapsed
+                               'elements' => array(
+                                       0 => array(
+                                               'type' => 'single',
+                                               'fieldName' => 'element4',
+                                               'fieldLabel' => 'element4',
+                                               'fieldHtml' => 'element4',
+                                       ),
+                                       1 => array(
+                                               'type' => 'linebreak',
+                                       ),
+                                       2 => array(
+                                               'type' => 'single',
+                                               'fieldName' => 'element5',
+                                               'fieldLabel' => 'element5',
+                                               'fieldHtml' => 'element5',
+                                       ),
+                               ),
+                       ),
+               );
+                */
+
+               // Create an intermediate structure of rendered sub elements and elements nested in palettes
+               $targetStructure = array();
+               $mainStructureCounter = -1;
+               $fieldsArray = $this->globalOptions['fieldsArray'];
+               $this->resultArray = $this->initializeResultArray();
+               foreach ($fieldsArray as $fieldString) {
+                       $fieldConfiguration = $this->explodeSingleFieldShowItemConfiguration($fieldString);
+                       $fieldName = $fieldConfiguration['fieldName'];
+                       if ($fieldName === '--palette--') {
+                               $paletteElementArray = $this->createPaletteContentArray($fieldConfiguration['paletteName']);
+                               if (!empty($paletteElementArray)) {
+                                       $mainStructureCounter ++;
+                                       $targetStructure[$mainStructureCounter] = array(
+                                               'type' => 'palette',
+                                               'fieldName' => $fieldConfiguration['paletteName'],
+                                               'fieldLabel' => $languageService->sL($fieldConfiguration['fieldLabel']),
+                                               'elements' => $paletteElementArray,
+                                       );
+                               }
+                       } else {
+                               if (!is_array($GLOBALS['TCA'][$table]['columns'][$fieldName])) {
+                                       continue;
+                               }
+
+                               $options = $this->globalOptions;
+                               $options['fieldName'] = $fieldName;
+                               $options['fieldExtra'] = $fieldConfiguration['fieldExtra'];
+
+                               /** @var SingleFieldContainer $singleFieldContainer */
+                               $singleFieldContainer = GeneralUtility::makeInstance(SingleFieldContainer::class);
+                               $singleFieldContainer->setGlobalOptions($options);
+                               $childResultArray = $singleFieldContainer->render();
+
+                               if (!empty($childResultArray['html'])) {
+                                       $mainStructureCounter ++;
+
+                                       $targetStructure[$mainStructureCounter] = array(
+                                               'type' => 'single',
+                                               'fieldName' => $fieldConfiguration['fieldName'],
+                                               'fieldLabel' => $this->getSingleFieldLabel($fieldName, $fieldConfiguration['fieldLabel']),
+                                               'fieldHtml' => $childResultArray['html'],
+                                       );
+
+                                       // If the third part of a show item field is given, this is a name of a palette that should be rendered
+                                       // below the single field - without palette header and only if single field produced content
+                                       if (!empty($childResultArray['html']) && !empty($fieldConfiguration['paletteName'])) {
+                                               $paletteElementArray = $this->createPaletteContentArray($fieldConfiguration['paletteName']);
+                                               if (!empty($paletteElementArray)) {
+                                                       $mainStructureCounter ++;
+                                                       $targetStructure[$mainStructureCounter] = array(
+                                                               'type' => 'palette',
+                                                               'fieldName' => $fieldConfiguration['paletteName'],
+                                                               'fieldLabel' => '', // An "additional palette" has no show label
+                                                               'canNotCollapse' => TRUE,
+                                                               'elements' => $paletteElementArray,
+                                                       );
+                                               }
+                                       }
+                               }
+
+                               $childResultArray['html'] = '';
+                               $this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $childResultArray);
+                       }
+               }
+
+               // Compile final content
+               $content = array();
+               foreach ($targetStructure as $element) {
+                       if ($element['type'] === 'palette') {
+                               $paletteName = $element['fieldName'];
+                               $paletteElementsHtml = $this->renderInnerPaletteContent($element);
+
+                               $isHiddenPalette = !empty($GLOBALS['TCA'][$table]['palettes'][$paletteName]['isHiddenPalette']);
+
+                               $renderUnCollapseButtonWrapper = TRUE;
+                               // No button if the palette is hidden
+                               if ($isHiddenPalette) {
+                                       $renderUnCollapseButtonWrapper = FALSE;
+                               }
+                               // No button if palette can not collapse on ctrl level
+                               if (!empty($GLOBALS['TCA'][$table]['ctrl']['canNotCollapse'])) {
+                                       $renderUnCollapseButtonWrapper = FALSE;
+                               }
+                               // No button if palette can not collapse on palette definition level
+                               if (!empty($GLOBALS['TCA'][$table]['palettes'][$paletteName]['canNotCollapse'])) {
+                                       $renderUnCollapseButtonWrapper = FALSE;
+                               }
+                               // No button if palettes are not collapsed - this is the checkbox at the end of the form
+                               if (!$this->globalOptions['palettesCollapsed']) {
+                                       $renderUnCollapseButtonWrapper = FALSE;
+                               }
+                               // No button if palette is set to no collapse on element level - this is the case if palette is an "additional palette" after a casual field
+                               if (!empty($element['canNotCollapse'])) {
+                                       $renderUnCollapseButtonWrapper = FALSE;
+                               }
+
+                               if ($renderUnCollapseButtonWrapper) {
+                                       $cssId = 'FORMENGINE_' . $this->globalOptions['table'] . '_' . $paletteName . '_' . $this->globalOptions['databaseRow']['uid'];
+                                       $paletteElementsHtml = $this->wrapPaletteWithCollapseButton($paletteElementsHtml, $cssId);
+                               } else {
+                                       $paletteElementsHtml = '<div class="row">' . $paletteElementsHtml . '</div>';
+                               }
+
+                               $content[] = $this->fieldSetWrap($paletteElementsHtml, $isHiddenPalette, $element['fieldLabel']);
+                       } else {
+                               // Return raw HTML only in case of user element with no wrapping requested
+                               if ($this->isUserNoTableWrappingField($element)) {
+                                       $content[] = $element['fieldHtml'];
+                               } else {
+                                       $content[] = $this->fieldSetWrap($this->wrapSingleFieldContent($element));
+                               }
+                       }
+               }
+
+               $finalResultArray = $this->resultArray;
+               $finalResultArray['html'] = implode(LF, $content);
+               return $finalResultArray;
+       }
+
+       /**
+        * Render single fields of a given palette
+        *
+        * @param string $paletteName The palette to render
+        * @return array
+        */
+       protected function createPaletteContentArray($paletteName) {
+               $table = $this->globalOptions['table'];
+               $excludeElements = $this->globalOptions['excludeElements'];
+
+               // palette needs a palette name reference, otherwise it does not make sense to try rendering of it
+               if (empty($paletteName) || empty($GLOBALS['TCA'][$table]['palettes'][$paletteName]['showitem'])) {
+                       return array();
+               }
+
+               $resultStructure = array();
+               $foundRealElement = FALSE; // Set to true if not only line breaks were rendered
+               $fieldsArray = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['palettes'][$paletteName]['showitem'], TRUE);
+               foreach ($fieldsArray as $fieldString) {
+                       $fieldArray = $this->explodeSingleFieldShowItemConfiguration($fieldString);
+                       $fieldName = $fieldArray['fieldName'];
+                       if ($fieldName === '--linebreak--') {
+                               $resultStructure[] = array(
+                                       'type' => 'linebreak',
+                               );
+                       } else {
+                               if (in_array($fieldName, $excludeElements, TRUE) || !is_array($GLOBALS['TCA'][$table]['columns'][$fieldName])) {
+                                       continue;
+                               }
+                               $options = $this->globalOptions;
+                               $options['fieldName'] = $fieldName;
+                               $options['fieldExtra'] = $fieldArray['fieldExtra'];
+
+                               /** @var SingleFieldContainer $singleFieldContainer */
+                               $singleFieldContainer = GeneralUtility::makeInstance(SingleFieldContainer::class);
+                               $singleFieldContainer->setGlobalOptions($options);
+                               $singleFieldContentArray = $singleFieldContainer->render();
+
+                               if (!empty($singleFieldContentArray['html'])) {
+                                       $foundRealElement = TRUE;
+                                       $resultStructure[] = array(
+                                               'type' => 'single',
+                                               'fieldName' => $fieldName,
+                                               'fieldLabel' => $this->getSingleFieldLabel($fieldName, $fieldArray['fieldLabel']),
+                                               'fieldHtml' => $singleFieldContentArray['html'],
+                                       );
+                                       $singleFieldContentArray['html'] = '';
+                                       $this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $singleFieldContentArray);
+                               }
+                       }
+               }
+
+               if ($foundRealElement) {
+                       return $resultStructure;
+               } else {
+                       return array();
+               }
+       }
+
+       /**
+        * Renders inner content of single elements of a palette and wrap it as needed
+        *
+        * @param array $elementArray Array of elements
+        * @return string Wrapped content
+        */
+       protected function renderInnerPaletteContent(array $elementArray) {
+               // Group fields
+               $groupedFields = array();
+               $row = 0;
+               $lastLineWasLinebreak = TRUE;
+               foreach ($elementArray['elements'] as $element) {
+                       if ($element['type'] === 'linebreak') {
+                               if (!$lastLineWasLinebreak) {
+                                       $row++;
+                                       $groupedFields[$row][] = $element;
+                                       $row++;
+                                       $lastLineWasLinebreak = TRUE;
+                               }
+                       } else {
+                               $lastLineWasLinebreak = FALSE;
+                               $groupedFields[$row][] = $element;
+                       }
+               }
+
+               $result = array();
+               // Process fields
+               foreach ($groupedFields as $fields) {
+                       $numberOfItems = count($fields);
+                       $colWidth = (int)floor(12 / $numberOfItems);
+                       // Column class calculation
+                       $colClass = "col-md-12";
+                       $colClear = array();
+                       if ($colWidth == 6) {
+                               $colClass = "col-sm-6";
+                               $colClear = array(
+                                       2 => 'visible-sm-block visible-md-block visible-lg-block',
+                               );
+                       } elseif ($colWidth === 4) {
+                               $colClass = "col-sm-4";
+                               $colClear = array(
+                                       3 => 'visible-sm-block visible-md-block visible-lg-block',
+                               );
+                       } elseif ($colWidth === 3) {
+                               $colClass = "col-sm-6 col-md-3";
+                               $colClear = array(
+                                       2 => 'visible-sm-block',
+                                       4 => 'visible-md-block visible-lg-block',
+                               );
+                       } elseif ($colWidth <= 2) {
+                               $colClass = "checkbox-column col-sm-6 col-md-3 col-lg-2";
+                               $colClear = array(
+                                       2 => 'visible-sm-block',
+                                       4 => 'visible-md-block',
+                                       6 => 'visible-lg-block'
+                               );
+                       }
+
+                       // Render fields
+                       for ($counter = 0; $counter < $numberOfItems; $counter++) {
+                               $element = $fields[$counter];
+                               if ($element['type'] === 'linebreak') {
+                                       if ($counter !== $numberOfItems) {
+                                               $result[] = '<div class="clearfix"></div>';
+                                       }
+                               } else {
+                                       $result[] = $this->wrapSingleFieldContent($element, array($colClass));
+
+                                       // Breakpoints
+                                       if ($counter + 1 < $numberOfItems && !empty($colClear)) {
+                                               foreach ($colClear as $rowBreakAfter => $clearClass) {
+                                                       if (($counter + 1) % $rowBreakAfter === 0) {
+                                                               $result[] = '<div class="clearfix '. $clearClass . '"></div>';
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               return implode(LF, $result);
+       }
+
+       /**
+        * Add a "collapsible" button around given content
+        *
+        * @param string $elementHtml HTML of handled palette content
+        * @param string $cssId A css id to be added
+        * @return string Wrapped content
+        */
+       protected function wrapPaletteWithCollapseButton($elementHtml, $cssId) {
+               $content = array();
+               $content[] = '<p>';
+               $content[] =    '<button class="btn btn-default" type="button" data-toggle="collapse" data-target="#' . $cssId . '" aria-expanded="false" aria-controls="' . $cssId . '">';
+               $content[] =            IconUtility::getSpriteIcon('actions-system-options-view');
+               $content[] =            htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.moreOptions'));
+               $content[] =    '</button>';
+               $content[] = '</p>';
+               $content[] = '<div id="' . $cssId . '" class="form-section-collapse collapse">';
+               $content[] =    '<div class="row">' . $elementHtml . '</div>';
+               $content[] = '</div>';
+               return implode(LF, $content);
+       }
+
+       /**
+        * Wrap content in a field set
+        *
+        * @param string $content Incoming content
+        * @param bool $paletteHidden TRUE if the palette is hidden
+        * @param string $label Given label
+        * @return string Wrapped content
+        */
+       protected function fieldSetWrap($content, $paletteHidden = FALSE, $label = '') {
+               $fieldSetClass = 'form-section';
+               if ($paletteHidden) {
+                       $fieldSetClass = 'hide';
+               }
+
+               $result = array();
+               $result[] = '<fieldset class="' . $fieldSetClass . '">';
+
+               if (!empty($label)) {
+                       $result[] = '<h4 class="form-section-headline">' . htmlspecialchars($label) . '</h4>';
+               }
+
+               $result[] = $content;
+               $result[] = '</fieldset>';
+               return implode(LF, $result);
+       }
+
+       /**
+        * Wrap a single element
+        *
+        * @param array $element Given element as documented above
+        * @param array $additionalPaletteClasses Additional classes to be added to HTML
+        * @return string Wrapped element
+        */
+       protected function wrapSingleFieldContent(array $element, array $additionalPaletteClasses = array()) {
+               $fieldName = $element['fieldName'];
+
+               $paletteFieldClasses = array(
+                       'form-group',
+                       't3js-formengine-palette-field',
+               );
+               foreach ($additionalPaletteClasses as $class) {
+                       $paletteFieldClasses[] = $class;
+               }
+
+               $fieldItemClasses = array(
+                       't3js-formengine-field-item'
+               );
+               $isNullValueField = $this->isDisabledNullValueField($fieldName);
+               if ($isNullValueField) {
+                       $fieldItemClasses[] = 'disabled';
+               }
+
+               $label = BackendUtility::wrapInHelp($this->globalOptions['table'], $fieldName, htmlspecialchars($element['fieldLabel']));
+
+               $content = array();
+               $content[] = '<div class="' . implode(' ', $paletteFieldClasses) . '">';
+               $content[] =    '<label class="t3js-formengine-label">';
+               $content[] =            $label;
+               $content[] =            '<img name="req_' . $this->globalOptions['table'] . '_' . $this->globalOptions['databaseRow']['uid'] . '_' . $fieldName . '" src="clear.gif" class="t3js-formengine-field-required" alt="" />';
+               $content[] =    '</label>';
+               $content[] =    '<div class="' . implode(' ', $fieldItemClasses) . '">';
+               $content[] =            '<div class="t3-form-field-disable"></div>';
+               $content[] =            $this->renderNullValueWidget($fieldName);
+               $content[] =            $element['fieldHtml'];
+               $content[] =    '</div>';
+               $content[] = '</div>';
+
+               return implode(LF, $content);
+       }
+
+       /**
+        * Determine label of a single field (not a palette label)
+        *
+        * @param string $fieldName The field name to calculate the label for
+        * @param string $labelFromShowItem Given label, typically from show item configuration
+        * @return string Field label
+        */
+       protected function getSingleFieldLabel($fieldName, $labelFromShowItem) {
+               $languageService = $this->getLanguageService();
+               $table = $this->globalOptions['table'];
+               $label = $labelFromShowItem;
+               if (!empty($GLOBALS['TCA'][$table]['columns'][$fieldName]['label'])) {
+                       $label = $GLOBALS['TCA'][$table]['columns'][$fieldName]['label'];
+               }
+               if (!empty($labelFromShowItem)) {
+                       $label = $labelFromShowItem;
+               }
+               $fieldTSConfig = FormEngineUtility::getTSconfigForTableRow($table, $this->globalOptions['databaseRow'], $fieldName);
+               if (!empty($fieldTSConfig['label'])) {
+                       $label = $fieldTSConfig['label'];
+               }
+               if (!empty($fieldTSConfig['label.'][$languageService->lang])) {
+                       $label = $fieldTSConfig['label.'][$languageService->lang];
+               }
+               return $languageService->sL($label);
+       }
+
+       /**
+        * TRUE if field is of type user and to wrapping is requested
+        *
+        * @param array $element Current element from "target structure" array
+        * @return boolean TRUE if user and noTableWrapping is set
+        */
+       protected function isUserNoTableWrappingField($element) {
+               $table = $this->globalOptions['table'];
+               $fieldName = $element['fieldName'];
+               if (
+                       $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['type'] === 'user'
+                       && !empty($GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['noTableWrapping'])
+               ) {
+                       return TRUE;
+               }
+               return FALSE;
+       }
+
+       /**
+        * Determines whether the current field value is considered as NULL value.
+        * Using NULL values is enabled by using 'null' in the 'eval' TCA definition.
+        * If NULL value is possible for a field and additional checkbox next to the element will be rendered.
+        *
+        * @param string $fieldName The field to handle
+        * @return bool
+        */
+       protected function isDisabledNullValueField($fieldName) {
+               $table = $this->globalOptions['table'];
+               $row = $this->globalOptions['databaseRow'];
+               $config = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
+               $result = FALSE;
+               $value = $row[$fieldName];
+               if (
+                       $value === NULL
+                       && !empty($config['eval']) && GeneralUtility::inList($config['eval'], 'null')
+                       && (empty($config['mode']) || $config['mode'] !== 'useOrOverridePlaceholder')
+               ) {
+                       $result = TRUE;
+               }
+               return $result;
+       }
+
+       /**
+        * Renders a view widget to handle and activate NULL values.
+        * The widget is enabled by using 'null' in the 'eval' TCA definition.
+        *
+        * @param string $fieldName The field to handle
+        * @return string
+        */
+       protected function renderNullValueWidget($fieldName) {
+               $table = $this->globalOptions['table'];
+               $row = $this->globalOptions['databaseRow'];
+               $value = $row[$fieldName];
+               $config = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
+
+               $widget = array();
+               // Checkbox should be rendered if eval null set and no override stuff is done
+               if (
+                       !empty($config['eval']) && GeneralUtility::inList($config['eval'], 'null')
+                       && (empty($config['mode']) || $config['mode'] !== 'useOrOverridePlaceholder')
+               ) {
+                       $checked = $value === NULL ? '' : ' checked="checked"';
+                       $formElementName = $this->globalOptions['prependFormFieldNames'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
+                       $formElementNameActive = $this->globalOptions['prependFormFieldNamesActive'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
+                       $onChange = htmlspecialchars(
+                               'typo3form.fieldSetNull(\'' . $formElementName . '\', !this.checked)'
+                       );
+
+                       $widget = array();
+                       $widget[] = '<div class="checkbox">';
+                       $widget[] =     '<label>';
+                       $widget[] =             '<input type="hidden" name="' . $formElementNameActive . '" value="0" />';
+                       $widget[] =             '<input type="checkbox" name="' . $formElementNameActive . '" value="1" onchange="' . $onChange . '"' . $checked . ' /> &nbsp;';
+                       $widget[] =     '</label>';
+                       $widget[] = '</div>';
+               }
+
+               return implode(LF, $widget);
+       }
+
+       /**
+        * @return LanguageService
+        */
+       protected function getLanguageService() {
+               return $GLOBALS['LANG'];
+       }
+
+}
\ No newline at end of file
diff --git a/typo3/sysext/backend/Classes/Form/Container/SingleFieldContainer.php b/typo3/sysext/backend/Classes/Form/Container/SingleFieldContainer.php
new file mode 100644 (file)
index 0000000..c43f76a
--- /dev/null
@@ -0,0 +1,518 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Container;
+
+/*
+ * 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\ElementConditionMatcher;
+use TYPO3\CMS\Backend\Form\NodeFactory;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
+use TYPO3\CMS\Lang\LanguageService;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Backend\Utility\IconUtility;
+use TYPO3\CMS\Core\Utility\DiffUtility;
+use TYPO3\CMS\Backend\Form\InlineStackProcessor;
+use TYPO3\CMS\Core\Database\RelationHandler;
+use TYPO3\CMS\Backend\Form\Element\NoneElement;
+
+/**
+ * Container around a "single field".
+ *
+ * This container is the last one in the chain before processing is hand over to single element classes.
+ * If a single field is of type flex or inline, it however creates FlexFormContainer or InlineControlContainer.
+ *
+ * The container does various checks and processing for a given single fields, for example it resolves
+ * display conditions and the HTML to compare compare different languages.
+ */
+class SingleFieldContainer extends AbstractContainer {
+
+       /**
+        * Entry method
+        *
+        * @return array As defined in initializeResultArray() of AbstractNode
+        */
+       public function render() {
+               $backendUser = $this->getBackendUserAuthentication();
+               $languageService = $this->getLanguageService();
+               $resultArray = $this->initializeResultArray();
+
+               $table = $this->globalOptions['table'];
+               $row = $this->globalOptions['databaseRow'];
+               $fieldName = $this->globalOptions['fieldName'];
+
+               if (!is_array($GLOBALS['TCA'][$table]['columns'][$fieldName])) {
+                       return $resultArray;
+               }
+
+               $parameterArray = array();
+               $parameterArray['extra'] = $this->globalOptions['fieldExtra'];
+               $parameterArray['fieldConf'] = $GLOBALS['TCA'][$table]['columns'][$fieldName];
+
+               // A couple of early returns in case the field should not be rendered
+               // Check if this field is configured and editable according to exclude fields and other configuration
+               if (
+                       $parameterArray['fieldConf']['exclude'] && !$backendUser->check('non_exclude_fields', $table . ':' . $fieldName)
+                       || $parameterArray['fieldConf']['config']['type'] === 'passthrough'
+                       || !$backendUser->isRTE() && $parameterArray['fieldConf']['config']['showIfRTE']
+                       || $GLOBALS['TCA'][$table]['ctrl']['languageField'] && !$parameterArray['fieldConf']['l10n_display'] && $parameterArray['fieldConf']['l10n_mode'] === 'exclude' && ($row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0)
+                       || $GLOBALS['TCA'][$table]['ctrl']['languageField'] && $this->globalOptions['localizationMode'] && $this->globalOptions['localizationMode'] !== $parameterArray['fieldConf']['l10n_cat']
+                       || $this->inlineFieldShouldBeSkipped()
+               ) {
+                       return $resultArray;
+               }
+               // Evaluate display condition
+               if ($parameterArray['fieldConf']['displayCond'] && is_array($row)) {
+                       // @todo: isn't $row = array() safe somewhere above already?
+                       /** @var $elementConditionMatcher ElementConditionMatcher */
+                       $elementConditionMatcher = GeneralUtility::makeInstance(ElementConditionMatcher::class);
+                       if (!$elementConditionMatcher->match($parameterArray['fieldConf']['displayCond'], $row)) {
+                               return $resultArray;
+                       }
+               }
+               // Fetching the TSconfig for the current table/field. This includes the $row which means that
+               $parameterArray['fieldTSConfig'] = FormEngineUtility::getTSconfigForTableRow($table, $row, $fieldName);
+               if ($parameterArray['fieldTSConfig']['disabled']) {
+                       return $resultArray;
+               }
+
+               // Override fieldConf by fieldTSconfig:
+               $parameterArray['fieldConf']['config'] = FormEngineUtility::overrideFieldConf($parameterArray['fieldConf']['config'], $parameterArray['fieldTSConfig']);
+               $parameterArray['itemFormElName'] = $this->globalOptions['prependFormFieldNames'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
+               // Form field name, in case of file uploads
+               $parameterArray['itemFormElName_file'] = $this->globalOptions['prependFormFieldNames_file'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
+               // Form field name, to activate elements
+               // If the "eval" list contains "null", elements can be deactivated which results in storing NULL to database
+               $parameterArray['itemFormElNameActive'] = $this->globalOptions['prependFormFieldNamesActive'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
+               $parameterArray['itemFormElID'] = $this->globalOptions['prependFormFieldNames'] . '_' . $table . '_' . $row['uid'] . '_' . $fieldName;
+
+               // The value to show in the form field.
+               $parameterArray['itemFormElValue'] = $row[$fieldName];
+               // Set field to read-only if configured for translated records to show default language content as readonly
+               if ($parameterArray['fieldConf']['l10n_display']
+                       && GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'defaultAsReadonly')
+                       && $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0
+               ) {
+                       $parameterArray['fieldConf']['config']['readOnly'] = TRUE;
+                       $parameterArray['itemFormElValue'] = $this->globalOptions['defaultLanguageData'][$table . ':' . $row['uid']][$fieldName];
+               }
+
+               if (strpos($GLOBALS['TCA'][$table]['ctrl']['type'], ':') === FALSE) {
+                       $typeField = $GLOBALS['TCA'][$table]['ctrl']['type'];
+               } else {
+                       $typeField = substr($GLOBALS['TCA'][$table]['ctrl']['type'], 0, strpos($GLOBALS['TCA'][$table]['ctrl']['type'], ':'));
+               }
+               // Create a JavaScript code line which will ask the user to save/update the form due to changing the element.
+               // This is used for eg. "type" fields and others configured with "requestUpdate"
+               if (
+                       !empty($GLOBALS['TCA'][$table]['ctrl']['type'])
+                       && $fieldName === $typeField
+                       || !empty($GLOBALS['TCA'][$table]['ctrl']['requestUpdate'])
+                       && GeneralUtility::inList(str_replace(' ', '', $GLOBALS['TCA'][$table]['ctrl']['requestUpdate']), $fieldName)
+               ) {
+                       if ($backendUser->jsConfirmation(1)) {
+                               $alertMsgOnChange = 'if (confirm(TBE_EDITOR.labels.onChangeAlert) && TBE_EDITOR.checkSubmit(-1)){ TBE_EDITOR.submitForm() };';
+                       } else {
+                               $alertMsgOnChange = 'if (TBE_EDITOR.checkSubmit(-1)){ TBE_EDITOR.submitForm() };';
+                       }
+               } else {
+                       $alertMsgOnChange = '';
+               }
+
+
+               if (in_array($fieldName, $this->globalOptions['hiddenFieldListArray'], TRUE)) {
+                       // Render as a hidden field if this field had a forced value in overrideVals
+                       // @todo: This is an ugly concept ... search for overrideVals and defVals for a full picture of this madness
+                       $resultArray = $this->initializeResultArray();
+                       // This hidden field can not just be returned as casual html since upper containers will then render a label and wrapping stuff - this is not wanted here
+                       $resultArray['additionalHiddenFields'] = '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($parameterArray['itemFormElValue']) . '" />';
+               } else {
+                       // JavaScript code for event handlers:
+                       $parameterArray['fieldChangeFunc'] = array();
+                       $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = 'TBE_EDITOR.fieldChanged(' . GeneralUtility::quoteJSvalue($table) . ',' . GeneralUtility::quoteJSvalue($row['uid']) . ',' . GeneralUtility::quoteJSvalue($fieldName) . ',' . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ');';
+                       $parameterArray['fieldChangeFunc']['alert'] = $alertMsgOnChange;
+
+                       // If this is the child of an inline type and it is the field creating the label
+                       if ($this->isInlineChildAndLabelField($table, $fieldName)) {
+                               /** @var InlineStackProcessor $inlineStackProcessor */
+                               $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
+                               $inlineStackProcessor->initializeByGivenStructure($this->globalOptions['inlineStructure']);
+                               $inlineDomObjectId = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->globalOptions['inlineFirstPid']);
+                               $inlineObjectId = implode(
+                                       '-',
+                                       array(
+                                               $inlineDomObjectId,
+                                               $table,
+                                               $row['uid']
+                                       )
+                               );
+                               $parameterArray['fieldChangeFunc']['inline'] = 'inline.handleChangedField(\'' . $parameterArray['itemFormElName'] . '\',\'' . $inlineObjectId . '\');';
+                       }
+
+                       // Based on the type of the item, call a render function on a child element
+                       $options = $this->globalOptions;
+                       $options['parameterArray'] = $parameterArray;
+                       /** @var NodeFactory $childFactory */
+                       $childFactory = GeneralUtility::makeInstance(NodeFactory::class);
+                       $childElement = $childFactory->create($parameterArray['fieldConf']['config']['type']);
+                       $resultArray = $childElement->setGlobalOptions($options)->render();
+                       $html = $resultArray['html'];
+
+                       // Add language + diff
+                       $renderLanguageDiff = TRUE;
+                       if (
+                               $parameterArray['fieldConf']['l10n_display'] && (GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'hideDiff')
+                               || GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'defaultAsReadonly'))
+                       ) {
+                               $renderLanguageDiff = FALSE;
+                       }
+                       if ($renderLanguageDiff) {
+                               $html = $this->renderDefaultLanguageContent($table, $fieldName, $row, $html);
+                               $html = $this->renderDefaultLanguageDiff($table, $fieldName, $row, $html);
+                       }
+
+                       if (isset($parameterArray['fieldConf']['config']['mode']) && $parameterArray['fieldConf']['config']['mode'] === 'useOrOverridePlaceholder') {
+                               $placeholder = $this->getPlaceholderValue($table, $parameterArray['fieldConf']['config'], $row);
+                               $onChange = 'typo3form.fieldTogglePlaceholder(' . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ', !this.checked)';
+                               $checked = $parameterArray['itemFormElValue'] === NULL ? '' : ' checked="checked"';
+
+                               $resultArray['additionalJavaScriptPost'][] = 'typo3form.fieldTogglePlaceholder('
+                                       . GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ', ' . ($checked ? 'false' : 'true') . ');';
+
+                               // Renders a input or textarea field depending on type of "parent"
+                               $options = array();
+                               $options['databaseRow'] = array();
+                               $options['table'] = '';
+                               $options['parameterArray'] = $parameterArray;
+                               $options['parameterArray']['itemFormElValue'] = GeneralUtility::fixed_lgd_cs($placeholder, 30);
+                               /** @var NoneElement $noneElement */
+                               $noneElement = GeneralUtility::makeInstance(NoneElement::class);
+                               $noneElementResult = $noneElement->setGlobalOptions($options)->render();
+                               $noneElementHtml = $noneElementResult['html'];
+
+                               $html = '
+                               <input type="hidden" name="' . htmlspecialchars($parameterArray['itemFormElNameActive']) . '" value="0" />
+                               <div class="checkbox">
+                                       <label>
+                                               <input type="checkbox" name="' . htmlspecialchars($parameterArray['itemFormElNameActive']) . '" value="1" id="tce-forms-textfield-use-override-' . $fieldName . '-' . $row['uid'] . '" onchange="' . htmlspecialchars($onChange) . '"' . $checked . ' />
+                                               ' . sprintf($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.placeholder.override'), BackendUtility::getRecordTitlePrep($placeholder, 20)) . '
+                                       </label>
+                               </div>
+                               <div class="t3js-formengine-placeholder-placeholder">
+                                       ' . $noneElementHtml . '
+                               </div>
+                               <div class="t3js-formengine-placeholder-formfield">' . $html . '</div>';
+                       }
+
+                       $resultArray['html'] = $html;
+               }
+               return $resultArray;
+       }
+
+       /**
+        * Renders the display of default language record content around current field.
+        * Will render content if any is found in the internal array, $this->defaultLanguageData,
+        * depending on registerDefaultLanguageData() being called prior to this.
+        *
+        * @param string $table Table name of the record being edited
+        * @param string $field Field name represented by $item
+        * @param array $row Record array of the record being edited
+        * @param string $item HTML of the form field. This is what we add the content to.
+        * @return string Item string returned again, possibly with the original value added to.
+        */
+       protected function renderDefaultLanguageContent($table, $field, $row, $item) {
+               if (is_array($this->globalOptions['defaultLanguageData'][$table . ':' . $row['uid']])) {
+                       $defaultLanguageValue = BackendUtility::getProcessedValue(
+                               $table,
+                               $field,
+                               $this->globalOptions['defaultLanguageData'][$table . ':' . $row['uid']][$field],
+                               0,
+                               1,
+                               FALSE,
+                               $this->globalOptions['defaultLanguageData'][$table . ':' . $row['uid']]['uid']
+                       );
+                       $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$field];
+                       // Don't show content if it's for IRRE child records:
+                       if ($fieldConfig['config']['type'] !== 'inline') {
+                               if ($defaultLanguageValue !== '') {
+                                       $item .= '<div class="t3-form-original-language">' . FormEngineUtility::getLanguageIcon($table, $row, 0)
+                                               . $this->getMergeBehaviourIcon($fieldConfig['l10n_mode'])
+                                               . $this->previewFieldValue($defaultLanguageValue, $fieldConfig, $field) . '</div>';
+                               }
+                               $additionalPreviewLanguages = $this->globalOptions['additionalPreviewLanguages'];
+                               foreach ($additionalPreviewLanguages as $previewLanguage) {
+                                       $defaultLanguageValue = BackendUtility::getProcessedValue(
+                                               $table,
+                                               $field,
+                                               $this->globalOptions['additionalPreviewLanguageData'][$table . ':' . $row['uid']][$previewLanguage['uid']][$field],
+                                               0,
+                                               1
+                                       );
+                                       if ($defaultLanguageValue !== '') {
+                                               $item .= '<div class="t3-form-original-language">'
+                                                       . FormEngineUtility::getLanguageIcon($table, $row, ('v' . $previewLanguage['ISOcode']))
+                                                       . $this->getMergeBehaviourIcon($fieldConfig['l10n_mode'])
+                                                       . $this->previewFieldValue($defaultLanguageValue, $fieldConfig, $field) . '</div>';
+                                       }
+                               }
+                       }
+               }
+               return $item;
+       }
+
+       /**
+        * Renders an icon to indicate the way the translation and the original is merged (if this is relevant).
+        *
+        * If a field is defined as 'mergeIfNotBlank' this is useful information for an editor. He/she can leave the field blank and
+        * the original value will be used. Without this hint editors are likely to copy the contents even if it is not necessary.
+        *
+        * @param string $l10nMode Localization mode from TCA
+        * @return string
+        */
+       protected function getMergeBehaviourIcon($l10nMode) {
+               $icon = '';
+               if ($l10nMode === 'mergeIfNotBlank') {
+                       $icon = IconUtility::getSpriteIcon(
+                               'actions-edit-merge-localization',
+                               array('title' => $this->getLanguageService()->sL('LLL:EXT:lang/locallang_misc.xlf:localizeMergeIfNotBlank'))
+                       );
+               }
+               return $icon;
+       }
+
+       /**
+        * Renders the diff-view of default language record content compared with what the record was originally translated from.
+        * Will render content if any is found in the internal array, $this->defaultLanguageData,
+        * depending on registerDefaultLanguageData() being called prior to this.
+        *
+        * @param string $table Table name of the record being edited
+        * @param string $field Field name represented by $item
+        * @param array $row Record array of the record being edited
+        * @param string  $item HTML of the form field. This is what we add the content to.
+        * @return string Item string returned again, possibly with the original value added to.
+        */
+       protected function renderDefaultLanguageDiff($table, $field, $row, $item) {
+               if (is_array($this->globalOptions['defaultLanguageDataDiff'][$table . ':' . $row['uid']])) {
+                       // Initialize:
+                       $dLVal = array(
+                               'old' => $this->globalOptions['defaultLanguageDataDiff'][$table . ':' . $row['uid']],
+                               'new' => $this->globalOptions['defaultLanguageData'][$table . ':' . $row['uid']]
+                       );
+                       // There must be diff-data:
+                       if (isset($dLVal['old'][$field])) {
+                               if ((string)$dLVal['old'][$field] !== (string)$dLVal['new'][$field]) {
+                                       // Create diff-result:
+                                       $diffUtility = GeneralUtility::makeInstance(DiffUtility::class);
+                                       $diffres = $diffUtility->makeDiffDisplay(
+                                               BackendUtility::getProcessedValue($table, $field, $dLVal['old'][$field], 0, 1),
+                                               BackendUtility::getProcessedValue($table, $field, $dLVal['new'][$field], 0, 1)
+                                       );
+                                       $item .= '<div class="t3-form-original-language-diff">
+                                               <div class="t3-form-original-language-diffheader">' .
+                                                       htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.changeInOrig')) .
+                                               '</div>
+                                               <div class="t3-form-original-language-diffcontent">' . $diffres . '</div>
+                                       </div>';
+                               }
+                       }
+               }
+               return $item;
+       }
+
+       /**
+        * Checks if the $table is the child of a inline type AND the $field is the label field of this table.
+        * This function is used to dynamically update the label while editing. This has no effect on labels,
+        * that were processed by a FormEngine-hook on saving.
+        *
+        * @param string $table The table to check
+        * @param string $field The field on this table to check
+        * @return bool Is inline child and field is responsible for the label
+        */
+       protected function isInlineChildAndLabelField($table, $field) {
+               /** @var InlineStackProcessor $inlineStackProcessor */
+               $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
+               $inlineStackProcessor->initializeByGivenStructure($this->globalOptions['inlineStructure']);
+               $level = $inlineStackProcessor->getStructureLevel(-1);
+               if ($level['config']['foreign_label']) {
+                       $label = $level['config']['foreign_label'];
+               } else {
+                       $label = $GLOBALS['TCA'][$table]['ctrl']['label'];
+               }
+               return $level['config']['foreign_table'] === $table && $label == $field ? TRUE : FALSE;
+       }
+
+       /**
+        * Rendering of inline fields should be skipped under certain circumstances
+        *
+        * @return boolean TRUE if field should be skipped based on inline configuration
+        */
+       protected function inlineFieldShouldBeSkipped() {
+               $table = $this->globalOptions['table'];
+               $row = $this->globalOptions['databaseRow'];
+               $fieldName = $this->globalOptions['fieldName'];
+               $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
+
+               /** @var InlineStackProcessor $inlineStackProcessor */
+               $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
+               $inlineStackProcessor->initializeByGivenStructure($this->globalOptions['inlineStructure']);
+               $structureDepth = $inlineStackProcessor->getStructureDepth();
+
+               $skipThisField = FALSE;
+               if ($structureDepth > 0) {
+                       $searchArray = array(
+                               '%OR' => array(
+                                       'config' => array(
+                                               0 => array(
+                                                       '%AND' => array(
+                                                               'foreign_table' => $table,
+                                                               '%OR' => array(
+                                                                       '%AND' => array(
+                                                                               'appearance' => array('useCombination' => TRUE),
+                                                                               'foreign_selector' => $fieldName
+                                                                       ),
+                                                                       'MM' => $fieldConfig['MM']
+                                                               )
+                                                       )
+                                               ),
+                                               1 => array(
+                                                       '%AND' => array(
+                                                               'foreign_table' => $fieldConfig['foreign_table'],
+                                                               'foreign_selector' => $fieldConfig['foreign_field']
+                                                       )
+                                               )
+                                       )
+                               )
+                       );
+                       // Get the parent record from structure stack
+                       $level = $inlineStackProcessor->getStructureLevel(-1);
+                       // If we have symmetric fields, check on which side we are and hide fields, that are set automatically:
+                       if (RelationHandler::isOnSymmetricSide($level['uid'], $level['config'], $row)) {
+                               $searchArray['%OR']['config'][0]['%AND']['%OR']['symmetric_field'] = $fieldName;
+                               $searchArray['%OR']['config'][0]['%AND']['%OR']['symmetric_sortby'] = $fieldName;
+                       } else {
+                               $searchArray['%OR']['config'][0]['%AND']['%OR']['foreign_field'] = $fieldName;
+                               $searchArray['%OR']['config'][0]['%AND']['%OR']['foreign_sortby'] = $fieldName;
+                       }
+                       $skipThisField = $this->arrayCompareComplex($level, $searchArray);
+               }
+               return $skipThisField;
+       }
+
+       /**
+        * Handles complex comparison requests on an array.
+        * A request could look like the following:
+        *
+        * $searchArray = array(
+        *   '%AND'     => array(
+        *     'key1'   => 'value1',
+        *     'key2'   => 'value2',
+        *     '%OR'    => array(
+        *       'subarray' => array(
+        *         'subkey' => 'subvalue'
+        *       ),
+        *       'key3' => 'value3',
+        *       'key4' => 'value4'
+        *     )
+        *   )
+        * );
+        *
+        * It is possible to use the array keys '%AND.1', '%AND.2', etc. to prevent
+        * overwriting the sub-array. It could be necessary, if you use complex comparisons.
+        *
+        * The example above means, key1 *AND* key2 (and their values) have to match with
+        * the $subjectArray and additional one *OR* key3 or key4 have to meet the same
+        * condition.
+        * It is also possible to compare parts of a sub-array (e.g. "subarray"), so this
+        * function recurses down one level in that sub-array.
+        *
+        * @param array $subjectArray The array to search in
+        * @param array $searchArray The array with keys and values to search for
+        * @param string $type Use '%AND' or '%OR' for comparison
+        * @return bool The result of the comparison
+        */
+       protected function arrayCompareComplex($subjectArray, $searchArray, $type = '') {
+               $localMatches = 0;
+               $localEntries = 0;
+               if (is_array($searchArray) && count($searchArray)) {
+                       // If no type was passed, try to determine
+                       if (!$type) {
+                               reset($searchArray);
+                               $type = key($searchArray);
+                               $searchArray = current($searchArray);
+                       }
+                       // We use '%AND' and '%OR' in uppercase
+                       $type = strtoupper($type);
+                       // Split regular elements from sub elements
+                       foreach ($searchArray as $key => $value) {
+                               $localEntries++;
+                               // Process a sub-group of OR-conditions
+                               if ($key === '%OR') {
+                                       $localMatches += $this->arrayCompareComplex($subjectArray, $value, '%OR') ? 1 : 0;
+                               } elseif ($key === '%AND') {
+                                       $localMatches += $this->arrayCompareComplex($subjectArray, $value, '%AND') ? 1 : 0;
+                               } elseif (is_array($value) && $this->isAssociativeArray($searchArray)) {
+                                       $localMatches += $this->arrayCompareComplex($subjectArray[$key], $value, $type) ? 1 : 0;
+                               } elseif (is_array($value)) {
+                                       $localMatches += $this->arrayCompareComplex($subjectArray, $value, $type) ? 1 : 0;
+                               } else {
+                                       if (isset($subjectArray[$key]) && isset($value)) {
+                                               // Boolean match:
+                                               if (is_bool($value)) {
+                                                       $localMatches += !($subjectArray[$key] xor $value) ? 1 : 0;
+                                               } elseif (is_numeric($subjectArray[$key]) && is_numeric($value)) {
+                                                       $localMatches += $subjectArray[$key] == $value ? 1 : 0;
+                                               } else {
+                                                       $localMatches += $subjectArray[$key] === $value ? 1 : 0;
+                                               }
+                                       }
+                               }
+                               // If one or more matches are required ('OR'), return TRUE after the first successful match
+                               if ($type === '%OR' && $localMatches > 0) {
+                                       return TRUE;
+                               }
+                               // If all matches are required ('AND') and we have no result after the first run, return FALSE
+                               if ($type === '%AND' && $localMatches == 0) {
+                                       return FALSE;
+                               }
+                       }
+               }
+               // Return the result for '%AND' (if nothing was checked, TRUE is returned)
+               return $localEntries == $localMatches ? TRUE : FALSE;
+       }
+
+       /**
+        * Checks whether an object is an associative array.
+        *
+        * @param mixed $object The object to be checked
+        * @return bool Returns TRUE, if the object is an associative array
+        */
+       protected function isAssociativeArray($object) {
+               return is_array($object) && count($object) && array_keys($object) !== range(0, sizeof($object) - 1) ? TRUE : FALSE;
+       }
+
+       /**
+        * @return BackendUserAuthentication
+        */
+       protected function getBackendUserAuthentication() {
+               return $GLOBALS['BE_USER'];
+       }
+
+       /**
+        * @return LanguageService
+        */
+       protected function getLanguageService() {
+               return $GLOBALS['LANG'];
+       }
+
+}
\ No newline at end of file
diff --git a/typo3/sysext/backend/Classes/Form/Container/SoloFieldContainer.php b/typo3/sysext/backend/Classes/Form/Container/SoloFieldContainer.php
new file mode 100644 (file)
index 0000000..9ae7138
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Container;
+
+/*
+ * 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\Utility\GeneralUtility;
+use TYPO3\CMS\Lang\LanguageService;
+
+/**
+ * An entry container to render just a single field.
+ *
+ * The container operates on $this->globalOptions['singleFieldToRender'] to render
+ * this field. It initializes language stuff and prepares data in globalOptions for
+ * processing of the single field in SingleFieldContainer.
+ */
+class SoloFieldContainer extends AbstractContainer {
+
+       /**
+        * Entry method
+        *
+        * @return array As defined in initializeResultArray() of AbstractNode
+        */
+       public function render() {
+               $table = $this->globalOptions['table'];
+               $row = $this->globalOptions['databaseRow'];
+               $fieldToRender = $this->globalOptions['singleFieldToRender'];
+
+               if (!$GLOBALS['TCA'][$table]) {
+                       return $this->initializeResultArray();
+               }
+
+               $languageService = $this->getLanguageService();
+
+               // Load the description content for the table if requested
+               if ($GLOBALS['TCA'][$table]['interface']['always_description']) {
+                       $languageService->loadSingleTableDescription($table);
+               }
+
+               // If this is a localized record, stuff data from original record to local registry, will then be given to child elements
+               $this->registerDefaultLanguageData($table, $row);
+
+               // Current type value of the record.
+               $recordTypeValue = $this->getRecordTypeValue($table, $row);
+
+               $excludeElements = $this->getExcludeElements($table, $row, $recordTypeValue);
+
+               $resultArray = $this->initializeResultArray();
+               if (isset($GLOBALS['TCA'][$table]['types'][$recordTypeValue])) {
+                       $itemList = $GLOBALS['TCA'][$table]['types'][$recordTypeValue]['showitem'];
+                       if ($itemList) {
+                               $fields = GeneralUtility::trimExplode(',', $itemList, TRUE);
+                               foreach ($fields as $fieldString) {
+                                       $fieldConfiguration = $this->explodeSingleFieldShowItemConfiguration($fieldString);
+                                       $fieldName = $fieldConfiguration['fieldName'];
+                                       if (!in_array($fieldName, $excludeElements, TRUE) && (string)$fieldName === (string)$fieldToRender) {
+                                               if ($GLOBALS['TCA'][$table]['columns'][$fieldName]) {
+                                                       $options = $this->globalOptions;
+                                                       $options['fieldName'] = $fieldName;
+                                                       $options['fieldExtra'] = $fieldConfiguration['fieldExtra'];
+
+                                                       /** @var SingleFieldContainer $singleFieldContainer */
+                                                       $singleFieldContainer = GeneralUtility::makeInstance(SingleFieldContainer::class);
+                                                       $singleFieldContainer->setGlobalOptions($options);
+                                                       $resultArray = $singleFieldContainer->render();
+                                               }
+                                       }
+                               }
+                       }
+               }
+               return $resultArray;
+       }
+
+       /**
+        * @return LanguageService
+        */
+       protected function getLanguageService() {
+               return $GLOBALS['LANG'];
+       }
+
+}
\ No newline at end of file
diff --git a/typo3/sysext/backend/Classes/Form/Container/TabsContainer.php b/typo3/sysext/backend/Classes/Form/Container/TabsContainer.php
new file mode 100644 (file)
index 0000000..c51f237
--- /dev/null
@@ -0,0 +1,126 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Container;
+
+/*
+ * 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\Lang\LanguageService;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Backend\Template\DocumentTemplate;
+
+/**
+ * Render all tabs of a record that has tabs.
+ *
+ * This container is called from FullRecordContainer and resolves the --div-- structure,
+ * operates on given fieldArrays and calls a PaletteAndSingleContainer for each single tab.
+ */
+class TabsContainer extends AbstractContainer {
+
+       /**
+        * Entry method
+        *
+        * @return array As defined in initializeResultArray() of AbstractNode
+        * @throws \RuntimeException
+        */
+       public function render() {
+               $languageService = $this->getLanguageService();
+               $docTemplate = $this->getDocumentTemplate();
+
+               // All the fields to handle in a flat list
+               $fieldsArray = $this->globalOptions['fieldsArray'];
+
+               // Create a nested array from flat fieldArray list
+               $tabsArray = array();
+               // First element will be a --div--, so it is safe to start -1 here to trigger 0 as first array index
+               $currentTabIndex = -1;
+               foreach ($fieldsArray as $fieldString) {
+                       $fieldArray = $this->explodeSingleFieldShowItemConfiguration($fieldString);
+                       if ($fieldArray['fieldName'] === '--div--') {
+                               $currentTabIndex++;
+                               if (empty($fieldArray['fieldLabel'])) {
+                                       throw new \RuntimeException(
+                                               'A --div-- has no label (--div--;fieldLabel) in showitem of ' . implode(',', $fieldsArray),
+                                               1426454001
+                                       );
+                               }
+                               $tabsArray[$currentTabIndex] = array(
+                                       'label' => $languageService->sL($fieldArray['fieldLabel']),
+                                       'elements' => array(),
+                               );
+                       } else {
+                               $tabsArray[$currentTabIndex]['elements'][] = $fieldArray;
+                       }
+               }
+
+               // Iterate over the tabs and compile content in $tabsContent array together with label
+               $tabsContent = array();
+               $resultArray = $this->initializeResultArray();
+
+               $tabId = 'TCEforms:' . $this->globalOptions['table'] . ':' . $this->globalOptions['databaseRow']['uid'];
+               // @todo: This duplicates parts of the docTemplate code
+               $tabIdString = $docTemplate->getDynTabMenuId($tabId);
+
+               $tabCounter = 0;
+               foreach ($tabsArray as $tabWithLabelAndElements) {
+                       $tabCounter ++;
+                       $elements = $tabWithLabelAndElements['elements'];
+
+                       // Merge elements of this tab into a single list again and hand over to
+                       // palette and single field container to render this group
+                       $options = $this->globalOptions;
+                       $options['tabAndInlineStack'][] = array(
+                               'tab',
+                               $tabIdString . '-' . $tabCounter,
+                       );
+                       $options['fieldsArray'] = array();
+                       foreach ($elements as $element) {
+                               $options['fieldsArray'][] = implode(';', $element);
+                       }
+                       /** @var PaletteAndSingleContainer $paletteAndSingleContainer */
+                       $paletteAndSingleContainer = GeneralUtility::makeInstance(PaletteAndSingleContainer::class);
+                       $paletteAndSingleContainer->setGlobalOptions($options);
+
+                       $childArray = $paletteAndSingleContainer->render();
+                       $tabsContent[] = array(
+                               'label' => $tabWithLabelAndElements['label'],
+                               'content' => $childArray['html'],
+                       );
+                       $childArray['html'] = '';
+                       $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
+               }
+
+               // Feed everything to document template for tab rendering
+               $resultArray['html'] = $docTemplate->getDynamicTabMenu($tabsContent, $tabId, 1, FALSE, FALSE);
+               return $resultArray;
+       }
+
+       /**
+        * @return LanguageService
+        */
+       protected function getLanguageService() {
+               return $GLOBALS['LANG'];
+       }
+
+       /**
+        * @throws \RuntimeException
+        * @return DocumentTemplate
+        */
+       protected function getDocumentTemplate() {
+               $docTemplate = $GLOBALS['TBE_TEMPLATE'];
+               if (!is_object($docTemplate)) {
+                       throw new \RuntimeException('No instance of DocumentTemplate found', 1426459735);
+               }
+               return $docTemplate;
+       }
+
+}
index 54f5281..6f12523 100644 (file)
@@ -37,14 +37,6 @@ class DataPreprocessor {
        public $lockRecords = 0;
 
        /**
-        * Is set externally if RTE is disabled.
-        *
-        * @var int
-        * @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8
-        */
-       public $disableRTE = 0;
-
-       /**
         * If the pid in the command is 'prev' then $prevPageID is used as pid for the record.
         * This is used to attach new records to other previous records eg. new pages.
         *
index 80d6a10..2465872 100644 (file)
@@ -17,8 +17,6 @@ namespace TYPO3\CMS\Backend\Form\Element;
 use TYPO3\CMS\Backend\Form\FormEngine;
 use TYPO3\CMS\Backend\Form\DataPreprocessor;
 use TYPO3\CMS\Backend\Template\DocumentTemplate;
-use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
-use TYPO3\CMS\Core\Database\DatabaseConnection;
 use TYPO3\CMS\Lang\LanguageService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
@@ -30,23 +28,13 @@ use TYPO3\CMS\Backend\Form\Wizard\ValueSliderWizard;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Backend\Form\DatabaseFileIconsHookInterface;
 use TYPO3\CMS\Backend\Clipboard\Clipboard;
+use TYPO3\CMS\Backend\Form\AbstractNode;
+use TYPO3\CMS\Backend\Form\InlineStackProcessor;
 
 /**
- * Base class for form elements of FormEngine
+ * Base class for form elements of FormEngine. Contains several helper methods used by single elements.
  */
-abstract class AbstractFormElement {
-
-       /**
-        * @var FormEngine
-        */
-       protected $formEngine;
-
-       /**
-        * A list of global options given from FormEngine to child elements
-        *
-        * @var array
-        */
-       protected $globalOptions = array();
+abstract class AbstractFormElement extends AbstractNode {
 
        /**
         * Default width value for a couple of elements like text
@@ -75,37 +63,6 @@ abstract class AbstractFormElement {
        protected $clipboard = NULL;
 
        /**
-        * Constructor function, setting the FormEngine
-        *
-        * @param FormEngine $formEngine
-        */
-       public function __construct(FormEngine $formEngine) {
-               $this->formEngine = $formEngine;
-       }
-
-       /**
-        * Set global options from parent FormEngine instance
-        *
-        * @param array $globalOptions Global options like 'readonly' for all elements
-        * @return AbstractFormElement
-        */
-       public function setGlobalOptions(array $globalOptions) {
-               $this->globalOptions = $globalOptions;
-               return $this;
-       }
-
-       /**
-        * Handler for Flex Forms
-        *
-        * @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 $additionalInformation An array with additional configuration options.
-        * @return string The HTML code for the TCEform field
-        */
-       abstract public function render($table, $field, $row, &$additionalInformation);
-
-       /**
         * @return bool TRUE if field is set to read only
         */
        protected function isGlobalReadonly() {
@@ -176,7 +133,7 @@ abstract class AbstractFormElement {
                // Manipulate the field name (to be the TRUE form field name) and remove
                // a suffix-value if the item is a selector box with renderMode "singlebox":
                $listFlag = '_list';
-               if ($PA['fieldConf']['config']['form_type'] == 'select') {
+               if ($PA['fieldConf']['config']['type'] == 'select') {
                        // Single select situation:
                        if ($PA['fieldConf']['config']['maxitems'] <= 1) {
                                $listFlag = '';
@@ -472,17 +429,17 @@ abstract class AbstractFormElement {
                                // Setting the item to a hidden-field.
                                $item = $itemKinds[1];
                                if (is_array($wizardConfiguration['hideParent'])) {
-                                       // NoneElement does not access formEngine properties, use a dummy for decoupling
-                                       $formEngineDummy = new FormEngine;
                                        /** @var NoneElement $noneElement */
-                                       $noneElement = GeneralUtility::makeInstance(NoneElement::class, $formEngineDummy);
-                                       $elementConfiguration = array(
+                                       $noneElement = GeneralUtility::makeInstance(NoneElement::class);
+                                       $noneElementOptions = $this->globalOptions;
+                                       $noneElementOptions['parameterArray'] = array(
                                                'fieldConf' => array(
                                                        'config' => $wizardConfiguration['hideParent'],
                                                ),
                                                'itemFormElValue' => $PA['itemFormElValue'],
                                        );
-                                       $item .= $noneElement->render('', '', '', $elementConfiguration);
+                                       $noneElementResult = $noneElement->setGlobalOptions($noneElementOptions)->render();
+                                       $item .= $noneElementResult['html'];
                                }
                        }
                }
@@ -614,13 +571,14 @@ abstract class AbstractFormElement {
                if (!$params['readOnly'] && !$params['noList']) {
                        if (!$params['noBrowser']) {
                                // Check against inline uniqueness
-                               /** @var InlineElement $inline */
-                               $inline = $this->globalOptions['inline'];
-                               $inlineParent = $inline->getStructureLevel(-1);
+                               /** @var InlineStackProcessor $inlineStackProcessor */
+                               $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
+                               $inlineStackProcessor->initializeByGivenStructure($this->globalOptions['inlineStructure']);
+                               $inlineParent = $inlineStackProcessor->getStructureLevel(-1);
                                $aOnClickInline = '';
                                if (is_array($inlineParent) && $inlineParent['uid']) {
                                        if ($inlineParent['config']['foreign_table'] == $table && $inlineParent['config']['foreign_unique'] == $field) {
-                                               $objectPrefix = $inline->inlineNames['object'] . InlineElement::Structure_Separator . $table;
+                                               $objectPrefix = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->globalOptions['inlineFirstPid']) . '-' . $table;
                                                $aOnClickInline = $objectPrefix . '|inline.checkUniqueElement|inline.setUniqueElement';
                                                $rOnClickInline = 'inline.revertUnique(\'' . $objectPrefix . '\',null,\'' . $uid . '\');';
                                        }
@@ -909,27 +867,6 @@ abstract class AbstractFormElement {
        }
 
        /**
-        * @return BackendUserAuthentication
-        */
-       protected function getBackendUserAuthentication() {
-               return $GLOBALS['BE_USER'];
-       }
-
-       /**
-        * @return DatabaseConnection
-        */
-       protected function getDatabaseConnection() {
-               return $GLOBALS['TYPO3_DB'];
-       }
-
-       /**
-        * @return DocumentTemplate
-        */
-       protected function getDocumentTemplate() {
-               return $GLOBALS['TBE_TEMPLATE'];
-       }
-
-       /**
         * @return DocumentTemplate
         */
        protected function getControllerDocumentTemplate() {
index c8bde24..b0468a0 100644 (file)
@@ -26,30 +26,26 @@ class CheckboxElement extends AbstractFormElement {
        /**
         * This will render a checkbox or an array of checkboxes
         *
-        * @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 $additionalInformation An array with additional configuration options.
-        * @return string The HTML code for the TCEform field
+        * @return array As defined in initializeResultArray() of AbstractNode
         */
-       public function render($table, $field, $row, &$additionalInformation) {
-               $config = $additionalInformation['fieldConf']['config'];
-               $item = '';
+       public function render() {
+               $config = $this->globalOptions['parameterArray']['fieldConf']['config'];
+               $html = '';
                $disabled = FALSE;
                if ($this->isGlobalReadonly() || $config['readOnly']) {
                        $disabled = TRUE;
                }
                // Traversing the array of items
-               $items = FormEngineUtility::initItemArray($additionalInformation['fieldConf']);
+               $items = FormEngineUtility::initItemArray($this->globalOptions['parameterArray']['fieldConf']);
                if ($config['itemsProcFunc']) {
                        $dataPreprocessor = GeneralUtility::makeInstance(DataPreprocessor::class);
                        $items = $dataPreprocessor->procItems(
                                $items,
-                               $additionalInformation['fieldTSConfig']['itemsProcFunc.'],
+                               $this->globalOptions['parameterArray']['fieldTSConfig']['itemsProcFunc.'],
                                $config,
-                               $table,
-                               $row,
-                               $field
+                               $this->globalOptions['table'],
+                               $this->globalOptions['databaseRow'],
+                               $this->globalOptions['fieldName']
                        );
                }
 
@@ -58,7 +54,7 @@ class CheckboxElement extends AbstractFormElement {
                        $items[] = array('', '');
                        $numberOfItems = 1;
                }
-               $formElementValue = (int)$additionalInformation['itemFormElValue'];
+               $formElementValue = (int)$this->globalOptions['parameterArray']['itemFormElValue'];
                $cols = (int)$config['cols'];
                if ($cols > 1) {
                        $colWidth = (int)floor(12 / $cols);
@@ -88,7 +84,7 @@ class CheckboxElement extends AbstractFormElement {
                                        6 => 'visible-lg-block'
                                );
                        }
-                       $item .= '<div class="checkbox-row row">';
+                       $html .= '<div class="checkbox-row row">';
                        for ($counter = 0; $counter < $numberOfItems; $counter++) {
                                // use "default" for typical single checkboxes
                                $tsConfigKey = ($numberOfItems === 1 ? 'default' : $items[$counter][1]);
@@ -96,24 +92,24 @@ class CheckboxElement extends AbstractFormElement {
                                if ($tsConfigKey === '') {
                                        $tsConfigKey = $counter;
                                }
-                               if (isset($additionalInformation['fieldTSConfig']['altLabels.'][$tsConfigKey])) {
-                                       $label = $this->getLanguageService()->sL($additionalInformation['fieldTSConfig']['altLabels.'][$tsConfigKey]);
+                               if (isset($this->globalOptions['parameterArray']['fieldTSConfig']['altLabels.'][$tsConfigKey])) {
+                                       $label = $this->getLanguageService()->sL($this->globalOptions['parameterArray']['fieldTSConfig']['altLabels.'][$tsConfigKey]);
                                } else {
                                        $label = $items[$counter][0];
                                }
-                               $item .=
+                               $html .=
                                        '<div class="checkbox-column ' . $colClass . '">'
-                                               . $this->renderSingleCheckboxElement($label, $counter,  $formElementValue, $numberOfItems, $additionalInformation, $disabled) .
+                                               . $this->renderSingleCheckboxElement($label, $counter,  $formElementValue, $numberOfItems, $this->globalOptions['parameterArray'], $disabled) .
                                        '</div>';
                                if ($counter + 1 < $numberOfItems && !empty($colClear)) {
                                        foreach ($colClear as $rowBreakAfter => $clearClass) {
                                                if (($counter + 1) % $rowBreakAfter === 0) {
-                                                       $item .= '<div class="clearfix '. $clearClass . '"></div>';
+                                                       $html .= '<div class="clearfix '. $clearClass . '"></div>';
                                                }
                                        }
                                }
                        }
-                       $item .= '</div>';
+                       $html .= '</div>';
                } else {
                        for ($counter = 0; $counter < $numberOfItems; $counter++) {
                                // use "default" for typical single checkboxes
@@ -122,18 +118,20 @@ class CheckboxElement extends AbstractFormElement {
                                if ($tsConfigKey === '') {
                                        $tsConfigKey = $counter;
                                }
-                               if (isset($additionalInformation['fieldTSConfig']['altLabels.'][$tsConfigKey])) {
-                                       $label = $this->getLanguageService()->sL($additionalInformation['fieldTSConfig']['altLabels.'][$tsConfigKey]);
+                               if (isset($this->globalOptions['parameterArray']['fieldTSConfig']['altLabels.'][$tsConfigKey])) {
+                                       $label = $this->getLanguageService()->sL($this->globalOptions['parameterArray']['fieldTSConfig']['altLabels.'][$tsConfigKey]);
                                } else {
                                        $label = $items[$counter][0];
                                }
-                               $item .=  $this->renderSingleCheckboxElement($label, $counter, $formElementValue, $numberOfItems, $additionalInformation, $disabled);
+                               $html .=  $this->renderSingleCheckboxElement($label, $counter, $formElementValue, $numberOfItems, $this->globalOptions['parameterArray'], $disabled);
         &n