From c5ee13f018a9659bb4a12922850737e41f88816c Mon Sep 17 00:00:00 2001
From: Morton Jonuschat <m.jonuschat@mojocode.de>
Date: Wed, 28 Oct 2015 19:47:05 +0100
Subject: [PATCH] [TASK] FormEngine: Move SelectTree processing into a data
 provider

The handling of select form elements with renderType "selectTree" has
been cleanly separated into a dedicated pair of element renderer and
data provider.

Methods used by the default select item provider as well as the tree
item provider have been moved into the AbstractItemProvider, the usage
of the AbstractItemProvider class has been cleaned up and is now limited
to data providers that deal with items.

Preparing the javascript for the tree rendering has been moved into a
dedicated method in the SelectTreeElement.

TcaSelectTreeItemsTest only covers the special data parsing/preparation
needed for the tree display, all common test cases for select item
handling are covered in TcaSelectItemsTest which covers the methods
provided by AbstractItemProvider.

Resolves: #69728
Releases: master
Change-Id: Idabb99263fe8dc860e4ec19c26a8cfe5b5a61fa3
Reviewed-on: https://review.typo3.org/43252
Reviewed-by: Andreas Fernandez <typo3@scripting-base.de>
Tested-by: Andreas Fernandez <typo3@scripting-base.de>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
---
 .../Form/Element/SelectTreeElement.php        |  308 +++--
 .../FormDataProvider/AbstractItemProvider.php | 1127 +++++++++++++++++
 .../Form/FormDataProvider/ReturnUrl.php       |    2 +-
 .../Form/FormDataProvider/TcaFlexFetch.php    |    2 +-
 .../Form/FormDataProvider/TcaFlexPrepare.php  |    2 +-
 .../Form/FormDataProvider/TcaFlexProcess.php  |    2 +-
 .../TcaInlineConfiguration.php                |    2 +-
 .../TcaInlineExpandCollapseState.php          |    2 +-
 .../FormDataProvider/TcaInputPlaceholders.php |   11 +-
 .../Form/FormDataProvider/TcaSelectItems.php  | 1060 +---------------
 .../FormDataProvider/TcaSelectTreeItems.php   |  189 +++
 .../FormDataProvider/TcaSelectItemsTest.php   |   48 +
 .../TcaSelectTreeItemsTest.php                |  119 ++
 .../Form/FormDataProvider/TcaFlexProcess.php  |    2 +-
 .../Configuration/DefaultConfiguration.php    |   21 +-
 15 files changed, 1674 insertions(+), 1223 deletions(-)
 create mode 100644 typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectTreeItems.php
 create mode 100644 typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectTreeItemsTest.php

diff --git a/typo3/sysext/backend/Classes/Form/Element/SelectTreeElement.php b/typo3/sysext/backend/Classes/Form/Element/SelectTreeElement.php
index 75ad4f6f1fc6..2708c5078911 100644
--- a/typo3/sysext/backend/Classes/Form/Element/SelectTreeElement.php
+++ b/typo3/sysext/backend/Classes/Form/Element/SelectTreeElement.php
@@ -16,9 +16,6 @@ namespace TYPO3\CMS\Backend\Form\Element;
 
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
-use TYPO3\CMS\Core\Tree\TableConfiguration\ExtJsArrayTreeRenderer;
-use TYPO3\CMS\Core\Tree\TableConfiguration\TableConfigurationTree;
-use TYPO3\CMS\Core\Tree\TableConfiguration\TreeDataProviderFactory;
 use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
@@ -29,101 +26,108 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  */
 class SelectTreeElement extends AbstractFormElement
 {
+
+    /**
+     * Default height of the tree in pixels.
+     *
+     * @const
+     */
+    const DEFAULT_HEIGHT = 280;
+
+    /**
+     * Default width of the tree in pixels.
+     *
+     * @const
+     */
+    const DEFAULT_WIDTH = 280;
+
     /**
      * Render tree widget
      *
      * @return array As defined in initializeResultArray() of AbstractNode
+     * @see AbstractNode::initializeResultArray()
      */
     public function render()
     {
-        $table = $this->data['tableName'];
-        $field = $this->data['fieldName'];
-        $row = $this->data['databaseRow'];
+        $resultArray = $this->initializeResultArray();
         $parameterArray = $this->data['parameterArray'];
+        $formElementId = md5($parameterArray['itemFormElName']);
 
         // Field configuration from TCA:
         $config = $parameterArray['fieldConf']['config'];
 
-        $possibleSelectboxItems = $config['items'];
+        $resultArray['extJSCODE'] .= LF . $this->generateJavascript($formElementId);
 
-        $selectedNodes = $parameterArray['itemFormElValue'];
+        $html = [];
+        $html[] = '<div class="typo3-tceforms-tree">';
+        $html[] = '    <input class="treeRecord" type="hidden"';
+        $html[] = '           ' . $this->getValidationDataAsDataAttribute($parameterArray['fieldConf']['config']);
+        $html[] = '           data-formengine-input-name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
+        $html[] = '           data-relatedfieldname="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
+        $html[] = '           name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
+        $html[] = '           id="treeinput' . $formElementId . '"';
+        $html[] = '           value="' . htmlspecialchars(implode(',', $config['treeData']['selectedNodes'])) . '"';
+        $html[] = '    />';
+        $html[] = '</div>';
+        $html[] = '<div id="tree_' . $formElementId . '"></div>';
 
-        $selectedNodesForApi = array();
-        foreach ($selectedNodes as $selectedNode) {
-            // @todo: this is ugly - the "old" pipe based value|label syntax is re-created here at the moment
-            foreach ($possibleSelectboxItems as $possibleSelectboxItem) {
-                if ((string)$possibleSelectboxItem[1] === (string)$selectedNode) {
-                    $selectedNodesForApi[] = $selectedNode . '|' . rawurlencode($possibleSelectboxItem[0]);
-                }
-            }
-        }
+        $resultArray['html'] = implode(LF, $html);
 
-        $allowedUids = array();
-        foreach ($possibleSelectboxItems as $item) {
-            if ((int)$item[1] > 0) {
-                $allowedUids[] = $item[1];
-            }
-        }
-        $treeDataProvider = TreeDataProviderFactory::getDataProvider($config, $table, $field, $row);
-        $treeDataProvider->setSelectedList(implode(',', $selectedNodes));
-        $treeDataProvider->setItemWhiteList($allowedUids);
-        $treeDataProvider->initializeTreeData();
-        $treeRenderer = GeneralUtility::makeInstance(ExtJsArrayTreeRenderer::class);
-        $tree = GeneralUtility::makeInstance(TableConfigurationTree::class);
-        $tree->setDataProvider($treeDataProvider);
-        $tree->setNodeRenderer($treeRenderer);
-        $treeData = $tree->render();
-        $itemArray = array();
-
-        /**
-         * @todo: Small bug here: In the past, this was the "not processed list" of default items, but now it is
-         * @todo: a full list of elements. This needs to be fixed later, so "additional" default items are shown again.
-        if (is_array($config['items'])) {
-            foreach ($config['items'] as $additionalItem) {
-                if ($additionalItem[1] !== '--div--') {
-                    $item = new \stdClass();
-                    $item->uid = $additionalItem[1];
-                    $item->text = $this->getLanguageService()->sL($additionalItem[0]);
-                    $item->selectable = TRUE;
-                    $item->leaf = TRUE;
-                    $item->checked = in_array($additionalItem[1], $selectedNodes);
-                    if (file_exists(PATH_typo3 . $additionalItem[3])) {
-                        $item->icon = $additionalItem[3];
-                    } elseif (trim($additionalItem[3]) !== '') {
-                        $item->iconCls = IconUtility::getSpriteIconClasses($additionalItem[3]);
-                    }
-                    $itemArray[] = $item;
-                }
-            }
+        // Wizards:
+        if (empty($config['readOnly'])) {
+            $resultArray['html'] = $this->renderWizards(
+                [$resultArray['html']],
+                $config['wizards'],
+                $this->data['tableName'],
+                $this->data['databaseRow'],
+                $this->data['fieldName'],
+                $parameterArray,
+                $parameterArray['itemFormElName'],
+                BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras'])
+            );
         }
-        */
 
-        $itemArray[] = $treeData;
-        $id = md5($parameterArray['itemFormElName']);
+        return $resultArray;
+    }
+
+    /**
+     * Generates the Ext JS tree JavaScript code.
+     *
+     * @param string $formElementId The HTML element ID of the tree select field.
+     * @return string
+     */
+    protected function generateJavascript($formElementId)
+    {
+        $table = $this->data['tableName'];
+        $field = $this->data['fieldName'];
+        $parameterArray = $this->data['parameterArray'];
+        $config = $parameterArray['fieldConf']['config'];
+
+        $selectedNodes = $this->data['databaseRow'][$this->data['fieldName']];
+
+        $disabled = !empty($config['readOnly']) ? 'true' : 'false';
+        $maxItems = $config['maxitems'] ? (int)$config['maxitems'] : 99999;
+        $exclusiveKeys = !empty($config['exclusiveKeys']) ? $config['exclusiveKeys'] : '';
+
+        $appearance = !empty($config['treeConfig']['appearance']) ? $config['treeConfig']['appearance'] : [];
+        $width = isset($appearance['width']) ? (int)$appearance['width'] : static::DEFAULT_WIDTH;
         if (isset($config['size']) && (int)$config['size'] > 0) {
             $height = (int)$config['size'] * 20;
         } else {
-            $height = 280;
+            $height = static::DEFAULT_HEIGHT;
         }
+        $showHeader = !empty($appearance['showHeader']);
+        $expanded = !empty($appearance['expandAll']);
+        $allowRecursiveMode = !empty($appearance['allowRecursiveMode']) ? 'true' : 'false';
+
         $autoSizeMax = null;
         if (isset($config['autoSizeMax']) && (int)$config['autoSizeMax'] > 0) {
             $autoSizeMax = (int)$config['autoSizeMax'] * 20;
         }
-        $header = false;
-        $expanded = false;
-        $width = 280;
-        $appearance = $config['treeConfig']['appearance'];
-        if (is_array($appearance)) {
-            $header = (bool)$appearance['showHeader'];
-            $expanded = (bool)$appearance['expandAll'];
-            if (isset($appearance['width'])) {
-                $width = (int)$appearance['width'];
-            }
-        }
-        $onChange = '';
-        if ($parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged']) {
-            $onChange = $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'];
-        }
+
+        $onChange = !empty($parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged']) ? $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] : '';
+        $onChange .= !empty($parameterArray['fieldChangeFunc']['alert']) ? $parameterArray['fieldChangeFunc']['alert'] : '';
+
         // 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 (
@@ -138,97 +142,79 @@ class SelectTreeElement extends AbstractFormElement
                 $onChange .= 'if (TBE_EDITOR.checkSubmit(-1)){ TBE_EDITOR.submitForm() };';
             }
         }
-        $html = '
-			<div class="typo3-tceforms-tree">
-				<input class="treeRecord" type="hidden" '
-                    .  $this->getValidationDataAsDataAttribute($config)
-                    . ' data-formengine-input-name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"'
-                    . ' data-relatedfieldname="' . htmlspecialchars($parameterArray['itemFormElName']) . '"'
-                    . ' name="' . htmlspecialchars($parameterArray['itemFormElName']) . '" id="treeinput' . $id . '" value="' . htmlspecialchars(implode(',', $selectedNodesForApi)) . '" />
-			</div>
-			<div id="tree_' . $id . '">
-
-			</div>';
 
-        // Wizards:
-        if (empty($config['readOnly'])) {
-            $html = $this->renderWizards(
-                array($html),
-                $config['wizards'],
-                $table,
-                $row,
-                $field,
-                $parameterArray,
-                $parameterArray['itemFormElName'],
-                BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras'])
-            );
+        $javascript = [];
+        $javascript[] = 'Ext.onReady(function() {';
+        $javascript[] = '    TYPO3.Components.Tree.StandardTreeItemData["' . $formElementId . '"] = ' . json_encode($config['treeData']['items']) . ';';
+        $javascript[] = '    var tree' . $formElementId . ' = new TYPO3.Components.Tree.StandardTree({';
+        $javascript[] = '        id: "' . $formElementId . '",';
+        $javascript[] = '        stateful: true,';
+        $javascript[] = '        stateId: "tcaTrees." + this.ucId,';
+        $javascript[] = '        stateEvents: [],';
+        $javascript[] = '        showHeader: ' . (int)$showHeader . ',';
+        $javascript[] = '        onChange: ' . GeneralUtility::quoteJSvalue($onChange) . ',';
+        $javascript[] = '        countSelectedNodes: ' . count($selectedNodes) . ',';
+        $javascript[] = '        width: ' . $width . ',';
+        $javascript[] = '        rendering: false,';
+        $javascript[] = '        listeners: {';
+        $javascript[] = '            click: function(node, event) {';
+        $javascript[] = '                if (typeof(node.attributes.checked) == "boolean") {';
+        $javascript[] = '                    node.attributes.checked = ! node.attributes.checked;';
+        $javascript[] = '                    node.getUI().toggleCheck(node.attributes.checked);';
+        $javascript[] = '                }';
+        $javascript[] = '            },';
+        $javascript[] = '            dblclick: function(node, event) {';
+        $javascript[] = '                if (typeof(node.attributes.checked) == "boolean") {';
+        $javascript[] = '                    node.attributes.checked = ! node.attributes.checked;';
+        $javascript[] = '                    node.getUI().toggleCheck(node.attributes.checked);';
+        $javascript[] = '                }';
+        $javascript[] = '            },';
+        $javascript[] = '            checkchange: TYPO3.Components.Tree.TcaCheckChangeHandler,';
+        $javascript[] = '            collapsenode: function(node) {';
+        $javascript[] = '                if (node.id !== "root" && !this.rendering) {';
+        $javascript[] = '                    top.TYPO3.Storage.Persistent.removeFromList("tcaTrees." + this.ucId, node.attributes.uid);';
+        $javascript[] = '                }';
+        $javascript[] = '            },';
+        $javascript[] = '            expandnode: function(node) {';
+        $javascript[] = '                if (node.id !== "root" && !this.rendering) {';
+        $javascript[] = '                    top.TYPO3.Storage.Persistent.addToList("tcaTrees." + this.ucId, node.attributes.uid);';
+        $javascript[] = '                }';
+        $javascript[] = '            },';
+        $javascript[] = '            beforerender: function(treeCmp) {';
+        $javascript[] = '                this.rendering = true';
+        $javascript[] = '                // Check if that tree element is already rendered. It is appended on the first tceforms_inline call.';
+        $javascript[] = '                if (Ext.fly(treeCmp.getId())) {';
+        $javascript[] = '                    return false;';
+        $javascript[] = '                }';
+        $javascript[] = '            },';
+        $javascript[] = '            afterrender: function(treeCmp) {';
+        if ($expanded) {
+            $javascript[] = '                treeCmp.expandAll();';
         }
-        $resultArray = $this->initializeResultArray();
-        $resultArray['extJSCODE'] .= LF .
-            'Ext.onReady(function() {
-			TYPO3.Components.Tree.StandardTreeItemData["' . $id . '"] = ' . json_encode($itemArray) . ';
-			var tree' . $id . ' = new TYPO3.Components.Tree.StandardTree({
-				id: "' . $id . '",
-				showHeader: ' . (int)$header . ',
-				onChange: ' . GeneralUtility::quoteJSvalue($onChange) . ',
-				countSelectedNodes: ' . count($selectedNodes) . ',
-				width: ' . (int)$width . ',
-				rendering: false,
-				listeners: {
-					click: function(node, event) {
-						if (typeof(node.attributes.checked) == "boolean") {
-							node.attributes.checked = ! node.attributes.checked;
-							node.getUI().toggleCheck(node.attributes.checked);
-						}
-					},
-					dblclick: function(node, event) {
-						if (typeof(node.attributes.checked) == "boolean") {
-							node.attributes.checked = ! node.attributes.checked;
-							node.getUI().toggleCheck(node.attributes.checked);
-						}
-					},
-					checkchange: TYPO3.Components.Tree.TcaCheckChangeHandler,
-					collapsenode: function(node) {
-						if (node.id !== "root" && !this.rendering) {
-							top.TYPO3.Storage.Persistent.removeFromList("tcaTrees." + this.ucId, node.attributes.uid);
-						}
-					},
-					expandnode: function(node) {
-						if (node.id !== "root" && !this.rendering) {
-							top.TYPO3.Storage.Persistent.addToList("tcaTrees." + this.ucId, node.attributes.uid);
-						}
-					},
-					beforerender: function(treeCmp) {
-					    this.rendering = true;
-						// Check if that tree element is already rendered. It is appended on the first tceforms_inline call.
-						if (Ext.fly(treeCmp.getId())) {
-							return false;
-						}
-					},
-					afterrender: function(treeCmp) {
-					    ' . ($expanded ? 'treeCmp.expandAll();' : '') . '
-					    this.rendering = false;
-					}
-				},
-				tcaMaxItems: ' . ($config['maxitems'] ? (int)$config['maxitems'] : 99999) . ',
-				tcaSelectRecursiveAllowed: ' . ($appearance['allowRecursiveMode'] ? 'true' : 'false') . ',
-				tcaSelectRecursive: false,
-				tcaExclusiveKeys: "' . ($config['exclusiveKeys'] ? $config['exclusiveKeys'] : '') . '",
-				ucId: "' . md5(($table . '|' . $field)) . '",
-				selModel: TYPO3.Components.Tree.EmptySelectionModel,
-				disabled: ' . ($config['readOnly'] ? 'true' : 'false') . '
-			});' . LF .
-            ($autoSizeMax
-                ? 'tree' . $id . '.bodyStyle = "max-height: ' . $autoSizeMax . 'px;min-height: ' . $height . 'px;";'
-                : 'tree' . $id . '.height = ' . $height . ';'
-            ) . LF .
-            'window.setTimeout(function() {
-				tree' . $id . '.render("tree_' . $id . '");
-			}, 200);
-		});';
-        $resultArray['html'] = $html;
+        $javascript[] = '                this.rendering = false;';
+        $javascript[] = '            }';
+        $javascript[] = '        },';
+        $javascript[] = '        tcaMaxItems: ' . $maxItems . ',';
+        $javascript[] = '        tcaSelectRecursiveAllowed: ' . $allowRecursiveMode . ',';
+        $javascript[] = '        tcaSelectRecursive: false,';
+        $javascript[] = '        tcaExclusiveKeys: "' . $exclusiveKeys . '",';
+        $javascript[] = '        ucId: "' . md5(($table . '|' . $field)) . '",';
+        $javascript[] = '        selModel: TYPO3.Components.Tree.EmptySelectionModel,';
+        $javascript[] = '        disabled: ' . $disabled;
+        $javascript[] = '    });';
 
