From 5fb094bd4b37c8297afbd920b6e8b46bd42518d5 Mon Sep 17 00:00:00 2001
From: Christian Kuhn <lolli@schwarzbu.ch>
Date: Thu, 1 Dec 2016 20:29:25 +0100
Subject: [PATCH] [BUGFIX] FormEngine suggest wizard in flex form sections

The suggest wizard does not work in FormEngine flex form
section containers for existing containers.

The patch splits the ajax controller action and the FormEngine
wizard display into two classes to have a dedicated entry point
for the ajax action and refactors the flex form field configuration
search logic to find the correct configuration for existing
containers, too.

Change-Id: Ia870618629943b1f3c665cb3a10a7f3769a02540
Resolves: #78558
Releases: master
Reviewed-on: https://review.typo3.org/50839
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Tested-by: Valentin Funk <valentin.funk@computerfabrik.de>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
---
 .../Wizard/SuggestWizardController.php        | 354 +++++++++++++++
 .../Classes/Form/Wizard/SuggestWizard.php     | 405 +-----------------
 .../Configuration/Backend/AjaxRoutes.php      |   4 +-
 .../Wizard/SuggestWizardControllerTest.php    | 300 +++++++++++++
 .../Unit/Form/Wizard/SuggestWizardTest.php    | 194 ---------
 5 files changed, 658 insertions(+), 599 deletions(-)
 create mode 100644 typo3/sysext/backend/Classes/Controller/Wizard/SuggestWizardController.php
 create mode 100644 typo3/sysext/backend/Tests/Unit/Controller/Wizard/SuggestWizardControllerTest.php

