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