Files
cms-backend/Classes/Module/ModuleProvider.php
T

330 lines
13 KiB
PHP

<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Backend\Module;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This is the central point to retrieve modules from the ModuleRegistry, while
* performing the necessary access checks, which ModuleRegistry does not deal with.
*/
readonly class ModuleProvider
{
public function __construct(
protected ModuleRegistry $moduleRegistry,
protected ModuleAccessGateRegistry $gateRegistry,
) {}
/**
* Simple wrapper for the registry, which just checks if a
* module is registered. Does NOT perform any access checks.
*/
public function isModuleRegistered(string $identifier): bool
{
return $this->moduleRegistry->hasModule($identifier);
}
/**
* Returns a Module for the given identifier. In case a user is given, also access checks are performed.
*/
public function getModule(
string $identifier,
?BackendUserAuthentication $user = null,
bool $respectWorkspaceRestrictions = true
): ?ModuleInterface {
if (($user === null && $this->moduleRegistry->hasModule($identifier))
|| $this->accessGranted($identifier, $user, $respectWorkspaceRestrictions)
) {
$module = $this->moduleRegistry->getModule($identifier);
if ($user !== null) {
$this->filterInaccessibleSubModules($module, $user, $respectWorkspaceRestrictions);
}
return $module;
}
return null;
}
/**
* Returns all modules either grouped by main modules or flat.
* In case a user is given, also access checks are performed.
*
* @return ModuleInterface[]
*/
public function getModules(
?BackendUserAuthentication $user = null,
bool $respectWorkspaceRestrictions = true,
bool $grouped = true
): array {
if (!$grouped) {
return array_filter(
$this->moduleRegistry->getModules(),
fn(ModuleInterface $module): bool => $user === null || $this->accessGranted($module->getIdentifier(), $user, $respectWorkspaceRestrictions)
);
}
$availableModules = array_filter($this->moduleRegistry->getModules(), static fn(ModuleInterface $module): bool => !$module->hasParentModule());
foreach ($availableModules as $identifier => $module) {
if ($user !== null && !$this->accessGranted($identifier, $user, $respectWorkspaceRestrictions)) {
unset($availableModules[$identifier]);
continue;
}
if ($user !== null) {
$this->filterInaccessibleSubModules($module, $user, $respectWorkspaceRestrictions);
}
}
return $availableModules;
}
/**
* Return the requested (main) module if exist and allowed, prepared
* for menu generation or similar structured output (nested). Takes
* TSConfig into account. Does not respect "appearance[renderInModuleMenu]".
*/
public function getModuleForMenu(
string $identifier,
BackendUserAuthentication $user,
bool $respectWorkspaceRestrictions = true
): ?MenuModule {
$module = $this->getModule($identifier, $user, $respectWorkspaceRestrictions);
if ($module === null) {
return null;
}
// Before preparing the module for the menu, check if it is defined ad hidden in TSconfig
$hideModules = GeneralUtility::trimExplode(',', $user->getTSConfig()['options.']['hideModules'] ?? '', true);
if (in_array($identifier, $hideModules, true)) {
return null;
}
$menuItem = new MenuModule(clone $module);
if ($menuItem->isStandalone()) {
return $menuItem;
}
$this->buildMenuModuleRecursively($menuItem, $module, $hideModules, $user, $respectWorkspaceRestrictions);
if (!$menuItem->hasSubModules()) {
// In case the main module does not have any submodules, unset it again
return null;
}
return $menuItem;
}
/**
* Returns all allowed modules for the current user, prepared
* for module menu generation or similar structured output (nested).
* Takes TSConfig and "appearance[renderInModuleMenu]" into account.
*
* @return MenuModule[]
*/
public function getModulesForModuleMenu(
BackendUserAuthentication $user,
bool $respectWorkspaceRestrictions = true
): array {
$moduleMenuItems = [];
$moduleMenuState = $this->getModuleMenuState($user);
// Before preparing the modules for the menu, check if we need to hide some of them (defined in TSconfig)
$hideModules = GeneralUtility::trimExplode(',', $user->getTSConfig()['options.']['hideModules'] ?? '', true);
foreach ($this->getModules($user, $respectWorkspaceRestrictions) as $identifier => $module) {
if (in_array($identifier, $hideModules, true)
|| !($module->getAppearance()['renderInModuleMenu'] ?? true)
) {
continue;
}
// Only use main modules for this first level
if ($module->hasParentModule()) {
continue;
}
$menuItem = new MenuModule(clone $module, isset($moduleMenuState[$identifier]));
$moduleMenuItems[$identifier] = $menuItem;
if ($menuItem->isStandalone()) {
continue;
}
$this->buildMenuModuleRecursively($menuItem, $module, $hideModules, $user, $respectWorkspaceRestrictions, true);
if (!$menuItem->hasSubModules()) {
// In case the main module does not have any submodules, unset it again
unset($moduleMenuItems[$identifier]);
}
}
return $moduleMenuItems;
}
/**
* Check access of a module for a given user
*/
public function accessGranted(
string $identifier,
BackendUserAuthentication $user,
bool $respectWorkspaceRestrictions = true
): bool {
if (!$this->moduleRegistry->hasModule($identifier)) {
return false;
}
$module = $this->moduleRegistry->getModule($identifier);
if ($respectWorkspaceRestrictions && ExtensionManagementUtility::isLoaded('workspaces')) {
$workspaceAccess = $module->getWorkspaceAccess();
if ($workspaceAccess === '' && $module->hasParentModule()) {
// In case workspace access is not set explicitly use the information
// from the parent module as this access restriction is inherited.
$workspaceAccess = $module->getParentModule()->getWorkspaceAccess();
}
if ($workspaceAccess !== '' && $workspaceAccess !== '*') {
if (($workspaceAccess === 'live' && $user->workspace !== 0)
|| ($workspaceAccess === 'offline' && $user->workspace === 0)
) {
return false;
}
} elseif ($user->workspace === -99 && $workspaceAccess !== '*') {
return false;
}
}
$moduleAccess = $module->getAccess();
if ($moduleAccess === '') {
// Early return since this module does not have any access permissions set
return true;
}
if ($this->gateRegistry->has($moduleAccess)) {
return $this->gateRegistry->get($moduleAccess)->decide($module, $user) === ModuleAccessResult::Granted;
}
return false;
}
/**
* Returns the first module, accessible for the given user.
* This will only return submodules or standalone modules.
*
* @internal not part of TYPO3's API. Only for use in TYPO3 Core.
*/
public function getFirstAccessibleModule(BackendUserAuthentication $user): ?ModuleInterface
{
$modules = array_filter($this->moduleRegistry->getModules(), function (ModuleInterface $module) use ($user): bool {
return $this->accessGranted($module->getIdentifier(), $user)
&& ($module->isStandalone() || $module->hasParentModule());
});
return reset($modules) ?: null;
}
/**
* Get all modules with access=user, to be selected in the user/group records
*
* @return ModuleInterface[]
* @internal not part of TYPO3's API. Only for use in TYPO3 Core.
*/
public function getUserModules(): array
{
return array_filter($this->moduleRegistry->getModules(), static fn(ModuleInterface $module): bool => $module->getAccess() === 'user');
}
/**
* Recursively removes inaccessible submodules from a module at any depth
*/
protected function filterInaccessibleSubModules(
ModuleInterface $module,
BackendUserAuthentication $user,
bool $respectWorkspaceRestrictions
): void {
if (!$module->hasSubModules()) {
return;
}
foreach ($module->getSubModules() as $subModuleIdentifier => $subModule) {
if (!$this->accessGranted($subModuleIdentifier, $user, $respectWorkspaceRestrictions)) {
$module->removeSubModule($subModuleIdentifier);
} else {
$this->filterInaccessibleSubModules($subModule, $user, $respectWorkspaceRestrictions);
}
}
}
/**
* Recursively builds menu module structure, checking access, TSConfig hideModules,
* and optionally renderInModuleMenu appearance setting at all nesting levels
*/
protected function buildMenuModuleRecursively(
MenuModule $menuItem,
ModuleInterface $module,
array $hideModules,
BackendUserAuthentication $user,
bool $respectWorkspaceRestrictions,
bool $checkRenderInModuleMenu = false
): void {
$moduleMenuState = $this->getModuleMenuState($user);
foreach ($module->getSubModules() as $subModuleIdentifier => $subModule) {
if (in_array($subModuleIdentifier, $hideModules, true)
|| ($checkRenderInModuleMenu && !($subModule->getAppearance()['renderInModuleMenu'] ?? true))
) {
continue;
}
// Skip submodules that depend on their own submodules if they don't have any accessible ones
if (($subModule->getAppearance()['dependsOnSubmodules'] ?? false)
&& !$this->hasAccessibleSubModules($subModule, $hideModules, $user, $respectWorkspaceRestrictions, $checkRenderInModuleMenu)
) {
continue;
}
$subMenuItem = new MenuModule(clone $subModule, isset($moduleMenuState[$subModuleIdentifier]));
$menuItem->addSubModule($subMenuItem);
// Recursively build deeper levels
if ($subModule->hasSubModules()) {
$this->buildMenuModuleRecursively($subMenuItem, $subModule, $hideModules, $user, $respectWorkspaceRestrictions, $checkRenderInModuleMenu);
}
}
}
/**
* Check if a module has at least one accessible submodule at any depth
*/
protected function hasAccessibleSubModules(
ModuleInterface $module,
array $hideModules,
BackendUserAuthentication $user,
bool $respectWorkspaceRestrictions,
bool $checkRenderInModuleMenu = false
): bool {
foreach ($module->getSubModules() as $subModuleIdentifier => $subModule) {
if (in_array($subModuleIdentifier, $hideModules, true)
|| ($checkRenderInModuleMenu && !($subModule->getAppearance()['renderInModuleMenu'] ?? true))
|| !$this->accessGranted($subModuleIdentifier, $user, $respectWorkspaceRestrictions)
) {
continue;
}
// If this submodule is accessible, return true
if (!($subModule->getAppearance()['dependsOnSubmodules'] ?? false)) {
return true;
}
// If it depends on submodules, check recursively
if ($this->hasAccessibleSubModules($subModule, $hideModules, $user, $respectWorkspaceRestrictions, $checkRenderInModuleMenu)) {
return true;
}
}
return false;
}
protected function getModuleMenuState(BackendUserAuthentication $user): array
{
return json_decode($user->uc['modulemenu'] ?? '{}', true);
}
}