diff --git a/typo3/sysext/backend/Classes/Controller/Wizard/SuggestWizardController.php b/typo3/sysext/backend/Classes/Controller/Wizard/SuggestWizardController.php
new file mode 100644
index 000000000000..390ea7b2be43
--- /dev/null
+++ b/typo3/sysext/backend/Classes/Controller/Wizard/SuggestWizardController.php
@@ -0,0 +1,354 @@
+<?php
+namespace TYPO3\CMS\Backend\Controller\Wizard;
+
+/*
+ * 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 Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Backend\Form\Wizard\SuggestWizardDefaultReceiver;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\MathUtility;
+
+/**
+ * Receives ajax request from FormEngine suggest wizard and creates suggest answer as json result
+ */
+class SuggestWizardController
+{
+    /**
+     * Ajax handler for the "suggest" feature in FormEngine.
+     *
+     * @param ServerRequestInterface $request
+     * @param ResponseInterface $response
+     * @throws \RuntimeException for incomplete or invalid arguments
+     * @return ResponseInterface
+     */
+    public function searchAction(ServerRequestInterface $request, ResponseInterface $response)
+    {
+        $parsedBody = $request->getParsedBody();
+
+        if (!isset($parsedBody['value'])
+            || !isset($parsedBody['table'])
+            || !isset($parsedBody['field'])
+            || !isset($parsedBody['uid'])
+            || !isset($parsedBody['dataStructureIdentifier'])
+            || !isset($parsedBody['hmac'])
+        ) {
+            throw new \RuntimeException(
+                'Missing at least one of the required arguments "value", "table", "field", "uid"'
+                . ', "dataStructureIdentifier" or "hmac"',
+                1478607036
+            );
+        }
+
+        $search = $parsedBody['value'];
+        $table = $parsedBody['table'];
+        $field = $parsedBody['field'];
+        $uid = $parsedBody['uid'];
+        $pid = (int)$parsedBody['pid'];
+
+        // flex form section container identifiers are created on js side dynamically "onClick". Those are
+        // not within the generated hmac ... the js side adds "idx{dateInMilliseconds}-", so this is removed here again.
+        // example outgoing in renderSuggestSelector():
+        // flex_1|data|sSuggestCheckCombination|lDEF|settings.subelements|el|ID-356586b0d3-form|item|el|content|vDEF
+        // incoming here:
+        // flex_1|data|sSuggestCheckCombination|lDEF|settings.subelements|el|ID-356586b0d3-idx1478611729574-form|item|el|content|vDEF
+        // Note: For existing containers, these parts are numeric, so "ID-356586b0d3-idx1478611729574-form" becomes 1 or 2, etc.
+        // @todo: This could be kicked is the flex form section containers are moved to an ajax call on creation
+        $fieldForHmac = preg_replace('/idx\d{13}-/', '', $field);
+
+        $dataStructureIdentifierString = '';
+        if (!empty($parsedBody['dataStructureIdentifier'])) {
+            $dataStructureIdentifierString = json_encode($parsedBody['dataStructureIdentifier']);
+        }
+
+        $incomingHmac = $parsedBody['hmac'];
+        $calculatedHmac = GeneralUtility::hmac(
+            $table . $fieldForHmac . $uid . $pid . $dataStructureIdentifierString,
+            'formEngineSuggest'
+        );
+        if ($incomingHmac !== $calculatedHmac) {
+            throw new \RuntimeException(
+                'Incoming and calculated hmac do not match',
+                1478608245
+            );
+        }
+
+        // If the $uid is numeric (existing page) and a suggest wizard in pages is handled, the effective
+        // pid is the uid of that page - important for page ts config configuration.
+        if (MathUtility::canBeInterpretedAsInteger($uid) && $table === 'pages') {
+            $pid = $uid;
+        }
+        $TSconfig = BackendUtility::getPagesTSconfig($pid);
+
+        // Determine TCA config of field
+        if (empty($dataStructureIdentifierString)) {
+            // Normal columns field
+            $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
+        } else {
+            // A flex flex form field
+            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
+            $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifierString);
+            $parts = explode('|', $field);
+            $fieldConfig = $this->getFlexFieldConfiguration($parts, $dataStructureArray);
+            // Flexform field name levels are separated with | instead of encapsulation in [];
+            // reverse this here to be compatible with regular field names.
+            $field = str_replace('|', '][', $field);
+        }
+
+        $wizardConfig = $fieldConfig['wizards']['suggest'];
+
+        $queryTables = $this->getTablesToQueryFromFieldConfiguration($fieldConfig);
+        $whereClause = $this->getWhereClause($fieldConfig);
+
+        $resultRows = [];
+
+        // fetch the records for each query table. A query table is a table from which records are allowed to
+        // be added to the TCEForm selector, originally fetched from the "allowed" config option in the TCA
+        foreach ($queryTables as $queryTable) {
+            // if the table does not exist, skip it
+            if (!is_array($GLOBALS['TCA'][$queryTable]) || empty($GLOBALS['TCA'][$queryTable])) {
+                continue;
+            }
+
+            $config = $this->getConfigurationForTable($queryTable, $wizardConfig, $TSconfig, $table, $field);
+
+            // process addWhere
+            if (!isset($config['addWhere']) && $whereClause) {
+                $config['addWhere'] = $whereClause;
+            }
+            if (isset($config['addWhere'])) {
+                $replacement = [
+                    '###THIS_UID###' => (int)$uid,
+                    '###CURRENT_PID###' => (int)$pid
+                ];
+                if (isset($TSconfig['TCEFORM.'][$table . '.'][$field . '.'])) {
+                    $fieldTSconfig = $TSconfig['TCEFORM.'][$table . '.'][$field . '.'];
+                    if (isset($fieldTSconfig['PAGE_TSCONFIG_ID'])) {
+                        $replacement['###PAGE_TSCONFIG_ID###'] = (int)$fieldTSconfig['PAGE_TSCONFIG_ID'];
+                    }
+                    if (isset($fieldTSconfig['PAGE_TSCONFIG_IDLIST'])) {
+                        $replacement['###PAGE_TSCONFIG_IDLIST###'] =  implode(',', GeneralUtility::intExplode(',', $fieldTSconfig['PAGE_TSCONFIG_IDLIST']));
+                    }
+                    if (isset($fieldTSconfig['PAGE_TSCONFIG_STR'])) {
+                        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($fieldConfig['foreign_table']);
+                        // nasty hack, but it's currently not possible to just quote anything "inside" the value but not escaping
+                        // the whole field as it is not known where it is used in the WHERE clause
+                        $replacement['###PAGE_TSCONFIG_STR###'] = trim($connection->quote($fieldTSconfig['PAGE_TSCONFIG_STR']), '\'');
+                    }
+                }
+                $config['addWhere'] = strtr(' ' . $config['addWhere'], $replacement);
+            }
+
+            // instantiate the class that should fetch the records for this $queryTable
+            $receiverClassName = $config['receiverClass'];
+            if (!class_exists($receiverClassName)) {
+                $receiverClassName = SuggestWizardDefaultReceiver::class;
+            }
+            $receiverObj = GeneralUtility::makeInstance($receiverClassName, $queryTable, $config);
+            $params = ['value' => $search];
+            $rows = $receiverObj->queryTable($params);
+            if (empty($rows)) {
+                continue;
+            }
+            $resultRows = $rows + $resultRows;
+            unset($rows);
+        }
+
+        // Limit the number of items in the result list
+        $maxItems = isset($config['maxItemsInResultList']) ? $config['maxItemsInResultList'] : 10;
+        $maxItems = min(count($resultRows), $maxItems);
+
+        array_splice($resultRows, $maxItems);
+
+        $response->getBody()->write(json_encode(array_values($resultRows)));
+        return $response;
+    }
+
+    /**
+     * Returns TRUE if a table has been marked as hidden in the configuration
+     *
+     * @param array $tableConfig
+     * @return bool
+     */
+    protected function isTableHidden(array $tableConfig)
+    {
+        return (bool)$tableConfig['ctrl']['hideTable'];
+    }
+
+    /**
+     * Checks if the current backend user is allowed to access the given table, based on the ctrl-section of the
+     * table's configuration array (TCA) entry.
+     *
+     * @param array $tableConfig
+     * @return bool
+     */
+    protected function currentBackendUserMayAccessTable(array $tableConfig)
+    {
+        if ($this->getBackendUser()->isAdmin()) {
+            return true;
+        }
+
+        // If the user is no admin, they may not access admin-only tables
+        if ($tableConfig['ctrl']['adminOnly']) {
+            return false;
+        }
+
+        // allow access to root level pages if security restrictions should be bypassed
+        return !$tableConfig['ctrl']['rootLevel'] || $tableConfig['ctrl']['security']['ignoreRootLevelRestriction'];
+    }
+
+    /**
+     * Get 'config' section of field from resolved data structure specified by flex form path in $parts
+     *
+     * @param array $parts
+     * @param array $dataStructure
+     * @return array
+     */
+    protected function getFlexFieldConfiguration(array $parts, array $dataStructure)
+    {
+        if (count($parts) === 6) {
+            // Search a flex field, example:
+            // flex_1|data|sDb|lDEF|group_db_1|vDEF
+            if (!isset($dataStructure['sheets'][$parts[2]]['ROOT']['el'][$parts[4]]['TCEforms']['config'])) {
+                throw new \RuntimeException(
+                    'Specified path ' . implode('|', $parts) . ' not found in flex form data structure',
+                    1480609491
+                );
+            }
+            $fieldConfig = $dataStructure['sheets'][$parts[2]]['ROOT']['el'][$parts[4]]['TCEforms']['config'];
+        } elseif (count($parts) === 11) {
+            // Search a flex field in a section container, example:
+            // flex_1|data|sSuggestCheckCombination|lDEF|settings.subelements|el|1|item|el|content|vDEF
+            if (!isset($dataStructure['sheets'][$parts[2]]['ROOT']['el'][$parts[4]]['el'][$parts[7]]['el'][$parts[9]]['TCEforms']['config'])) {
+                throw new \RuntimeException(
+                    'Specified path ' . implode('|', $parts) . ' not found in flex form section container data structure',
+                    1480611208
+                );
+            }
+            $fieldConfig = $dataStructure['sheets'][$parts[2]]['ROOT']['el'][$parts[4]]['el'][$parts[7]]['el'][$parts[9]]['TCEforms']['config'];
+        } else {
+            throw new \RuntimeException(
+                'Invalid flex form path ' . implode('|', $parts),
+                1480611252
+            );
+        }
+        return $fieldConfig;
+    }
+
+    /**
+     * Returns the configuration for the suggest wizard for the given table. This does multiple overlays from the
+     * TSconfig.
+     *
+     * @param string $queryTable The table to query
+     * @param array $wizardConfig The configuration for the wizard as configured in the data structure
+     * @param array $TSconfig The TSconfig array of the current page
+     * @param string $table The table where the wizard is used
+     * @param string $field The field where the wizard is used
+     * @return array
+     */
+    protected function getConfigurationForTable($queryTable, array $wizardConfig, array $TSconfig, $table, $field)
+    {
+        $config = (array)$wizardConfig['default'];
+
+        if (is_array($wizardConfig[$queryTable])) {
+            ArrayUtility::mergeRecursiveWithOverrule($config, $wizardConfig[$queryTable]);
+        }
+        $globalSuggestTsConfig = $TSconfig['TCEFORM.']['suggest.'];
+        $currentFieldSuggestTsConfig = $TSconfig['TCEFORM.'][$table . '.'][$field . '.']['suggest.'];
+
+        // merge the configurations of different "levels" to get the working configuration for this table and
+        // field (i.e., go from the most general to the most special configuration)
+        if (is_array($globalSuggestTsConfig['default.'])) {
+            ArrayUtility::mergeRecursiveWithOverrule($config, $globalSuggestTsConfig['default.']);
+        }
+
+        if (is_array($globalSuggestTsConfig[$queryTable . '.'])) {
+            ArrayUtility::mergeRecursiveWithOverrule($config, $globalSuggestTsConfig[$queryTable . '.']);
+        }
+
+        // use $table instead of $queryTable here because we overlay a config
+        // for the input-field here, not for the queried table
+        if (is_array($currentFieldSuggestTsConfig['default.'])) {
+            ArrayUtility::mergeRecursiveWithOverrule($config, $currentFieldSuggestTsConfig['default.']);
+        }
+
+        if (is_array($currentFieldSuggestTsConfig[$queryTable . '.'])) {
+            ArrayUtility::mergeRecursiveWithOverrule($config, $currentFieldSuggestTsConfig[$queryTable . '.']);
+        }
+
+        return $config;
+    }
+
+    /**
+     * Checks the given field configuration for the tables that should be used for querying and returns them as an
+     * array.
+     *
+     * @param array $fieldConfig
+     * @return array
+     */
+    protected function getTablesToQueryFromFieldConfiguration(array $fieldConfig)
+    {
+        $queryTables = [];
+
+        if (isset($fieldConfig['allowed'])) {
+            if ($fieldConfig['allowed'] !== '*') {
+                // list of allowed tables
+                $queryTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed']);
+            } else {
+                // all tables are allowed, if the user can access them
+                foreach ($GLOBALS['TCA'] as $tableName => $tableConfig) {
+                    if (!$this->isTableHidden($tableConfig) && $this->currentBackendUserMayAccessTable($tableConfig)) {
+                        $queryTables[] = $tableName;
+                    }
+                }
+                unset($tableName, $tableConfig);
+            }
+        } elseif (isset($fieldConfig['foreign_table'])) {
+            // use the foreign table
+            $queryTables = [$fieldConfig['foreign_table']];
+        }
+
+        return $queryTables;
+    }
+
+    /**
+     * Returns the SQL WHERE clause to use for querying records. This is currently only relevant if a foreign_table
+     * is configured and should be used; it could e.g. be used to limit to a certain subset of records from the
+     * foreign table
+     *
+     * @param array $fieldConfig
+     * @return string
+     */
+    protected function getWhereClause(array $fieldConfig)
+    {
+        if (!isset($fieldConfig['foreign_table'])) {
+            return '';
+        }
+
+        // strip ORDER BY clause
+        return trim(preg_replace('/ORDER[[:space:]]+BY.*/i', '', $fieldConfig['foreign_table_where']));
+    }
+
+    /**
+     * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
+     */
+    protected function getBackendUser()
+    {
+        return $GLOBALS['BE_USER'];
+    }
+}
diff --git a/typo3/sysext/backend/Classes/Form/Wizard/SuggestWizard.php b/typo3/sysext/backend/Classes/Form/Wizard/SuggestWizard.php
index 2fc6c07a7673..1f7b3216ff05 100644
--- a/typo3/sysext/backend/Classes/Form/Wizard/SuggestWizard.php
+++ b/typo3/sysext/backend/Classes/Form/Wizard/SuggestWizard.php
@@ -14,19 +14,12 @@ namespace TYPO3\CMS\Backend\Form\Wizard;
  * The TYPO3 project - inspiring people to share!
  */
 
