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 0000000000000000000000000000000000000000..390ea7b2be432329a6b8502b9066f4111dcb4200 --- /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 2fc6c07a7673bb54a799f566b655ebbd8c89243a..1f7b3216ff05043224dada87d4c58ec8b513eec2 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 242de565d92c5cd005a59e8d965a9178da519265..3002df2f88f4d7d717755e646c60cb2d1ed8524f 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 0000000000000000000000000000000000000000..fe2f736602aaa6dc7c692f81279aafb490e697d2 --- /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 2d0a79b75d3e6c859a701a8cd403d22d3d454453..586f23924ff62a7c9afd90dc7c0d2dc08eb19b35 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']]], - ]; - } }