diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/EvaluateDisplayConditions.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/EvaluateDisplayConditions.php index 8c30e23b98a2ff86057ac9b37e0bc8cecaa13d67..b75faa82eeaa374a2d531ac4e8e50bbd3327f3ed 100644 --- a/typo3/sysext/backend/Classes/Form/FormDataProvider/EvaluateDisplayConditions.php +++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/EvaluateDisplayConditions.php @@ -15,6 +15,7 @@ namespace TYPO3\CMS\Backend\Form\FormDataProvider; */ use TYPO3\CMS\Backend\Form\FormDataProviderInterface; +use TYPO3\CMS\Backend\Form\Utility\DisplayConditionEvaluator; use TYPO3\CMS\Core\Utility\GeneralUtility; /** @@ -52,7 +53,11 @@ class EvaluateDisplayConditions implements FormDataProviderInterface continue; } - if (!$this->evaluateDisplayCondition($columnConfiguration['displayCond'], $result['databaseRow'])) { + $displayConditionValid = $this->getDisplayConditionEvaluator()->evaluateDisplayCondition( + $columnConfiguration['displayCond'], + $result['databaseRow'] + ); + if (!$displayConditionValid) { unset($result['processedTca']['columns'][$columnName]); } } @@ -86,7 +91,12 @@ class EvaluateDisplayConditions implements FormDataProviderInterface if (!isset($sheetConfiguration['ROOT']['displayCond'])) { continue; } - if (!$this->evaluateDisplayCondition($sheetConfiguration['ROOT']['displayCond'], $flexFormRowData, true)) { + $displayConditionValid = $this->getDisplayConditionEvaluator()->evaluateDisplayCondition( + $sheetConfiguration['ROOT']['displayCond'], + $flexFormRowData, + true + ); + if (!$displayConditionValid) { unset($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'][$sheetName]); } } @@ -142,7 +152,7 @@ class EvaluateDisplayConditions implements FormDataProviderInterface if ($key === 'el' && is_array($value)) { $newSubStructure = []; foreach ($value as $subKey => $subValue) { - if (!isset($subValue['displayCond']) || $this->evaluateDisplayCondition($subValue['displayCond'], $flexFormRowData, true)) { + if (!isset($subValue['displayCond']) || $this->getDisplayConditionEvaluator()->evaluateDisplayCondition($subValue['displayCond'], $flexFormRowData, true)) { $newSubStructure[$subKey] = $subValue; } } @@ -176,316 +186,12 @@ class EvaluateDisplayConditions implements FormDataProviderInterface } /** - * Evaluates the provided condition and returns TRUE if the form - * element should be displayed. - * - * The condition string is separated by colons and the first part - * indicates what type of evaluation should be performed. - * - * @param string $displayCondition - * @param array $record - * @param bool $flexformContext - * @param int $recursionLevel Internal level of recursion - * @return bool TRUE if condition evaluates successfully - */ - protected function evaluateDisplayCondition($displayCondition, array $record = [], $flexformContext = false, $recursionLevel = 0) - { - if ($recursionLevel > 99) { - // This should not happen, treat as misconfiguration - return true; - } - if (!is_array($displayCondition)) { - // DisplayCondition is not an array - just get its value - $result = $this->evaluateSingleDisplayCondition($displayCondition, $record, $flexformContext); - } else { - // Multiple conditions given as array ('AND|OR' => condition array) - $conditionEvaluations = [ - 'AND' => [], - 'OR' => [], - ]; - foreach ($displayCondition as $logicalOperator => $groupedDisplayConditions) { - $logicalOperator = strtoupper($logicalOperator); - if (($logicalOperator !== 'AND' && $logicalOperator !== 'OR') || !is_array($groupedDisplayConditions)) { - // Invalid line. Skip it. - continue; - } else { - foreach ($groupedDisplayConditions as $key => $singleDisplayCondition) { - $key = strtoupper($key); - if (($key === 'AND' || $key === 'OR') && is_array($singleDisplayCondition)) { - // Recursion statement: condition is 'AND' or 'OR' and is pointing to an array (should be conditions again) - $conditionEvaluations[$logicalOperator][] = $this->evaluateDisplayCondition( - [$key => $singleDisplayCondition], - $record, - $flexformContext, - $recursionLevel + 1 - ); - } else { - // Condition statement: collect evaluation of this single condition. - $conditionEvaluations[$logicalOperator][] = $this->evaluateSingleDisplayCondition( - $singleDisplayCondition, - $record, - $flexformContext - ); - } - } - } - } - if (!empty($conditionEvaluations['OR']) && in_array(true, $conditionEvaluations['OR'], true)) { - // There are OR conditions and at least one of them is TRUE - $result = true; - } elseif (!empty($conditionEvaluations['AND']) && !in_array(false, $conditionEvaluations['AND'], true)) { - // There are AND conditions and none of them is FALSE - $result = true; - } elseif (!empty($conditionEvaluations['OR']) || !empty($conditionEvaluations['AND'])) { - // There are some conditions. But no OR was TRUE and at least one AND was FALSE - $result = false; - } else { - // There are no proper conditions - misconfiguration. Return TRUE. - $result = true; - } - } - return $result; - } - - /** - * Evaluates the provided condition and returns TRUE if the form - * element should be displayed. - * - * The condition string is separated by colons and the first part - * indicates what type of evaluation should be performed. - * - * @param string $displayCondition - * @param array $record - * @param bool $flexformContext - * @return bool - * @see evaluateDisplayCondition() - */ - protected function evaluateSingleDisplayCondition($displayCondition, array $record = [], $flexformContext = false) - { - $result = false; - list($matchType, $condition) = explode(':', $displayCondition, 2); - switch ($matchType) { - case 'FIELD': - $result = $this->matchFieldCondition($condition, $record, $flexformContext); - break; - case 'HIDE_FOR_NON_ADMINS': - $result = $this->matchHideForNonAdminsCondition(); - break; - case 'REC': - $result = $this->matchRecordCondition($condition, $record); - break; - case 'VERSION': - $result = $this->matchVersionCondition($condition, $record); - break; - case 'USER': - $result = $this->matchUserCondition($condition, $record); - break; - } - return $result; - } - - /** - * Evaluates conditions concerning a field of the current record. - * Requires a record set via ->setRecord() - * - * Example: - * "FIELD:sys_language_uid:>:0" => TRUE, if the field 'sys_language_uid' is greater than 0 - * - * @param string $condition - * @param array $record - * @param bool $flexformContext - * @return bool - */ - protected function matchFieldCondition($condition, $record, $flexformContext = false) - { - list($fieldName, $operator, $operand) = explode(':', $condition, 3); - if ($flexformContext) { - if (strpos($fieldName, 'parentRec.') !== false) { - $fieldNameParts = explode('.', $fieldName, 2); - $fieldValue = $record['parentRec'][$fieldNameParts[1]]; - } else { - $fieldValue = $record[$fieldName]['vDEF']; - } - } else { - $fieldValue = $record[$fieldName]; - } - $result = false; - switch ($operator) { - case 'REQ': - if (is_array($fieldValue) && count($fieldValue) <= 1) { - $fieldValue = array_shift($fieldValue); - } - if (strtoupper($operand) === 'TRUE') { - $result = (bool)$fieldValue; - } else { - $result = !$fieldValue; - } - break; - case '>': - if (is_array($fieldValue) && count($fieldValue) <= 1) { - $fieldValue = array_shift($fieldValue); - } - $result = $fieldValue > $operand; - break; - case '<': - if (is_array($fieldValue) && count($fieldValue) <= 1) { - $fieldValue = array_shift($fieldValue); - } - $result = $fieldValue < $operand; - break; - case '>=': - if (is_array($fieldValue) && count($fieldValue) <= 1) { - $fieldValue = array_shift($fieldValue); - } - $result = $fieldValue >= $operand; - break; - case '<=': - if (is_array($fieldValue) && count($fieldValue) <= 1) { - $fieldValue = array_shift($fieldValue); - } - $result = $fieldValue <= $operand; - break; - case '-': - case '!-': - if (is_array($fieldValue) && count($fieldValue) <= 1) { - $fieldValue = array_shift($fieldValue); - } - list($minimum, $maximum) = explode('-', $operand); - $result = $fieldValue >= $minimum && $fieldValue <= $maximum; - if ($operator[0] === '!') { - $result = !$result; - } - break; - case '=': - case '!=': - if (is_array($fieldValue) && count($fieldValue) <= 1) { - $fieldValue = array_shift($fieldValue); - } - $result = $fieldValue == $operand; - if ($operator[0] === '!') { - $result = !$result; - } - break; - case 'IN': - case '!IN': - if (is_array($fieldValue)) { - $result = count(array_intersect($fieldValue, explode(',', $operand))) > 0; - } else { - $result = GeneralUtility::inList($operand, $fieldValue); - } - if ($operator[0] === '!') { - $result = !$result; - } - break; - case 'BIT': - case '!BIT': - $result = (bool)((int)$fieldValue & $operand); - if ($operator[0] === '!') { - $result = !$result; - } - break; - } - return $result; - } - - /** - * Evaluates TRUE if current backend user is an admin. - * - * @return bool - */ - protected function matchHideForNonAdminsCondition() - { - return (bool)$this->getBackendUser()->isAdmin(); - } - - /** - * Evaluates conditions concerning the status of the current record. - * Requires a record set via ->setRecord() - * - * Example: - * "REC:NEW:FALSE" => TRUE, if the record is already persisted (has a uid > 0) - * - * @param string $condition - * @param array $record - * @return bool - */ - protected function matchRecordCondition($condition, $record) - { - $result = false; - list($operator, $operand) = explode(':', $condition, 2); - if ($operator === 'NEW') { - if (strtoupper($operand) === 'TRUE') { - $result = !((int)$record['uid'] > 0); - } elseif (strtoupper($operand) === 'FALSE') { - $result = ((int)$record['uid'] > 0); - } - } - return $result; - } - - /** - * Evaluates whether the current record is versioned. - * Requires a record set via ->setRecord() - * - * @param string $condition - * @param array $record - * @return bool - */ - protected function matchVersionCondition($condition, $record) - { - $result = false; - list($operator, $operand) = explode(':', $condition, 2); - if ($operator === 'IS') { - $isNewRecord = !((int)$record['uid'] > 0); - // Detection of version can be done be detecting the workspace of the user - $isUserInWorkspace = $this->getBackendUser()->workspace > 0; - if ((int)$record['pid'] === -1 || (int)$record['_ORIG_pid'] === -1) { - $isRecordDetectedAsVersion = true; - } else { - $isRecordDetectedAsVersion = false; - } - // New records in a workspace are not handled as a version record - // if it's no new version, we detect versions like this: - // -- if user is in workspace: always TRUE - // -- if editor is in live ws: only TRUE if pid == -1 - $isVersion = ($isUserInWorkspace || $isRecordDetectedAsVersion) && !$isNewRecord; - if (strtoupper($operand) === 'TRUE') { - $result = $isVersion; - } elseif (strtoupper($operand) === 'FALSE') { - $result = !$isVersion; - } - } - return $result; - } - - /** - * Evaluates via the referenced user-defined method - * - * @param string $condition - * @param array $record - * @return bool - */ - protected function matchUserCondition($condition, $record) - { - $conditionParameters = explode(':', $condition); - $userFunction = array_shift($conditionParameters); - - $parameter = [ - 'record' => $record, - 'flexformValueKey' => 'vDEF', - 'conditionParameters' => $conditionParameters - ]; - - return (bool)GeneralUtility::callUserFunction($userFunction, $parameter, $this); - } - - /** - * Get current backend user + * Returns the DisplayConditionEvaluator utility. * - * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication + * @return DisplayConditionEvaluator */ - protected function getBackendUser() + protected function getDisplayConditionEvaluator() { - return $GLOBALS['BE_USER']; + return GeneralUtility::makeInstance(DisplayConditionEvaluator::class); } } diff --git a/typo3/sysext/backend/Classes/Form/Utility/DisplayConditionEvaluator.php b/typo3/sysext/backend/Classes/Form/Utility/DisplayConditionEvaluator.php new file mode 100644 index 0000000000000000000000000000000000000000..e34021335ef834c4fbad019782683d19c269e6e6 --- /dev/null +++ b/typo3/sysext/backend/Classes/Form/Utility/DisplayConditionEvaluator.php @@ -0,0 +1,338 @@ +<?php +namespace TYPO3\CMS\Backend\Form\Utility; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Core\SingletonInterface; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Utility class for evaluating TCA display conditions. + */ +class DisplayConditionEvaluator implements SingletonInterface +{ + /** + * Evaluates the provided condition and returns TRUE if the form + * element should be displayed. + * + * The condition string is separated by colons and the first part + * indicates what type of evaluation should be performed. + * + * @param string $displayCondition + * @param array $record + * @param bool $flexformContext + * @param int $recursionLevel Internal level of recursion + * @return bool TRUE if condition evaluates successfully + */ + public function evaluateDisplayCondition($displayCondition, array $record = [], $flexformContext = false, $recursionLevel = 0) + { + if ($recursionLevel > 99) { + // This should not happen, treat as misconfiguration + return true; + } + if (!is_array($displayCondition)) { + // DisplayCondition is not an array - just get its value + $result = $this->evaluateSingleDisplayCondition($displayCondition, $record, $flexformContext); + } else { + // Multiple conditions given as array ('AND|OR' => condition array) + $conditionEvaluations = [ + 'AND' => [], + 'OR' => [], + ]; + foreach ($displayCondition as $logicalOperator => $groupedDisplayConditions) { + $logicalOperator = strtoupper($logicalOperator); + if (($logicalOperator !== 'AND' && $logicalOperator !== 'OR') || !is_array($groupedDisplayConditions)) { + // Invalid line. Skip it. + continue; + } else { + foreach ($groupedDisplayConditions as $key => $singleDisplayCondition) { + $key = strtoupper($key); + if (($key === 'AND' || $key === 'OR') && is_array($singleDisplayCondition)) { + // Recursion statement: condition is 'AND' or 'OR' and is pointing to an array (should be conditions again) + $conditionEvaluations[$logicalOperator][] = $this->evaluateDisplayCondition( + [$key => $singleDisplayCondition], + $record, + $flexformContext, + $recursionLevel + 1 + ); + } else { + // Condition statement: collect evaluation of this single condition. + $conditionEvaluations[$logicalOperator][] = $this->evaluateSingleDisplayCondition( + $singleDisplayCondition, + $record, + $flexformContext + ); + } + } + } + } + if (!empty($conditionEvaluations['OR']) && in_array(true, $conditionEvaluations['OR'], true)) { + // There are OR conditions and at least one of them is TRUE + $result = true; + } elseif (!empty($conditionEvaluations['AND']) && !in_array(false, $conditionEvaluations['AND'], true)) { + // There are AND conditions and none of them is FALSE + $result = true; + } elseif (!empty($conditionEvaluations['OR']) || !empty($conditionEvaluations['AND'])) { + // There are some conditions. But no OR was TRUE and at least one AND was FALSE + $result = false; + } else { + // There are no proper conditions - misconfiguration. Return TRUE. + $result = true; + } + } + return $result; + } + + /** + * Evaluates the provided condition and returns TRUE if the form + * element should be displayed. + * + * The condition string is separated by colons and the first part + * indicates what type of evaluation should be performed. + * + * @param string $displayCondition + * @param array $record + * @param bool $flexformContext + * @return bool + * @see evaluateDisplayCondition() + */ + protected function evaluateSingleDisplayCondition($displayCondition, array $record = [], $flexformContext = false) + { + $result = false; + list($matchType, $condition) = explode(':', $displayCondition, 2); + switch ($matchType) { + case 'FIELD': + $result = $this->matchFieldCondition($condition, $record, $flexformContext); + break; + case 'HIDE_FOR_NON_ADMINS': + $result = $this->matchHideForNonAdminsCondition(); + break; + case 'REC': + $result = $this->matchRecordCondition($condition, $record); + break; + case 'VERSION': + $result = $this->matchVersionCondition($condition, $record); + break; + case 'USER': + $result = $this->matchUserCondition($condition, $record); + break; + } + return $result; + } + + /** + * Evaluates conditions concerning a field of the current record. + * Requires a record set via ->setRecord() + * + * Example: + * "FIELD:sys_language_uid:>:0" => TRUE, if the field 'sys_language_uid' is greater than 0 + * + * @param string $condition + * @param array $record + * @param bool $flexformContext + * @return bool + */ + protected function matchFieldCondition($condition, $record, $flexformContext = false) + { + list($fieldName, $operator, $operand) = explode(':', $condition, 3); + if ($flexformContext) { + if (strpos($fieldName, 'parentRec.') !== false) { + $fieldNameParts = explode('.', $fieldName, 2); + $fieldValue = $record['parentRec'][$fieldNameParts[1]]; + } else { + $fieldValue = $record[$fieldName]['vDEF']; + } + } else { + $fieldValue = $record[$fieldName]; + } + $result = false; + switch ($operator) { + case 'REQ': + if (is_array($fieldValue) && count($fieldValue) <= 1) { + $fieldValue = array_shift($fieldValue); + } + if (strtoupper($operand) === 'TRUE') { + $result = (bool)$fieldValue; + } else { + $result = !$fieldValue; + } + break; + case '>': + if (is_array($fieldValue) && count($fieldValue) <= 1) { + $fieldValue = array_shift($fieldValue); + } + $result = $fieldValue > $operand; + break; + case '<': + if (is_array($fieldValue) && count($fieldValue) <= 1) { + $fieldValue = array_shift($fieldValue); + } + $result = $fieldValue < $operand; + break; + case '>=': + if (is_array($fieldValue) && count($fieldValue) <= 1) { + $fieldValue = array_shift($fieldValue); + } + $result = $fieldValue >= $operand; + break; + case '<=': + if (is_array($fieldValue) && count($fieldValue) <= 1) { + $fieldValue = array_shift($fieldValue); + } + $result = $fieldValue <= $operand; + break; + case '-': + case '!-': + if (is_array($fieldValue) && count($fieldValue) <= 1) { + $fieldValue = array_shift($fieldValue); + } + list($minimum, $maximum) = explode('-', $operand); + $result = $fieldValue >= $minimum && $fieldValue <= $maximum; + if ($operator[0] === '!') { + $result = !$result; + } + break; + case '=': + case '!=': + if (is_array($fieldValue) && count($fieldValue) <= 1) { + $fieldValue = array_shift($fieldValue); + } + $result = $fieldValue == $operand; + if ($operator[0] === '!') { + $result = !$result; + } + break; + case 'IN': + case '!IN': + if (is_array($fieldValue)) { + $result = count(array_intersect($fieldValue, explode(',', $operand))) > 0; + } else { + $result = GeneralUtility::inList($operand, $fieldValue); + } + if ($operator[0] === '!') { + $result = !$result; + } + break; + case 'BIT': + case '!BIT': + $result = (bool)((int)$fieldValue & $operand); + if ($operator[0] === '!') { + $result = !$result; + } + break; + } + return $result; + } + + /** + * Evaluates TRUE if current backend user is an admin. + * + * @return bool + */ + protected function matchHideForNonAdminsCondition() + { + return (bool)$this->getBackendUser()->isAdmin(); + } + + /** + * Evaluates conditions concerning the status of the current record. + * Requires a record set via ->setRecord() + * + * Example: + * "REC:NEW:FALSE" => TRUE, if the record is already persisted (has a uid > 0) + * + * @param string $condition + * @param array $record + * @return bool + */ + protected function matchRecordCondition($condition, $record) + { + $result = false; + list($operator, $operand) = explode(':', $condition, 2); + if ($operator === 'NEW') { + if (strtoupper($operand) === 'TRUE') { + $result = !((int)$record['uid'] > 0); + } elseif (strtoupper($operand) === 'FALSE') { + $result = ((int)$record['uid'] > 0); + } + } + return $result; + } + + /** + * Evaluates whether the current record is versioned. + * Requires a record set via ->setRecord() + * + * @param string $condition + * @param array $record + * @return bool + */ + protected function matchVersionCondition($condition, $record) + { + $result = false; + list($operator, $operand) = explode(':', $condition, 2); + if ($operator === 'IS') { + $isNewRecord = !((int)$record['uid'] > 0); + // Detection of version can be done be detecting the workspace of the user + $isUserInWorkspace = $this->getBackendUser()->workspace > 0; + if ((int)$record['pid'] === -1 || (int)$record['_ORIG_pid'] === -1) { + $isRecordDetectedAsVersion = true; + } else { + $isRecordDetectedAsVersion = false; + } + // New records in a workspace are not handled as a version record + // if it's no new version, we detect versions like this: + // -- if user is in workspace: always TRUE + // -- if editor is in live ws: only TRUE if pid == -1 + $isVersion = ($isUserInWorkspace || $isRecordDetectedAsVersion) && !$isNewRecord; + if (strtoupper($operand) === 'TRUE') { + $result = $isVersion; + } elseif (strtoupper($operand) === 'FALSE') { + $result = !$isVersion; + } + } + return $result; + } + + /** + * Evaluates via the referenced user-defined method + * + * @param string $condition + * @param array $record + * @return bool + */ + protected function matchUserCondition($condition, $record) + { + $conditionParameters = explode(':', $condition); + $userFunction = array_shift($conditionParameters); + + $parameter = [ + 'record' => $record, + 'flexformValueKey' => 'vDEF', + 'conditionParameters' => $conditionParameters + ]; + + return (bool)GeneralUtility::callUserFunction($userFunction, $parameter, $this); + } + + /** + * Get current backend user + * + * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication + */ + protected function getBackendUser() + { + return $GLOBALS['BE_USER']; + } +} diff --git a/typo3/sysext/core/Classes/Category/CategoryRegistry.php b/typo3/sysext/core/Classes/Category/CategoryRegistry.php index 31a9a86c55da0e56d28332d7390e2c9b68b4255f..34bd3c068a029c66cebe72848012644ea59dbdeb 100644 --- a/typo3/sysext/core/Classes/Category/CategoryRegistry.php +++ b/typo3/sysext/core/Classes/Category/CategoryRegistry.php @@ -14,6 +14,7 @@ namespace TYPO3\CMS\Core\Category; * The TYPO3 project - inspiring people to share! */ +use TYPO3\CMS\Backend\Form\Utility\DisplayConditionEvaluator; use TYPO3\CMS\Core\SingletonInterface; use TYPO3\CMS\Core\Utility\ArrayUtility; use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; @@ -130,27 +131,15 @@ class CategoryRegistry implements SingletonInterface } /** - * Returns a list of category fields for a given table for populating selector "category_field" - * in tt_content table (called as itemsProcFunc). + * Returns a list of category fields for the table configured in the categoryFieldsTable setting. + * For use in an itemsProcFunc of a TCA select field. * - * @param array $configuration Current field configuration - * @throws \UnexpectedValueException + * @param array $configuration The TCA and row arrays passed to the itemsProcFunc. * @return void */ - public function getCategoryFieldsForTable(array &$configuration) + public function getCategoryFieldItems(array &$configuration) { - $table = ''; - $menuType = isset($configuration['row']['menu_type'][0]) ? $configuration['row']['menu_type'][0] : ''; - // Define the table being looked up from the type of menu - if ($menuType === 'categorized_pages') { - $table = 'pages'; - } elseif ($menuType === 'categorized_content') { - $table = 'tt_content'; - } - // Return early if no table is defined - if (empty($table)) { - throw new \UnexpectedValueException('The given menu_type is not supported.', 1381823570); - } + $table = $this->getActiveCategoryFieldsTable($configuration); // Loop on all registries and find entries for the correct table foreach ($this->registry as $tableName => $fields) { if ($tableName === $table) { @@ -162,6 +151,113 @@ class CategoryRegistry implements SingletonInterface } } + /** + * Tries to determine of which table the category fields should be collected. + * It looks in the categoryFieldsTable TCA entry in the config section of the current field. + * + * It is possible to pass a plain string with a table name or an array of table names + * that can be activated with an active condition. There must exactly be one active + * table at once. A possible array configuration might look like this: + * + * 'categoryFieldsTable' => array( + * 'categorized_pages' => array( + * 'table' => 'pages', + * 'activeCondition' => 'FIELD:menu_type:=:categorized_pages' + * ), + * 'categorized_content' => array( + * 'table' => 'tt_content', + * 'activeCondition' => 'FIELD:menu_type:=:categorized_content' + * ) + * ), + * + * @param array $configuration The TCA and row arrays passed to the itemsProcFunc. + * @throws \RuntimeException In case of an invalid configuration. + * @return string + */ + protected function getActiveCategoryFieldsTable(array $configuration) + { + $fieldAndTableInfo = sprintf(' (field: %s, table: %s)', $configuration['field'], $configuration['table']); + + if (empty($configuration['config']['categoryFieldsTable'])) { + throw new \RuntimeException( + 'The categoryFieldsTable setting is missing in the config section' . $fieldAndTableInfo, + 1447273908 + ); + } + + if (is_string($configuration['config']['categoryFieldsTable'])) { + return $configuration['config']['categoryFieldsTable']; + } + + if (!is_array($configuration['config']['categoryFieldsTable'])) { + throw new \RuntimeException( + sprintf( + 'The categoryFieldsTable table setting must be a string or an array, %s given' . $fieldAndTableInfo, + gettype($configuration['config']['categoryFieldsTable']) + ), + 1447274126 + ); + } + + $activeTable = null; + + foreach ($configuration['config']['categoryFieldsTable'] as $configKey => $tableConfig) { + if (empty($tableConfig['table'])) { + throw new \RuntimeException( + sprintf( + 'The table setting is missing for the categoryFieldsTable %s' . $fieldAndTableInfo, + $configKey + ), + 1447274131 + ); + } + if (empty($tableConfig['activeCondition'])) { + throw new \RuntimeException( + sprintf( + 'The activeCondition setting is missing for the categoryFieldsTable %s' . $fieldAndTableInfo, + $configKey + ), + 1480786868 + ); + } + + if ($this->getDisplayConditionEvaluator()->evaluateDisplayCondition( + $tableConfig['activeCondition'], + $configuration['row'] + ) + ) { + if (!empty($activeTable)) { + throw new \RuntimeException( + sprintf( + 'There must only be one active categoryFieldsTable. Multiple active tables (%s, %s) ' + . 'were found' . $fieldAndTableInfo, + $activeTable, + $tableConfig['table'] + ), + 1480787321 + ); + } + $activeTable = $tableConfig['table']; + } + } + + if (empty($activeTable)) { + throw new \RuntimeException('No active was found' . $fieldAndTableInfo, 1447274507); + } + + return $activeTable; + } + + /** + * Returns the display condition evaluator utility class. + * + * @return DisplayConditionEvaluator + */ + protected function getDisplayConditionEvaluator() + { + return GeneralUtility::makeInstance(DisplayConditionEvaluator::class); + } + /** * Tells whether a table has a category configuration in the registry. * diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-53045-GetCategoryFieldsForTableMethodRemovedFromCategoryRegistry.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-53045-GetCategoryFieldsForTableMethodRemovedFromCategoryRegistry.rst new file mode 100644 index 0000000000000000000000000000000000000000..7a0ec3fc05f33d54a7cc17f9fd2fcee8325aa25a --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Breaking-53045-GetCategoryFieldsForTableMethodRemovedFromCategoryRegistry.rst @@ -0,0 +1,36 @@ +.. include:: ../../Includes.txt + +=================================================================================== +Breaking: #53045 - getCategoryFieldsForTable() method removed from CategoryRegistry +=================================================================================== + +See :issue:`53045` + +Description +=========== + +The method :php:`getCategoryFieldsForTable()` is removed from the :php:`\TYPO3\CMS\Core\Category\CategoryRegistry` +class. + +It could only handle the `tt_content` menus `categorized_pages` and `categorized_content`. + + +Impact +====== + +The method :php:`getCategoryFieldsForTable()` is removed. Any third party code that uses it will break. + + +Affected Installations +====================== + +All installations with third party code making using the removed method. + + +Migration +========= + +A new method :php:`getCategoryFieldItems()` is added that can be used by third party code for any +categorized table. + +.. index:: Backend, PHP-API, TCA diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-53045-CategorizedFieldsForTableItemsProcFuncInCategoryRegistry.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-53045-CategorizedFieldsForTableItemsProcFuncInCategoryRegistry.rst new file mode 100644 index 0000000000000000000000000000000000000000..0ec0cc3df13963b5bf0eed02643b27693b17ba99 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-53045-CategorizedFieldsForTableItemsProcFuncInCategoryRegistry.rst @@ -0,0 +1,58 @@ +.. include:: ../../Includes.txt + +================================================================================ +Feature: #53045 - Categorized fields for table itemsProcFunc in CategoryRegistry +================================================================================ + +See :issue:`53045` + +Description +=========== + +A new method :php:`getCategoryFieldItems()` is added to the :php:`\TYPO3\CMS\Core\Category\CategoryRegistry` class. + +This method can be used as an `itemsProcFunc` in TCA and returns a list of all categorized fields of a table. + +The table for which the categorized fields should be returned can be specified in two ways. + +Static table +------------ + +You can provide a static table name in the config of your TCA field: + +.. code-block:: php + + 'itemsProcFunc' => \TYPO3\CMS\Core\Category\CategoryRegistry::class . '->getCategoryFieldItems', + 'categoryFieldsTable' => 'my_table_name', + + +Dynamic table selection +----------------------- + +You can also provide a list of tables. The active table can be selected by using a display condition: + +.. code-block:: php + + 'itemsProcFunc' => \TYPO3\CMS\Core\Category\CategoryRegistry::class . '->getCategoryFieldItems', + 'categoryFieldsTable' => [ + 'categorized_pages' => [ + 'table' => 'pages', + 'activeCondition' => 'FIELD:menu_type:=:categorized_pages' + ], + 'categorized_content' => [ + 'table' => 'tt_content', + 'activeCondition' => 'FIELD:menu_type:=:categorized_content' + ] + ] + + +Impact +====== + +The method :php:`getCategoryFieldsForTable()` is removed. It could only handle the `tt_content` menus +`categorized_pages` and `categorized_content`. + +A new method :php:`getCategoryFieldItems()` is added that can be used by third party code for any +categorized table. + +.. index:: Backend, PHP-API, TCA diff --git a/typo3/sysext/core/Tests/Unit/Category/CategoryRegistryTest.php b/typo3/sysext/core/Tests/Unit/Category/CategoryRegistryTest.php index b5b68e9de0eb53b7f05345b433077f560634190f..bc9aeea723ea5363c1105d08318300ae2af98e19 100644 --- a/typo3/sysext/core/Tests/Unit/Category/CategoryRegistryTest.php +++ b/typo3/sysext/core/Tests/Unit/Category/CategoryRegistryTest.php @@ -13,6 +13,8 @@ namespace TYPO3\CMS\Core\Tests\Unit\Category; * * The TYPO3 project - inspiring people to share! */ +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Lang\LanguageService; /** * Testcase for CategoryRegistry @@ -332,4 +334,154 @@ class CategoryRegistryTest extends \TYPO3\CMS\Core\Tests\UnitTestCase $sqlData = $this->subject->addExtensionCategoryDatabaseSchemaToTablesDefinition([], 'text_extension_a'); $this->assertEmpty($sqlData['sqlString'][0]); } + + /** + * @test + */ + public function getCategoryFieldItemsReturnsFieldsForStaticTable() + { + $GLOBALS['LANG'] = GeneralUtility::makeInstance(LanguageService::class); + $this->subject->add('text_extension_a', $this->tables['first']); + $this->subject->add('text_extension_a', $this->tables['first'], 'categories2'); + $configuration = [ + 'config' => [ + 'categoryFieldsTable' => $this->tables['first'] + ] + ]; + $this->subject->getCategoryFieldItems($configuration); + $this->assertEquals([['Categories', 'categories'], ['Categories', 'categories2']], $configuration['items']); + } + + /** + * @test + */ + public function getCategoryFieldItemsReturnsFieldsForDynamicTables() + { + $GLOBALS['LANG'] = GeneralUtility::makeInstance(LanguageService::class); + $this->subject->add('text_extension_a', $this->tables['first']); + $this->subject->add('text_extension_a', $this->tables['first'], 'categories2'); + $configuration = [ + 'row' => [ + 'menu_type' => 'categorized_pages', + ], + 'config' => [ + 'categoryFieldsTable' => [ + [ + 'table' => 'othertable', + 'activeCondition' => 'FIELD:menu_type:=:categorized_content', + ], + [ + 'table' => $this->tables['first'], + 'activeCondition' => 'FIELD:menu_type:=:categorized_pages', + ], + ], + ], + ]; + $this->subject->getCategoryFieldItems($configuration); + $this->assertEquals([['Categories', 'categories'], ['Categories', 'categories2']], $configuration['items']); + } + + /** + * @test + */ + public function getCategoryFieldItemsThrowsExceptionIfConfigMissing() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1447273908); + $configuration = []; + $this->subject->getCategoryFieldItems($configuration); + } + + /** + * @test + */ + public function getCategoryFieldItemsThrowsExceptionIfTypeIsInvalid() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1447274126); + $configuration = [ + 'config' => [ + 'categoryFieldsTable' => new \stdClass() + ] + ]; + $this->subject->getCategoryFieldItems($configuration); + } + + /** + * @test + */ + public function getCategoryFieldItemsThrowsExceptionIfTableIsMissing() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1447274131); + $configuration = [ + 'config' => [ + 'categoryFieldsTable' => [['activeCondition' => 'TRUE']] + ] + ]; + $this->subject->getCategoryFieldItems($configuration); + } + + /** + * @test + */ + public function getCategoryFieldItemsThrowsExceptionIfActiveConditionIsMissing() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1480786868); + $configuration = [ + 'config' => [ + 'categoryFieldsTable' => [['table' => 'testtable']] + ] + ]; + $this->subject->getCategoryFieldItems($configuration); + } + + /** + * @test + */ + public function getCategoryFieldItemsThrowsExceptionIfNoActiveTableIsFound() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1447274507); + $configuration = [ + 'row' => [], + 'config' => [ + 'categoryFieldsTable' => [ + [ + 'table' => 'testtable', + 'activeCondition' => 'FALSE', + ] + ], + ] + ]; + $this->subject->getCategoryFieldItems($configuration); + } + + /** + * @test + */ + public function getCategoryFieldItemsThrowsExceptionIfMultipleTablesAreActive() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1480787321); + $configuration = [ + 'row' => [ + 'menu_type' => 'categorized_pages', + ], + 'config' => [ + 'categoryFieldsTable' => [ + [ + 'table' => 'testtable', + 'activeCondition' => 'FIELD:menu_type:=:categorized_pages' + ], + [ + 'table' => 'testtable2', + 'activeCondition' => 'FIELD:menu_type:=:categorized_pages' + ] + ], + ] + ]; + $this->subject->getCategoryFieldItems($configuration); + } } diff --git a/typo3/sysext/frontend/Configuration/TCA/tt_content.php b/typo3/sysext/frontend/Configuration/TCA/tt_content.php index ba72c9e3c6bda3bec113434ae8e3d6b718a83b9d..9f79707ef03fdc33c45c4455334357f860f8fd10 100644 --- a/typo3/sysext/frontend/Configuration/TCA/tt_content.php +++ b/typo3/sysext/frontend/Configuration/TCA/tt_content.php @@ -1034,7 +1034,17 @@ return [ 'size' => 1, 'minitems' => 0, 'maxitems' => 1, - 'itemsProcFunc' => \TYPO3\CMS\Core\Category\CategoryRegistry::class . '->getCategoryFieldsForTable', + 'itemsProcFunc' => \TYPO3\CMS\Core\Category\CategoryRegistry::class . '->getCategoryFieldItems', + 'categoryFieldsTable' => [ + 'categorized_pages' => [ + 'table' => 'pages', + 'activeCondition' => 'FIELD:menu_type:=:categorized_pages' + ], + 'categorized_content' => [ + 'table' => 'tt_content', + 'activeCondition' => 'FIELD:menu_type:=:categorized_content' + ] + ] ] ], 'table_caption' => [