-use Psr\Http\Message\ResponseInterface;
-use Psr\Http\Message\ServerRequestInterface;
-use TYPO3\CMS\Backend\Utility\BackendUtility;
-use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
-use TYPO3\CMS\Core\Database\ConnectionPool;
-use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Fluid\View\StandaloneView;
-use TYPO3\CMS\Lang\LanguageService;
 
 /**
- * Wizard for rendering an AJAX selector for records
+ * Wizard for rendering an AJAX selector for records.
+ * See SuggestWizardController for the ajax handling counterpart.
  */
 class SuggestWizard
 {
@@ -116,392 +109,6 @@ class SuggestWizard
         return $this->view->render();
     }
 
-    /**
-     * Ajax handler for the "suggest" feature in FormEngine.
-     *
-     * @param ServerRequestInterface $request
-     * @param ResponseInterface $response
-     * @throws \RuntimeException for incomplete or invalid arguments
-     * @return ResponseInterface
-     */
-    public function searchAction(ServerRequestInterface $request, ResponseInterface $response)
-    {
-        $parsedBody = $request->getParsedBody();
-
-        if (!isset($parsedBody['value'])
-            || !isset($parsedBody['table'])
-            || !isset($parsedBody['field'])
-            || !isset($parsedBody['uid'])
-            || !isset($parsedBody['dataStructureIdentifier'])
-            || !isset($parsedBody['hmac'])
-        ) {
-            throw new \RuntimeException(
-                'Missing at least one of the required arguments "value", "table", "field", "uid"'
-                . ', "dataStructureIdentifier" or "hmac"',
-                1478607036
-            );
-        }
-
-        $search = $parsedBody['value'];
-        $table = $parsedBody['table'];
-        $field = $parsedBody['field'];
-        $uid = $parsedBody['uid'];
-        $pid = (int)$parsedBody['pid'];
-
-        // flex form section container identifiers are created on js side dynamically "onClick". Those are
-        // not within the generated hmac ... the js side adds "idx{dateInMilliseconds}-", so this is removed here again.
-        // example outgoing in renderSuggestSelector():
-        // flex_1|data|sSuggestCheckCombination|lDEF|settings.subelements|el|ID-356586b0d3-form|item|el|content|vDEF
-        // incoming here:
-        // flex_1|data|sSuggestCheckCombination|lDEF|settings.subelements|el|ID-356586b0d3-idx1478611729574-form|item|el|content|vDEF
-        // Note: For existing containers, these parts are numeric, so "ID-356586b0d3-idx1478611729574-form" becomes 1 or 2, etc.
-        // @todo: This could be kicked is the flex form section containers are moved to an ajax call on creation
-        $fieldForHmac = preg_replace('/idx\d{13}-/', '', $field);
-
-        $dataStructureIdentifierString = '';
-        if (!empty($parsedBody['dataStructureIdentifier'])) {
-            $dataStructureIdentifierString = json_encode($parsedBody['dataStructureIdentifier']);
-        }
-
-        $incomingHmac = $parsedBody['hmac'];
-        $calculatedHmac = GeneralUtility::hmac(
-            $table . $fieldForHmac . $uid . $pid . $dataStructureIdentifierString,
-            'formEngineSuggest'
-        );
-        if ($incomingHmac !== $calculatedHmac) {
-            throw new \RuntimeException(
-                'Incoming and calculated hmac do not match',
-                1478608245
-            );
-        }
-
-        // If the $uid is numeric (existing page) and a suggest wizard in pages is handled, the effective
-        // pid is the uid of that page - important for page ts config configuration.
-        if (MathUtility::canBeInterpretedAsInteger($uid) && $table === 'pages') {
-            $pid = $uid;
-        }
-        $TSconfig = BackendUtility::getPagesTSconfig($pid);
-
-        // Determine TCA config of field
-        if (empty($dataStructureIdentifierString)) {
-            // Normal columns field
-            $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
-        } else {
-            // A flex flex form field
-            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
-            $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifierString);
-            $parts = explode('|', $field);
-            $fieldConfig = $this->getFieldConfiguration($parts, $dataStructureArray);
-            // Flexform field name levels are separated with | instead of encapsulation in [];
-            // reverse this here to be compatible with regular field names.
-            $field = str_replace('|', '][', $field);
-        }
-
-        $wizardConfig = $fieldConfig['wizards']['suggest'];
-
-        $queryTables = $this->getTablesToQueryFromFieldConfiguration($fieldConfig);
-        $whereClause = $this->getWhereClause($fieldConfig);
-
-        $resultRows = [];
-
-        // fetch the records for each query table. A query table is a table from which records are allowed to
-        // be added to the TCEForm selector, originally fetched from the "allowed" config option in the TCA
-        foreach ($queryTables as $queryTable) {
-            // if the table does not exist, skip it
-            if (!is_array($GLOBALS['TCA'][$queryTable]) || empty($GLOBALS['TCA'][$queryTable])) {
-                continue;
-            }
-
-            $config = $this->getConfigurationForTable($queryTable, $wizardConfig, $TSconfig, $table, $field);
-
-            // process addWhere
-            if (!isset($config['addWhere']) && $whereClause) {
-                $config['addWhere'] = $whereClause;
-            }
-            if (isset($config['addWhere'])) {
-                $replacement = [
-                    '###THIS_UID###' => (int)$uid,
-                    '###CURRENT_PID###' => (int)$pid
-                ];
-                if (isset($TSconfig['TCEFORM.'][$table . '.'][$field . '.'])) {
-                    $fieldTSconfig = $TSconfig['TCEFORM.'][$table . '.'][$field . '.'];
-                    if (isset($fieldTSconfig['PAGE_TSCONFIG_ID'])) {
-                        $replacement['###PAGE_TSCONFIG_ID###'] = (int)$fieldTSconfig['PAGE_TSCONFIG_ID'];
-                    }
-                    if (isset($fieldTSconfig['PAGE_TSCONFIG_IDLIST'])) {
-                        $replacement['###PAGE_TSCONFIG_IDLIST###'] =  implode(',', GeneralUtility::intExplode(',', $fieldTSconfig['PAGE_TSCONFIG_IDLIST']));
-                    }
-                    if (isset($fieldTSconfig['PAGE_TSCONFIG_STR'])) {
-                        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($fieldConfig['foreign_table']);
-                        // nasty hack, but it's currently not possible to just quote anything "inside" the value but not escaping
-                        // the whole field as it is not known where it is used in the WHERE clause
-                        $replacement['###PAGE_TSCONFIG_STR###'] = trim($connection->quote($fieldTSconfig['PAGE_TSCONFIG_STR']), '\'');
-                    }
-                }
-                $config['addWhere'] = strtr(' ' . $config['addWhere'], $replacement);
-            }
-
-            // instantiate the class that should fetch the records for this $queryTable
-            $receiverClassName = $config['receiverClass'];
-            if (!class_exists($receiverClassName)) {
-                $receiverClassName = SuggestWizardDefaultReceiver::class;
-            }
-            $receiverObj = GeneralUtility::makeInstance($receiverClassName, $queryTable, $config);
-            $params = ['value' => $search];
-            $rows = $receiverObj->queryTable($params);
-            if (empty($rows)) {
-                continue;
-            }
-            $resultRows = $rows + $resultRows;
-            unset($rows);
-        }
-
-        // Limit the number of items in the result list
-        $maxItems = isset($config['maxItemsInResultList']) ? $config['maxItemsInResultList'] : 10;
-        $maxItems = min(count($resultRows), $maxItems);
-
-        array_splice($resultRows, $maxItems);
-
-        $response->getBody()->write(json_encode(array_values($resultRows)));
-        return $response;
-    }
-
-    /**
-     * Returns TRUE if a table has been marked as hidden in the configuration
-     *
-     * @param array $tableConfig
-     * @return bool
-     */
-    protected function isTableHidden(array $tableConfig)
-    {
-        return (bool)$tableConfig['ctrl']['hideTable'];
-    }
-
-    /**
-     * Checks if the current backend user is allowed to access the given table, based on the ctrl-section of the
-     * table's configuration array (TCA) entry.
-     *
-     * @param array $tableConfig
-     * @return bool
-     */
-    protected function currentBackendUserMayAccessTable(array $tableConfig)
-    {
-        if ($this->getBackendUser()->isAdmin()) {
-            return true;
-        }
-
-        // If the user is no admin, they may not access admin-only tables
-        if ($tableConfig['ctrl']['adminOnly']) {
-            return false;
-        }
-
-        // allow access to root level pages if security restrictions should be bypassed
-        return !$tableConfig['ctrl']['rootLevel'] || $tableConfig['ctrl']['security']['ignoreRootLevelRestriction'];
-    }
-
-    /**
-     * Get configuration for given field by traversing the flexform path to field
-     * given in $parts
-     *
-     * @param array $parts
-     * @param array $flexformDSArray
-     * @return array
-     */
-    protected function getFieldConfiguration(array $parts, array $flexformDSArray)
-    {
-        $relevantParts = [];
-        foreach ($parts as $part) {
-            if ($this->isRelevantFlexField($part)) {
-                $relevantParts[] = $part;
-            }
-        }
-        // throw away db field name for flexform field
-        array_shift($relevantParts);
-
-        $flexformElement = array_pop($relevantParts);
-        $sheetName = array_shift($relevantParts);
-        $flexSubDataStructure = $flexformDSArray['sheets'][$sheetName];
-        foreach ($relevantParts as $relevantPart) {
-            $flexSubDataStructure = $this->getSubConfigurationForSections($flexSubDataStructure, $relevantPart);
-        }
-        $fieldConfig = $this->getNestedDsFieldConfig($flexSubDataStructure, $flexformElement);
-        return $fieldConfig;
-    }
-
-    /**
-     * Recursively get sub sections in data structure by name
-     *
-     * @param array $dataStructure
-     * @param string $fieldName
-     * @return array
-     */
-    protected function getSubConfigurationForSections(array $dataStructure, $fieldName)
-    {
-        $fieldConfig = [];
-        $elements = $dataStructure['ROOT']['el'] ? $dataStructure['ROOT']['el'] : $dataStructure['el'];
-        if (is_array($elements)) {
-            foreach ($elements as $k => $ds) {
-                if ($k === $fieldName) {
-                    $fieldConfig = $ds;
-                    break;
-                } elseif (isset($ds['el'][$fieldName])) {
-                    $fieldConfig = $ds['el'][$fieldName];
-                    break;
-                } else {
-                    $fieldConfig = $this->getSubConfigurationForSections($ds, $fieldName);
-                }
-            }
-        }
-        return $fieldConfig;
-    }
-
-    /**
-     * Search a data structure array recursively -- including within nested
-     * (repeating) elements -- for a particular field config.
-     *
-     * @param array $dataStructure The data structure
-     * @param string $fieldName The field name
-     * @return array
-     */
-    protected function getNestedDsFieldConfig(array $dataStructure, $fieldName)
-    {
-        $fieldConfig = [];
-        $elements = $dataStructure['ROOT']['el'] ? $dataStructure['ROOT']['el'] : $dataStructure['el'];
-        if (is_array($elements)) {
-            foreach ($elements as $k => $ds) {
-                if ($k === $fieldName) {
-                    $fieldConfig = $ds['TCEforms']['config'];
-                    break;
-                } elseif (isset($ds['el'][$fieldName]['TCEforms']['config'])) {
-                    $fieldConfig = $ds['el'][$fieldName]['TCEforms']['config'];
-                    break;
-                } else {
-                    $fieldConfig = $this->getNestedDsFieldConfig($ds, $fieldName);
-                }
-            }
-        }
-        return $fieldConfig;
-    }
-
-    /**
-     * Checks whether the field is an actual identifier or just "array filling"
-     *
-     * @param string $fieldName
-     * @return bool
-     */
-    protected function isRelevantFlexField($fieldName)
-    {
-        return !(
-            strpos($fieldName, 'ID-') === 0 ||
-            $fieldName === 'lDEF' ||
-            $fieldName === 'vDEF' ||
-            $fieldName === 'data' ||
-            $fieldName === 'el'
-        );
-    }
-
-    /**
-     * Returns the configuration for the suggest wizard for the given table. This does multiple overlays from the
-     * TSconfig.
-     *
-     * @param string $queryTable The table to query
-     * @param array $wizardConfig The configuration for the wizard as configured in the data structure
-     * @param array $TSconfig The TSconfig array of the current page
-     * @param string $table The table where the wizard is used
-     * @param string $field The field where the wizard is used
-     * @return array
-     */
-    protected function getConfigurationForTable($queryTable, array $wizardConfig, array $TSconfig, $table, $field)
-    {
-        $config = (array)$wizardConfig['default'];
-
-        if (is_array($wizardConfig[$queryTable])) {
-            ArrayUtility::mergeRecursiveWithOverrule($config, $wizardConfig[$queryTable]);
-        }
-        $globalSuggestTsConfig = $TSconfig['TCEFORM.']['suggest.'];
-        $currentFieldSuggestTsConfig = $TSconfig['TCEFORM.'][$table . '.'][$field . '.']['suggest.'];
-
-        // merge the configurations of different "levels" to get the working configuration for this table and
-        // field (i.e., go from the most general to the most special configuration)
-        if (is_array($globalSuggestTsConfig['default.'])) {
-            ArrayUtility::mergeRecursiveWithOverrule($config, $globalSuggestTsConfig['default.']);
-        }
-
-        if (is_array($globalSuggestTsConfig[$queryTable . '.'])) {
-            ArrayUtility::mergeRecursiveWithOverrule($config, $globalSuggestTsConfig[$queryTable . '.']);
-        }
-
-        // use $table instead of $queryTable here because we overlay a config
-        // for the input-field here, not for the queried table
-        if (is_array($currentFieldSuggestTsConfig['default.'])) {
-            ArrayUtility::mergeRecursiveWithOverrule($config, $currentFieldSuggestTsConfig['default.']);
-        }
-
-        if (is_array($currentFieldSuggestTsConfig[$queryTable . '.'])) {
-            ArrayUtility::mergeRecursiveWithOverrule($config, $currentFieldSuggestTsConfig[$queryTable . '.']);
-        }
-
-        return $config;
-    }
-
-    /**
-     * Checks the given field configuration for the tables that should be used for querying and returns them as an
-     * array.
-     *
-     * @param array $fieldConfig
-     * @return array
-     */
-    protected function getTablesToQueryFromFieldConfiguration(array $fieldConfig)
-    {
-        $queryTables = [];
-
-        if (isset($fieldConfig['allowed'])) {
-            if ($fieldConfig['allowed'] !== '*') {
-                // list of allowed tables
-                $queryTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed']);
-            } else {
-                // all tables are allowed, if the user can access them
-                foreach ($GLOBALS['TCA'] as $tableName => $tableConfig) {
-                    if (!$this->isTableHidden($tableConfig) && $this->currentBackendUserMayAccessTable($tableConfig)) {
-                        $queryTables[] = $tableName;
-                    }
-                }
-                unset($tableName, $tableConfig);
-            }
-        } elseif (isset($fieldConfig['foreign_table'])) {
-            // use the foreign table
-            $queryTables = [$fieldConfig['foreign_table']];
-        }
-
-        return $queryTables;
-    }
-
-    /**
-     * Returns the SQL WHERE clause to use for querying records. This is currently only relevant if a foreign_table
-     * is configured and should be used; it could e.g. be used to limit to a certain subset of records from the
-     * foreign table
-     *
-     * @param array $fieldConfig
-     * @return string
-     */
-    protected function getWhereClause(array $fieldConfig)
-    {
-        if (!isset($fieldConfig['foreign_table'])) {
-            return '';
-        }
-
-        // strip ORDER BY clause
-        return trim(preg_replace('/ORDER[[:space:]]+BY.*/i', '', $fieldConfig['foreign_table_where']));
-    }
-
-    /**
-     * @return LanguageService
-     */
-    protected function getLanguageService()
-    {
-        return $GLOBALS['LANG'];
-    }
-
     /**
      * Returns a new standalone view, shorthand function
      *
@@ -526,12 +133,4 @@ class SuggestWizard
         $view->getRequest()->setControllerExtensionName('Backend');
         return $view;
     }
-
-    /**
-     * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
-     */
-    protected function getBackendUser()
-    {
-        return $GLOBALS['BE_USER'];
-    }
 }