-        return $resultArray;
+        if ($autoSizeMax) {
+            $javascript[] = '    tree' . $formElementId . '.bodyStyle = "max-height: ' . $autoSizeMax . 'px;min-height: ' . $height . 'px;";';
+        } else {
+            $javascript[] = '    tree' . $formElementId . '.height = ' . $height . ';';
+        }
+
+        $javascript[] = '    window.setTimeout(function() {';
+        $javascript[] = '        tree' . $formElementId . '.render("tree_' . $formElementId . '");';
+        $javascript[] = '    }, 200);';
+        $javascript[] = '});';
+
+        return implode(LF, $javascript);
     }
 
     /**
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractItemProvider.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractItemProvider.php
index ab3947778392..4526c21d1c1d 100644
--- a/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractItemProvider.php
+++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractItemProvider.php
@@ -14,9 +14,20 @@ namespace TYPO3\CMS\Backend\Form\FormDataProvider;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
+use TYPO3\CMS\Backend\Module\ModuleLoader;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Database\DatabaseConnection;
+use TYPO3\CMS\Core\Database\RelationHandler;
+use TYPO3\CMS\Core\Imaging\IconFactory;
 use TYPO3\CMS\Core\Messaging\FlashMessage;
+use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
 use TYPO3\CMS\Core\Messaging\FlashMessageService;
+use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\MathUtility;
+use TYPO3\CMS\Core\Utility\PathUtility;
 use TYPO3\CMS\Lang\LanguageService;
 
 /**
@@ -89,6 +100,8 @@ abstract class AbstractItemProvider
      * TCEFORMS.aTable.aField[.types][.aType].addItems.aValue = aLabel,
      * with type specific options merged by pageTsConfig already
      *
+     * Used by TcaSelectItems and TcaSelectTreeItems data providers
+     *
      * @param array $result result array
      * @param string $fieldName Current handle field name
      * @param array $items Incoming items
@@ -120,6 +133,1104 @@ abstract class AbstractItemProvider
         return $items;
     }
 
+    /**
+     * TCA config "special" evaluation. Add them to $items
+     *
+     * Used by TcaSelectItems and TcaSelectTreeItems data providers
+     *
+     * @param array $result Result array
+     * @param string $fieldName Current handle field name
+     * @param array $items Incoming items
+     * @return array Modified item array
+     * @throws \UnexpectedValueException
+     */
+    protected function addItemsFromSpecial(array $result, $fieldName, array $items)
+    {
+        // Guard
+        if (empty($result['processedTca']['columns'][$fieldName]['config']['special'])
+            || !is_string($result['processedTca']['columns'][$fieldName]['config']['special'])
+        ) {
+            return $items;
+        }
+
+        $languageService = $this->getLanguageService();
+        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
+
+        $special = $result['processedTca']['columns'][$fieldName]['config']['special'];
+        if ($special === 'tables') {
+            foreach ($GLOBALS['TCA'] as $currentTable => $_) {
+                if (!empty($GLOBALS['TCA'][$currentTable]['ctrl']['adminOnly'])) {
+                    // Hide "admin only" tables
+                    continue;
+                }
+                $label = !empty($GLOBALS['TCA'][$currentTable]['ctrl']['title']) ? $GLOBALS['TCA'][$currentTable]['ctrl']['title'] : '';
+                $icon = $iconFactory->mapRecordTypeToIconIdentifier($currentTable, []);
+                $helpText = [];
+                $languageService->loadSingleTableDescription($currentTable);
+                // @todo: check if this actually works, currently help texts are missing
+                $helpTextArray = $GLOBALS['TCA_DESCR'][$currentTable]['columns'][''];
+                if (!empty($helpTextArray['description'])) {
+                    $helpText['description'] = $helpTextArray['description'];
+                }
+                $items[] = [$label, $currentTable, $icon, $helpText];
+            }
+        } elseif ($special === 'pagetypes') {
+            if (isset($GLOBALS['TCA']['pages']['columns']['doktype']['config']['items'])
+                && is_array($GLOBALS['TCA']['pages']['columns']['doktype']['config']['items'])
+            ) {
+                $specialItems = $GLOBALS['TCA']['pages']['columns']['doktype']['config']['items'];
+                foreach ($specialItems as $specialItem) {
+                    if (!is_array($specialItem) || $specialItem[1] === '--div--') {
+                        // Skip non arrays and divider items
+                        continue;
+                    }
+                    $label = $specialItem[0];
+                    $value = $specialItem[1];
+                    $icon = $iconFactory->mapRecordTypeToIconIdentifier('pages', ['doktype' => $specialItem[1]]);
+                    $items[] = [$label, $value, $icon];
+                }
+            }
+        } elseif ($special === 'exclude') {
+            $excludeArrays = $this->getExcludeFields();
+            foreach ($excludeArrays as $excludeArray) {
+                list($theTable, $theFullField) = explode(':', $excludeArray[1]);
+                // If the field comes from a FlexForm, the syntax is more complex
+                $theFieldParts = explode(';', $theFullField);
+                $theField = array_pop($theFieldParts);
+                // Add header if not yet set for table:
+                if (!array_key_exists($theTable, $items)) {
+                    $icon = $iconFactory->mapRecordTypeToIconIdentifier($theTable, []);
+                    $items[$theTable] = [
+                        $GLOBALS['TCA'][$theTable]['ctrl']['title'],
+                        '--div--',
+                        $icon
+                    ];
+                }
+                // Add help text
+                $helpText = [];
+                $languageService->loadSingleTableDescription($theTable);
+                $helpTextArray = $GLOBALS['TCA_DESCR'][$theTable]['columns'][$theFullField];
+                if (!empty($helpTextArray['description'])) {
+                    $helpText['description'] = $helpTextArray['description'];
+                }
+                // Item configuration:
+                // @todo: the title calculation does not work well for flex form fields, see unit tests
+                $items[] = [
+                    rtrim($languageService->sL($GLOBALS['TCA'][$theTable]['columns'][$theField]['label']), ':') . ' (' . $theField . ')',
+                    $excludeArray[1],
+                    'empty-empty',
+                    $helpText
+                ];
+            }
+        } elseif ($special === 'explicitValues') {
+            $theTypes = $this->getExplicitAuthFieldValues();
+            $icons = [
+                'ALLOW' => 'status-status-permission-granted',
+                'DENY' => 'status-status-permission-denied'
+            ];
+            // Traverse types:
+            foreach ($theTypes as $tableFieldKey => $theTypeArrays) {
+                if (is_array($theTypeArrays['items'])) {
+                    // Add header:
+                    $items[] = [
+                        $theTypeArrays['tableFieldLabel'],
+                        '--div--',
+                    ];
+                    // Traverse options for this field:
+                    foreach ($theTypeArrays['items'] as $itemValue => $itemContent) {
+                        // Add item to be selected:
+                        $items[] = [
+                            '[' . $itemContent[2] . '] ' . $itemContent[1],
+                            $tableFieldKey . ':' . preg_replace('/[:|,]/', '', $itemValue) . ':' . $itemContent[0],
+                            $icons[$itemContent[0]]
+                        ];
+                    }
+                }
+            }
+        } elseif ($special === 'languages') {
+            // @todo: This should probably use the data provided by DatabaseSystemLanguageRows sitting in $result['systemLanguageRows']
+            /** @var TranslationConfigurationProvider $translationConfigurationProvider */
+            $translationConfigurationProvider = GeneralUtility::makeInstance(TranslationConfigurationProvider::class);
+            $languages = $translationConfigurationProvider->getSystemLanguages();
+            foreach ($languages as $language) {
+                if ($language['uid'] !== -1) {
+                    $items[] = [
+                        0 => $language['title'] . ' [' . $language['uid'] . ']',
+                        1 => $language['uid'],
+                        2 => $language['flagIcon']
+                    ];
+                }
+            }
+        } elseif ($special === 'custom') {
+            $customOptions = $GLOBALS['TYPO3_CONF_VARS']['BE']['customPermOptions'];
+            if (is_array($customOptions)) {
+                foreach ($customOptions as $coKey => $coValue) {
+                    if (is_array($coValue['items'])) {
+                        // Add header:
+                        $items[] = [
+                            $languageService->sL($coValue['header']),
+                            '--div--'
+                        ];
+                        // Traverse items:
+                        foreach ($coValue['items'] as $itemKey => $itemCfg) {
+                            $icon = 'empty-empty';
+                            $helpText = [];
+                            if (!empty($itemCfg[2])) {
+                                $helpText['description'] = $languageService->sL($itemCfg[2]);
+                            }
+                            $items[] = [
+                                $languageService->sL($itemCfg[0]),
+                                $coKey . ':' . preg_replace('/[:|,]/', '', $itemKey),
+                                $icon,
+                                $helpText
+                            ];
+                        }
+                    }
+                }
+            }
+        } elseif ($special === 'modListGroup' || $special === 'modListUser') {
+            $loadModules = GeneralUtility::makeInstance(ModuleLoader::class);
+            $loadModules->load($GLOBALS['TBE_MODULES']);
+            $modList = $special === 'modListUser' ? $loadModules->modListUser : $loadModules->modListGroup;
+            if (is_array($modList)) {
+                foreach ($modList as $theMod) {
+                    // Icon:
+                    $icon = $languageService->moduleLabels['tabs_images'][$theMod . '_tab'];
+                    if ($icon) {
+                        $icon = '../' . PathUtility::stripPathSitePrefix($icon);
+                    }
+                    // Add help text
+                    $helpText = [
+                        'title' => $languageService->moduleLabels['labels'][$theMod . '_tablabel'],
+                        'description' => $languageService->moduleLabels['labels'][$theMod . '_tabdescr']
+                    ];
+
+                    $label = '';
+                    // Add label for main module:
+                    $pp = explode('_', $theMod);
+                    if (count($pp) > 1) {
+                        $label .= $languageService->moduleLabels['tabs'][($pp[0] . '_tab')] . '>';
+                    }
+                    // Add modules own label now:
+                    $label .= $languageService->moduleLabels['tabs'][$theMod . '_tab'];
+
+                    // Item configuration:
+                    $items[] = [$label, $theMod, $icon, $helpText];
+                }
+            }
+        } else {
+            throw new \UnexpectedValueException(
+                'Unknown special value ' . $special . ' for field ' . $fieldName . ' of table ' . $result['tableName'],
+                1439298496
+            );
+        }
+
+        return $items;
+    }
+
+    /**
+     * TCA config "fileFolder" evaluation. Add them to $items
+     *
+     * Used by TcaSelectItems and TcaSelectTreeItems data providers
+     *
+     * @param array $result Result array
+     * @param string $fieldName Current handle field name
+     * @param array $items Incoming items
+     * @return array Modified item array
+     */
+    protected function addItemsFromFolder(array $result, $fieldName, array $items)
+    {
+        if (empty($result['processedTca']['columns'][$fieldName]['config']['fileFolder'])
+            || !is_string($result['processedTca']['columns'][$fieldName]['config']['fileFolder'])
+        ) {
+            return $items;
+        }
+
+        $fileFolder = $result['processedTca']['columns'][$fieldName]['config']['fileFolder'];
+        $fileFolder = GeneralUtility::getFileAbsFileName($fileFolder);
+        $fileFolder = rtrim($fileFolder, '/') . '/';
+
+        if (@is_dir($fileFolder)) {
+            $fileExtensionList = '';
+            if (!empty($result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList'])
+                && is_string($result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList'])
+            ) {
+                $fileExtensionList = $result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList'];
+            }
+            $recursionLevels = isset($fieldValue['config']['fileFolder_recursions'])
+                ? MathUtility::forceIntegerInRange($fieldValue['config']['fileFolder_recursions'], 0, 99)
+                : 99;
+            $fileArray = GeneralUtility::getAllFilesAndFoldersInPath([], $fileFolder, $fileExtensionList, 0, $recursionLevels);
+            $fileArray = GeneralUtility::removePrefixPathFromList($fileArray, $fileFolder);
+            foreach ($fileArray as $fileReference) {
+                $fileInformation = pathinfo($fileReference);
+                $icon = GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], strtolower($fileInformation['extension']))
+                    ? '../' . PathUtility::stripPathSitePrefix($fileFolder) . $fileReference
+                    : '';
+                $items[] = [
+                    $fileReference,
+                    $fileReference,
+                    $icon
+                ];
+            }
+        }
+
+        return $items;
+    }
+
+    /**
+     * TCA config "foreign_table" evaluation. Add them to $items
+     *
+     * Used by TcaSelectItems and TcaSelectTreeItems data providers
+     *
+     * @param array $result Result array
+     * @param string $fieldName Current handle field name
+     * @param array $items Incoming items
+     * @return array Modified item array
+     */
+    protected function addItemsFromForeignTable(array $result, $fieldName, array $items)
+    {
+        // Guard
+        if (empty($result['processedTca']['columns'][$fieldName]['config']['foreign_table'])
+            || !is_string($result['processedTca']['columns'][$fieldName]['config']['foreign_table'])
+        ) {
+            return $items;
+        }
+
+        $languageService = $this->getLanguageService();
+        $database = $this->getDatabaseConnection();
+
+        $foreignTable = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'];
+        $foreignTableQueryArray = $this->buildForeignTableQuery($result, $fieldName);
+        $queryResource = $database->exec_SELECT_queryArray($foreignTableQueryArray);
+
+        // Early return on error with flash message
+        $databaseError = $database->sql_error();
+        if (!empty($databaseError)) {
+            $msg = htmlspecialchars($databaseError) . '<br />' . LF;
+            $msg .= $languageService->sL('LLL:EXT:lang/locallang_core.xlf:error.database_schema_mismatch');
+            $msgTitle = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:error.database_schema_mismatch_title');
+            /** @var $flashMessage FlashMessage */
+            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, $msgTitle, FlashMessage::ERROR, true);
+            /** @var $flashMessageService FlashMessageService */
+            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
+            /** @var $defaultFlashMessageQueue FlashMessageQueue */
+            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
+            $defaultFlashMessageQueue->enqueue($flashMessage);
+            $database->sql_free_result($queryResource);
+            return $items;
+        }
+
+        $labelPrefix = '';
+        if (!empty($result['processedTca']['columns'][$fieldName]['config']['foreign_table_prefix'])) {
+            $labelPrefix = $result['processedTca']['columns'][$fieldName]['config']['foreign_table_prefix'];
+            $labelPrefix = $languageService->sL($labelPrefix);
+        }
+        $iconFieldName = '';
+        if (!empty($result['processedTca']['ctrl']['selicon_field'])) {
+            $iconFieldName = $result['processedTca']['ctrl']['selicon_field'];
+        }
+        $iconPath = '';
+        if (!empty($result['processedTca']['ctrl']['selicon_field_path'])) {
+            $iconPath = $result['processedTca']['ctrl']['selicon_field_path'];
+        }
+
+        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
+
+        while ($foreignRow = $database->sql_fetch_assoc($queryResource)) {
+            BackendUtility::workspaceOL($foreignTable, $foreignRow);
+            if (is_array($foreignRow)) {
+                // Prepare the icon if available:
+                if ($iconFieldName && $iconPath && $foreignRow[$iconFieldName]) {
+                    $iParts = GeneralUtility::trimExplode(',', $foreignRow[$iconFieldName], true);
+                    $icon = '../' . $iconPath . '/' . trim($iParts[0]);
+                } else {
+                    $icon = $iconFactory->mapRecordTypeToIconIdentifier($foreignTable, $foreignRow);
+                }
+                // Add the item
+                $items[] = [
+                    $labelPrefix . htmlspecialchars(BackendUtility::getRecordTitle($foreignTable, $foreignRow)),
+                    $foreignRow['uid'],
+                    $icon
+                ];
+            }
+        }
+
+        $database->sql_free_result($queryResource);
+
+        return $items;
+    }
+
+    /**
+     * Remove items using "keepItems" pageTsConfig
+     *
+     * Used by TcaSelectItems and TcaSelectTreeItems data providers
+     *
+     * @param array $result Result array
+     * @param string $fieldName Current handle field name
+     * @param array $items Incoming items
+     * @return array Modified item array
+     */
+    protected function removeItemsByKeepItemsPageTsConfig(array $result, $fieldName, array $items)
+    {
+        $table = $result['tableName'];
+        if (empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'])
+            || !is_string($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'])
+        ) {
+            return $items;
+        }
+
+        return ArrayUtility::keepItemsInArray(
+            $items,
+            $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'],
+            function ($value) {
+                return $value[1];
+            }
+        );
+    }
+
+    /**
+     * Remove items using "removeItems" pageTsConfig
+     *
+     * Used by TcaSelectItems and TcaSelectTreeItems data providers
+     *
+     * @param array $result Result array
+     * @param string $fieldName Current handle field name
+     * @param array $items Incoming items
+     * @return array Modified item array
+     */
+    protected function removeItemsByRemoveItemsPageTsConfig(array $result, $fieldName, array $items)
+    {
+        $table = $result['tableName'];
+        if (empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'])
+            || !is_string($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'])
+        ) {
+            return $items;
+        }
+
+        $removeItems = GeneralUtility::trimExplode(
+            ',',
+            $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'],
+            true
+        );
+        foreach ($items as $key => $itemValues) {
+            if (in_array($itemValues[1], $removeItems)) {
+                unset($items[$key]);
+            }
+        }
+
+        return $items;
+    }
+
+    /**
+     * Remove items user restriction on language field
+     *
+     * Used by TcaSelectItems and TcaSelectTreeItems data providers
+     *
+     * @param array $result Result array
+     * @param string $fieldName Current handle field name
+     * @param array $items Incoming items
+     * @return array Modified item array
+     */
+    protected function removeItemsByUserLanguageFieldRestriction(array $result, $fieldName, array $items)
+    {
+        // Guard clause returns if not a language field is handled
+        if (empty($result['processedTca']['ctrl']['languageField'])
+            || $result['processedTca']['ctrl']['languageField'] !== $fieldName
+        ) {
+            return $items;
+        }
+
+        $backendUser = $this->getBackendUser();
+        foreach ($items as $key => $itemValues) {
+            if (!$backendUser->checkLanguageAccess($itemValues[1])) {
+                unset($items[$key]);
+            }
+        }
+
+        return $items;
+    }
+
+    /**
+     * Remove items by user restriction on authMode items
+     *
+     * Used by TcaSelectItems and TcaSelectTreeItems data providers
+     *
+     * @param array $result Result array
+     * @param string $fieldName Current handle field name
+     * @param array $items Incoming items
+     * @return array Modified item array
+     */
+    protected function removeItemsByUserAuthMode(array $result, $fieldName, array $items)
+    {
+        // Guard clause returns early if no authMode field is configured
+        if (!isset($result['processedTca']['columns'][$fieldName]['config']['authMode'])
+            || !is_string($result['processedTca']['columns'][$fieldName]['config']['authMode'])
+        ) {
+            return $items;
+        }
+
+        $backendUser = $this->getBackendUser();
+        $authMode = $result['processedTca']['columns'][$fieldName]['config']['authMode'];
+        foreach ($items as $key => $itemValues) {
+            // @todo: checkAuthMode() uses $GLOBAL access for "individual" authMode - get rid of this
+            if (!$backendUser->checkAuthMode($result['tableName'], $fieldName, $itemValues[1], $authMode)) {
+                unset($items[$key]);
+            }
+        }
+
+        return $items;
+    }
+
+    /**
+     * Remove items if doktype is handled for non admin users
+     *
+     * Used by TcaSelectItems and TcaSelectTreeItems data providers
+     *
+     * @param array $result Result array
+     * @param string $fieldName Current handle field name
+     * @param array $items Incoming items
+     * @return array Modified item array
+     */
+    protected function removeItemsByDoktypeUserRestriction(array $result, $fieldName, array $items)
+    {
+        $table = $result['tableName'];
+        $backendUser = $this->getBackendUser();
+        // Guard clause returns if not correct table and field or if user is admin
+        if ($table !== 'pages' && $table !== 'pages_language_overlay'
+            || $fieldName !== 'doktype' || $backendUser->isAdmin()
+        ) {
+            return $items;
+        }
+
+        $allowedPageTypes = $backendUser->groupData['pagetypes_select'];
+        foreach ($items as $key => $itemValues) {
+            if (!GeneralUtility::inList($allowedPageTypes, $itemValues[1])) {
+                unset($items[$key]);
+            }
+        }
+
+        return $items;
+    }
+
+    /**
+     * Returns an array with the exclude fields as defined in TCA and FlexForms
+     * Used for listing the exclude fields in be_groups forms.
+     *
+     * @return array Array of arrays with excludeFields (fieldName, table:fieldName) from TCA
+     *               and FlexForms (fieldName, table:extKey;sheetName;fieldName)
+     */
+    protected function getExcludeFields()
+    {
+        $languageService = $this->getLanguageService();
+        $finalExcludeArray = [];
+
+        // Fetch translations for table names
+        $tableToTranslation = [];
+        // All TCA keys
+        foreach ($GLOBALS['TCA'] as $table => $conf) {
+            $tableToTranslation[$table] = $languageService->sl($conf['ctrl']['title']);
+        }
+        // Sort by translations
+        asort($tableToTranslation);
+        foreach ($tableToTranslation as $table => $translatedTable) {
+            $excludeArrayTable = [];
+
+            // All field names configured and not restricted to admins
+            if (is_array($GLOBALS['TCA'][$table]['columns'])
+                && empty($GLOBALS['TCA'][$table]['ctrl']['adminOnly'])
+                && (empty($GLOBALS['TCA'][$table]['ctrl']['rootLevel']) || !empty($GLOBALS['TCA'][$table]['ctrl']['security']['ignoreRootLevelRestriction']))
+            ) {
+                foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $_) {
+                    if ($GLOBALS['TCA'][$table]['columns'][$field]['exclude']) {
+                        // Get human readable names of fields
+                        $translatedField = $languageService->sl($GLOBALS['TCA'][$table]['columns'][$field]['label']);
+                        // Add entry
+                        $excludeArrayTable[] = [$translatedTable . ': ' . $translatedField, $table . ':' . $field];
+                    }
+                }
+            }
+            // All FlexForm fields
+            $flexFormArray = $this->getRegisteredFlexForms($table);
+            foreach ($flexFormArray as $tableField => $flexForms) {
+                // Prefix for field label, e.g. "Plugin Options:"
+                $labelPrefix = '';
+                if (!empty($GLOBALS['TCA'][$table]['columns'][$tableField]['label'])) {
+                    $labelPrefix = $languageService->sl($GLOBALS['TCA'][$table]['columns'][$tableField]['label']);
+                }
+                // Get all sheets and title
+                foreach ($flexForms as $extIdent => $extConf) {
+                    $extTitle = $languageService->sl($extConf['title']);
+                    // Get all fields in sheet
+                    foreach ($extConf['ds']['sheets'] as $sheetName => $sheet) {
+                        if (empty($sheet['ROOT']['el']) || !is_array($sheet['ROOT']['el'])) {
+                            continue;
+                        }
+                        foreach ($sheet['ROOT']['el'] as $fieldName => $field) {
+                            // Use only fields that have exclude flag set
+                            if (empty($field['TCEforms']['exclude'])) {
+                                continue;
+                            }
+                            $fieldLabel = !empty($field['TCEforms']['label']) ? $languageService->sl($field['TCEforms']['label']) : $fieldName;
+                            $fieldIdent = $table . ':' . $tableField . ';' . $extIdent . ';' . $sheetName . ';' . $fieldName;
+                            $excludeArrayTable[] = [trim($labelPrefix . ' ' . $extTitle, ': ') . ': ' . $fieldLabel, $fieldIdent];
+                        }
+                    }
+                }
+            }
+            // Sort fields by the translated value
+            if (!empty($excludeArrayTable)) {
+                usort($excludeArrayTable, function (array $array1, array $array2) {
+                    $array1 = reset($array1);
+                    $array2 = reset($array2);
+                    if (is_string($array1) && is_string($array2)) {
+                        return strcasecmp($array1, $array2);
+                    }
+                    return 0;
+                });
+                $finalExcludeArray = array_merge($finalExcludeArray, $excludeArrayTable);
+            }
+        }
+
+        return $finalExcludeArray;
+    }
+
+    /**
+     * Returns all registered FlexForm definitions with title and fields
+     *
+     * @param string $table Table to handle
+     * @return array Data structures with speaking extension title
+     */
+    protected function getRegisteredFlexForms($table)
+    {
+        if (empty($table) || empty($GLOBALS['TCA'][$table]['columns'])) {
+            return [];
+        }
+        $flexForms = [];
+        foreach ($GLOBALS['TCA'][$table]['columns'] as $tableField => $fieldConf) {
+            if (!empty($fieldConf['config']['type']) && !empty($fieldConf['config']['ds']) && $fieldConf['config']['type'] == 'flex') {
+                $flexForms[$tableField] = [];
+                unset($fieldConf['config']['ds']['default']);
+                // Get pointer fields
+                $pointerFields = !empty($fieldConf['config']['ds_pointerField']) ? $fieldConf['config']['ds_pointerField'] : 'list_type,CType';
+                $pointerFields = GeneralUtility::trimExplode(',', $pointerFields);
+                // Get FlexForms
+                foreach ($fieldConf['config']['ds'] as $flexFormKey => $dataStructure) {
+                    // Get extension identifier (uses second value if it's not empty, "list" or "*", else first one)
+                    $identFields = GeneralUtility::trimExplode(',', $flexFormKey);
+                    $extIdent = $identFields[0];
+                    if (!empty($identFields[1]) && $identFields[1] !== 'list' && $identFields[1] !== '*') {
+                        $extIdent = $identFields[1];
+                    }
+                    // Load external file references
+                    if (!is_array($dataStructure)) {
+                        $file = GeneralUtility::getFileAbsFileName(str_ireplace('FILE:', '', $dataStructure));
+                        if ($file && @is_file($file)) {
+                            $dataStructure = GeneralUtility::getUrl($file);
+                        }
+                        $dataStructure = GeneralUtility::xml2array($dataStructure);
+                        if (!is_array($dataStructure)) {
+                            continue;
+                        }
+                    }
+                    // Get flexform content
+                    $dataStructure = GeneralUtility::resolveAllSheetsInDS($dataStructure);
+                    if (empty($dataStructure['sheets']) || !is_array($dataStructure['sheets'])) {
+                        continue;
+                    }
+                    // Use DS pointer to get extension title from TCA
+                    // @todo: I don't understand this code ... does it make sense at all?
+                    $title = $extIdent;
+                    $keyFields = GeneralUtility::trimExplode(',', $flexFormKey);
+                    foreach ($pointerFields as $pointerKey => $pointerName) {
+                        if (empty($keyFields[$pointerKey]) || $keyFields[$pointerKey] === '*' || $keyFields[$pointerKey] === 'list') {
+                            continue;
+                        }
+                        if (!empty($GLOBALS['TCA'][$table]['columns'][$pointerName]['config']['items'])) {
+                            $items = $GLOBALS['TCA'][$table]['columns'][$pointerName]['config']['items'];
+                            if (!is_array($items)) {
+                                continue;
+                            }
+                            foreach ($items as $itemConf) {
+                                if (!empty($itemConf[0]) && !empty($itemConf[1]) && $itemConf[1] == $keyFields[$pointerKey]) {
+                                    $title = $itemConf[0];
+                                    break 2;
+                                }
+                            }
+                        }
+                    }
+                    $flexForms[$tableField][$extIdent] = [
+                        'title' => $title,
+                        'ds' => $dataStructure
+                    ];
+                }
+            }
+        }
+        return $flexForms;
+    }
+
+    /**
+     * Returns an array with explicit Allow/Deny fields.
+     * Used for listing these field/value pairs in be_groups forms
+     *
+     * @return array Array with information from all of $GLOBALS['TCA']
+     */
+    protected function getExplicitAuthFieldValues()
+    {
+        $languageService = static::getLanguageService();
+        $adLabel = [
+            'ALLOW' => $languageService->sl('LLL:EXT:lang/locallang_core.xlf:labels.allow'),
+            'DENY' => $languageService->sl('LLL:EXT:lang/locallang_core.xlf:labels.deny')
+        ];
+        $allowDenyOptions = [];
+        foreach ($GLOBALS['TCA'] as $table => $_) {
+            // All field names configured:
+            if (is_array($GLOBALS['TCA'][$table]['columns'])) {
+                foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $_) {
+                    $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
+                    if ($fieldConfig['type'] === 'select' && $fieldConfig['authMode']) {
+                        // Check for items
+                        if (is_array($fieldConfig['items'])) {
+                            // Get Human Readable names of fields and table:
+                            $allowDenyOptions[$table . ':' . $field]['tableFieldLabel'] =
+                                $languageService->sl($GLOBALS['TCA'][$table]['ctrl']['title']) . ': '
+                                . $languageService->sl($GLOBALS['TCA'][$table]['columns'][$field]['label']);
+                            foreach ($fieldConfig['items'] as $iVal) {
+                                // Values '' is not controlled by this setting.
+                                if ((string)$iVal[1] !== '') {
+                                    // Find iMode
+                                    $iMode = '';
+                                    switch ((string)$fieldConfig['authMode']) {
+                                        case 'explicitAllow':
+                                            $iMode = 'ALLOW';
+                                            break;
+                                        case 'explicitDeny':
+                                            $iMode = 'DENY';
+                                            break;
+                                        case 'individual':
+                                            if ($iVal[4] === 'EXPL_ALLOW') {
+                                                $iMode = 'ALLOW';
+                                            } elseif ($iVal[4] === 'EXPL_DENY') {
+                                                $iMode = 'DENY';
+                                            }
+                                            break;
+                                    }
+                                    // Set iMode
+                                    if ($iMode) {
+                                        $allowDenyOptions[$table . ':' . $field]['items'][$iVal[1]] = [
+                                            $iMode,
+                                            $languageService->sl($iVal[0]),
+                                            $adLabel[$iMode]
+                                        ];
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return $allowDenyOptions;
+    }
+
+    /**
+     * Build query to fetch foreign records
+     *
+     * @param array $result Result array
+     * @param string $localFieldName Current handle field name
+     * @return array Query array ready to be executed via Database->exec_SELECT_queryArray()
+     * @throws \UnexpectedValueException
+     */
+    protected function buildForeignTableQuery(array $result, $localFieldName)
+    {
+        $backendUser = $this->getBackendUser();
+
+        $foreignTableName = $result['processedTca']['columns'][$localFieldName]['config']['foreign_table'];
+
+        if (!is_array($GLOBALS['TCA'][$foreignTableName])) {
+            throw new \UnexpectedValueException(
+                'Field ' . $localFieldName . ' of table ' . $result['tableName'] . ' reference to foreign table '
+                . $foreignTableName . ', but this table is not defined in TCA',
+                1439569743
+            );
+        }
+
+        $foreignTableClauseArray = $this->processForeignTableClause($result, $foreignTableName, $localFieldName);
+
+        $queryArray = [];
+        $queryArray['SELECT'] = BackendUtility::getCommonSelectFields($foreignTableName, $foreignTableName . '.');
+
+        // rootLevel = -1 means that elements can be on the rootlevel OR on any page (pid!=-1)
+        // rootLevel = 0 means that elements are not allowed on root level
+        // rootLevel = 1 means that elements are only on the root level (pid=0)
+        $rootLevel = 0;
+        if (isset($GLOBALS['TCA'][$foreignTableName]['ctrl']['rootLevel'])) {
+            $rootLevel = $GLOBALS['TCA'][$foreignTableName]['ctrl']['rootLevel'];
+        }
+        $deleteClause = BackendUtility::deleteClause($foreignTableName);
+        if ($rootLevel == 1 || $rootLevel == -1) {
+            $pidWhere = $foreignTableName . '.pid' . (($rootLevel == -1) ? '<>-1' : '=0');
+            $queryArray['FROM'] = $foreignTableName;
+            $queryArray['WHERE'] = $pidWhere . $deleteClause . $foreignTableClauseArray['WHERE'];
+        } else {
+            $pageClause = $backendUser->getPagePermsClause(1);
+            if ($foreignTableName === 'pages') {
+                $queryArray['FROM'] = 'pages';
+                $queryArray['WHERE'] = '1=1' . $deleteClause . ' AND' . $pageClause . $foreignTableClauseArray['WHERE'];
+            } else {
+                $queryArray['FROM'] = $foreignTableName . ', pages';
+                $queryArray['WHERE'] = 'pages.uid=' . $foreignTableName . '.pid AND pages.deleted=0'
+                    . $deleteClause . ' AND' . $pageClause . $foreignTableClauseArray['WHERE'];
+            }
+        }
+
+        $queryArray['GROUPBY'] = $foreignTableClauseArray['GROUPBY'];
+        $queryArray['ORDERBY'] = $foreignTableClauseArray['ORDERBY'];
+        $queryArray['LIMIT'] = $foreignTableClauseArray['LIMIT'];
+
+        return $queryArray;
+    }
+
+    /**
+     * Replace markers in a where clause from TCA foreign_table_where
+     *
+     * ###REC_FIELD_[field name]###
+     * ###THIS_UID### - is current element uid (zero if new).
+     * ###CURRENT_PID### - is the current page id (pid of the record).
+     * ###SITEROOT###
+     * ###PAGE_TSCONFIG_ID### - a value you can set from Page TSconfig dynamically.
+     * ###PAGE_TSCONFIG_IDLIST### - a value you can set from Page TSconfig dynamically.
+     * ###PAGE_TSCONFIG_STR### - a value you can set from Page TSconfig dynamically.
+     *
+     * @param array $result Result array
+     * @param string $foreignTableName Name of foreign table
+     * @param string $localFieldName Current handle field name
+     * @return array Query parts with keys WHERE, ORDERBY, GROUPBY, LIMIT
+     */
+    protected function processForeignTableClause(array $result, $foreignTableName, $localFieldName)
+    {
+        $database = $this->getDatabaseConnection();
+        $localTable = $result['tableName'];
+
+        $foreignTableClause = '';
+        if (!empty($result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'])
+            && is_string($result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'])
+        ) {
+            $foreignTableClause = $result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'];
+            // Replace possible markers in query
+            if (strstr($foreignTableClause, '###REC_FIELD_')) {
+                // " AND table.field='###REC_FIELD_field1###' AND ..." -> array(" AND table.field='", "field1###' AND ...")
+                $whereClauseParts = explode('###REC_FIELD_', $foreignTableClause);
+                foreach ($whereClauseParts as $key => $value) {
+                    if ($key !== 0) {
+                        // "field1###' AND ..." -> array("field1", "' AND ...")
+                        $whereClauseSubParts = explode('###', $value, 2);
+                        // @todo: Throw exception if there is no value? What happens for NEW records?
+                        $rowFieldValue = $result['databaseRow'][$whereClauseSubParts[0]];
+                        if (is_array($rowFieldValue)) {
+                            // If a select or group field is used here, it may have been processed already and
+                            // is now an array. Use first selected value in this case.
+                            $rowFieldValue = $rowFieldValue[0];
+                        }
+                        if (substr($whereClauseParts[0], -1) === '\'' && $whereClauseSubParts[1][0] === '\'') {
+                            $whereClauseParts[$key] = $database->quoteStr($rowFieldValue, $foreignTableName) . $whereClauseSubParts[1];
+                        } else {
+                            $whereClauseParts[$key] = $database->fullQuoteStr($rowFieldValue, $foreignTableName) . $whereClauseSubParts[1];
+                        }
+                    }
+                }
+                $foreignTableClause = implode('', $whereClauseParts);
+            }
+
+            $siteRootUid = 0;
+            foreach ($result['rootline'] as $rootlinePage) {
+                if (!empty($rootlinePage['is_siteroot'])) {
+                    $siteRootUid = (int)$rootlinePage['uid'];
+                    break;
+                }
+            }
+            $pageTsConfigId = 0;
+            if ($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID']) {
+                $pageTsConfigId = (int)$result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID'];
+            }
+            $pageTsConfigIdList = 0;
+            if ($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST']) {
+                $pageTsConfigIdList = $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST'];
+                $pageTsConfigIdListArray = GeneralUtility::trimExplode(',', $pageTsConfigIdList, true);
+                $pageTsConfigIdList = [];
+                foreach ($pageTsConfigIdListArray as $pageTsConfigIdListElement) {
+                    if (MathUtility::canBeInterpretedAsInteger($pageTsConfigIdListElement)) {
+                        $pageTsConfigIdList[] = (int)$pageTsConfigIdListElement;
+                    }
+                }
+                $pageTsConfigIdList = implode(',', $pageTsConfigIdList);
+            }
+            $pageTsConfigString = '';
+            if ($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR']) {
+                $pageTsConfigString = $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR'];
+                $pageTsConfigString = $database->quoteStr($pageTsConfigString, $foreignTableName);
+            }
+
+            $foreignTableClause = str_replace(
+                [
+                    '###CURRENT_PID###',
+                    '###THIS_UID###',
+                    '###SITEROOT###',
+                    '###PAGE_TSCONFIG_ID###',
+                    '###PAGE_TSCONFIG_IDLIST###',
+                    '###PAGE_TSCONFIG_STR###'
+                ],
+                [
+                    (int)$result['effectivePid'],
+                    (int)$result['databaseRow']['uid'],
+                    $siteRootUid,
+                    $pageTsConfigId,
+                    $pageTsConfigIdList,
+                    $pageTsConfigString
+                ],
+                $foreignTableClause
+            );
+        }
+
+        // Split the clause into an array with keys WHERE, GROUPBY, ORDERBY, LIMIT
+        // Prepend a space to make sure "[[:space:]]+" will find a space there for the first element.
+        $foreignTableClause = ' ' . $foreignTableClause;
+        $foreignTableClauseArray = [
+            'WHERE' => '',
+            'GROUPBY' => '',
+            'ORDERBY' => '',
+            'LIMIT' => '',
+        ];
+        // Find LIMIT
+        $reg = [];
+        if (preg_match('/^(.*)[[:space:]]+LIMIT[[:space:]]+([[:alnum:][:space:],._]+)$/i', $foreignTableClause, $reg)) {
+            $foreignTableClauseArray['LIMIT'] = trim($reg[2]);
+            $foreignTableClause = $reg[1];
+        }
+        // Find ORDER BY
+        $reg = [];
+        if (preg_match('/^(.*)[[:space:]]+ORDER[[:space:]]+BY[[:space:]]+([[:alnum:][:space:],._]+)$/i', $foreignTableClause, $reg)) {
+            $foreignTableClauseArray['ORDERBY'] = trim($reg[2]);
+            $foreignTableClause = $reg[1];
+        }
+        // Find GROUP BY
+        $reg = [];
+        if (preg_match('/^(.*)[[:space:]]+GROUP[[:space:]]+BY[[:space:]]+([[:alnum:][:space:],._]+)$/i', $foreignTableClause, $reg)) {
+            $foreignTableClauseArray['GROUPBY'] = trim($reg[2]);
+            $foreignTableClause = $reg[1];
+        }
+        // Rest is assumed to be "WHERE" clause
+        $foreignTableClauseArray['WHERE'] = $foreignTableClause;
+
+        return $foreignTableClauseArray;
+    }
+
+    /**
+     * Validate and sanitize database row values of the select field with the given name.
+     * Creates an array out of databaseRow[selectField] values.
+     *
+     * Used by TcaSelectItems and TcaSelectTreeItems data providers
+     *
+     * @param array $result The current result array.
+     * @param string $fieldName Name of the current select field.
+     * @param array $staticValues Array with statically defined items, item value is used as array key.
+     * @return array
+     */
+    protected function processSelectFieldValue(array $result, $fieldName, array $staticValues)
+    {
+        $fieldConfig = $result['processedTca']['columns'][$fieldName];
+
+        // For single select fields we just keep the current value because the renderer
+        // will take care of showing the "Invalid value" text.
+        // For maxitems=1 select fields is is also possible to select empty values.
+        // @todo: move handling of invalid values to this data provider.
+        if ($fieldConfig['config']['maxitems'] === 1 && empty($fieldConfig['config']['MM'])) {
+            return [$result['databaseRow'][$fieldName]];
+        }
+
+        $currentDatabaseValues = array_key_exists($fieldName, $result['databaseRow']) ? $result['databaseRow'][$fieldName] : '';
+        // Selecting empty values does not make sense for fields that can contain more than one item
+        // because it is impossible to determine if the empty value or nothing is selected.
+        // This is why empty values will be removed for multi value fields.
+        $currentDatabaseValuesArray = GeneralUtility::trimExplode(',', $currentDatabaseValues, true);
+        $newDatabaseValueArray = [];
+
+        // Add all values that were defined by static methods and do not come from the relation
+        // e.g. TCA, TSconfig, itemProcFunc etc.
+        foreach ($currentDatabaseValuesArray as $value) {
+            if (isset($staticValues[$value])) {
+                $newDatabaseValueArray[] = $value;
+            }
+        }
+
+        if (isset($fieldConfig['config']['foreign_table']) && !empty($fieldConfig['config']['foreign_table'])) {
+            /** @var RelationHandler $relationHandler */
+            $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
+            $relationHandler->registerNonTableValues = !empty($fieldConfig['config']['allowNonIdValues']);
+            if (!empty($fieldConfig['config']['MM']) && $result['command'] !== 'new') {
+                // MM relation
+                $relationHandler->start(
+                    $currentDatabaseValues,
+                    $fieldConfig['config']['foreign_table'],
+                    $fieldConfig['config']['MM'],
+                    $result['databaseRow']['uid'],
+                    $result['tableName'],
+                    $fieldConfig['config']
+                );
+            } else {
+                // Non MM relation
+                // If not dealing with MM relations, use default live uid, not versioned uid for record relations
+                $relationHandler->start(
+                    $currentDatabaseValues,
+                    $fieldConfig['config']['foreign_table'],
+                    '',
+                    $this->getLiveUid($result),
+                    $result['tableName'],
+                    $fieldConfig['config']
+                );
+            }
+            $newDatabaseValueArray = array_merge($newDatabaseValueArray, $relationHandler->getValueArray());
+        }
+
+        return array_unique($newDatabaseValueArray);
+    }
+
+    /**
+     * Translate the item labels
+     *
+     * Used by TcaSelectItems and TcaSelectTreeItems data providers
+     *
+     * @param array $result Result array
+     * @param array $itemArray Items
+     * @param string $table
+     * @param string $fieldName
+     * @return array
+     */
+    public function translateLabels(array $result, array $itemArray, $table, $fieldName)
+    {
+        $languageService = $this->getLanguageService();
+
+        foreach ($itemArray as $key => $item) {
+            if (!isset($dynamicItems[$key])) {
+                $staticValues[$item[1]] = $item;
+            }
+            if (isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]])
+                && !empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]])
+            ) {
+                $label = $languageService->sL($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]]);
+            } else {
+                $label = $languageService->sL($item[0]);
+            }
+            $value = strlen((string)$item[1]) > 0 ? $item[1] : '';
+            $icon = $item[2] ?: null;
+            $helpText = $item[3] ?: null;
+            $itemArray[$key] = [
+                $label,
+                $value,
+                $icon,
+                $helpText
+            ];
+        }
+
+        return $itemArray;
+    }
+
+    /**
+     * Sanitize incoming item array
+     *
+     * Used by TcaSelectItems and TcaSelectTreeItems data providers
+     *
+     * @param mixed $itemArray
+     * @param string $tableName
+     * @param string $fieldName
+     * @return array
+     */
+    public function sanitizeItemArray($itemArray, $tableName, $fieldName)
+    {
+        if (!is_array($itemArray)) {
+            $itemArray = [];
+        }
+        foreach ($itemArray as $item) {
+            if (!is_array($item)) {
+                throw new \UnexpectedValueException(
+                    'An item in field ' . $fieldName . ' of table ' . $tableName . ' is not an array as expected',
+                    1439288036
+                );
+            }
+        }
+
+        return $itemArray;
+    }
+
+    /**
+     * Make sure maxitems is always filled with a valid integer value.
+     *
+     * Used by TcaSelectItems and TcaSelectTreeItems data providers
+     *
+     * @param mixed $maxItems
+     * @return int
+     */
+    public function sanitizeMaxItems($maxItems)
+    {
+        if (
+            !empty($maxItems)
+            && (int)$maxItems > 1
+        ) {
+            $maxItems = (int)$maxItems;
+        } else {
+            $maxItems = 1;
+        }
+
+        return $maxItems;
+    }
+
+    /**
+     * Gets the record uid of the live default record. If already
+     * pointing to the live record, the submitted record uid is returned.
+     *
+     * @param array $result Result array
+     * @return int
+     * @throws \UnexpectedValueException
+     */
+    protected function getLiveUid(array $result)
+    {
+        $table = $result['tableName'];
+        $row = $result['databaseRow'];
+        $uid = $row['uid'];
+        if (!empty($result['processedTca']['ctrl']['versioningWS'])
+            && $result['pid'] === -1
+        ) {
+            if (empty($row['t3ver_oid'])) {
+                throw new \UnexpectedValueException(
+                    'No t3ver_oid found for record ' . $row['uid'] . ' on table ' . $table,
+                    1440066481
+                );
+            }
+            $uid = $row['t3ver_oid'];
+        }
+        return $uid;
+    }
+
+    /**
+     * Determine the static values in the item array
+     *
+     * Used by TcaSelectItems and TcaSelectTreeItems data providers
+     *
+     * @param array $itemArray All item records for the select field
+     * @param array $dynamicItemArray Item records from dynamic sources
+     * @return array
+     */
+    public function getStaticValues($itemArray, $dynamicItemArray)
+    {
+        $staticValues = [];
+        foreach ($itemArray as $key => $item) {
+            if (!isset($dynamicItemArray[$key])) {
+                $staticValues[$item[1]] = $item;
+            }
+        }
+        return $staticValues;
+    }
+
     /**
      * @return LanguageService
      */
@@ -127,4 +1238,20 @@ abstract class AbstractItemProvider
     {
         return $GLOBALS['LANG'];
     }
+
+    /**
+     * @return DatabaseConnection
+     */
+    protected function getDatabaseConnection()
+    {
+        return $GLOBALS['TYPO3_DB'];
+    }
+
+    /**
+     * @return BackendUserAuthentication
+     */
+    protected function getBackendUser()
+    {
+        return $GLOBALS['BE_USER'];
+    }
 }
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/ReturnUrl.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/ReturnUrl.php
index 51c94df19748..857f5e74f31e 100644
--- a/typo3/sysext/backend/Classes/Form/FormDataProvider/ReturnUrl.php
+++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/ReturnUrl.php
@@ -20,7 +20,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 /**
  * Resolve return Url if not set otherwise.
  */
-class ReturnUrl extends AbstractItemProvider implements FormDataProviderInterface
+class ReturnUrl implements FormDataProviderInterface
 {
     /**
      * Add return unl
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaFlexFetch.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaFlexFetch.php
index 3f605bc24449..64bfe0b016d0 100644
--- a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaFlexFetch.php
+++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaFlexFetch.php
@@ -23,7 +23,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  *
  * This is the first data provider in the chain of flex form related providers.
  */
-class TcaFlexFetch extends AbstractItemProvider implements FormDataProviderInterface
+class TcaFlexFetch implements FormDataProviderInterface
 {
     /**
      * Resolve ds pointer stuff and parse both ds and dv
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaFlexPrepare.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaFlexPrepare.php
index c138b8ae37f7..6a7385846b03 100644
--- a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaFlexPrepare.php
+++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaFlexPrepare.php
@@ -23,7 +23,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  *
  * This data provider is typically executed directly after TcaFlexFetch
  */
-class TcaFlexPrepare extends AbstractItemProvider implements FormDataProviderInterface
+class TcaFlexPrepare implements FormDataProviderInterface
 {
     /**
      * Resolve flex data structures and prepare flex data values.
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaFlexProcess.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaFlexProcess.php
index 62f403ee097e..fb37f3f7a3d3 100644
--- a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaFlexProcess.php
+++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaFlexProcess.php
@@ -25,7 +25,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  *
  * This is typically the last provider, executed after TcaFlexPrepare
  */
-class TcaFlexProcess extends AbstractItemProvider implements FormDataProviderInterface
+class TcaFlexProcess implements FormDataProviderInterface
 {
     /**
      * Determine possible pageTsConfig overrides and apply them to ds.
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInlineConfiguration.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInlineConfiguration.php
index 90ab878e5b29..ffb3021dddd2 100644
--- a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInlineConfiguration.php
+++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInlineConfiguration.php
@@ -21,7 +21,7 @@ use TYPO3\CMS\Core\Utility\MathUtility;
 /**
  * Set or initialize configuration for inline fields in TCA
  */
-class TcaInlineConfiguration extends AbstractItemProvider implements FormDataProviderInterface
+class TcaInlineConfiguration implements FormDataProviderInterface
 {
     /**
      * Find all inline fields and force proper configuration
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInlineExpandCollapseState.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInlineExpandCollapseState.php
index 9789d5c6c784..8fc55a97bce0 100644
--- a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInlineExpandCollapseState.php
+++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInlineExpandCollapseState.php
@@ -21,7 +21,7 @@ use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
  * Fetch information of user specific inline record expanded / collapsed state
  * from user->uc and put it into $result['inlineExpandCollapseStateArray']
  */
-class TcaInlineExpandCollapseState extends AbstractItemProvider implements FormDataProviderInterface
+class TcaInlineExpandCollapseState implements FormDataProviderInterface
 {
     /**
      * Add inline expand / collapse state
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInputPlaceholders.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInputPlaceholders.php
index 0832b1f892c5..4e437249ed2e 100644
--- a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInputPlaceholders.php
+++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInputPlaceholders.php
@@ -20,13 +20,14 @@ use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
+use TYPO3\CMS\Lang\LanguageService;
 
 /**
  * Resolve placeholders for fields of type input or text. The placeholder value
  * in the processedTca section of the result will be replaced with the resolved
  * value.
  */
-class TcaInputPlaceholders extends AbstractItemProvider implements FormDataProviderInterface
+class TcaInputPlaceholders implements FormDataProviderInterface
 {
     /**
      * Resolve placeholders for input/text fields. Placeholders that are simple
@@ -208,4 +209,12 @@ class TcaInputPlaceholders extends AbstractItemProvider implements FormDataProvi
 
         return $allowedTable;
     }
+
+    /**
+     * @return LanguageService
+     */
+    protected function getLanguageService()
+    {
+        return $GLOBALS['LANG'];
+    }
 }
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectItems.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectItems.php
index b25f308d29f4..1a29d46ca478 100644
--- a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectItems.php
+++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectItems.php
@@ -14,22 +14,7 @@ namespace TYPO3\CMS\Backend\Form\FormDataProvider;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
 use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
-use TYPO3\CMS\Backend\Module\ModuleLoader;
-use TYPO3\CMS\Backend\Utility\BackendUtility;
-use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
-use TYPO3\CMS\Core\Database\DatabaseConnection;
-use TYPO3\CMS\Core\Database\RelationHandler;
-use TYPO3\CMS\Core\Imaging\IconFactory;
-use TYPO3\CMS\Core\Messaging\FlashMessage;
-use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
-use TYPO3\CMS\Core\Messaging\FlashMessageService;
-use TYPO3\CMS\Core\Utility\ArrayUtility;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Core\Utility\MathUtility;
-use TYPO3\CMS\Core\Utility\PathUtility;
-use TYPO3\CMS\Lang\LanguageService;
 
 /**
  * Resolve select items, set processed item list in processedTca, sanitize and resolve database field
@@ -45,8 +30,6 @@ class TcaSelectItems extends AbstractItemProvider implements FormDataProviderInt
      */
     public function addData(array $result)
     {
-        $languageService = $this->getLanguageService();
-
         $table = $result['tableName'];
 
         foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
@@ -54,29 +37,13 @@ class TcaSelectItems extends AbstractItemProvider implements FormDataProviderInt
                 continue;
             }
 
-            // Sanitize incoming item array
-            if (!is_array($fieldConfig['config']['items'])) {
-                $fieldConfig['config']['items'] = [];
-            }
-
-            // Make sure maxitems is always filled with a valid integer value.
-            if (
-                !empty($fieldConfig['config']['maxitems'])
-                && (int)$fieldConfig['config']['maxitems'] > 1
-            ) {
-                $fieldConfig['config']['maxitems'] = (int)$fieldConfig['config']['maxitems'];
-            } else {
-                $fieldConfig['config']['maxitems'] = 1;
+            // Make sure we are only processing supported renderTypes
+            if (!in_array($fieldConfig['config']['renderType'], ['selectSingle', 'selectSingleBox', 'selectCheckBox', 'selectMultipleSideBySide'])) {
+                continue;
             }
 
-            foreach ($fieldConfig['config']['items'] as $item) {
-                if (!is_array($item)) {
-                    throw new \UnexpectedValueException(
-                        'An item in field ' . $fieldName . ' of table ' . $table . ' is not an array as expected',
-                        1439288036
-                    );
-                }
-            }
+            $fieldConfig['config']['items'] = $this->sanitizeItemArray($fieldConfig['config']['items'], $table, $fieldName);
+            $fieldConfig['config']['maxitems'] = $this->sanitizeMaxItems($fieldConfig['config']['maxitems']);
 
             $fieldConfig['config']['items'] = $this->addItemsFromPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
             $fieldConfig['config']['items'] = $this->addItemsFromSpecial($result, $fieldName, $fieldConfig['config']['items']);
@@ -100,28 +67,10 @@ class TcaSelectItems extends AbstractItemProvider implements FormDataProviderInt
             }
 
             // Translate labels
-            $staticValues = [];
-            foreach ($fieldConfig['config']['items'] as $key => $item) {
-                if (!isset($dynamicItems[$key])) {
-                    $staticValues[$item[1]] = $item;
-                }
-                if (isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]])
-                    && !empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]])
-                ) {
-                    $label = $languageService->sL($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]]);
-                } else {
-                    $label = $languageService->sL($item[0]);
-                }
-                $value = strlen((string)$item[1]) > 0 ? $item[1] : '';
-                $icon = $item[2] ?: null;
-                $helpText = $item[3] ?: null;
-                $fieldConfig['config']['items'][$key] = [
-                    $label,
-                    $value,
-                    $icon,
-                    $helpText
-                ];
-            }
+            $fieldConfig['config']['items'] = $this->translateLabels($result, $fieldConfig['config']['items'], $table, $fieldName);
+
+            $staticValues = $this->getStaticValues($fieldConfig['config']['items'], $dynamicItems);
+
             // Keys may contain table names, so a numeric array is created
             $fieldConfig['config']['items'] = array_values($fieldConfig['config']['items']);
 
