TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:00 +02:00
commit f9941541b7
1178 changed files with 135377 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
<?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\AccessGate;
use TYPO3\CMS\Backend\Module\ModuleAccessGateInterface;
use TYPO3\CMS\Backend\Module\ModuleAccessResult;
use TYPO3\CMS\Backend\Module\ModuleInterface;
use TYPO3\CMS\Core\Attribute\AsModuleAccessGate;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
/**
* Grants access only to admin users.
*/
#[AsModuleAccessGate(identifier: 'admin')]
final readonly class AdminGate implements ModuleAccessGateInterface
{
public function decide(ModuleInterface $module, BackendUserAuthentication $user): ModuleAccessResult
{
if ($module->getAccess() !== 'admin') {
return ModuleAccessResult::Abstain;
}
return $user->isAdmin() ? ModuleAccessResult::Granted : ModuleAccessResult::Denied;
}
}
@@ -0,0 +1,40 @@
<?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\AccessGate;
use TYPO3\CMS\Backend\Module\ModuleAccessGateInterface;
use TYPO3\CMS\Backend\Module\ModuleAccessResult;
use TYPO3\CMS\Backend\Module\ModuleInterface;
use TYPO3\CMS\Core\Attribute\AsModuleAccessGate;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
/**
* Grants access only to system maintainers (admins listed in
* $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers']).
*/
#[AsModuleAccessGate(identifier: 'systemMaintainer')]
final readonly class SystemMaintainerGate implements ModuleAccessGateInterface
{
public function decide(ModuleInterface $module, BackendUserAuthentication $user): ModuleAccessResult
{
if ($module->getAccess() !== BackendUserAuthentication::ROLE_SYSTEMMAINTAINER) {
return ModuleAccessResult::Abstain;
}
return $user->isSystemMaintainer() ? ModuleAccessResult::Granted : ModuleAccessResult::Denied;
}
}
+56
View File
@@ -0,0 +1,56 @@
<?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\AccessGate;
use TYPO3\CMS\Backend\Module\ModuleAccessGateInterface;
use TYPO3\CMS\Backend\Module\ModuleAccessResult;
use TYPO3\CMS\Backend\Module\ModuleInterface;
use TYPO3\CMS\Backend\Module\ModuleRegistry;
use TYPO3\CMS\Core\Attribute\AsModuleAccessGate;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
/**
* Grants access based on explicit module permissions in
* be_users.userMods / be_groups.groupMods. Admin users
* are always granted access.
*/
#[AsModuleAccessGate(identifier: 'user')]
final readonly class UserGate implements ModuleAccessGateInterface
{
public function __construct(
private ModuleRegistry $moduleRegistry,
) {}
public function decide(ModuleInterface $module, BackendUserAuthentication $user): ModuleAccessResult
{
if ($module->getAccess() !== 'user') {
return ModuleAccessResult::Abstain;
}
if ($user->isAdmin()) {
return ModuleAccessResult::Granted;
}
if ($user->check('modules', $module->getIdentifier())) {
return ModuleAccessResult::Granted;
}
$alias = array_search($module->getIdentifier(), $this->moduleRegistry->getModuleAliases(), true);
if ($alias !== false && $user->check('modules', $alias)) {
return ModuleAccessResult::Granted;
}
return ModuleAccessResult::Denied;
}
}
+304
View File
@@ -0,0 +1,304 @@
<?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;
/**
* The basic backend module, to be extended by more detailed implementations (e.g. ExtbaseModule)
*
* @internal
*/
abstract class BaseModule
{
protected string $identifier;
protected string $packageName = '';
protected string $absolutePackagePath = '';
protected string $path = '';
protected string $iconIdentifier = '';
protected string $title = '';
protected string $description = '';
protected string $shortDescription = '';
protected array $position = [];
protected array $appearance = [];
protected string $access = '';
protected string $workspaceAccess = '';
protected string $parent = '';
protected ?ModuleInterface $parentModule = null;
/** @var array<string, ModuleInterface> */
protected array $subModules = [];
protected bool $standalone = false;
protected string $component = '@typo3/backend/module/iframe';
protected string $navigationComponent = '';
protected array $defaultModuleData = [];
protected array $aliases = [];
protected bool $inheritNavigationComponent = true;
protected array $routeOptions = [];
protected bool $showSubmoduleOverview = false;
final protected function __construct(string $identifier)
{
$this->identifier = $identifier;
}
public function getIdentifier(): string
{
return $this->identifier;
}
public function getPath(): string
{
return $this->path;
}
public function getIconIdentifier(): string
{
if ($this->iconIdentifier === '' && $this->hasParentModule()) {
return $this->getParentModule()->getIconIdentifier();
}
return $this->iconIdentifier;
}
public function getTitle(): string
{
return $this->title;
}
public function getDescription(): string
{
return $this->description;
}
public function getShortDescription(): string
{
return $this->shortDescription;
}
public function isStandalone(): bool
{
return $this->standalone;
}
public function getNavigationComponent(): string
{
if ($this->inheritNavigationComponent && $this->hasParentModule()) {
// Use parent navigation component if inheritance is enabled.
// Fallback if parent does not define a navigation component.
return $this->getParentModule()->getNavigationComponent() ?: $this->navigationComponent;
}
return $this->navigationComponent;
}
public function getComponent(): string
{
return $this->component;
}
public function getPosition(): array
{
return $this->position;
}
public function getAccess(): string
{
return $this->access;
}
public function getWorkspaceAccess(): string
{
return $this->workspaceAccess;
}
public function getParentIdentifier(): string
{
return $this->parent;
}
public function setParentModule(ModuleInterface $module): void
{
$this->parentModule = $module;
}
public function getParentModule(): ?ModuleInterface
{
return $this->parentModule;
}
public function hasParentModule(): bool
{
return $this->parentModule !== null;
}
public function addSubModule(ModuleInterface $module): void
{
$this->subModules[$module->getIdentifier()] = $module;
}
public function hasSubModule(string $identifier): bool
{
return isset($this->subModules[$identifier]);
}
public function hasSubModules(): bool
{
return $this->subModules !== [];
}
public function getSubModule(string $identifier): ?ModuleInterface
{
return $this->subModules[$identifier] ?? null;
}
public function removeSubModule(string $identifier): void
{
unset($this->subModules[$identifier]);
}
/**
* @return array<string, ModuleInterface>
*/
public function getSubModules(): array
{
return $this->subModules;
}
public function getAppearance(): array
{
return $this->appearance;
}
public function getAliases(): array
{
return $this->aliases;
}
public function hasSubmoduleOverview(): bool
{
return $this->showSubmoduleOverview;
}
abstract public function getDefaultRouteOptions(): array;
public function getDefaultModuleData(): array
{
return $this->defaultModuleData;
}
/**
* Promotes this module to a standalone top-level module, inheriting properties from its parent.
*
* @internal Only to be used by ModuleRegistry
*/
public function promoteToStandalone(string $navigationComponent, array $position, array $additionalAliases): void
{
$this->standalone = true;
$this->parent = '';
$this->parentModule = null;
if ($this->getNavigationComponent() === '') {
$this->navigationComponent = $navigationComponent;
}
if ($this->position === []) {
$this->position = $position;
}
$this->aliases = array_merge($this->aliases, $additionalAliases);
}
public static function createFromConfiguration(string $identifier, array $configuration): static
{
$obj = new static($identifier);
$obj->packageName = (string)($configuration['packageName'] ?? '');
$obj->absolutePackagePath = (string)($configuration['absolutePackagePath'] ?? '');
$obj->path = '/' . ltrim((string)$configuration['path'], '/');
$obj->standalone = (bool)($configuration['standalone'] ?? false);
if ($configuration['parent'] ?? false) {
$obj->parent = (string)$configuration['parent'];
}
if ($configuration['iconIdentifier'] ?? false) {
$obj->iconIdentifier = (string)$configuration['iconIdentifier'];
}
if ($configuration['access'] ?? false) {
$obj->access = (string)$configuration['access'];
}
if ($configuration['workspaces'] ?? false) {
$obj->workspaceAccess = (string)$configuration['workspaces'];
}
if ($configuration['component'] ?? false) {
$obj->component = (string)$configuration['component'];
}
$labelInformation = $configuration['labels'] ?? false;
if (is_array($labelInformation)) {
$obj->title = (string)($labelInformation['title'] ?? '');
$obj->description = (string)($labelInformation['description'] ?? '');
$obj->shortDescription = (string)($labelInformation['shortDescription'] ?? '');
} elseif (str_starts_with((string)$labelInformation, 'LLL:')) {
$labelsFile = $labelInformation;
$obj->title = $labelsFile . ':mlang_tabs_tab';
$obj->description = $labelsFile . ':mlang_labels_tabdescr';
$obj->shortDescription = $labelsFile . ':mlang_labels_tablabel';
} elseif (is_string($labelInformation) && !str_contains($labelInformation, ':') && str_contains($labelInformation, '.')) {
// New File Format. Uses "backend.modules.<modulename>" (for example "backend.modules.preview") as identifier,
// which results in "backend.modules.preview:title" etc.
$obj->title = $labelInformation . ':title';
$obj->description = $labelInformation . ':description';
$obj->shortDescription = $labelInformation . ':short_description';
}
if (is_array($configuration['position'] ?? false)) {
if (in_array('top', $configuration['position'], true)) {
$configuration['position'] = [
'before' => '*',
];
}
if (in_array('bottom', $configuration['position'], true)) {
$configuration['position'] = [
'after' => '*',
];
}
$obj->position = $configuration['position'];
}
if (is_array($configuration['appearance'] ?? false)) {
$obj->appearance = $configuration['appearance'];
}
if (is_array($configuration['moduleData'] ?? false)) {
$obj->defaultModuleData = $configuration['moduleData'];
}
if (is_array($configuration['aliases'] ?? false)) {
$obj->aliases = $configuration['aliases'];
}
if (isset($configuration['inheritNavigationComponentFromMainModule'])) {
$obj->inheritNavigationComponent = (bool)$configuration['inheritNavigationComponentFromMainModule'];
}
if (isset($configuration['navigationComponent'])) {
$obj->navigationComponent = (string)$configuration['navigationComponent'];
} elseif (isset($configuration['navigationComponentId'])) {
$obj->navigationComponent = (string)$configuration['navigationComponentId'];
}
if (is_array($configuration['routeOptions'] ?? null)) {
$obj->routeOptions = $configuration['routeOptions'];
}
if (isset($configuration['showSubmoduleOverview'])) {
$obj->showSubmoduleOverview = (bool)$configuration['showSubmoduleOverview'];
}
return $obj;
}
}
@@ -0,0 +1,56 @@
<?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;
/**
* Listeners can adjust the module configuration before the module gets created and registered
*/
final class BeforeModuleCreationEvent
{
public function __construct(private readonly string $identifier, private array $configuration) {}
public function getIdentifier(): string
{
return $this->identifier;
}
public function getConfiguration(): array
{
return $this->configuration;
}
public function setConfiguration(array $configuration): void
{
$this->configuration = $configuration;
}
public function hasConfigurationValue(string $key): bool
{
return isset($this->configuration[$key]);
}
public function getConfigurationValue(string $key, mixed $default = null): mixed
{
return $this->configuration[$key] ?? $default;
}
public function setConfigurationValue(string $key, mixed $value): void
{
$this->configuration[$key] = $value;
}
}
+116
View File
@@ -0,0 +1,116 @@
<?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\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Core\Bootstrap;
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
/**
* An extbase built backend module
*
* @internal only for use within TYPO3 Core.
*/
class ExtbaseModule extends BaseModule implements ModuleInterface
{
protected string $extensionName;
protected array $controllerActions;
/**
* Extbase modules always need a parent, use "content" as default
*/
protected string $parent = 'content';
/**
* Access is restricted to "admin" by default for extbase modules
*/
protected string $access = 'admin';
public function getExtensionName(): string
{
return $this->extensionName;
}
public function getControllerActions(): array
{
return $this->controllerActions;
}
public function getDefaultRouteOptions(): array
{
$allRoutes = [];
foreach ($this->controllerActions as $controllerConfiguration) {
foreach ($controllerConfiguration['actions'] as $actionName) {
if ($allRoutes === []) {
$allRoutes['_default'] = array_replace_recursive(
$this->routeOptions,
[
'module' => $this,
'packageName' => $this->packageName,
'absolutePackagePath' => $this->absolutePackagePath,
'access' => $this->access,
'target' => Bootstrap::class . '::handleBackendRequest',
'controller' => $controllerConfiguration['alias'],
'action' => $actionName,
]
);
}
$allRoutes[$controllerConfiguration['alias'] . '_' . $actionName] = array_replace_recursive(
$this->routeOptions,
[
'module' => $this,
'path' => $controllerConfiguration['alias'] . '/' . $actionName,
'packageName' => $this->packageName,
'absolutePackagePath' => $this->absolutePackagePath,
'access' => $this->access,
'target' => Bootstrap::class . '::handleBackendRequest',
'controller' => $controllerConfiguration['alias'],
'action' => $actionName,
]
);
}
}
return $allRoutes;
}
protected static function sanitizeExtensionName(string $extensionName): string
{
return (string)str_replace(' ', '', ucwords(str_replace('_', ' ', $extensionName)));
}
protected static function sanitizeControllerActions(array $controllerActions): array
{
$sanitizedControllerActions = [];
foreach ($controllerActions as $controllerName => $actions) {
$sanitizedControllerActions[$controllerName] = [
'actions' => (is_array($actions) ? $actions : GeneralUtility::trimExplode(',', $actions)),
'alias' => ExtensionUtility::resolveControllerAliasFromControllerClassName($controllerName),
'className' => $controllerName,
];
}
return $sanitizedControllerActions;
}
public static function createFromConfiguration(string $identifier, array $configuration): static
{
$obj = parent::createFromConfiguration($identifier, $configuration);
$obj->extensionName = self::sanitizeExtensionName((string)($configuration['extensionName'] ?? ''));
$obj->controllerActions = self::sanitizeControllerActions((array)($configuration['controllerActions'] ?? []));
return $obj;
}
}
+192
View File
@@ -0,0 +1,192 @@
<?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;
/**
* A representation for a module used for the menu generation.
*
* @internal only to be used by TYPO3 Core.
*/
class MenuModule implements ModuleInterface
{
/**
* @var ModuleInterface[]
*/
protected array $subModules = [];
public function __construct(protected readonly ModuleInterface $module, protected $isCollapsed = false) {}
public function getIdentifier(): string
{
return $this->module->getIdentifier();
}
public function getIconIdentifier(): string
{
return $this->module->getIconIdentifier();
}
public function getTitle(): string
{
return $this->module->getTitle();
}
public function getDescription(): string
{
return $this->module->getDescription();
}
public function getShortDescription(): string
{
return $this->module->getShortDescription();
}
public function isStandalone(): bool
{
return $this->module->isStandalone();
}
public function getComponent(): string
{
return $this->module->getComponent();
}
public function getNavigationComponent(): string
{
return $this->module->getNavigationComponent();
}
public function getPosition(): array
{
return $this->module->getPosition();
}
public function getAppearance(): array
{
return $this->module->getAppearance();
}
public function getAccess(): string
{
return $this->module->getAccess();
}
public function getWorkspaceAccess(): string
{
return $this->module->getWorkspaceAccess();
}
public function getParentIdentifier(): string
{
return $this->module->getParentIdentifier();
}
public function setParentModule(ModuleInterface $module): void
{
$this->module->setParentModule($module);
}
public function getParentModule(): ?ModuleInterface
{
return $this->module->getParentModule();
}
public function hasParentModule(): bool
{
return $this->module->hasParentModule();
}
public function addSubModule(ModuleInterface $module): void
{
$this->subModules[$module->getIdentifier()] = $module;
}
public function removeSubModule(string $identifier): void
{
unset($this->subModules[$identifier]);
}
public function hasSubModule(string $identifier): bool
{
return isset($this->subModules[$identifier]);
}
public function hasSubModules(): bool
{
return $this->subModules !== [];
}
public function getSubModule(string $identifier): ?ModuleInterface
{
return $this->subModules[$identifier] ?? null;
}
/**
* @return ModuleInterface[]
*/
public function getSubModules(): array
{
return $this->subModules;
}
public function getPath(): string
{
return $this->module->getPath();
}
public function getDefaultRouteOptions(): array
{
return $this->module->getDefaultRouteOptions();
}
public function getDefaultModuleData(): array
{
return $this->module->getDefaultModuleData();
}
public function getAliases(): array
{
return $this->module->getAliases();
}
public function hasSubmoduleOverview(): bool
{
return $this->module->hasSubmoduleOverview();
}
public function isCollapsed(): bool
{
return $this->isCollapsed;
}
public function getIsCollapsed(): bool
{
return $this->isCollapsed();
}
public function getShouldBeLinked(): bool
{
if ($this->module->isStandalone()) {
return true;
}
if ($this->module->hasParentModule()) {
return true;
}
return false;
}
}
+87
View File
@@ -0,0 +1,87 @@
<?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\Backend\Exception\NonRoutableModuleException;
/**
* A standard backend nodule
*/
class Module extends BaseModule implements ModuleInterface
{
protected array $routes;
public function getRoutes(): array
{
return $this->routes;
}
public function getDefaultRouteOptions(): array
{
$defaultRouteOptions = [];
if ($this->routes !== []) {
foreach ($this->routes as $routeIdentifier => $routeOptions) {
$defaultRouteOptions[$routeIdentifier] = array_replace_recursive(
$this->routeOptions,
$routeOptions,
[
'module' => $this,
'packageName' => $this->packageName,
'absolutePackagePath' => $this->absolutePackagePath,
'access' => $this->access,
]
);
}
} elseif ($this->hasSubmoduleOverview()) {
// Show card-based overview of all submodules
$defaultRouteOptions['_default'] = array_replace_recursive(
$this->routeOptions,
[
'target' => \TYPO3\CMS\Backend\Controller\SubmoduleOverviewController::class . '::handleRequest',
'module' => $this,
'packageName' => $this->packageName,
'absolutePackagePath' => $this->absolutePackagePath,
'access' => $this->access,
]
);
} elseif ($this->hasSubModules()) {
// In case no routes are defined but the module has submodules,
// fall back and use the first submodules' route options instead.
$submodules = $this->getSubModules();
$firstSubModule = reset($submodules);
$defaultRouteOptions = $firstSubModule->getDefaultRouteOptions();
}
if (!isset($defaultRouteOptions['_default'])) {
throw new NonRoutableModuleException(
'No default route could be resolved for module ' . $this->identifier,
1674063354
);
}
return $defaultRouteOptions;
}
public static function createFromConfiguration(string $identifier, array $configuration): static
{
$obj = parent::createFromConfiguration($identifier, $configuration);
$obj->routes = $configuration['routes'] ?? [];
return $obj;
}
}
@@ -0,0 +1,32 @@
<?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;
/**
* Interface for module access gates.
*
* Gates are responsible for deciding whether a backend user has access to a module.
* Each gate is registered with a unique identifier that corresponds to the `access`
* value in a module's configuration.
*/
interface ModuleAccessGateInterface
{
public function decide(ModuleInterface $module, BackendUserAuthentication $user): ModuleAccessResult;
}
@@ -0,0 +1,67 @@
<?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;
/**
* Registry for module access gates, ordered by before/after dependencies
* during container compilation.
*/
class ModuleAccessGateRegistry
{
/**
* @var array<string, ModuleAccessGateInterface>
*/
private array $gates = [];
/**
* @param array<string, ModuleAccessGateInterface> $gates Pre-ordered gates (injected by ModuleAccessGatePass)
*/
public function __construct(array $gates = [])
{
foreach ($gates as $identifier => $gate) {
$this->gates[$identifier] = $gate;
}
}
public function has(string $identifier): bool
{
return isset($this->gates[$identifier]);
}
/**
* @throws \InvalidArgumentException if gate does not exist
*/
public function get(string $identifier): ModuleAccessGateInterface
{
if (!$this->has($identifier)) {
throw new \InvalidArgumentException(
sprintf('Module access gate with identifier "%s" is not registered.', $identifier),
1774436666
);
}
return $this->gates[$identifier];
}
/**
* @return array<string, ModuleAccessGateInterface>
*/
public function getAll(): array
{
return $this->gates;
}
}
+39
View File
@@ -0,0 +1,39 @@
<?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;
/**
* Result of a module access gate decision.
*/
enum ModuleAccessResult
{
/**
* Access is explicitly granted.
*/
case Granted;
/**
* Access is explicitly denied.
*/
case Denied;
/**
* The gate cannot decide - pass to next gate.
*/
case Abstain;
}
+126
View File
@@ -0,0 +1,126 @@
<?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;
/**
* A simple DTO containing the user specific module settings, e.g. whether the clipboard is shown.
* The DTO is created in the PSR-15 middleware BackendModuleValidator, in case a backend module
* is requested and the user has necessary access permissions. The created DTO is then added as
* attribute to the PSR-7 Request and can be further used in components, such as middlewares or
* the route target (usually a backend controller).
*
* @see BackendModuleValidator
*/
class ModuleData
{
protected array $properties = [];
protected string $moduleIdentifier;
protected array $defaultData = [];
public function __construct(string $moduleIdentifier, array $data, array $defaultData = [])
{
$this->moduleIdentifier = $moduleIdentifier;
$this->defaultData = $defaultData;
$this->properties = array_replace_recursive($defaultData, $data);
}
public static function createFromModule(ModuleInterface $module, array $data): self
{
return new self(
$module->getIdentifier(),
$data,
$module->getDefaultModuleData(),
);
}
public function getModuleIdentifier(): string
{
return $this->moduleIdentifier;
}
public function get(string $propertyName, mixed $default = null): mixed
{
return $this->properties[$propertyName] ?? $default;
}
public function has(string $propertyName): bool
{
return isset($this->properties[$propertyName]);
}
public function set(string $propertyName, mixed $value): void
{
$this->properties[$propertyName] = $value;
}
/**
* Cleans a single property by the given allowed list. First fallback
* is the default data list. If this list does also not contain an
* allowed value, the first value from the allowed list is taken.
*
* @return bool True if something has been cleaned up
*/
public function clean(string $propertyName, array $allowedValues): bool
{
if (!$this->has($propertyName)) {
throw new \InvalidArgumentException('Property ' . $propertyName . ' can not be cleaned, since it does not exist.', 1644600510);
}
if ($allowedValues === []) {
throw new \InvalidArgumentException('Define at least one allowed value.', 1644600511);
}
if (in_array($this->properties[$propertyName], $allowedValues)) {
// Current value is allowed, nothing to do
return false;
}
if (isset($this->defaultData[$propertyName]) && in_array($this->defaultData[$propertyName], $allowedValues)) {
// Set property to its default value - if it is allowed
$this->properties[$propertyName] = $this->defaultData[$propertyName];
} else {
// Fall back to the first value of the allow list
$this->properties[$propertyName] = reset($allowedValues);
}
return true;
}
/**
* Cleans up all module data, which are defined in the
* given allowed data list. Usually called with $MOD_MENU.
*/
public function cleanUp(array $allowedData, bool $useKeys = true): bool
{
$cleanUp = false;
foreach ($allowedData as $propertyName => $allowedValues) {
if (is_array($allowedValues)
&& $this->has($propertyName)
&& $this->clean($propertyName, $useKeys ? array_keys($allowedValues) : $allowedValues)
) {
$cleanUp = true;
}
}
return $cleanUp;
}
public function toArray(): array
{
return $this->properties;
}
}
+98
View File
@@ -0,0 +1,98 @@
<?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 Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Imaging\IconRegistry;
/**
* @internal only to be used within TYPO3 Core
*/
#[Autoconfigure(public: true)]
readonly class ModuleFactory
{
public function __construct(
protected IconRegistry $iconRegistry,
protected EventDispatcherInterface $eventDispatcher,
) {}
public function createModule(string $identifier, array $configuration): ModuleInterface
{
$configuration = $this->eventDispatcher->dispatch(
new BeforeModuleCreationEvent($identifier, $configuration)
)->getConfiguration();
$configuration = $this->sanitizeConfiguration($identifier, $configuration);
if (is_array($configuration['controllerActions'] ?? false)) {
return ExtbaseModule::createFromConfiguration($identifier, $configuration);
}
return Module::createFromConfiguration($identifier, $configuration);
}
private function sanitizeConfiguration(string $identifier, array $configuration): array
{
if (empty($configuration['path'])) {
$configuration['path'] = '/module/' . trim(str_replace('_', '/', $identifier), '/');
}
if ($configuration['icon'] ?? false) {
$iconPath = $configuration['icon'];
if ($iconPath !== '') {
$iconIdentifier = 'module-' . $identifier;
$iconProvider = $this->iconRegistry->detectIconProvider($iconPath);
$this->iconRegistry->registerIcon($iconIdentifier, $iconProvider, ['source' => $iconPath]);
$configuration['iconIdentifier'] = $iconIdentifier;
}
unset($configuration['icon']);
}
return $configuration;
}
/**
* In order to keep modules that reference to an alias (e.g. switch of main module form "web" to "content"),
* the modules need to be re-located to reference the "new" identifier and not the now available alias. This
* concerns "parent" and the "position" options, which reference other module identifiers.
*/
public function adaptAliasMappingFromModuleConfiguration(array $moduleConfigurations): array
{
// collect ALL aliases
$availableAliases = [];
foreach ($moduleConfigurations as $moduleIdentifier => $moduleConfiguration) {
foreach ($moduleConfiguration['aliases'] ?? [] as $aliasIdentifier) {
$availableAliases[$aliasIdentifier] = $moduleIdentifier;
}
}
// rewrite references
$adaptedModuleConfiguration = [];
foreach ($moduleConfigurations as $moduleIdentifier => $moduleConfiguration) {
if (isset($moduleConfiguration['parent'], $availableAliases[$moduleConfiguration['parent']])) {
$moduleConfiguration['parent'] = $availableAliases[$moduleConfiguration['parent']];
}
if (isset($moduleConfiguration['position']['before'], $availableAliases[$moduleConfiguration['position']['before']])) {
$moduleConfiguration['position']['before'] = $availableAliases[$moduleConfiguration['position']['before']];
}
if (isset($moduleConfiguration['position']['after'], $availableAliases[$moduleConfiguration['position']['after']])) {
$moduleConfiguration['position']['after'] = $availableAliases[$moduleConfiguration['position']['after']];
}
$adaptedModuleConfiguration[$moduleIdentifier] = $moduleConfiguration;
}
return $adaptedModuleConfiguration;
}
}
+167
View File
@@ -0,0 +1,167 @@
<?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;
/**
* An interface representing a TYPO3 Backend module.
*/
interface ModuleInterface
{
/**
* The internal name of the module, used for referencing in permissions etc
*/
public function getIdentifier(): string;
/**
* Return the main route path
*/
public function getPath(): string;
/**
* The icon identifier for the module
*/
public function getIconIdentifier(): string;
/**
* The title of the module, used in the menu
*/
public function getTitle(): string;
/**
* A longer description, common for the "About" section with a long explanation
*/
public function getDescription(): string;
/**
* A shorter description, used when hovering over a module in the menu as title attribute
*/
public function getShortDescription(): string;
/**
* Useful for main modules that are also "clickable" such as the dashboard module
*/
public function isStandalone(): bool;
/**
* Returns the view component responsible for rendering the module (iFrame or name of the web component)
*/
public function getComponent(): string;
/**
* The web component to be rendering the navigation area
*/
public function getNavigationComponent(): string;
/**
* The position of the module, such as [top] or [bottom] or [after => anotherModule] or [before => anotherModule]
*/
public function getPosition(): array;
/**
* Returns a modules appearance options, e.g. used for module menu
*/
public function getAppearance(): array;
/**
* Can be user (editor permissions), admin, or systemMaintainer
*/
public function getAccess(): string;
/**
* Can be "*" (= empty) or "live" or "offline"
*/
public function getWorkspaceAccess(): string;
/**
* The identifier of the parent module during registration
*/
public function getParentIdentifier(): string;
/**
* Set a reference to the next upper menu item
*
* @internal Might vanish soon
*/
public function setParentModule(ModuleInterface $module): void;
/**
* Get the reference to the next upper menu item
*/
public function getParentModule(): ?ModuleInterface;
/**
* Can be checked if the module is a "main module"
*/
public function hasParentModule(): bool;
/**
* Used to set another module as part of the parent module
*
* @internal Might vanish soon
*/
public function addSubModule(ModuleInterface $module): void;
/**
* Remove a submodule
*
* @internal Might vanish soon
*/
public function removeSubModule(string $identifier): void;
/**
* Checks whether this module has a submodule with the given identifier
*/
public function hasSubModule(string $identifier): bool;
/**
* Checks if this module has further submodules
*/
public function hasSubModules(): bool;
/**
* Return a submodule given by its full identifier
*/
public function getSubModule(string $identifier): ?ModuleInterface;
/**
* Return all direct descendants of this module
* @return ModuleInterface[]
*/
public function getSubModules(): array;
/**
* Returns module related route options - used for the router
*/
public function getDefaultRouteOptions(): array;
/**
* Get allowed and available module data properties and their default values.
*/
public function getDefaultModuleData(): array;
/**
* Return a list of identifiers that are aliases to this module
*/
public function getAliases(): array;
/**
* Whether this module should display a card-based overview of its submodules
* instead of automatically routing to the first available submodule.
*/
public function hasSubmoduleOverview(): bool;
}
+329
View File
@@ -0,0 +1,329 @@
<?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);
}
}
+370
View File
@@ -0,0 +1,370 @@
<?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\Backend\Routing\Route;
use TYPO3\CMS\Backend\Routing\RouterConfigurationEvent;
use TYPO3\CMS\Core\Routing\RouteCollection;
/**
* @internal Always use the ModuleProvider API to access modules
*/
final class ModuleRegistry
{
/**
* @var ModuleInterface[]
*/
private array $modules = [];
/**
* This contains all available "aliases" as key, and the real module identifier as value
* @var array<string, string>
*/
private array $moduleAliases = [];
/**
* @param ModuleInterface[] $modules
*/
public function __construct(array $modules)
{
array_walk($modules, $this->addModule(...));
$this->modules = $this->applyHierarchy($this->modules);
$this->populateAliasMapping();
}
private function addModule(ModuleInterface $module): void
{
$identifier = $module->getIdentifier();
if (isset($this->modules[$identifier])) {
throw new \LogicException(
'A module with the identifier ' . $identifier . ' is already registered.',
1642174843
);
}
$this->modules[$module->getIdentifier()] = $module;
}
public function hasModule(string $identifier): bool
{
return isset($this->modules[$identifier]) || isset($this->moduleAliases[$identifier]);
}
public function getModule(string $identifier): ModuleInterface
{
if (!$this->hasModule($identifier)) {
throw new \InvalidArgumentException(
'Module with identifier ' . $identifier . ' does not exist.',
1642375889
);
}
// Resolve the alias to the real module
if (isset($this->moduleAliases[$identifier])) {
$identifier = $this->moduleAliases[$identifier];
}
return $this->modules[$identifier];
}
/**
* @return ModuleInterface[]
*/
public function getModules(): array
{
return $this->modules;
}
/**
* Handle router configuration when the router is set up.
*
* The event registration is wired in `TYPO3\CMS\Backend\ServiceProvider::addEventListeners`
* instead of using an `AsEventListener` because this service is not autowired, but has a
* custom factory in `TYPO3\CMS\Backend\ServiceProvider`.
*/
public function registerRoutesForModules(RouterConfigurationEvent $event): void
{
$router = $event->router;
foreach ($this->modules as $module) {
if (!$module->hasParentModule() && !$module->isStandalone()) {
// Skip first level modules, which are not standalone
continue;
}
$routeCollection = new RouteCollection();
foreach ($module->getDefaultRouteOptions() as $routeIdentifier => $routeOptions) {
$path = (string)(($routeOptions['path'] ?? false) ?: ('/' . $routeIdentifier));
$methods = (array)($routeOptions['methods'] ?? []);
unset($routeOptions['path'], $routeOptions['methods']);
if ($routeIdentifier === '_default') {
// Add the first
$route = new Route($module->getPath(), $routeOptions);
if ($methods !== []) {
$route->setMethods($methods);
}
$router->addRoute($module->getIdentifier(), $route, $module->getAliases());
} else {
$route = new Route($path, $routeOptions);
if ($methods !== []) {
$route->setMethods($methods);
}
$routeCollection->add($routeIdentifier, $route);
}
}
$routeCollection->addNamePrefix($module->getIdentifier() . '.');
$routeCollection->addPrefix($module->getPath());
$router->addRouteCollection($routeCollection);
}
}
/**
* Applies sorting (based on the "position" configuration) to all
* registered modules and resolves the hierarchy (creating relations
* by attaching modules to the $parentModule and $subModules properties).
*
* @param ModuleInterface[] $modules
* @return ModuleInterface[]
*/
private function applyHierarchy(array $modules): array
{
// Fetch top-level (parent) modules and fill them with sorted sub modules
$topLevelModules = [];
foreach ($modules as $identifier => $module) {
if ($module->getParentIdentifier() === '') {
$topLevelModules[$identifier] = $module;
}
$subModules = array_filter(
$modules,
static fn(ModuleInterface $mod): bool => $mod->getParentIdentifier() === $identifier
);
if ($subModules === []) {
continue;
}
// Sort sub modules and connect them with their parent module
$subModules = $this->applySorting($subModules);
foreach ($subModules as $subModule) {
$module->addSubModule($subModule);
$subModule->setParentModule($module);
}
}
// Promote single submodules to standalone modules and rebuild top-level modules afterwards
$topLevelModules = array_filter(
$this->promoteSingleSubmodulesToStandalone($modules, $topLevelModules),
static fn(ModuleInterface $module): bool => !$module->hasParentModule() || $module->isStandalone()
);
// Sort top level modules and return all modules (flat) with the correct sorting
return $this->flattenModules($this->applySorting($topLevelModules));
}
/**
* Promotes submodules to standalone if their parent module has only one submodule and is not
* standalone itself. This makes single submodules behave like standalone top-level modules.
*
* @param ModuleInterface[] $modules
* @param ModuleInterface[] $topLevelModules
* @return ModuleInterface[]
*/
private function promoteSingleSubmodulesToStandalone(array $modules, array $topLevelModules): array
{
foreach ($topLevelModules as $parentIdentifier => $parentModule) {
// Skip already standalone modules
if ($parentModule->isStandalone()) {
continue;
}
// Only promote if explicitly enabled via appearance setting
if (!($parentModule->getAppearance()['promotesSingleSubmoduleToStandalone'] ?? false)) {
continue;
}
$subModules = $parentModule->getSubModules();
if (count($subModules) !== 1) {
continue;
}
/** @var BaseModule $subModule */
$subModule = reset($subModules);
// Promote the submodule to standalone, inheriting parent properties
$subModule->promoteToStandalone(
$parentModule->getNavigationComponent(),
$parentModule->getPosition(),
$parentModule->getAliases()
);
// Remove parent module from registry
unset($modules[$parentIdentifier]);
}
return $modules;
}
/**
* Ensures that modules within one level of hierarchy are ordered properly the way
* they were given respecting their "top", "bottom", "before" or "after" definition.
*
* @param ModuleInterface[] $modules
* @return ModuleInterface[]
*/
private function applySorting(array $modules): array
{
$modulePositionInformation = [];
// First create a list of all needed data, that is the identifier, and its position
foreach ($modules as $identifier => $module) {
// @todo Should we enforce ['after' => '*'] in case ->getPosition() is empty?
$modulePositionInformation[$identifier] = $module->getPosition();
$modulePositionInformation[$identifier]['modulesToBeAddedDirectlyBefore'] = [];
$modulePositionInformation[$identifier]['modulesToBeAddedDirectlyAfter'] = [];
}
// Identifiers of modules to be added at the top
$highPriorityModules = [];
// Identifiers of modules to be added at the bottom
$lowPriorityModules = [];
// Sort out the "top" and "bottom", and also build a graph of directly dependant (before/after) modules
foreach ($modulePositionInformation as $identifier => $positionInformation) {
if ($positionInformation['before'] ?? false) {
if ($positionInformation['before'] === '*') {
// Module should be added on top
$highPriorityModules[] = $identifier;
} elseif (isset($modules[$positionInformation['before']])) {
// Build the dependencies in case a valid module identifier is configured
$modulePositionInformation[$positionInformation['before']]['modulesToBeAddedDirectlyBefore'][] = $identifier;
$modulePositionInformation[$identifier]['modulesToBeAddedDirectlyAfter'][] = $positionInformation['before'];
}
} elseif ($positionInformation['after'] ?? false) {
if ($positionInformation['after'] === '*') {
// Module should be added at the bottom
$lowPriorityModules[] = $identifier;
} elseif (isset($modules[$positionInformation['after']])) {
// Build the dependencies in case a valid module identifier is configured
$modulePositionInformation[$identifier]['modulesToBeAddedDirectlyBefore'][] = $positionInformation['after'];
$modulePositionInformation[$positionInformation['after']]['modulesToBeAddedDirectlyAfter'][] = $identifier;
}
}
}
// First add the top items and their dependant modules
$orderedModuleIdentifiers = $this->populateOrderingsForDependencies($highPriorityModules, $modulePositionInformation);
// Now add the bottom items and their dependant modules
// They will be cut out later, however, this is done now to also add their dependencies NOW (and not when looping over all items again)
$orderedModuleIdentifiers = $this->populateOrderingsForDependencies($lowPriorityModules, $modulePositionInformation, $orderedModuleIdentifiers);
$lastLowPriorityModule = end($orderedModuleIdentifiers);
// Loop through all items and see which have not been added yet. Keep the original sorting.
$orderedModuleIdentifiers = $this->populateOrderingsForDependencies(array_keys($modulePositionInformation), $modulePositionInformation, $orderedModuleIdentifiers);
// Find the lowest priority module and move everything after that module to the very end
if ($lowPriorityModules !== [] && $lastLowPriorityModule) {
$firstLowPriorityModule = reset($lowPriorityModules);
if ($firstLowPriorityModule !== $lastLowPriorityModule) {
$firstPosition = array_search($firstLowPriorityModule, $orderedModuleIdentifiers, true);
$lastPosition = array_search($lastLowPriorityModule, $orderedModuleIdentifiers, true);
if ($firstPosition !== false && $lastPosition !== false) {
$extractedItems = array_slice($orderedModuleIdentifiers, $firstPosition, $lastPosition);
$orderedModuleIdentifiers = array_merge($orderedModuleIdentifiers, $extractedItems);
}
}
}
// Use the ordered list and replace all valid identifiers with the corresponding
return array_replace(array_intersect_key(array_flip($orderedModuleIdentifiers), $modules), $modules);
}
/**
* Given module identifiers are added based on their module position information to the
* $alreadyOrderedModuleIdentifiers array (if not already present). In case the modules
* to be added have dependencies to other modules and those modules exist, corresponding
* modules are added to $alreadyOrderedModuleIdentifiers on the correct position as well.
*/
private function populateOrderingsForDependencies(
array $moduleIdentifiersToBeAdded,
array $modulePositionInformation,
array $alreadyOrderedModuleIdentifiers = []
): array {
foreach ($moduleIdentifiersToBeAdded as $identifier) {
// already placed somewhere
if (in_array($identifier, $alreadyOrderedModuleIdentifiers, true)) {
continue;
}
// Check if the current module has dependencies, which should be added BEFORE
foreach ($modulePositionInformation[$identifier]['modulesToBeAddedDirectlyBefore'] ?? [] as $dependantIdentifier) {
// already placed somewhere
if (in_array($dependantIdentifier, $alreadyOrderedModuleIdentifiers, true)) {
continue;
}
// Check if the dependent module has dependencies, which should be added BEFORE
foreach ($modulePositionInformation[$dependantIdentifier]['modulesToBeAddedDirectlyBefore'] ?? [] as $dependantDependantIdentifier) {
// already placed somewhere
if (in_array($dependantDependantIdentifier, $alreadyOrderedModuleIdentifiers, true)) {
continue;
}
// Add the sub dependency right away
$alreadyOrderedModuleIdentifiers[] = $dependantDependantIdentifier;
}
// Add the dependant module now
$alreadyOrderedModuleIdentifiers[] = $dependantIdentifier;
}
// Add the actual module now
$alreadyOrderedModuleIdentifiers[] = $identifier;
// Check if the current module has dependencies, which should be added AFTER
foreach ($modulePositionInformation[$identifier]['modulesToBeAddedDirectlyAfter'] ?? [] as $dependantIdentifier) {
// already placed somewhere
if (in_array($dependantIdentifier, $alreadyOrderedModuleIdentifiers, true)) {
continue;
}
// Add the dependant module right away
$alreadyOrderedModuleIdentifiers[] = $dependantIdentifier;
// Check if the dependent module has dependencies, which should be added AFTER
foreach ($modulePositionInformation[$dependantIdentifier]['modulesToBeAddedDirectlyAfter'] ?? [] as $dependantDependantIdentifier) {
// already placed somewhere
if (in_array($dependantDependantIdentifier, $alreadyOrderedModuleIdentifiers, true)) {
continue;
}
// Add the sub dependency right away
$alreadyOrderedModuleIdentifiers[] = $dependantDependantIdentifier;
}
}
}
return $alreadyOrderedModuleIdentifiers;
}
/**
* Create a flat modules array (looping through each level by calling "getSubmodules()" on the parent)
*/
private function flattenModules(array $modules, $flatModules = []): array
{
foreach ($modules as $module) {
$flatModules[$module->getIdentifier()] = $module;
if ($module->hasSubmodules()) {
$flatModules = $this->flattenModules($module->getSubmodules(), $flatModules);
}
}
return $flatModules;
}
private function populateAliasMapping(): void
{
foreach ($this->modules as $moduleIdentifier => $module) {
foreach ($module->getAliases() as $aliasIdentifier) {
// Note: The last module defining the same alias wins in general
$this->moduleAliases[$aliasIdentifier] = $moduleIdentifier;
}
}
}
public function getModuleAliases(): array
{
return $this->moduleAliases;
}
}
+80
View File
@@ -0,0 +1,80 @@
<?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 Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
/**
* Resolves the current module from a request.
*
* This service provides a centralized way to determine which backend module
* is currently active based on various request attributes and parameters.
*
* @internal This class is not part of TYPO3's public API.
*/
final readonly class ModuleResolver
{
public function __construct(
private ModuleProvider $moduleProvider,
) {}
/**
* Resolves the current module from a request.
*
* Checks multiple sources in order:
* 1. Routing attribute (from route configuration)
* 2. Request attribute 'module'
* 3. Query parameter 'module'
*
* @param ServerRequestInterface|null $request The current request
* @return ModuleInterface|null The resolved module or null if not found
*/
public function resolveModule(?ServerRequestInterface $request): ?ModuleInterface
{
if ($request === null) {
return null;
}
// Check routing attribute first
$routeResult = $request->getAttribute('routing');
$currentModule = $routeResult?->getRoute()?->getOption('module');
if ($currentModule !== null) {
return $currentModule;
}
// Check request attribute
$currentModule = $request->getAttribute('module');
if ($currentModule !== null) {
return $currentModule;
}
// Check query parameter
if (isset($request->getQueryParams()['module'])) {
$module = (string)$request->getQueryParams()['module'];
return $this->moduleProvider->getModule($module, $this->getBackendUser());
}
return null;
}
private function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}