diff --git a/typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php b/typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php
index 242de565d92c..3002df2f88f4 100644
--- a/typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php
+++ b/typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php
@@ -52,10 +52,10 @@ return [
         'target' => Controller\FormInlineAjaxController::class . '::expandOrCollapseAction'
     ],
 
-    // Search records
+    // FormEngine suggest wizard result generator
     'record_suggest' => [
         'path' => '/wizard/suggest/search',
-        'target' => \TYPO3\CMS\Backend\Form\Wizard\SuggestWizard::class . '::searchAction'
+        'target' => \TYPO3\CMS\Backend\Controller\Wizard\SuggestWizardController::class . '::searchAction'
     ],
 
     // Fetch the tree-structured data from a record for the tree selection
diff --git a/typo3/sysext/backend/Tests/Unit/Controller/Wizard/SuggestWizardControllerTest.php b/typo3/sysext/backend/Tests/Unit/Controller/Wizard/SuggestWizardControllerTest.php
new file mode 100644
index 000000000000..fe2f736602aa
--- /dev/null
+++ b/typo3/sysext/backend/Tests/Unit/Controller/Wizard/SuggestWizardControllerTest.php
@@ -0,0 +1,300 @@
+<?php
+namespace TYPO3\CMS\Backend\Tests\Unit\Controller\Wizard;
+
+/*
+ * 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 Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Backend\Controller\Wizard\SuggestWizardController;
+use TYPO3\CMS\Core\Tests\AccessibleObjectInterface;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+use TYPO3\CMS\Fluid\View\StandaloneView;
+
+/**
+ * Test case
+ */
+class SuggestWizardControllerTest extends UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function searchActionThrowsExceptionWithMissingArgument()
+    {
+        $viewProphecy = $this->prophesize(StandaloneView::class);
+        $responseProphecy = $this->prophesize(ResponseInterface::class);
+        $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class);
+        $serverRequestProphecy->getParsedBody()->willReturn([
+            'value' => 'theSearchValue',
+            'table' => 'aTable',
+            'field' => 'aField',
+            'uid' => 'aUid',
+            'dataStructureIdentifier' => 'anIdentifier',
+            // hmac missing
+        ]);
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionCode(1478607036);
+        (new SuggestWizardController($viewProphecy->reveal()))
+            ->searchAction($serverRequestProphecy->reveal(), $responseProphecy->reveal());
+    }
+
+    /**
+     * @test
+     */
+    public function searchActionThrowsExceptionWithWrongHmac()
+    {
+        $viewProphecy = $this->prophesize(StandaloneView::class);
+        $responseProphecy = $this->prophesize(ResponseInterface::class);
+        $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class);
+        $serverRequestProphecy->getParsedBody()->willReturn([
+            'value' => 'theSearchValue',
+            'table' => 'aTable',
+            'field' => 'aField',
+            'uid' => 'aUid',
+            'dataStructureIdentifier' => 'anIdentifier',
+            'hmac' => 'wrongHmac'
+        ]);
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionCode(1478608245);
+        (new SuggestWizardController($viewProphecy->reveal()))
+            ->searchAction($serverRequestProphecy->reveal(), $responseProphecy->reveal());
+    }
+
+    /**
+     * @test
+     */
+    public function getFlexFieldConfigurationThrowsExceptionIfSimpleFlexFieldIsNotFound()
+    {
+        $dataStructure = [
+            'sheets' => [
+                'sDb' => [
+                    'ROOT' => [
+                        'el' => [
+                            'differentField' => [
+                                'TCEforms' => [
+                                    'config' => [
+                                        'Sublevel field configuration',
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ];
+
+        $parts = [
+            0 => 'flex_1',
+            1 => 'data',
+            2 => 'sDb',
+            3 => 'lDEF',
+            4 => 'group_db_1',
+            5 => 'vDEF',
+        ];
+
+        /** @var SuggestWizardController|AccessibleObjectInterface|\PHPUnit_Framework_MockObject_MockObject $subject */
+        $subject = $this->getAccessibleMock(SuggestWizardController::class, ['dummy'], [], '', false);
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionCode(1480609491);
+        $subject->_call('getFlexFieldConfiguration', $parts, $dataStructure);
+    }
+
+    /**
+     * @test
+     */
+    public function getFlexFieldConfigurationThrowsExceptionIfSectionContainerFlexFieldIsNotFound()
+    {
+        $dataStructure = [
+            'sheets' => [
+                'sDb' => [
+                    'ROOT' => [
+                        'el' => [
+                            'notTheFieldYouAreLookingFor' => [
+                                'TCEforms' => [
+                                    'config' => [
+                                        'Sublevel field configuration',
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ];
+
+        $parts = [
+            0 => 'flex_1',
+            1 => 'data',
+            2 => 'sSuggestCheckCombination',
+            3 => 'lDEF',
+            4 => 'settings.subelements',
+            5 => 'el',
+            6 => '1',
+            7 => 'item',
+            8 => 'el',
+            9 => 'content',
+            10 => 'vDEF',
+        ];
+
+        /** @var SuggestWizardController|AccessibleObjectInterface|\PHPUnit_Framework_MockObject_MockObject $subject */
+        $subject = $this->getAccessibleMock(SuggestWizardController::class, ['dummy'], [], '', false);
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionCode(1480611208);
+        $subject->_call('getFlexFieldConfiguration', $parts, $dataStructure);
+    }
+
+    /**
+     * @test
+     */
+    public function getFlexFieldConfigurationThrowsExceptionPartsIsOfUnexpectedLength()
+    {
+        /** @var SuggestWizardController|AccessibleObjectInterface|\PHPUnit_Framework_MockObject_MockObject $subject */
+        $subject = $this->getAccessibleMock(SuggestWizardController::class, ['dummy'], [], '', false);
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionCode(1480611252);
+        $subject->_call('getFlexFieldConfiguration', [], []);
+    }
+
+    /**
+     * @test
+     */
+    public function getFlexFieldConfigurationFindsConfigurationOfSimpleFlexField()
+    {
+        $dataStructure = [
+            'sheets' => [
+                'sDb' => [
+                    'ROOT' => [
+                        'el' => [
+                            'group_db_1' => [
+                                'TCEforms' => [
+                                    'config' => [
+                                        'Sublevel field configuration',
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ];
+
+        $parts = [
+            0 => 'flex_1',
+            1 => 'data',
+            2 => 'sDb',
+            3 => 'lDEF',
+            4 => 'group_db_1',
+            5 => 'vDEF',
+        ];
+
+        $expected = $dataStructure['sheets']['sDb']['ROOT']['el']['group_db_1']['TCEforms']['config'];
+
+        /** @var SuggestWizardController|AccessibleObjectInterface|\PHPUnit_Framework_MockObject_MockObject $subject */
+        $subject = $this->getAccessibleMock(SuggestWizardController::class, ['dummy'], [], '', false);
+        $result = $subject->_call('getFlexFieldConfiguration', $parts, $dataStructure);
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * @test
+     */
+    public function getFlexFieldConfigurationFindsConfigurationOfSectionContainerField()
+    {
+        $dataStructure = [
+            'sheets' => [
+                'sSuggestCheckCombination' => [
+                    'ROOT' => [
+                        'type' => 'array',
+                        'el' => [
+                            'settings.subelements' => [
+                                'title' => 'Subelements',
+                                'section' => 1,
+                                'type' => 'array',
+                                'el' => [
+                                    'item' => [
+                                        'type' => 'array',
+                                        'title' => 'Subelement',
+                                        'el' => [
+                                            'content' => [
+                                                'TCEforms' => [
+                                                    'label' => 'Content',
+                                                    'config' => [
+                                                        'type' => 'group',
+                                                        'internal_type' => 'db',
+                                                        'allowed' => 'pages',
+                                                        'size' => 5,
+                                                        'maxitems' => 10,
+                                                        'minitems' => 1,
+                                                        'show_thumbs' => 1,
+                                                        'wizards' => [
+                                                            'suggest' => [
+                                                                'type' => 'suggest',
+                                                            ],
+                                                        ],
+                                                    ],
+                                                ],
+                                            ],
+                                        ],
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ];
+
+        $parts = [
+            0 => 'flex_1',
+            1 => 'data',
+            2 => 'sSuggestCheckCombination',
+            3 => 'lDEF',
+            4 => 'settings.subelements',
+            5 => 'el',
+            6 => '1',
+            7 => 'item',
+            8 => 'el',
+            9 => 'content',
+            10 => 'vDEF',
+        ];
+
+        $expected = $dataStructure['sheets']['sSuggestCheckCombination']['ROOT']['el']['settings.subelements']
+            ['el']['item']['el']['content']['TCEforms']['config'];
+
+        /** @var SuggestWizardController|AccessibleObjectInterface|\PHPUnit_Framework_MockObject_MockObject $subject */
+        $subject = $this->getAccessibleMock(SuggestWizardController::class, ['dummy'], [], '', false);
+        $result = $subject->_call('getFlexFieldConfiguration', $parts, $dataStructure);
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * @test
+     * @dataProvider isTableHiddenIsProperlyRetrievedDataProvider
+     */
+    public function isTableHiddenIsProperlyRetrieved($expected, $array)
+    {
+        $subject = $this->getAccessibleMock(SuggestWizardController::class, ['dummy'], [], '', false);
+        $this->assertEquals($expected, $subject->_call('isTableHidden', $array));
+    }
+
+    public function isTableHiddenIsProperlyRetrievedDataProvider()
+    {
+        return [
+          'notSetValue' => [false, ['ctrl' => ['hideTable' => null]]],
+          'true' => [true, ['ctrl' => ['hideTable' => true]]],
+          'false' => [false, ['ctrl' => ['hideTable' => false]]],
+          'string with true' => [true, ['ctrl' => ['hideTable' => '1']]],
+          'string with false' => [false, ['ctrl' => ['hideTable' => '0']]],
+        ];
+    }
+}
diff --git a/typo3/sysext/backend/Tests/Unit/Form/Wizard/SuggestWizardTest.php b/typo3/sysext/backend/Tests/Unit/Form/Wizard/SuggestWizardTest.php
index 2d0a79b75d3e..586f23924ff6 100644
--- a/typo3/sysext/backend/Tests/Unit/Form/Wizard/SuggestWizardTest.php
+++ b/typo3/sysext/backend/Tests/Unit/Form/Wizard/SuggestWizardTest.php
@@ -14,10 +14,7 @@ namespace TYPO3\CMS\Backend\Tests\Unit\Form\Wizard;
  * The TYPO3 project - inspiring people to share!
  */
 
-use Psr\Http\Message\ResponseInterface;
-use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Backend\Form\Wizard\SuggestWizard;
-use TYPO3\CMS\Core\Tests\AccessibleObjectInterface;
 use TYPO3\CMS\Core\Tests\UnitTestCase;
 use TYPO3\CMS\Fluid\View\StandaloneView;
 
@@ -48,195 +45,4 @@ class SuggestWizardTest extends UnitTestCase
             ]
         );
     }
-
-    /**
-     * @test
-     */
-    public function searchActionThrowsExceptionWithMissingArgument()
-    {
-        $viewProphecy = $this->prophesize(StandaloneView::class);
-        $responseProphecy = $this->prophesize(ResponseInterface::class);
-        $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class);
-        $serverRequestProphecy->getParsedBody()->willReturn([
-            'value' => 'theSearchValue',
-            'table' => 'aTable',
-            'field' => 'aField',
-            'uid' => 'aUid',
-            'dataStructureIdentifier' => 'anIdentifier',
-            // hmac missing
-        ]);
-        $this->expectException(\RuntimeException::class);
-        $this->expectExceptionCode(1478607036);
-        (new SuggestWizard($viewProphecy->reveal()))
-            ->searchAction($serverRequestProphecy->reveal(), $responseProphecy->reveal());
-    }
-
-    /**
-     * @test
-     */
-    public function searchActionThrowsExceptionWithWrongHmac()
-    {
-        $viewProphecy = $this->prophesize(StandaloneView::class);
-        $responseProphecy = $this->prophesize(ResponseInterface::class);
-        $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class);
-        $serverRequestProphecy->getParsedBody()->willReturn([
-            'value' => 'theSearchValue',
-            'table' => 'aTable',
-            'field' => 'aField',
-            'uid' => 'aUid',
-            'dataStructureIdentifier' => 'anIdentifier',
-            'hmac' => 'wrongHmac'
-        ]);
-        $this->expectException(\RuntimeException::class);
-        $this->expectExceptionCode(1478608245);
-        (new SuggestWizard($viewProphecy->reveal()))
-            ->searchAction($serverRequestProphecy->reveal(), $responseProphecy->reveal());
-    }
-
-    /**
-     * @test
-     */
-    public function getFieldConfigurationFetchesConfigurationDependentOnTheFullPathToField()
-    {
-        $config = [
-            'el' => [
-                'content' => [
-                    'TCEforms' => [
-                        'config' => [
-                            'Sublevel field configuration',
-                        ],
-                    ],
-                ],
-            ],
-        ];
-
-        $dataStructure['sheets']['sSuggestCheckCombination']['ROOT']['el'] = [
-            'settings.topname1' => [
-                'el' => [
-                    'item' => [
-                        'el' => [
-                            'content' => [
-                                'TCEforms' => [
-                                    'config' => [
-                                        'different foo config for field with same name',
-                                    ],
-                                ],
-                            ],
-                        ],
-                    ],
-                ],
-            ],
-            'settings.topname3' => [
-                'el' => ['item' => $config]
-            ],
-            'settings.topname2' => [
-                'el' => [
-                    'item' => [
-                        'el' => [
-                            'content' => [
-                                'TCEforms' => [
-                                    'config' => [
-                                        'different foo config for field with same name',
-                                    ],
-                                ],
-                            ],
-                        ],
-                    ],
-                ],
-            ],
-        ];
-        $parts = [
-            0 => 'flex_1',
-            1 => 'data',
-            2 => 'sSuggestCheckCombination',
-            3 => 'lDEF',
-            4 => 'settings.topname3',
-            5 => 'el',
-            6 => 'ID-efa3ff7ed5-idx1460636854058-form',
-            7 => 'item',
-            8 => 'el',
-            9 => 'content',
-            10 => 'vDEF',
-        ];
-
-        /** @var SuggestWizard|AccessibleObjectInterface|\PHPUnit_Framework_MockObject_MockObject $subject */
-        $subject = $this->getAccessibleMock(SuggestWizard::class, ['getNestedDsFieldConfig'], [], '', false);
-        $subject
-            ->expects($this->once())
-            ->method('getNestedDsFieldConfig')
-            ->with($config, 'content');
-        $subject->_call('getFieldConfiguration', $parts, $dataStructure);
-    }
-
-    /**
-     * @test
-     */
-    public function getFieldConfigurationFetchesConfigurationForFieldsWithoutSheets()
-    {
-        $config = [
-            'ROOT' => [
-                'type' => 'array',
-                'el' => [
-                    'content' => [
-                        'TCEforms' => [
-                            'label' => 'group_db_1 wizard suggest',
-                            'config' => [
-                                'type' => 'group',
-                                'internal_type' => 'db',
-                                'allowed' => 'tx_styleguide_staticdata',
-                                'wizards' => [
-                                    'suggest' => [
-                                        'type' => 'suggest',
-                                    ],
-                                ],
-                            ],
-                        ],
-                    ],
-                ],
-            ]
-        ];
-        $dataStructure = [
-            'sheets' => [
-                'sDEF' => $config
-            ],
-        ];
-        $parts = [
-            0 => 'flex_1',
-            1 => 'data',
-            2 => 'sDEF',
-            3 => 'lDEF',
-            4 => 'content',
-            5 => 'vDEF',
-        ];
-
-        /** @var SuggestWizard|AccessibleObjectInterface|\PHPUnit_Framework_MockObject_MockObject $subject */
-        $subject = $this->getAccessibleMock(SuggestWizard::class, ['getNestedDsFieldConfig'], [], '', false);
-        $subject
-            ->expects($this->once())
-            ->method('getNestedDsFieldConfig')
-            ->with($config, 'content');
-
-        $subject->_call('getFieldConfiguration', $parts, $dataStructure);
-    }
-
-    /**
-     * @test
-     * @dataProvider isTableHiddenIsProperlyRetrievedDataProvider
-     */
-    public function isTableHiddenIsProperlyRetrieved($expected, $array)
-    {
-        $subject = $this->getAccessibleMock(SuggestWizard::class, ['dummy'], [], '', false);
-        $this->assertEquals($expected, $subject->_call('isTableHidden', $array));
-    }
-
-    public function isTableHiddenIsProperlyRetrievedDataProvider()
-    {
-        return [
-          'notSetValue' => [false, ['ctrl' => ['hideTable' => null]]],
-          'true' => [true, ['ctrl' => ['hideTable' => true]]],
-          'false' => [false, ['ctrl' => ['hideTable' => false]]],
-          'string with true' => [true, ['ctrl' => ['hideTable' => '1']]],
-          'string with false' => [false, ['ctrl' => ['hideTable' => '0']]],
-        ];
-    }
 }
-- 
GitLab