@@ -131,995 +80,4 @@ class TcaSelectItems extends AbstractItemProvider implements FormDataProviderInt
 
         return $result;
     }
-
-    /**
-     * TCA config "special" evaluation. Add them to $items
-     *
-     * @param array $result Result array
-     * @param string $fieldName Current handle field name
-     * @param array $items Incoming items
-     * @return array Modified item array
-     * @throws \UnexpectedValueException
-     */
-    protected function addItemsFromSpecial(array $result, $fieldName, array $items)
-    {
-        // Guard
-        if (empty($result['processedTca']['columns'][$fieldName]['config']['special'])
-            || !is_string($result['processedTca']['columns'][$fieldName]['config']['special'])
-        ) {
-            return $items;
-        }
-
-        $languageService = $this->getLanguageService();
-        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
-
-        $special = $result['processedTca']['columns'][$fieldName]['config']['special'];
-        if ($special === 'tables') {
-            foreach ($GLOBALS['TCA'] as $currentTable => $_) {
-                if (!empty($GLOBALS['TCA'][$currentTable]['ctrl']['adminOnly'])) {
-                    // Hide "admin only" tables
-                    continue;
-                }
-                $label = !empty($GLOBALS['TCA'][$currentTable]['ctrl']['title']) ? $GLOBALS['TCA'][$currentTable]['ctrl']['title'] : '';
-                $icon = $iconFactory->mapRecordTypeToIconIdentifier($currentTable, array());
-                $helpText = array();
-                $languageService->loadSingleTableDescription($currentTable);
-                // @todo: check if this actually works, currently help texts are missing
-                $helpTextArray = $GLOBALS['TCA_DESCR'][$currentTable]['columns'][''];
-                if (!empty($helpTextArray['description'])) {
-                    $helpText['description'] = $helpTextArray['description'];
-                }
-                $items[] = array($label, $currentTable, $icon, $helpText);
-            }
-        } elseif ($special === 'pagetypes') {
-            if (isset($GLOBALS['TCA']['pages']['columns']['doktype']['config']['items'])
-                && is_array($GLOBALS['TCA']['pages']['columns']['doktype']['config']['items'])
-            ) {
-                $specialItems = $GLOBALS['TCA']['pages']['columns']['doktype']['config']['items'];
-                foreach ($specialItems as $specialItem) {
-                    if (!is_array($specialItem) || $specialItem[1] === '--div--') {
-                        // Skip non arrays and divider items
-                        continue;
-                    }
-                    $label = $specialItem[0];
-                    $value = $specialItem[1];
-                    $icon = $iconFactory->mapRecordTypeToIconIdentifier('pages', array('doktype' => $specialItem[1]));
-                    $items[] = array($label, $value, $icon);
-                }
-            }
-        } elseif ($special === 'exclude') {
-            $excludeArrays = $this->getExcludeFields();
-            foreach ($excludeArrays as $excludeArray) {
-                list($theTable, $theFullField) = explode(':', $excludeArray[1]);
-                // If the field comes from a FlexForm, the syntax is more complex
-                $theFieldParts = explode(';', $theFullField);
-                $theField = array_pop($theFieldParts);
-                // Add header if not yet set for table:
-                if (!array_key_exists($theTable, $items)) {
-                    $icon = $iconFactory->mapRecordTypeToIconIdentifier($theTable, array());
-                    $items[$theTable] = array(
-                        $GLOBALS['TCA'][$theTable]['ctrl']['title'],
-                        '--div--',
-                        $icon
-                    );
-                }
-                // Add help text
-                $helpText = array();
-                $languageService->loadSingleTableDescription($theTable);
-                $helpTextArray = $GLOBALS['TCA_DESCR'][$theTable]['columns'][$theFullField];
-                if (!empty($helpTextArray['description'])) {
-                    $helpText['description'] = $helpTextArray['description'];
-                }
-                // Item configuration:
-                // @todo: the title calculation does not work well for flex form fields, see unit tests
-                $items[] = array(
-                    rtrim($languageService->sL($GLOBALS['TCA'][$theTable]['columns'][$theField]['label']), ':') . ' (' . $theField . ')',
-                    $excludeArray[1],
-                    'empty-empty',
-                    $helpText
-                );
-            }
-        } elseif ($special === 'explicitValues') {
-            $theTypes = $this->getExplicitAuthFieldValues();
-            $icons = array(
-                'ALLOW' => 'status-status-permission-granted',
-                'DENY' => 'status-status-permission-denied'
-            );
-            // Traverse types:
-            foreach ($theTypes as $tableFieldKey => $theTypeArrays) {
-                if (is_array($theTypeArrays['items'])) {
-                    // Add header:
-                    $items[] = array(
-                        $theTypeArrays['tableFieldLabel'],
-                        '--div--',
-                    );
-                    // Traverse options for this field:
-                    foreach ($theTypeArrays['items'] as $itemValue => $itemContent) {
-                        // Add item to be selected:
-                        $items[] = array(
-                            '[' . $itemContent[2] . '] ' . $itemContent[1],
-                            $tableFieldKey . ':' . preg_replace('/[:|,]/', '', $itemValue) . ':' . $itemContent[0],
-                            $icons[$itemContent[0]]
-                        );
-                    }
-                }
-            }
-        } elseif ($special === 'languages') {
-            // @todo: This should probably use the data provided by DatabaseSystemLanguageRows sitting in $result['systemLanguageRows']
-            /** @var TranslationConfigurationProvider $translationConfigurationProvider */
-            $translationConfigurationProvider = GeneralUtility::makeInstance(TranslationConfigurationProvider::class);
-            $languages = $translationConfigurationProvider->getSystemLanguages();
-            foreach ($languages as $language) {
-                if ($language['uid'] !== -1) {
-                    $items[] = array(
-                        0 => $language['title'] . ' [' . $language['uid'] . ']',
-                        1 => $language['uid'],
-                        2 => $language['flagIcon']
-                    );
-                }
-            }
-        } elseif ($special === 'custom') {
-            $customOptions = $GLOBALS['TYPO3_CONF_VARS']['BE']['customPermOptions'];
-            if (is_array($customOptions)) {
-                foreach ($customOptions as $coKey => $coValue) {
-                    if (is_array($coValue['items'])) {
-                        // Add header:
-                        $items[] = array(
-                            $languageService->sL($coValue['header']),
-                            '--div--'
-                        );
-                        // Traverse items:
-                        foreach ($coValue['items'] as $itemKey => $itemCfg) {
-                            $icon = 'empty-empty';
-                            $helpText = array();
-                            if (!empty($itemCfg[2])) {
-                                $helpText['description'] = $languageService->sL($itemCfg[2]);
-                            }
-                            $items[] = array(
-                                $languageService->sL($itemCfg[0]),
-                                $coKey . ':' . preg_replace('/[:|,]/', '', $itemKey),
-                                $icon,
-                                $helpText
-                            );
-                        }
-                    }
-                }
-            }
-        } elseif ($special === 'modListGroup' || $special === 'modListUser') {
-            $loadModules = GeneralUtility::makeInstance(ModuleLoader::class);
-            $loadModules->load($GLOBALS['TBE_MODULES']);
-            $modList = $special === 'modListUser' ? $loadModules->modListUser : $loadModules->modListGroup;
-            if (is_array($modList)) {
-                foreach ($modList as $theMod) {
-                    // Icon:
-                    $icon = $languageService->moduleLabels['tabs_images'][$theMod . '_tab'];
-                    if ($icon) {
-                        $icon = '../' . PathUtility::stripPathSitePrefix($icon);
-                    }
-                    // Add help text
-                    $helpText = array(
-                        'title' => $languageService->moduleLabels['labels'][$theMod . '_tablabel'],
-                        'description' => $languageService->moduleLabels['labels'][$theMod . '_tabdescr']
-                    );
-
-                    $label = '';
-                    // Add label for main module:
-                    $pp = explode('_', $theMod);
-                    if (count($pp) > 1) {
-                        $label .= $languageService->moduleLabels['tabs'][($pp[0] . '_tab')] . '>';
-                    }
-                    // Add modules own label now:
-                    $label .= $languageService->moduleLabels['tabs'][$theMod . '_tab'];
-
-                    // Item configuration:
-                    $items[] = array($label, $theMod, $icon, $helpText);
-                }
-            }
-        } else {
-            throw new \UnexpectedValueException(
-                'Unknown special value ' . $special . ' for field ' . $fieldName . ' of table ' . $result['tableName'],
-                1439298496
-            );
-        }
-
-        return $items;
-    }
-
-    /**
-     * TCA config "fileFolder" evaluation. Add them to $items
-     *
-     * @param array $result Result array
-     * @param string $fieldName Current handle field name
-     * @param array $items Incoming items
-     * @return array Modified item array
-     */
-    protected function addItemsFromFolder(array $result, $fieldName, array $items)
-    {
-        if (empty($result['processedTca']['columns'][$fieldName]['config']['fileFolder'])
-            || !is_string($result['processedTca']['columns'][$fieldName]['config']['fileFolder'])
-        ) {
-            return $items;
-        }
-
-        $fileFolder = $result['processedTca']['columns'][$fieldName]['config']['fileFolder'];
-        $fileFolder = GeneralUtility::getFileAbsFileName($fileFolder);
-        $fileFolder = rtrim($fileFolder, '/') . '/';
-
-        if (@is_dir($fileFolder)) {
-            $fileExtensionList = '';
-            if (!empty($result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList'])
-                && is_string($result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList'])
-            ) {
-                $fileExtensionList = $result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList'];
-            }
-            $recursionLevels = isset($fieldValue['config']['fileFolder_recursions'])
-                ? MathUtility::forceIntegerInRange($fieldValue['config']['fileFolder_recursions'], 0, 99)
-                : 99;
-            $fileArray = GeneralUtility::getAllFilesAndFoldersInPath(array(), $fileFolder, $fileExtensionList, 0, $recursionLevels);
-            $fileArray = GeneralUtility::removePrefixPathFromList($fileArray, $fileFolder);
-            foreach ($fileArray as $fileReference) {
-                $fileInformation = pathinfo($fileReference);
-                $icon = GeneralUtility::inList('gif,png,jpeg,jpg', strtolower($fileInformation['extension']))
-                    ? '../' . PathUtility::stripPathSitePrefix($fileFolder) . $fileReference
-                    : '';
-                $items[] = array(
-                    $fileReference,
-                    $fileReference,
-                    $icon
-                );
-            }
-        }
-
-        return $items;
-    }
-
-    /**
-     * TCA config "foreign_table" evaluation. Add them to $items
-     *
-     * @param array $result Result array
-     * @param string $fieldName Current handle field name
-     * @param array $items Incoming items
-     * @return array Modified item array
-     */
-    protected function addItemsFromForeignTable(array $result, $fieldName, array $items)
-    {
-        // Guard
-        if (empty($result['processedTca']['columns'][$fieldName]['config']['foreign_table'])
-            || !is_string($result['processedTca']['columns'][$fieldName]['config']['foreign_table'])
-        ) {
-            return $items;
-        }
-
-        $languageService = $this->getLanguageService();
-        $database = $this->getDatabaseConnection();
-
-        $foreignTable = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'];
-        $foreignTableQueryArray = $this->buildForeignTableQuery($result, $fieldName);
-        $queryResource = $database->exec_SELECT_queryArray($foreignTableQueryArray);
-
-        // Early return on error with flash message
-        $databaseError = $database->sql_error();
-        if (!empty($databaseError)) {
-            $msg = htmlspecialchars($databaseError) . '<br />' . LF;
-            $msg .= $languageService->sL('LLL:EXT:lang/locallang_core.xlf:error.database_schema_mismatch');
-            $msgTitle = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:error.database_schema_mismatch_title');
-            /** @var $flashMessage FlashMessage */
-            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, $msgTitle, FlashMessage::ERROR, true);
-            /** @var $flashMessageService FlashMessageService */
-            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
-            /** @var $defaultFlashMessageQueue FlashMessageQueue */
-            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
-            $defaultFlashMessageQueue->enqueue($flashMessage);
-            $database->sql_free_result($queryResource);
-            return $items;
-        }
-
-        $labelPrefix = '';
-        if (!empty($result['processedTca']['columns'][$fieldName]['config']['foreign_table_prefix'])) {
-            $labelPrefix = $result['processedTca']['columns'][$fieldName]['config']['foreign_table_prefix'];
-            $labelPrefix = $languageService->sL($labelPrefix);
-        }
-        $iconFieldName = '';
-        if (!empty($result['processedTca']['ctrl']['selicon_field'])) {
-            $iconFieldName = $result['processedTca']['ctrl']['selicon_field'];
-        }
-        $iconPath = '';
-        if (!empty($result['processedTca']['ctrl']['selicon_field_path'])) {
-            $iconPath = $result['processedTca']['ctrl']['selicon_field_path'];
-        }
-
-        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
-
-        while ($foreignRow = $database->sql_fetch_assoc($queryResource)) {
-            BackendUtility::workspaceOL($foreignTable, $foreignRow);
-            if (is_array($foreignRow)) {
-                // Prepare the icon if available:
-                if ($iconFieldName && $iconPath && $foreignRow[$iconFieldName]) {
-                    $iParts = GeneralUtility::trimExplode(',', $foreignRow[$iconFieldName], true);
-                    $icon = '../' . $iconPath . '/' . trim($iParts[0]);
-                } else {
-                    $icon = $iconFactory->mapRecordTypeToIconIdentifier($foreignTable, $foreignRow);
-                }
-                // Add the item
-                $items[] = array(
-                    $labelPrefix . htmlspecialchars(BackendUtility::getRecordTitle($foreignTable, $foreignRow)),
-                    $foreignRow['uid'],
-                    $icon
-                );
-            }
-        }
-
-        $database->sql_free_result($queryResource);
-
-        return $items;
-    }
-
-    /**
-     * Remove items using "keepItems" pageTsConfig
-     *
-     * @param array $result Result array
-     * @param string $fieldName Current handle field name
-     * @param array $items Incoming items
-     * @return array Modified item array
-     */
-    protected function removeItemsByKeepItemsPageTsConfig(array $result, $fieldName, array $items)
-    {
-        $table = $result['tableName'];
-        if (empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'])
-            || !is_string($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'])
-        ) {
-            return $items;
-        }
-
-        return ArrayUtility::keepItemsInArray(
-            $items,
-            $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'],
-            function ($value) {
-                return $value[1];
-            }
-        );
-    }
-
-    /**
-     * Remove items using "removeItems" pageTsConfig
-     *
-     * @param array $result Result array
-     * @param string $fieldName Current handle field name
-     * @param array $items Incoming items
-     * @return array Modified item array
-     */
-    protected function removeItemsByRemoveItemsPageTsConfig(array $result, $fieldName, array $items)
-    {
-        $table = $result['tableName'];
-        if (empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'])
-            || !is_string($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'])
-        ) {
-            return $items;
-        }
-
-        $removeItems = GeneralUtility::trimExplode(
-            ',',
-            $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'],
-            true
-        );
-        foreach ($items as $key => $itemValues) {
-            if (in_array($itemValues[1], $removeItems)) {
-                unset($items[$key]);
-            }
-        }
-
-        return $items;
-    }
-
-    /**
-     * Remove items user restriction on language field
-     *
-     * @param array $result Result array
-     * @param string $fieldName Current handle field name
-     * @param array $items Incoming items
-     * @return array Modified item array
-     */
-    protected function removeItemsByUserLanguageFieldRestriction(array $result, $fieldName, array $items)
-    {
-        // Guard clause returns if not a language field is handled
-        if (empty($result['processedTca']['ctrl']['languageField'])
-            || $result['processedTca']['ctrl']['languageField'] !== $fieldName
-        ) {
-            return $items;
-        }
-
-        $backendUser = $this->getBackendUser();
-        foreach ($items as $key => $itemValues) {
-            if (!$backendUser->checkLanguageAccess($itemValues[1])) {
-                unset($items[$key]);
-            }
-        }
-
-        return $items;
-    }
-
-    /**
-     * Remove items by user restriction on authMode items
-     *
-     * @param array $result Result array
-     * @param string $fieldName Current handle field name
-     * @param array $items Incoming items
-     * @return array Modified item array
-     */
-    protected function removeItemsByUserAuthMode(array $result, $fieldName, array $items)
-    {
-        // Guard clause returns early if no authMode field is configured
-        if (!isset($result['processedTca']['columns'][$fieldName]['config']['authMode'])
-            || !is_string($result['processedTca']['columns'][$fieldName]['config']['authMode'])
-        ) {
-            return $items;
-        }
-
-        $backendUser = $this->getBackendUser();
-        $authMode = $result['processedTca']['columns'][$fieldName]['config']['authMode'];
-        foreach ($items as $key => $itemValues) {
-            // @todo: checkAuthMode() uses $GLOBAL access for "individual" authMode - get rid of this
-            if (!$backendUser->checkAuthMode($result['tableName'], $fieldName, $itemValues[1], $authMode)) {
-                unset($items[$key]);
-            }
-        }
-
-        return $items;
-    }
-
-    /**
-     * Remove items if doktype is handled for non admin users
-     *
-     * @param array $result Result array
-     * @param string $fieldName Current handle field name
-     * @param array $items Incoming items
-     * @return array Modified item array
-     */
-    protected function removeItemsByDoktypeUserRestriction(array $result, $fieldName, array $items)
-    {
-        $table = $result['tableName'];
-        $backendUser = $this->getBackendUser();
-        // Guard clause returns if not correct table and field or if user is admin
-        if ($table !== 'pages' && $table !== 'pages_language_overlay'
-            || $fieldName !== 'doktype' || $backendUser->isAdmin()
-        ) {
-            return $items;
-        }
-
-        $allowedPageTypes = $backendUser->groupData['pagetypes_select'];
-        foreach ($items as $key => $itemValues) {
-            if (!GeneralUtility::inList($allowedPageTypes, $itemValues[1])) {
-                unset($items[$key]);
-            }
-        }
-
-        return $items;
-    }
-
-    /**
-     * Returns an array with the exclude fields as defined in TCA and FlexForms
-     * Used for listing the exclude fields in be_groups forms.
-     *
-     * @return array Array of arrays with excludeFields (fieldName, table:fieldName) from TCA
-     *               and FlexForms (fieldName, table:extKey;sheetName;fieldName)
-     */
-    protected function getExcludeFields()
-    {
-        $languageService = $this->getLanguageService();
-        $finalExcludeArray = array();
-
-        // Fetch translations for table names
-        $tableToTranslation = array();
-        // All TCA keys
-        foreach ($GLOBALS['TCA'] as $table => $conf) {
-            $tableToTranslation[$table] = $languageService->sl($conf['ctrl']['title']);
-        }
-        // Sort by translations
-        asort($tableToTranslation);
-        foreach ($tableToTranslation as $table => $translatedTable) {
-            $excludeArrayTable = array();
-
-            // All field names configured and not restricted to admins
-            if (is_array($GLOBALS['TCA'][$table]['columns'])
-                && empty($GLOBALS['TCA'][$table]['ctrl']['adminOnly'])
-                && (empty($GLOBALS['TCA'][$table]['ctrl']['rootLevel']) || !empty($GLOBALS['TCA'][$table]['ctrl']['security']['ignoreRootLevelRestriction']))
-            ) {
-                foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $_) {
-                    if ($GLOBALS['TCA'][$table]['columns'][$field]['exclude']) {
-                        // Get human readable names of fields
-                        $translatedField = $languageService->sl($GLOBALS['TCA'][$table]['columns'][$field]['label']);
-                        // Add entry
-                        $excludeArrayTable[] = array($translatedTable . ': ' . $translatedField, $table . ':' . $field);
-                    }
-                }
-            }
-            // All FlexForm fields
-            $flexFormArray = $this->getRegisteredFlexForms($table);
-            foreach ($flexFormArray as $tableField => $flexForms) {
-                // Prefix for field label, e.g. "Plugin Options:"
-                $labelPrefix = '';
-                if (!empty($GLOBALS['TCA'][$table]['columns'][$tableField]['label'])) {
-                    $labelPrefix = $languageService->sl($GLOBALS['TCA'][$table]['columns'][$tableField]['label']);
-                }
-                // Get all sheets and title
-                foreach ($flexForms as $extIdent => $extConf) {
-                    $extTitle = $languageService->sl($extConf['title']);
-                    // Get all fields in sheet
-                    foreach ($extConf['ds']['sheets'] as $sheetName => $sheet) {
-                        if (empty($sheet['ROOT']['el']) || !is_array($sheet['ROOT']['el'])) {
-                            continue;
-                        }
-                        foreach ($sheet['ROOT']['el'] as $fieldName => $field) {
-                            // Use only fields that have exclude flag set
-                            if (empty($field['TCEforms']['exclude'])) {
-                                continue;
-                            }
-                            $fieldLabel = !empty($field['TCEforms']['label']) ? $languageService->sl($field['TCEforms']['label']) : $fieldName;
-                            $fieldIdent = $table . ':' . $tableField . ';' . $extIdent . ';' . $sheetName . ';' . $fieldName;
-                            $excludeArrayTable[] = array(trim($labelPrefix . ' ' . $extTitle, ': ') . ': ' . $fieldLabel, $fieldIdent);
-                        }
-                    }
-                }
-            }
-            // Sort fields by the translated value
-            if (!empty($excludeArrayTable)) {
-                usort($excludeArrayTable, function (array $array1, array $array2) {
-                    $array1 = reset($array1);
-                    $array2 = reset($array2);
-                    if (is_string($array1) && is_string($array2)) {
-                        return strcasecmp($array1, $array2);
-                    }
-                    return 0;
-                });
-                $finalExcludeArray = array_merge($finalExcludeArray, $excludeArrayTable);
-            }
-        }
-
-        return $finalExcludeArray;
-    }
-
-    /**
-     * Returns all registered FlexForm definitions with title and fields
-     *
-     * @param string $table Table to handle
-     * @return array Data structures with speaking extension title
-     */
-    protected function getRegisteredFlexForms($table)
-    {
-        if (empty($table) || empty($GLOBALS['TCA'][$table]['columns'])) {
-            return array();
-        }
-        $flexForms = array();
-        foreach ($GLOBALS['TCA'][$table]['columns'] as $tableField => $fieldConf) {
-            if (!empty($fieldConf['config']['type']) && !empty($fieldConf['config']['ds']) && $fieldConf['config']['type'] == 'flex') {
-                $flexForms[$tableField] = array();
-                unset($fieldConf['config']['ds']['default']);
-                // Get pointer fields
-                $pointerFields = !empty($fieldConf['config']['ds_pointerField']) ? $fieldConf['config']['ds_pointerField'] : 'list_type,CType';
-                $pointerFields = GeneralUtility::trimExplode(',', $pointerFields);
-                // Get FlexForms
-                foreach ($fieldConf['config']['ds'] as $flexFormKey => $dataStructure) {
-                    // Get extension identifier (uses second value if it's not empty, "list" or "*", else first one)
-                    $identFields = GeneralUtility::trimExplode(',', $flexFormKey);
-                    $extIdent = $identFields[0];
-                    if (!empty($identFields[1]) && $identFields[1] !== 'list' && $identFields[1] !== '*') {
-                        $extIdent = $identFields[1];
-                    }
-                    // Load external file references
-                    if (!is_array($dataStructure)) {
-                        $file = GeneralUtility::getFileAbsFileName(str_ireplace('FILE:', '', $dataStructure));
-                        if ($file && @is_file($file)) {
-                            $dataStructure = GeneralUtility::getUrl($file);
-                        }
-                        $dataStructure = GeneralUtility::xml2array($dataStructure);
-                        if (!is_array($dataStructure)) {
-                            continue;
-                        }
-                    }
-                    // Get flexform content
-                    $dataStructure = GeneralUtility::resolveAllSheetsInDS($dataStructure);
-                    if (empty($dataStructure['sheets']) || !is_array($dataStructure['sheets'])) {
-                        continue;
-                    }
-                    // Use DS pointer to get extension title from TCA
-                    // @todo: I don't understand this code ... does it make sense at all?
-                    $title = $extIdent;
-                    $keyFields = GeneralUtility::trimExplode(',', $flexFormKey);
-                    foreach ($pointerFields as $pointerKey => $pointerName) {
-                        if (empty($keyFields[$pointerKey]) || $keyFields[$pointerKey] === '*' || $keyFields[$pointerKey] === 'list') {
-                            continue;
-                        }
-                        if (!empty($GLOBALS['TCA'][$table]['columns'][$pointerName]['config']['items'])) {
-                            $items = $GLOBALS['TCA'][$table]['columns'][$pointerName]['config']['items'];
-                            if (!is_array($items)) {
-                                continue;
-                            }
-                            foreach ($items as $itemConf) {
-                                if (!empty($itemConf[0]) && !empty($itemConf[1]) && $itemConf[1] == $keyFields[$pointerKey]) {
-                                    $title = $itemConf[0];
-                                    break 2;
-                                }
-                            }
-                        }
-                    }
-                    $flexForms[$tableField][$extIdent] = array(
-                        'title' => $title,
-                        'ds' => $dataStructure
-                    );
-                }
-            }
-        }
-        return $flexForms;
-    }
-
-    /**
-     * Returns an array with explicit Allow/Deny fields.
-     * Used for listing these field/value pairs in be_groups forms
-     *
-     * @return array Array with information from all of $GLOBALS['TCA']
-     */
-    protected function getExplicitAuthFieldValues()
-    {
-        $languageService = static::getLanguageService();
-        $adLabel = array(
-            'ALLOW' => $languageService->sl('LLL:EXT:lang/locallang_core.xlf:labels.allow'),
-            'DENY' => $languageService->sl('LLL:EXT:lang/locallang_core.xlf:labels.deny')
-        );
-        $allowDenyOptions = array();
-        foreach ($GLOBALS['TCA'] as $table => $_) {
-            // All field names configured:
-            if (is_array($GLOBALS['TCA'][$table]['columns'])) {
-                foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $_) {
-                    $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
-                    if ($fieldConfig['type'] === 'select' && $fieldConfig['authMode']) {
-                        // Check for items
-                        if (is_array($fieldConfig['items'])) {
-                            // Get Human Readable names of fields and table:
-                            $allowDenyOptions[$table . ':' . $field]['tableFieldLabel'] =
-                                $languageService->sl($GLOBALS['TCA'][$table]['ctrl']['title']) . ': '
-                                . $languageService->sl($GLOBALS['TCA'][$table]['columns'][$field]['label']);
-                            foreach ($fieldConfig['items'] as $iVal) {
-                                // Values '' is not controlled by this setting.
-                                if ((string)$iVal[1] !== '') {
-                                    // Find iMode
-                                    $iMode = '';
-                                    switch ((string)$fieldConfig['authMode']) {
-                                        case 'explicitAllow':
-                                            $iMode = 'ALLOW';
-                                            break;
-                                        case 'explicitDeny':
-                                            $iMode = 'DENY';
-                                            break;
-                                        case 'individual':
-                                            if ($iVal[4] === 'EXPL_ALLOW') {
-                                                $iMode = 'ALLOW';
-                                            } elseif ($iVal[4] === 'EXPL_DENY') {
-                                                $iMode = 'DENY';
-                                            }
-                                            break;
-                                    }
-                                    // Set iMode
-                                    if ($iMode) {
-                                        $allowDenyOptions[$table . ':' . $field]['items'][$iVal[1]] = array($iMode, $languageService->sl($iVal[0]), $adLabel[$iMode]);
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        }
-        return $allowDenyOptions;
-    }
-
-    /**
-     * Build query to fetch foreign records
-     *
-     * @param array $result Result array
-     * @param string $localFieldName Current handle field name
-     * @return array Query array ready to be executed via Database->exec_SELECT_queryArray()
-     * @throws \UnexpectedValueException
-     */
-    protected function buildForeignTableQuery(array $result, $localFieldName)
-    {
-        $backendUser = $this->getBackendUser();
-
-        $foreignTableName = $result['processedTca']['columns'][$localFieldName]['config']['foreign_table'];
-
-        if (!is_array($GLOBALS['TCA'][$foreignTableName])) {
-            throw new \UnexpectedValueException(
-                'Field ' . $localFieldName . ' of table ' . $result['tableName'] . ' reference to foreign table '
-                . $foreignTableName . ', but this table is not defined in TCA',
-                1439569743
-            );
-        }
-
-        $foreignTableClauseArray = $this->processForeignTableClause($result, $foreignTableName, $localFieldName);
-
-        $queryArray = array();
-        $queryArray['SELECT'] = BackendUtility::getCommonSelectFields($foreignTableName, $foreignTableName . '.');
-
-        // rootLevel = -1 means that elements can be on the rootlevel OR on any page (pid!=-1)
-        // rootLevel = 0 means that elements are not allowed on root level
-        // rootLevel = 1 means that elements are only on the root level (pid=0)
-        $rootLevel = 0;
-        if (isset($GLOBALS['TCA'][$foreignTableName]['ctrl']['rootLevel'])) {
-            $rootLevel = $GLOBALS['TCA'][$foreignTableName]['ctrl']['rootLevel'];
-        }
-        $deleteClause = BackendUtility::deleteClause($foreignTableName);
-        if ($rootLevel == 1 || $rootLevel == -1) {
-            $pidWhere = $foreignTableName . '.pid' . (($rootLevel == -1) ? '<>-1' : '=0');
-            $queryArray['FROM'] = $foreignTableName;
-            $queryArray['WHERE'] = $pidWhere . $deleteClause . $foreignTableClauseArray['WHERE'];
-        } else {
-            $pageClause = $backendUser->getPagePermsClause(1);
-            if ($foreignTableName === 'pages') {
-                $queryArray['FROM'] = 'pages';
-                $queryArray['WHERE'] = '1=1' . $deleteClause . ' AND' . $pageClause . $foreignTableClauseArray['WHERE'];
-            } else {
-                $queryArray['FROM'] = $foreignTableName . ', pages';
-                $queryArray['WHERE'] = 'pages.uid=' . $foreignTableName . '.pid AND pages.deleted=0'
-                    . $deleteClause . ' AND' . $pageClause .  $foreignTableClauseArray['WHERE'];
-            }
-        }
-
-        $queryArray['GROUPBY'] = $foreignTableClauseArray['GROUPBY'];
-        $queryArray['ORDERBY'] = $foreignTableClauseArray['ORDERBY'];
-        $queryArray['LIMIT'] = $foreignTableClauseArray['LIMIT'];
-
-        return $queryArray;
-    }
-
-    /**
-     * Replace markers in a where clause from TCA foreign_table_where
-     *
-     * ###REC_FIELD_[field name]###
-     * ###THIS_UID### - is current element uid (zero if new).
-     * ###CURRENT_PID### - is the current page id (pid of the record).
-     * ###SITEROOT###
-     * ###PAGE_TSCONFIG_ID### - a value you can set from Page TSconfig dynamically.
-     * ###PAGE_TSCONFIG_IDLIST### - a value you can set from Page TSconfig dynamically.
-     * ###PAGE_TSCONFIG_STR### - a value you can set from Page TSconfig dynamically.
-     *
-     * @param array $result Result array
-     * @param string $foreignTableName Name of foreign table
-     * @param string $localFieldName Current handle field name
-     * @return array Query parts with keys WHERE, ORDERBY, GROUPBY, LIMIT
-     */
-    protected function processForeignTableClause(array $result, $foreignTableName, $localFieldName)
-    {
-        $database = $this->getDatabaseConnection();
-        $localTable = $result['tableName'];
-
-        $foreignTableClause = '';
-        if (!empty($result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'])
-            && is_string($result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'])
-        ) {
-            $foreignTableClause = $result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'];
-            // Replace possible markers in query
-            if (strstr($foreignTableClause, '###REC_FIELD_')) {
-                // " AND table.field='###REC_FIELD_field1###' AND ..." -> array(" AND table.field='", "field1###' AND ...")
-                $whereClauseParts = explode('###REC_FIELD_', $foreignTableClause);
-                foreach ($whereClauseParts as $key => $value) {
-                    if ($key !== 0) {
-                        // "field1###' AND ..." -> array("field1", "' AND ...")
-                        $whereClauseSubParts = explode('###', $value, 2);
-                        // @todo: Throw exception if there is no value? What happens for NEW records?
-                        $rowFieldValue = $result['databaseRow'][$whereClauseSubParts[0]];
-                        if (is_array($rowFieldValue)) {
-                            // If a select or group field is used here, it may have been processed already and
-                            // is now an array. Use first selected value in this case.
-                            $rowFieldValue = $rowFieldValue[0];
-                        }
-                        if (substr($whereClauseParts[0], -1) === '\'' && $whereClauseSubParts[1][0] === '\'') {
-                            $whereClauseParts[$key] = $database->quoteStr($rowFieldValue, $foreignTableName) . $whereClauseSubParts[1];
-                        } else {
-                            $whereClauseParts[$key] = $database->fullQuoteStr($rowFieldValue, $foreignTableName) . $whereClauseSubParts[1];
-                        }
-                    }
-                }
-                $foreignTableClause = implode('', $whereClauseParts);
-            }
-
-            $siteRootUid = 0;
-            foreach ($result['rootline'] as $rootlinePage) {
-                if (!empty($rootlinePage['is_siteroot'])) {
-                    $siteRootUid = (int)$rootlinePage['uid'];
-                    break;
-                }
-            }
-            $pageTsConfigId = 0;
-            if ($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID']) {
-                $pageTsConfigId = (int)$result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID'];
-            }
-            $pageTsConfigIdList = 0;
-            if ($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST']) {
-                $pageTsConfigIdList = $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST'];
-                $pageTsConfigIdListArray = GeneralUtility::trimExplode(',', $pageTsConfigIdList, true);
-                $pageTsConfigIdList = array();
-                foreach ($pageTsConfigIdListArray as $pageTsConfigIdListElement) {
-                    if (MathUtility::canBeInterpretedAsInteger($pageTsConfigIdListElement)) {
-                        $pageTsConfigIdList[] = (int)$pageTsConfigIdListElement;
-                    }
-                }
-                $pageTsConfigIdList = implode(',', $pageTsConfigIdList);
-            }
-            $pageTsConfigString = '';
-            if ($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR']) {
-                $pageTsConfigString = $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR'];
-                $pageTsConfigString = $database->quoteStr($pageTsConfigString, $foreignTableName);
-            }
-
-            $foreignTableClause = str_replace(
-                array(
-                    '###CURRENT_PID###',
-                    '###THIS_UID###',
-                    '###SITEROOT###',
-                    '###PAGE_TSCONFIG_ID###',
-                    '###PAGE_TSCONFIG_IDLIST###',
-                    '###PAGE_TSCONFIG_STR###'
-                ),
-                array(
-                    (int)$result['effectivePid'],
-                    (int)$result['databaseRow']['uid'],
-                    $siteRootUid,
-                    $pageTsConfigId,
-                    $pageTsConfigIdList,
-                    $pageTsConfigString
-                ),
-                $foreignTableClause
-            );
-        }
-
-        // Split the clause into an array with keys WHERE, GROUPBY, ORDERBY, LIMIT
-        // Prepend a space to make sure "[[:space:]]+" will find a space there for the first element.
-        $foreignTableClause = ' ' . $foreignTableClause;
-        $foreignTableClauseArray = array(
-            'WHERE' => '',
-            'GROUPBY' => '',
-            'ORDERBY' => '',
-            'LIMIT' => '',
-        );
-        // Find LIMIT
-        $reg = array();
-        if (preg_match('/^(.*)[[:space:]]+LIMIT[[:space:]]+([[:alnum:][:space:],._]+)$/i', $foreignTableClause, $reg)) {
-            $foreignTableClauseArray['LIMIT'] = trim($reg[2]);
-            $foreignTableClause = $reg[1];
-        }
-        // Find ORDER BY
-        $reg = array();
-        if (preg_match('/^(.*)[[:space:]]+ORDER[[:space:]]+BY[[:space:]]+([[:alnum:][:space:],._]+)$/i', $foreignTableClause, $reg)) {
-            $foreignTableClauseArray['ORDERBY'] = trim($reg[2]);
-            $foreignTableClause = $reg[1];
-        }
-        // Find GROUP BY
-        $reg = array();
-        if (preg_match('/^(.*)[[:space:]]+GROUP[[:space:]]+BY[[:space:]]+([[:alnum:][:space:],._]+)$/i', $foreignTableClause, $reg)) {
-            $foreignTableClauseArray['GROUPBY'] = trim($reg[2]);
-            $foreignTableClause = $reg[1];
-        }
-        // Rest is assumed to be "WHERE" clause
-        $foreignTableClauseArray['WHERE'] = $foreignTableClause;
-
-        return $foreignTableClauseArray;
-    }
-
-    /**
-     * Validate and sanitize database row values of the select field with the given name.
-     * Creates an array out of databaseRow[selectField] values.
-     *
-     * @param array $result The current result array.
-     * @param string $fieldName Name of the current select field.
-     * @param array $staticValues Array with statically defined items, item value is used as array key.
-     * @return array
-     */
-    protected function processSelectFieldValue(array $result, $fieldName, array $staticValues)
-    {
-        $fieldConfig = $result['processedTca']['columns'][$fieldName];
-
-        // For single select fields we just keep the current value because the renderer
-        // will take care of showing the "Invalid value" text.
-        // For maxitems=1 select fields is is also possible to select empty values.
-        // @todo: move handling of invalid values to this data provider.
-        if ($fieldConfig['config']['maxitems'] === 1 && empty($fieldConfig['config']['MM'])) {
-            return array($result['databaseRow'][$fieldName]);
-        }
-
-        $currentDatabaseValues = array_key_exists($fieldName, $result['databaseRow']) ? $result['databaseRow'][$fieldName] : '';
-        // Selecting empty values does not make sense for fields that can contain more than one item
-        // because it is impossible to determine if the empty value or nothing is selected.
-        // This is why empty values will be removed for multi value fields.
-        $currentDatabaseValuesArray = GeneralUtility::trimExplode(',', $currentDatabaseValues, true);
-        $newDatabaseValueArray = [];
-
-        // Add all values that were defined by static methods and do not come from the relation
-        // e.g. TCA, TSconfig, itemProcFunc etc.
-        foreach ($currentDatabaseValuesArray as $value) {
-            if (isset($staticValues[$value])) {
-                $newDatabaseValueArray[] = $value;
-            }
-        }
-
-        if (isset($fieldConfig['config']['foreign_table']) && !empty($fieldConfig['config']['foreign_table'])) {
-            /** @var RelationHandler $relationHandler */
-            $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
-            $relationHandler->registerNonTableValues = !empty($fieldConfig['config']['allowNonIdValues']);
-            if (!empty($fieldConfig['config']['MM']) && $result['command'] !== 'new') {
-                // MM relation
-                $relationHandler->start(
-                    $currentDatabaseValues,
-                    $fieldConfig['config']['foreign_table'],
-                    $fieldConfig['config']['MM'],
-                    $result['databaseRow']['uid'],
-                    $result['tableName'],
-                    $fieldConfig['config']
-                );
-            } else {
-                // Non MM relation
-                // If not dealing with MM relations, use default live uid, not versioned uid for record relations
-                $relationHandler->start(
-                    $currentDatabaseValues,
-                    $fieldConfig['config']['foreign_table'],
-                    '',
-                    $this->getLiveUid($result),
-                    $result['tableName'],
-                    $fieldConfig['config']
-                );
-            }
-            $newDatabaseValueArray = array_merge($newDatabaseValueArray, $relationHandler->getValueArray());
-        }
-
-        return array_unique($newDatabaseValueArray);
-    }
-
-    /**
-     * Gets the record uid of the live default record. If already
-     * pointing to the live record, the submitted record uid is returned.
-     *
-     * @param array $result Result array
-     * @return int
-     * @throws \UnexpectedValueException
-     */
-    protected function getLiveUid(array $result)
-    {
-        $table = $result['tableName'];
-        $row = $result['databaseRow'];
-        $uid = $row['uid'];
-        if (!empty($result['processedTca']['ctrl']['versioningWS'])
-            && $result['pid'] === -1
-        ) {
-            if (empty($row['t3ver_oid'])) {
-                throw new \UnexpectedValueException(
-                    'No t3ver_oid found for record ' . $row['uid'] . ' on table ' . $table,
-                    1440066481
-                );
-            }
-            $uid = $row['t3ver_oid'];
-        }
-        return $uid;
-    }
-
-    /**
-     * @return LanguageService
-     */
-    protected function getLanguageService()
-    {
-        return $GLOBALS['LANG'];
-    }
-
-    /**
-     * @return DatabaseConnection
-     */
-    protected function getDatabaseConnection()
-    {
-        return $GLOBALS['TYPO3_DB'];
-    }
-
-    /**
-     * @return BackendUserAuthentication
-     */
-    protected function getBackendUser()
-    {
-        return $GLOBALS['BE_USER'];
-    }
 }
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectTreeItems.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectTreeItems.php
new file mode 100644
index 000000000000..ca1af2ee793f
--- /dev/null
+++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectTreeItems.php
@@ -0,0 +1,189 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\FormDataProvider;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
+use TYPO3\CMS\Core\Tree\TableConfiguration\ExtJsArrayTreeRenderer;
+use TYPO3\CMS\Core\Tree\TableConfiguration\TableConfigurationTree;
+use TYPO3\CMS\Core\Tree\TableConfiguration\TreeDataProviderFactory;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Resolve select items, set processed item list in processedTca, sanitize and resolve database field
+ */
+class TcaSelectTreeItems extends AbstractItemProvider implements FormDataProviderInterface
+{
+    /**
+     * Resolve select items
+     *
+     * @param array $result
+     * @return array
+     * @throws \UnexpectedValueException
+     */
+    public function addData(array $result)
+    {
+        $table = $result['tableName'];
+
+        foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
+            if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'select') {
+                continue;
+            }
+
+            // Make sure we are only processing supported renderTypes
+            if ($fieldConfig['config']['renderType'] !== 'selectTree') {
+                continue;
+            }
+
+            $fieldConfig['config']['items'] = $this->sanitizeItemArray($fieldConfig['config']['items'], $table, $fieldName);
+            $fieldConfig['config']['maxitems'] = $this->sanitizeMaxItems($fieldConfig['config']['maxitems']);
+
+            $fieldConfig['config']['items'] = $this->addItemsFromPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
+            $fieldConfig['config']['items'] = $this->addItemsFromSpecial($result, $fieldName, $fieldConfig['config']['items']);
+            $fieldConfig['config']['items'] = $this->addItemsFromFolder($result, $fieldName, $fieldConfig['config']['items']);
+            $staticItems = $fieldConfig['config']['items'];
+
+            $fieldConfig['config']['items'] = $this->addItemsFromForeignTable($result, $fieldName, $fieldConfig['config']['items']);
+            $dynamicItems = array_diff_key($fieldConfig['config']['items'], $staticItems);
+
+            $fieldConfig['config']['items'] = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
+            $fieldConfig['config']['items'] = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
+            $fieldConfig['config']['items'] = $this->removeItemsByUserLanguageFieldRestriction($result, $fieldName, $fieldConfig['config']['items']);
+            $fieldConfig['config']['items'] = $this->removeItemsByUserAuthMode($result, $fieldName, $fieldConfig['config']['items']);
+            $fieldConfig['config']['items'] = $this->removeItemsByDoktypeUserRestriction($result, $fieldName, $fieldConfig['config']['items']);
+
+            // Resolve "itemsProcFunc"
+            if (!empty($fieldConfig['config']['itemsProcFunc'])) {
+                $fieldConfig['config']['items'] = $this->resolveItemProcessorFunction($result, $fieldName, $fieldConfig['config']['items']);
+                // itemsProcFunc must not be used anymore
+                unset($fieldConfig['config']['itemsProcFunc']);
+            }
+
+            // Translate labels
+            $fieldConfig['config']['items'] = $this->translateLabels($result, $fieldConfig['config']['items'], $table, $fieldName);
+
+            $staticValues = $this->getStaticValues($fieldConfig['config']['items'], $dynamicItems);
+            $result['databaseRow'][$fieldName] = $this->processSelectFieldValue($result, $fieldName, $staticValues);
+
+            // Keys may contain table names, so a numeric array is created
+            $fieldConfig['config']['items'] = array_values($fieldConfig['config']['items']);
+
+            $fieldConfig['config']['treeData'] = $this->renderTree($result, $fieldConfig, $fieldName, $staticItems);
+
+            $result['processedTca']['columns'][$fieldName] = $fieldConfig;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Renders the Ext JS tree.
+     *
+     * @param array $result The current result array.
+     * @param array $fieldConfig The configuration of the current field.
+     * @param string $fieldName The name of the current field.
+     * @param array $staticItems The static items from the field config.
+     * @return array The tree data configuration
+     */
+    protected function renderTree(array $result, array $fieldConfig, $fieldName, array $staticItems)
+    {
+        $allowedUids = [];
+        foreach ($fieldConfig['config']['items'] as $item) {
+            if ((int)$item[1] > 0) {
+                $allowedUids[] = $item[1];
+            }
+        }
+
+        $treeDataProvider = TreeDataProviderFactory::getDataProvider(
+            $fieldConfig['config'],
+            $result['tableName'],
+            $fieldName,
+            $result['databaseRow']
+        );
+        $treeDataProvider->setSelectedList(implode(',', $result['databaseRow'][$fieldName]));
+        $treeDataProvider->setItemWhiteList($allowedUids);
+        $treeDataProvider->initializeTreeData();
+
+        /** @var ExtJsArrayTreeRenderer $treeRenderer */
+        $treeRenderer = GeneralUtility::makeInstance(ExtJsArrayTreeRenderer::class);
+
+        /** @var TableConfigurationTree $tree */
+        $tree = GeneralUtility::makeInstance(TableConfigurationTree::class);
+        $tree->setDataProvider($treeDataProvider);
+        $tree->setNodeRenderer($treeRenderer);
+
+        $treeItems = $this->prepareAdditionalItems($staticItems, $result['databaseRow'][$fieldName]);
+        $treeItems[] = $tree->render();
+
+        $treeConfig = [
+            'items' => $treeItems,
+            'selectedNodes' => $this->prepareSelectedNodes($fieldConfig['config']['items'], $result['databaseRow'][$fieldName])
+        ];
+
+        return $treeConfig;
+    }
+
+    /**
+     * Prepare the additional items that get prepended to the tree as leaves
+     *
+     * @param array $itemArray
+     * @param array $selectedNodes
+     * @return array
+     */
+    protected function prepareAdditionalItems(array $itemArray, array $selectedNodes)
+    {
+        $additionalItems = [];
+
+        foreach ($itemArray as $item) {
+            if ($item[1] === '--div--') {
+                continue;
+            }
+
+            $additionalItems[] = [
+                'uid' => $item[1],
+                'text' => $item[0],
+                'selectable' => true,
+                'leaf' => true,
+                'checked' => in_array($item[1], $selectedNodes),
+                'icon' => $item[3]
+            ];
+        }
+
+        return $additionalItems;
+    }
+
+    /**
+     * Re-create the old pipe based syntax of selected nodes for the ExtJS rendering part
+     *
+     * @param array $itemArray
+     * @param array $databaseValues
+     * @return array
+     * @todo: this is ugly - should be removed with the tree rewrite
+     */
+    protected function prepareSelectedNodes(array $itemArray, array $databaseValues)
+    {
+        $selectedNodes = [];
+        if (!empty($databaseValues)) {
+            foreach ($databaseValues as $selectedNode) {
+                foreach ($itemArray as $possibleSelectBoxItem) {
+                    if ((string)$possibleSelectBoxItem[1] === (string)$selectedNode) {
+                        $selectedNodes[] = $selectedNode . '|' . rawurlencode($possibleSelectBoxItem[0]);
+                    }
+                }
+            }
+        }
+
+        return $selectedNodes;
+    }
+}
diff --git a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectItemsTest.php b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectItemsTest.php
index 54cc42d982c0..d3404629e0ec 100644
--- a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectItemsTest.php
+++ b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectItemsTest.php
@@ -110,6 +110,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'items' => [
                                 0 => 'foo',
                             ],
@@ -138,6 +139,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'items' => [
                                 0 => [
                                     0 => 'aLabel',
@@ -181,6 +183,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'items' => [
                                 0 => [
                                     0 => 'aLabel',
@@ -219,6 +222,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'special' => 'anUnknownValue',
                         ],
                     ],
@@ -246,6 +250,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'special' => 'tables',
                             'maxitems' => 1,
                         ],
@@ -305,6 +310,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'special' => 'pagetypes',
                             'items' => [],
                             'maxitems' => 1,
@@ -481,6 +487,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'special' => 'exclude',
                         ],
                     ],
@@ -512,6 +519,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'special' => 'exclude',
                         ],
                     ],
@@ -600,6 +608,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'special' => 'explicitValues',
                         ],
                     ],
@@ -617,6 +626,7 @@ class TcaSelectItemsTest extends UnitTestCase
                         'label' => 'aFieldTitle',
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'authMode' => 'explicitAllow',
                             'items' => [
                                 0 => [
@@ -668,6 +678,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'special' => 'explicitValues',
                         ],
                     ],
@@ -685,6 +696,7 @@ class TcaSelectItemsTest extends UnitTestCase
                         'label' => 'aFieldTitle',
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'authMode' => 'explicitDeny',
                             'items' => [
                                 0 => [
@@ -736,6 +748,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'special' => 'explicitValues',
                         ],
                     ],
@@ -753,6 +766,7 @@ class TcaSelectItemsTest extends UnitTestCase
                         'label' => 'aFieldTitle',
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'authMode' => 'individual',
                             'items' => [
                                 0 => [
@@ -825,6 +839,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'special' => 'explicitValues',
                         ],
                     ],
@@ -842,6 +857,7 @@ class TcaSelectItemsTest extends UnitTestCase
                         'label' => 'aFieldTitle',
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'authMode' => 'individual',
                             'items' => [
                                 0 => [
@@ -914,6 +930,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'special' => 'languages',
                         ],
                     ],
@@ -964,6 +981,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'special' => 'custom',
                         ],
                     ],
@@ -1019,6 +1037,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'special' => 'modListGroup',
                         ],
                     ],
