395 lines
15 KiB
PHP
395 lines
15 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* This file is part of the TYPO3 CMS project.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*
|
|
* The TYPO3 project - inspiring people to share!
|
|
*/
|
|
|
|
namespace TYPO3\CMS\Backend\Template;
|
|
|
|
use Psr\Http\Message\ResponseInterface;
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use TYPO3\CMS\Backend\Module\ModuleInterface;
|
|
use TYPO3\CMS\Backend\Module\ModuleProvider;
|
|
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
|
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
|
|
use TYPO3\CMS\Backend\Template\Components\Buttons\ButtonInterface;
|
|
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
|
|
use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;
|
|
use TYPO3\CMS\Backend\Template\Enum\ModuleLayout;
|
|
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
|
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
|
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
|
|
use TYPO3\CMS\Core\Imaging\IconFactory;
|
|
use TYPO3\CMS\Core\Localization\LanguageService;
|
|
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
|
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
|
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
|
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
|
use TYPO3\CMS\Core\Page\PageRenderer;
|
|
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Core\View\ResponsableViewInterface;
|
|
use TYPO3\CMS\Core\View\ViewInterface;
|
|
use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters;
|
|
|
|
/**
|
|
* A class taking care of the "outer" HTML of a module, especially
|
|
* the doc header and other related parts.
|
|
*/
|
|
final class ModuleTemplate implements ViewInterface, ResponsableViewInterface
|
|
{
|
|
use PageRendererBackendSetupTrait;
|
|
|
|
private bool $uiBlock = false;
|
|
|
|
private string $moduleId = '';
|
|
private string $moduleName = '';
|
|
private string $moduleClass = '';
|
|
/**
|
|
* @internal
|
|
*/
|
|
private ModuleLayout $moduleLayout = ModuleLayout::WIDE;
|
|
private string $title = '';
|
|
private string $bodyTag = '<body>';
|
|
private string $formTag = '';
|
|
|
|
private FlashMessageQueue $flashMessageQueue;
|
|
private DocHeaderComponent $docHeaderComponent;
|
|
|
|
/**
|
|
* Init PageRenderer and properties.
|
|
*/
|
|
public function __construct(
|
|
protected readonly PageRenderer $pageRenderer,
|
|
protected readonly IconFactory $iconFactory,
|
|
protected readonly UriBuilder $uriBuilder,
|
|
protected readonly ModuleProvider $moduleProvider,
|
|
protected readonly FlashMessageService $flashMessageService,
|
|
protected readonly ExtensionConfiguration $extensionConfiguration,
|
|
protected readonly ViewInterface $view,
|
|
protected readonly ComponentFactory $componentFactory,
|
|
protected readonly ServerRequestInterface $request,
|
|
) {
|
|
$module = $request->getAttribute('module');
|
|
if ($module instanceof ModuleInterface) {
|
|
// third level, needs the second level in order to keep highlighting in the module menu
|
|
if ($module->getParentModule()?->getParentModule()) {
|
|
$this->setModuleName($module->getParentModule()->getIdentifier());
|
|
} else {
|
|
$this->setModuleName($module->getIdentifier());
|
|
}
|
|
$this->setModuleName($module->getIdentifier());
|
|
} else {
|
|
$this->setModuleName($request->getAttribute('route')?->getOption('_identifier') ?? '');
|
|
}
|
|
$this->flashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
|
|
$this->docHeaderComponent = GeneralUtility::makeInstance(DocHeaderComponent::class);
|
|
$this->setUpBasicPageRendererForBackend($pageRenderer, $extensionConfiguration, $request, $this->getLanguageService());
|
|
}
|
|
|
|
/**
|
|
* Add a variable to the view data collection.
|
|
*/
|
|
public function assign(string $key, mixed $value): self
|
|
{
|
|
$this->view->assign($key, $value);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Add multiple variables to the view data collection.
|
|
*/
|
|
public function assignMultiple(array $values): self
|
|
{
|
|
$this->view->assignMultiple($values);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Render the module.
|
|
*/
|
|
public function render(string $templateFileName = ''): string
|
|
{
|
|
$this->prepareRender($templateFileName);
|
|
return $this->pageRenderer->render($this->request);
|
|
}
|
|
|
|
/**
|
|
* Render the module and create an HTML 200 response from it. This is a
|
|
* lazy shortcut so controllers don't need to take care of this in the backend.
|
|
*/
|
|
public function renderResponse(string $templateFileName = ''): ResponseInterface
|
|
{
|
|
$this->prepareRender($templateFileName);
|
|
return $this->pageRenderer->renderResponse($this->request);
|
|
}
|
|
|
|
private function prepareRender(string $templateFileName): void
|
|
{
|
|
if ($templateFileName === '') {
|
|
$extbaseRequestMessage = '';
|
|
/** @var ExtbaseRequestParameters|null $extbaseRequestParameters */
|
|
$extbaseRequestParameters = $this->request->getAttribute('extbase');
|
|
if ($extbaseRequestParameters) {
|
|
// This extbase specific code is a helper for a more detailed exception
|
|
// message, and a tribute to extbase backend extensions being upgraded.
|
|
// Introduced with v13, it could potentially vanish at some point again.
|
|
$templateFileName = $extbaseRequestParameters->getControllerName() . '/'
|
|
. ucfirst($extbaseRequestParameters->getControllerActionName());
|
|
$extbaseRequestMessage = ' Expected template filename is "' . $templateFileName . '".';
|
|
}
|
|
throw new \InvalidArgumentException('A template filename must be provided.' . $extbaseRequestMessage, 1732184506);
|
|
}
|
|
|
|
$this->assignMultiple([
|
|
'docHeader' => $this->docHeaderComponent->docHeaderContent($this->request),
|
|
'moduleId' => $this->moduleId,
|
|
'moduleName' => $this->moduleName,
|
|
'moduleClass' => $this->moduleClass,
|
|
'moduleLayout' => $this->moduleLayout->value,
|
|
'uiBlock' => $this->uiBlock,
|
|
'flashMessageQueueIdentifier' => $this->flashMessageQueue->getIdentifier(),
|
|
'formTag' => $this->formTag,
|
|
]);
|
|
$this->pageRenderer->getJavaScriptRenderer()->includeAllImports();
|
|
$this->pageRenderer->loadJavaScriptModule('bootstrap');
|
|
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/dropdown.js');
|
|
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/context-help.js');
|
|
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/global-event-handler.js');
|
|
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/action-dispatcher.js');
|
|
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/element/immediate-action-element.js');
|
|
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/hotkeys.js');
|
|
$this->pageRenderer->addBodyContent($this->bodyTag . $this->view->render($templateFileName));
|
|
$this->pageRenderer->setTitle($this->title);
|
|
$updateSignalDetails = BackendUtility::getUpdateSignalDetails();
|
|
if (!empty($updateSignalDetails['html'])) {
|
|
$this->pageRenderer->addHeaderData(implode("\n", $updateSignalDetails['html']));
|
|
}
|
|
$this->dispatchNotificationMessages();
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
public function setLayout(ModuleLayout $moduleLayout): self
|
|
{
|
|
$this->moduleLayout = $moduleLayout;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set to something like '<body id="foo">' when a special body tag is needed.
|
|
*/
|
|
public function setBodyTag(string $bodyTag): self
|
|
{
|
|
$this->bodyTag = $bodyTag;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Title string of the module: "My module · Edit view"
|
|
*/
|
|
public function setTitle(string $title, string $context = ''): self
|
|
{
|
|
$titleComponents = [$title];
|
|
if ($context !== '') {
|
|
$titleComponents[] = $context;
|
|
}
|
|
$this->title = implode(' · ', $titleComponents);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get the DocHeader. Can be used in controllers to add custom
|
|
* buttons / menus / ... to the doc header.
|
|
*/
|
|
public function getDocHeaderComponent(): DocHeaderComponent
|
|
{
|
|
return $this->docHeaderComponent;
|
|
}
|
|
|
|
/**
|
|
* A "<form>" tag encapsulating the entire module, including doc-header.
|
|
*/
|
|
public function setForm(string $formTag = ''): self
|
|
{
|
|
$this->formTag = $formTag;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Optional 'data-module-id="{moduleId}"' on first <div> in body.
|
|
* Can be helpful in JavaScript.
|
|
*/
|
|
public function setModuleId(string $moduleId): self
|
|
{
|
|
$this->moduleId = $moduleId;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Optional 'data-module-name="{moduleName}"' on first <div> in body.
|
|
* Can be helpful in JavaScript.
|
|
*/
|
|
public function setModuleName(string $moduleName): self
|
|
{
|
|
$this->moduleName = $moduleName;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Optional 'class="module {moduleClass}"' on first <div> in body.
|
|
* Can be helpful styling modules.
|
|
*/
|
|
public function setModuleClass(string $moduleClass): self
|
|
{
|
|
$this->moduleClass = $moduleClass;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Creates a message object and adds it to the FlashMessageQueue.
|
|
* These messages are automatically rendered when the view is rendered.
|
|
*/
|
|
public function addFlashMessage(string $messageBody, string $messageTitle = '', ContextualFeedbackSeverity $severity = ContextualFeedbackSeverity::OK, bool $storeInSession = true): self
|
|
{
|
|
$flashMessage = new FlashMessage($messageBody, $messageTitle, $severity, $storeInSession);
|
|
$this->flashMessageQueue->enqueue($flashMessage);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* ModuleTemplate by default uses queue 'core.template.flashMessages'. Modules
|
|
* may want to maintain an own queue. Use this method to render flash messages
|
|
* of a non-default queue at the default position in module HTML output. Call
|
|
* this method *before* adding single messages with addFlashMessage().
|
|
*/
|
|
public function setFlashMessageQueue(FlashMessageQueue $flashMessageQueue): self
|
|
{
|
|
$this->flashMessageQueue = $flashMessageQueue;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* UI block is a spinner shown during browser rendering phase of the module,
|
|
* automatically removed when rendering finished. This is done by default,
|
|
* but the UI block can be turned off when needed for whatever reason.
|
|
*/
|
|
public function setUiBlock(bool $uiBlock): self
|
|
{
|
|
$this->uiBlock = $uiBlock;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Generates a module actions dropdown in the docheader button bar.
|
|
*
|
|
* Creates a dropdown button on the LEFT side (group 0) containing navigation to
|
|
* submodules or module actions. The button label shows the currently active module/action.
|
|
*/
|
|
public function makeDocHeaderModuleMenu(array $additionalQueryParams = []): self
|
|
{
|
|
$currentModule = $this->request->getAttribute('module');
|
|
if (!($currentModule instanceof ModuleInterface)) {
|
|
// Early return in case the current request does not provide a module
|
|
return $this;
|
|
}
|
|
if ($currentModule->getParentModule()?->hasParentModule()) {
|
|
$menuModule = $this->moduleProvider->getModuleForMenu($currentModule->getParentIdentifier(), $this->getBackendUser());
|
|
} else {
|
|
// This is a fallback in case a second level module is called here
|
|
$menuModule = $this->moduleProvider->getModuleForMenu($currentModule->getIdentifier(), $this->getBackendUser());
|
|
}
|
|
|
|
if ($menuModule === null || !$menuModule->hasSubModules()) {
|
|
return $this;
|
|
}
|
|
|
|
$itemCount = 0;
|
|
$dropdownButton = $this->componentFactory->createDropDownButton()
|
|
->setLabel($this->getLanguageService()->sL('backend.messages:moduleMenu.dropdown.label'))
|
|
->setShowActiveLabelText(true)
|
|
->setShowLabelText(true);
|
|
|
|
// Add "Overview" link if exists
|
|
if ($menuModule->hasSubmoduleOverview()) {
|
|
$isActive = $menuModule->getIdentifier() === $currentModule->getIdentifier();
|
|
$overviewLabel = $this->getLanguageService()->sL('backend.messages:moduleMenu.dropdown.overview');
|
|
$dropdownItem = $this->componentFactory->createDropDownRadio()
|
|
->setHref((string)$this->uriBuilder->buildUriFromRoute($menuModule->getIdentifier(), $additionalQueryParams))
|
|
->setLabel($overviewLabel)
|
|
->setActive($isActive);
|
|
|
|
$dropdownButton->addItem($dropdownItem);
|
|
$itemCount++;
|
|
}
|
|
|
|
// Add all submodules
|
|
foreach ($menuModule->getSubModules() as $module) {
|
|
$isActive = $module->getIdentifier() === $currentModule->getIdentifier();
|
|
$moduleTitle = $this->getLanguageService()->sL($module->getTitle());
|
|
$dropdownItem = $this->componentFactory->createDropDownRadio()
|
|
->setHref((string)$this->uriBuilder->buildUriFromRoute($module->getIdentifier(), $additionalQueryParams))
|
|
->setLabel($moduleTitle)
|
|
->setActive($isActive);
|
|
|
|
$dropdownButton->addItem($dropdownItem);
|
|
$itemCount++;
|
|
}
|
|
|
|
// Only add dropdown if there's more than one item
|
|
if ($itemCount > 1) {
|
|
// Add to button bar at LEFT, group 0 (first position)
|
|
$this->getDocHeaderComponent()->getButtonBar()->addButton($dropdownButton, ButtonBar::BUTTON_POSITION_LEFT, 0);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Shorthand method to add a new button to the button bar
|
|
*/
|
|
public function addButtonToButtonBar(
|
|
ButtonInterface $button,
|
|
string $buttonPosition = ButtonBar::BUTTON_POSITION_LEFT,
|
|
int $buttonGroup = 1
|
|
): self {
|
|
$this->getDocHeaderComponent()->getButtonBar()->addButton($button, $buttonPosition, $buttonGroup);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Dispatches all messages in a special FlashMessageQueue to the PageRenderer to be rendered as inline notifications
|
|
*/
|
|
private function dispatchNotificationMessages(): void
|
|
{
|
|
$notificationQueue = $this->flashMessageService->getMessageQueueByIdentifier(FlashMessageQueue::NOTIFICATION_QUEUE);
|
|
foreach ($notificationQueue->getAllMessagesAndFlush() as $message) {
|
|
$notificationInstruction = JavaScriptModuleInstruction::create('@typo3/backend/notification.js');
|
|
$notificationInstruction->invoke('showMessage', $message->getTitle(), $message->getMessage(), $message->getSeverity());
|
|
$this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction($notificationInstruction);
|
|
}
|
|
}
|
|
|
|
private function getLanguageService(): LanguageService
|
|
{
|
|
return $GLOBALS['LANG'];
|
|
}
|
|
|
|
private function getBackendUser(): BackendUserAuthentication
|
|
{
|
|
return $GLOBALS['BE_USER'];
|
|
}
|
|
}
|