TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user