@@ -1083,6 +1102,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'fileFolder' => $directory,
                             'fileFolder_extList' => 'gif',
                             'fileFolder_recursions' => 1,
@@ -1265,6 +1285,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'foreign_table' => 'fTable',
                             'foreign_table_where' => $foreignTableWhere,
                         ],
@@ -1343,6 +1364,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'foreign_table' => 'fTable',
                         ],
                     ],
@@ -1367,6 +1389,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'foreign_table' => 'fTable',
                             'foreign_table_where' => 'AND ftable.uid=1 GROUP BY groupField ORDER BY orderField LIMIT 1,2',
                         ],
@@ -1423,6 +1446,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'foreign_table' => 'fTable',
                             'items' => [
                                 0 => [
@@ -1493,6 +1517,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'foreign_table' => 'fTable',
                             'foreign_table_prefix' => 'aPrefix',
                             'items' => [],
@@ -1571,6 +1596,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'items' => [
                                 0 => [
                                     0 => 'keepMe',
@@ -1626,6 +1652,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'items' => [
                                 0 => [
                                     0 => 'keepMe',
@@ -1684,6 +1711,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'items' => [
                                 0 => [
                                     0 => 'keepMe',
@@ -1736,6 +1764,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'authMode' => 'explicitAllow',
                             'items' => [
                                 0 => [
@@ -1789,6 +1818,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'doktype' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'items' => [
                                 0 => [
                                     0 => 'keepMe',
@@ -1835,6 +1865,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'doktype' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'items' => [
                                 0 => [
                                     0 => 'keepMe',
@@ -1889,6 +1920,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'items' => [],
                             'itemsProcFunc' => function (array $parameters, $pObj) {
                                 $parameters['items'] = [
@@ -1915,6 +1947,7 @@ class TcaSelectItemsTest extends UnitTestCase
         $expected['databaseRow']['aField'] = ['aValue'];
         $expected['processedTca']['columns']['aField']['config'] = [
             'type' => 'select',
+            'renderType' => 'selectSingle',
             'items' => [
                 0 => [
                     0 => 'aLabel',
@@ -1955,6 +1988,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'aKey' => 'aValue',
                             'items' => [
                                 0 => [
@@ -2024,6 +2058,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'aKey' => 'aValue',
                             'items' => [
                                 0 => [
@@ -2072,6 +2107,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'items' => [
                                 0 => [
                                     0 => 'aLabel',
@@ -2140,6 +2176,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'maxitems' => 999,
                             'foreign_table' => 'foreignTable',
                             'MM' => 'aTable_foreignTable_mm',
@@ -2195,6 +2232,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'maxitems' => 999,
                             'foreign_table' => 'foreignTable',
                             'items' => [],
@@ -2256,6 +2294,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'foreign_table' => 'foreignTable',
                             'maxitems' => 999,
                             'items' => [
@@ -2292,6 +2331,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'maxitems' => 999,
                             'items' => [
                                 ['foo', 'foo', null, null],
@@ -2331,6 +2371,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'maxitems' => 1,
                             'items' => [],
                         ],
@@ -2366,6 +2407,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'maxitems' => 999,
                             'items' => [
                                 ['a', '', null, null],
@@ -2411,6 +2453,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'maxitems' => 999,
                             'items' => [
                                 ['foo', 'foo', null, null],
@@ -2451,6 +2494,7 @@ class TcaSelectItemsTest extends UnitTestCase
                     'aField' => [
                         'config' => [
                             'type' => 'select',
+                            'renderType' => 'selectSingle',
                             'maxitems' => 1,
                             'items' => [
                                 ['foo', 'foo', null, null],
@@ -2489,6 +2533,7 @@ class TcaSelectItemsTest extends UnitTestCase
                             'aField' => [
                                 'config' => [
                                     'type' => 'select',
+                                    'renderType' => 'selectSingle',
                                     'maxitems' => 999,
                                     'MM' => 'mm_aTable_foreignTable',
                                     'foreign_table' => 'foreignTable',
@@ -2517,6 +2562,7 @@ class TcaSelectItemsTest extends UnitTestCase
                             'aField' => [
                                 'config' => [
                                     'type' => 'select',
+                                    'renderType' => 'selectSingle',
                                     'maxitems' => 999,
                                     'MM' => 'mm_aTable_foreignTable',
                                     'foreign_table' => 'foreignTable',
@@ -2542,6 +2588,7 @@ class TcaSelectItemsTest extends UnitTestCase
                             'aField' => [
                                 'config' => [
                                     'type' => 'select',
+                                    'renderType' => 'selectSingle',
                                     'maxitems' => 1,
                                     'MM' => 'mm_aTable_foreignTable',
                                     'foreign_table' => 'foreignTable',
@@ -2569,6 +2616,7 @@ class TcaSelectItemsTest extends UnitTestCase
                             'aField' => [
                                 'config' => [
                                     'type' => 'select',
+                                    'renderType' => 'selectSingle',
                                     'maxitems' => 1,
                                     'MM' => 'mm_aTable_foreignTable',
                                     'foreign_table' => 'foreignTable',
diff --git a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectTreeItemsTest.php b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectTreeItemsTest.php
new file mode 100644
index 000000000000..577cf6442cc2
--- /dev/null
+++ b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectTreeItemsTest.php
@@ -0,0 +1,119 @@
+<?php
+namespace TYPO3\CMS\Backend\Tests\Unit\Form\FormDataProvider;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Prophecy\Argument;
+use Prophecy\Prophecy\ObjectProphecy;
+use TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectTreeItems;
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Database\DatabaseConnection;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+use TYPO3\CMS\Core\Tree\TableConfiguration\DatabaseTreeDataProvider;
+use TYPO3\CMS\Core\Tree\TableConfiguration\TableConfigurationTree;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Tests for the TcaSelectTreeItems provider.
+ *
+ * This test only covers the renderTree() method. All other methods are covered TcaSelecItemsTest
+ *
+ * @see TcaSelecItemsTest
+ */
+class TcaSelectTreeItemsTest extends UnitTestCase
+{
+    /**
+     * @var TcaSelectTreeItems
+     */
+    protected $subject;
+
+    /**
+     * @var array A backup of registered singleton instances
+     */
+    protected $singletonInstances = [];
+
+    /**
+     * Initializes the mock object.
+     */
+    public function setUp()
+    {
+        $this->singletonInstances = GeneralUtility::getSingletonInstances();
+        $this->subject = new TcaSelectTreeItems();
+    }
+
+    protected function tearDown()
+    {
+        GeneralUtility::purgeInstances();
+        GeneralUtility::resetSingletonInstances($this->singletonInstances);
+        parent::tearDown();
+    }
+
+    /**
+     * @test
+     */
+    public function addDataAddsTreeConfigurationForExtJs()
+    {
+        $GLOBALS['TCA']['foreignTable'] = [];
+
+        /** @var DatabaseConnection|ObjectProphecy $database */
+        $database = $this->prophesize(DatabaseConnection::class);
+        $GLOBALS['TYPO3_DB'] = $database->reveal();
+
+        /** @var BackendUserAuthentication|ObjectProphecy $backendUserProphecy */
+        $backendUserProphecy = $this->prophesize(BackendUserAuthentication::class);
+        $GLOBALS['BE_USER'] = $backendUserProphecy->reveal();
+
+        /** @var  DatabaseTreeDataProvider|ObjectProphecy $treeDataProviderProphecy */
+        $treeDataProviderProphecy = $this->prophesize(DatabaseTreeDataProvider::class);
+        GeneralUtility::addInstance(DatabaseTreeDataProvider::class, $treeDataProviderProphecy->reveal());
+
+        /** @var  TableConfigurationTree|ObjectProphecy $treeDataProviderProphecy */
+        $tableConfigurationTreeProphecy = $this->prophesize(TableConfigurationTree::class);
+        GeneralUtility::addInstance(TableConfigurationTree::class, $tableConfigurationTreeProphecy->reveal());
+        $tableConfigurationTreeProphecy->setDataProvider(Argument::cetera())->shouldBeCalled();
+        $tableConfigurationTreeProphecy->setNodeRenderer(Argument::cetera())->shouldBeCalled();
+        $tableConfigurationTreeProphecy->render()->shouldBeCalled()->willReturn(['fake', 'tree', 'data']);
+
+        $input = [
+            'tableName' => 'aTable',
+            'databaseRow' => [
+                'aField' => '1'
+            ],
+            'processedTca' => [
+                'columns' => [
+                    'aField' => [
+                        'config' => [
+                            'type' => 'select',
+                            'renderType' => 'selectTree',
+                            'treeConfig' => [
+                                'childrenField' => 'childrenField'
+                            ],
+                            'foreign_table' => 'foreignTable',
+                            'items' => [],
+                            'maxitems' => 1
+                        ],
+                    ],
+                ],
+            ],
+        ];
+
+        $expected = $input;
+        $expected['databaseRow']['aField'] = ['1'];
+        $expected['processedTca']['columns']['aField']['config']['treeData'] = [
+            'items' => [['fake', 'tree', 'data']],
+            'selectedNodes' => []
+        ];
+        $this->assertEquals($expected, $this->subject->addData($input));
+    }
+}
diff --git a/typo3/sysext/compatibility6/Classes/Form/FormDataProvider/TcaFlexProcess.php b/typo3/sysext/compatibility6/Classes/Form/FormDataProvider/TcaFlexProcess.php
index 521b90c464c4..3d462a11783f 100644
--- a/typo3/sysext/compatibility6/Classes/Form/FormDataProvider/TcaFlexProcess.php
+++ b/typo3/sysext/compatibility6/Classes/Form/FormDataProvider/TcaFlexProcess.php
@@ -26,7 +26,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  *
  * This is typically the last provider, executed after TcaFlexPrepare
  */
-class TcaFlexProcess extends AbstractItemProvider implements FormDataProviderInterface
+class TcaFlexProcess implements FormDataProviderInterface
 {
     /**
      * Determine possible pageTsConfig overrides and apply them to ds.
diff --git a/typo3/sysext/core/Configuration/DefaultConfiguration.php b/typo3/sysext/core/Configuration/DefaultConfiguration.php
index 12bd13929c26..2b6a6b7385d5 100644
--- a/typo3/sysext/core/Configuration/DefaultConfiguration.php
+++ b/typo3/sysext/core/Configuration/DefaultConfiguration.php
@@ -488,11 +488,16 @@ return array(
                             \TYPO3\CMS\Backend\Form\FormDataProvider\TcaFlexFetch::class,
                         ),
                     ),
-                    \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class => array(
+                    \TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectTreeItems::class => array(
                         'depends' => array(
                             \TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectItems::class,
                         ),
                     ),
+                    \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class => array(
+                        'depends' => array(
+                            \TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectTreeItems::class,
+                        ),
+                    ),
                     \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineConfiguration::class => array(
                         'depends' => array(
                             \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class,
@@ -541,11 +546,16 @@ return array(
                             \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRowDefaultValues::class,
                         ),
                     ),
-                    \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class => array(
+                    \TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectTreeItems::class => array(
                         'depends' => array(
                             \TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectItems::class,
                         ),
                     ),
+                    \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class => array(
+                        'depends' => array(
+                            \TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectTreeItems::class,
+                        ),
+                    ),
                     \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineConfiguration::class => array(
                         'depends' => array(
                             \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class,
@@ -608,11 +618,16 @@ return array(
                             \TYPO3\CMS\Backend\Form\FormDataProvider\TcaCheckboxItems::class,
                         ),
                     ),
-                    \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class => array(
+                    \TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectTreeItems::class => array(
                         'depends' => array(
                             \TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectItems::class,
                         ),
                     ),
+                    \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class => array(
+                        'depends' => array(
+                            \TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectTreeItems::class,
+                        ),
+                    ),
                     \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineConfiguration::class => array(
                         'depends' => array(
                             \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class,
-- 
GitLab