loadModules = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Module\ModuleLoader::class); * $this->loadModules->load($TBE_MODULES); * @internal */ class ModuleLoader { /** * After the init() function this array will contain the structure of available modules for the backend user. * * @var array */ public $modules = []; /** * This array will hold the elements that should go into the select-list of modules for groups... * * @var array */ public $modListGroup = []; /** * This array will hold the elements that should go into the select-list of modules for users... * * @var array */ public $modListUser = []; /** * The backend user for use internally * * @var \TYPO3\CMS\Core\Authentication\BackendUserAuthentication */ public $BE_USER; /** * If set TRUE, workspace "permissions" will be observed so non-allowed modules will not be included in the array of modules. * * @var bool */ public $observeWorkspaces = false; /** * Contains the registered navigation components * * @var array */ protected $navigationComponents = []; /** * Labels for the modules * @var array */ protected $moduleLabels = []; /** * Init. * The outcome of the load() function will be a $this->modules array populated with the backend module structure available to the BE_USER * Further the global var $LANG will have labels and images for the modules loaded in an internal array. * * @param array $modulesArray Should be the global var $TBE_MODULES, $BE_USER can optionally be set to an alternative Backend user object than the global var $BE_USER (which is the currently logged in user) * @param BackendUserAuthentication $beUser Optional backend user object to use. If not set, the global BE_USER object is used. */ public function load($modulesArray, BackendUserAuthentication $beUser = null) { // Setting the backend user for use internally $this->BE_USER = $beUser ?: $GLOBALS['BE_USER']; // Unset the array for calling backend modules based on external backend module dispatchers in typo3/index.php unset($modulesArray['_configuration']); $this->navigationComponents = $modulesArray['_navigationComponents']; unset($modulesArray['_navigationComponents']); $mainModules = $this->parseModulesArray($modulesArray); // Traverses the module setup and creates the internal array $this->modules foreach ($mainModules as $mainModuleName => $subModules) { $mainModuleConfiguration = $this->checkMod($mainModuleName); // If $mainModuleConfiguration is not set (FALSE) there is no access to the module !(?) if (is_array($mainModuleConfiguration)) { $this->modules[$mainModuleName] = $mainModuleConfiguration; // Load the submodules if (is_array($subModules)) { foreach ($subModules as $subModuleName) { $subModuleConfiguration = $this->checkMod($mainModuleName . '_' . $subModuleName); if (is_array($subModuleConfiguration)) { $this->modules[$mainModuleName]['sub'][$subModuleName] = $subModuleConfiguration; } } } } elseif ($mainModuleConfiguration !== false) { // Although the configuration was not found, still check if there are submodules // This must be done in order to fill out the select-lists for modules correctly!! if (is_array($subModules)) { foreach ($subModules as $subModuleName) { $this->checkMod($mainModuleName . '_' . $subModuleName); } } } } } /** * Here we check for the module. * * Return values: * 'notFound': If the module was not found in the path (no "conf.php" file) * FALSE: If no access to the module (access check failed) * array(): Configuration array, in case a valid module where access IS granted exists. * * @param string $name Module name * @return string|bool|array See description of function */ public function checkMod($name) { // Check for own way of configuring module if (is_array($GLOBALS['TBE_MODULES']['_configuration'][$name]['configureModuleFunction'] ?? false)) { trigger_error('Registering a module using "configureModuleFunction" will be removed in TYPO3 v10.0.', E_USER_DEPRECATED); $obj = $GLOBALS['TBE_MODULES']['_configuration'][$name]['configureModuleFunction']; if (is_callable($obj)) { $MCONF = call_user_func($obj, $name); if ($this->checkModAccess($name, $MCONF) !== true) { return false; } $this->addLabelsForModule($name, $MCONF['labels']); return $MCONF; } } // merge configuration and labels into one array $setupInformation = $this->getModuleSetupInformation($name); // clean up the configuration part if (empty($setupInformation['configuration'])) { return 'notFound'; } $finalModuleConfiguration = $setupInformation['configuration']; if (!empty($finalModuleConfiguration['shy']) || !$this->checkModAccess($name, $setupInformation['configuration']) || !$this->checkModWorkspace($name, $setupInformation['configuration']) ) { return false; } $finalModuleConfiguration['name'] = $name; // Language processing. This will add module labels and image reference to the internal ->moduleLabels array of the LANG object. $this->addLabelsForModule($name, $finalModuleConfiguration['labels'] ?? $setupInformation['labels']); /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */ $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class); if (isset($setupInformation['configuration']['routeTarget'])) { $finalModuleConfiguration['script'] = (string)$uriBuilder->buildUriFromRoute($name); } else { $finalModuleConfiguration['script'] = (string)$uriBuilder->buildUriFromRoute('dummy'); } if (!empty($setupInformation['configuration']['navigationFrameModule'])) { $finalModuleConfiguration['navFrameScript'] = (string)$uriBuilder->buildUriFromRoute( $setupInformation['configuration']['navigationFrameModule'], !empty($setupInformation['configuration']['navigationFrameModuleParameters']) ? $setupInformation['configuration']['navigationFrameModuleParameters'] : [] ); } // Check if this is a submodule $mainModule = ''; if (strpos($name, '_') !== false) { list($mainModule, ) = explode('_', $name, 2); } // check if there is a navigation component (like the pagetree) if (is_array($this->navigationComponents[$name] ?? false)) { $finalModuleConfiguration['navigationComponentId'] = $this->navigationComponents[$name]['componentId']; } elseif ($mainModule && is_array($this->navigationComponents[$mainModule] ?? false) && $setupInformation['configuration']['inheritNavigationComponentFromMainModule'] !== false) { // navigation component can be overridden by the main module component $finalModuleConfiguration['navigationComponentId'] = $this->navigationComponents[$mainModule]['componentId']; } return $finalModuleConfiguration; } /** * fetches the conf.php file of a certain module, and also merges that with * some additional configuration * * @param string $moduleName the combined name of the module, can be "web", "web_info", or "tools_log" * @return array an array with subarrays, named "configuration" (aka $MCONF), "labels" (previously known as $MLANG) and the stripped path */ protected function getModuleSetupInformation($moduleName) { $moduleSetupInformation = [ 'configuration' => [], 'labels' => [] ]; $moduleConfiguration = !empty($GLOBALS['TBE_MODULES']['_configuration'][$moduleName]) ? $GLOBALS['TBE_MODULES']['_configuration'][$moduleName] : null; if ($moduleConfiguration !== null) { // Overlay setup with additional labels if (!empty($moduleConfiguration['labels']) && is_array($moduleConfiguration['labels'])) { if (empty($moduleSetupInformation['labels']['default']) || !is_array($moduleSetupInformation['labels']['default'])) { $moduleSetupInformation['labels']['default'] = $moduleConfiguration['labels']; } else { $moduleSetupInformation['labels']['default'] = array_replace_recursive($moduleSetupInformation['labels']['default'], $moduleConfiguration['labels']); } unset($moduleConfiguration['labels']); } // Overlay setup with additional configuration if (is_array($moduleConfiguration)) { $moduleSetupInformation['configuration'] = array_replace_recursive($moduleSetupInformation['configuration'], $moduleConfiguration); } } // Add some default configuration if (!isset($moduleSetupInformation['configuration']['inheritNavigationComponentFromMainModule'])) { $moduleSetupInformation['configuration']['inheritNavigationComponentFromMainModule'] = true; } return $moduleSetupInformation; } /** * Returns TRUE if the internal BE_USER has access to the module $name with $MCONF (based on security level set for that module) * * @param string $name Module name * @param array $MCONF MCONF array (module configuration array) from the modules conf.php file (contains settings about what access level the module has) * @return bool TRUE if access is granted for $this->BE_USER */ public function checkModAccess($name, $MCONF) { if (empty($MCONF['access'])) { return true; } $access = strtolower($MCONF['access']); // Check if this module is only allowed by system maintainers (= admins who are in the list of system maintainers) if (strpos($MCONF['access'], BackendUserAuthentication::ROLE_SYSTEMMAINTAINER) !== false) { return $this->BE_USER->isSystemMaintainer(); } // Checking if admin-access is required // If admin-permissions is required then return TRUE if user is admin if (strpos($access, 'admin') !== false && $this->BE_USER->isAdmin()) { return true; } // This will add modules to the select-lists of user and groups if (strpos($access, 'user') !== false) { $this->modListUser[] = $name; } if (strpos($access, 'group') !== false) { $this->modListGroup[] = $name; } // This checks if a user is permitted to access the module if ($this->BE_USER->isAdmin() || $this->BE_USER->check('modules', $name)) { return true; } return false; } /** * Check if a module is allowed inside the current workspace for be user * Processing happens only if $this->observeWorkspaces is TRUE * * @param string $name Module name (unused) * @param array $MCONF MCONF array (module configuration array) from the modules conf.php file (contains settings about workspace restrictions) * @return bool TRUE if access is granted for $this->BE_USER */ public function checkModWorkspace($name, $MCONF) { if (!$this->observeWorkspaces) { return true; } $status = true; if (!empty($MCONF['workspaces'])) { $status = $this->BE_USER->workspace === 0 && GeneralUtility::inList($MCONF['workspaces'], 'online') || $this->BE_USER->workspace === -1 && GeneralUtility::inList($MCONF['workspaces'], 'offline') || $this->BE_USER->workspace > 0 && GeneralUtility::inList($MCONF['workspaces'], 'custom'); } elseif ($this->BE_USER->workspace === -99) { $status = false; } return $status; } /** * Parses the moduleArray ($TBE_MODULES) into an internally useful structure. * Returns an array where the keys are names of the module and the values may be TRUE (only module) or an array (of submodules) * * @param array $arr ModuleArray ($TBE_MODULES) * @return array Output structure with available modules */ public function parseModulesArray($arr) { $theMods = []; if (is_array($arr)) { foreach ($arr as $mod => $subs) { // Clean module name to alphanum $mod = $this->cleanName($mod); if ($mod) { if ($subs) { $subsArr = GeneralUtility::trimExplode(',', $subs); foreach ($subsArr as $subMod) { $subMod = $this->cleanName($subMod); if ($subMod) { $theMods[$mod][] = $subMod; } } } else { $theMods[$mod] = 1; } } } } return $theMods; } /** * The $str is cleaned so that it contains alphanumerical characters only. * Module names must only consist of these characters * * @param string $str String to clean up * @return string */ public function cleanName($str) { return preg_replace('/[^a-z0-9]/i', '', $str); } /** * Registers labels for a module in a unified way. * * Legacy info: This was previously named * - labels->tablabel (now called "shortdescription") * - labels->tabdescr (now called "description") * - tabs->tab (now called "title") * * The LLL information is stored, not the actual translated string. * * @param string $moduleName the name of the module * @param string|array $labels the information about the three labels */ public function addLabelsForModule($moduleName, $labels) { // If LOCAL_LANG references are used for labels of the module: if (is_string($labels)) { // Extbase-based modules $this->moduleLabels[$moduleName] = [ 'shortdescription' => $labels . ':mlang_labels_tablabel', 'description' => $labels . ':mlang_labels_tabdescr', 'title' => $labels . ':mlang_tabs_tab', ]; } elseif (isset($labels['title'])) { // New way, where all labels can be LLL references $this->moduleLabels[$moduleName] = $labels; } elseif (isset($labels['ll_ref'])) { // Classic, non-extbase module labels $this->addLabelsForModule($moduleName, $labels['ll_ref']); } else { // Very old obsolete approach, don't use anymore, use one of the ways above. if (is_object($this->getLanguageService())) { $language = $this->getLanguageService()->lang; } else { $language = 'default'; } if (isset($labels[$language]['ll_ref'])) { $this->addLabelsForModule($moduleName, $labels[$language]['ll_ref']); } elseif (isset($labels['default']['ll_ref'])) { $this->addLabelsForModule($moduleName, $labels['default']['ll_ref']); } else { $this->moduleLabels[$moduleName] = [ 'shortdescription' => $labels[$language]['labels']['tablabel'] ?? $labels['default']['labels']['tablabel'], 'description' => $labels[$language]['labels']['tabdescr'] ?? $labels['default']['labels']['tabdescr'], 'title' => $labels[$language]['tabs']['tab'] ?? $labels['default']['tabs']['tab'], ]; } } } /** * Returns the labels for the given module * * @param string $moduleName * @return array */ public function getLabelsForModule($moduleName) { return $this->moduleLabels[$moduleName] ?? []; } /** * @return LanguageService */ protected function getLanguageService() { return $GLOBALS['LANG']; } }