Commit 5787565b authored by Christian Kuhn's avatar Christian Kuhn
Browse files

[TASK] Modernize various ext:backend views

A series of ext:backend related controllers and
views are cleaned up. Some of them need more work,
especially LoginController, BackendController and
NewRecordController. This can be done with further
patches, but for now:

* Use BackendTemplateView
* Hand over template to render as view->render('MyTemplate')
* Use DI in related controllers and ToolbarItems
* Declare strict_types=1
* Return and property types in PHP classes
* PageRenderer JS module loading via fluid, not PHP
* f:translate with full LLL: keys

Change-Id: I191c4799d90d6effa890e71c27eefabc26530213
Resolves: #96545
Related: #96513
Releases: main
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/73020

Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Stefan Bürk's avatarStefan Bürk <stefan@buerk.tech>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Stefan Bürk's avatarStefan Bürk <stefan@buerk.tech>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
parent 972d0ef4
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
......@@ -16,12 +18,12 @@
namespace TYPO3\CMS\Backend\Backend\Avatar;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Service\DependencyOrderingService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
/**
* Main class to render an avatar image of a certain Backend user, resolving any avatar provider
......@@ -32,92 +34,67 @@ use TYPO3\CMS\Fluid\View\StandaloneView;
class Avatar
{
/**
* Array of sorted and initialized avatar providers
* Sorted and initialized avatar providers
*
* @var AvatarProviderInterface[]
*/
protected $avatarProviders = [];
protected array $avatarProviders = [];
protected FrontendInterface $cache;
protected DependencyOrderingService $dependencyOrderingService;
protected IconFactory $iconFactory;
public function __construct(
FrontendInterface $cache,
DependencyOrderingService $dependencyOrderingService,
IconFactory $iconFactory
) {
$this->cache = $cache;
$this->dependencyOrderingService = $dependencyOrderingService;
$this->iconFactory = $iconFactory;
}
/**
* Renders an avatar based on a Fluid template which contains some base wrapper classes and does
* a simple caching functionality, used in Avatar ViewHelper for instance
*
* @param array $backendUser be_users record
* @param int $size width and height of the image
* @param bool $showIcon show the record icon
* @return string
* Renders an avatar based on a Fluid template which contains some base wrapper css classes.
* Has a simple caching functionality. Used in Avatar ViewHelper for instance.
* Renders avatar of a given backend user record, or of current logged-in backend user.
*/
public function render(array $backendUser = null, int $size = 32, bool $showIcon = false)
public function render(array $backendUser = null, int $size = 32, bool $showIcon = false): string
{
if (!is_array($backendUser)) {
/** @var array $backendUser */
$backendUser = $this->getBackendUser()->user;
}
$cacheId = 'avatar_' . md5(
$backendUser['uid'] . '/' .
(string)$size . '/' .
(string)$showIcon
);
$avatar = $this->getCache()->get($cacheId);
$cacheId = 'avatar_' . sha1($backendUser['uid'] . $size . $showIcon);
$avatar = $this->cache->get($cacheId);
if (!$avatar) {
$this->validateSortAndInitiateAvatarProviders();
$view = $this->getFluidTemplateObject();
$view->assignMultiple([
'image' => $this->getImgTag($backendUser, $size),
'showIcon' => $showIcon,
'backendUser' => $backendUser,
]);
$avatar = $view->render();
$this->getCache()->set($cacheId, $avatar);
$icon = $showIcon ? $this->iconFactory->getIconForRecord('be_users', $backendUser, Icon::SIZE_SMALL)->render() : '';
$avatar =
'<span class="avatar">'
. '<span class="avatar-image">' . $this->getImgTag($backendUser, $size) . '</span>'
. ($showIcon ? '<span class="avatar-icon">' . $icon . '</span>' : '')
. '</span>';
$this->cache->set($cacheId, $avatar);
}
return $avatar;
}
/**
* Returns an HTML <img> tag for the avatar
*
* @param array $backendUser be_users record
* @param int $size
* @return string
* Returns an HTML <img> tag of given backend users avatar.
*/
public function getImgTag(array $backendUser = null, $size = 32)
protected function getImgTag(array $backendUser, int $size = 32): string
{
if (!is_array($backendUser)) {
$backendUser = $this->getBackendUser()->user;
}
$avatarImage = false;
if ($backendUser !== null) {
$avatarImage = $this->getImage($backendUser, $size);
}
if (!$avatarImage) {
$avatarImage = GeneralUtility::makeInstance(
Image::class,
PathUtility::getPublicResourceWebPath('EXT:core/Resources/Public/Icons/T3Icons/svgs/avatar/avatar-default.svg'),
$size,
$size
);
}
$imageTag = '<img src="' . htmlspecialchars($avatarImage->getUrl()) . '" ' .
$avatarImage = $this->getImage($backendUser, $size);
return '<img src="' . htmlspecialchars($avatarImage->getUrl()) . '" ' .
'width="' . (int)$avatarImage->getWidth() . '" ' .
'height="' . (int)$avatarImage->getHeight() . '" />';
return $imageTag;
}
/**
* Get Image from first provider that returns one
*
* @param array $backendUser be_users record
* @param int $size
* @return Image|null
* Get Image from first provider that returns one.
*/
public function getImage(array $backendUser, $size)
protected function getImage(array $backendUser, int $size): Image
{
foreach ($this->avatarProviders as $provider) {
$avatarImage = $provider->getImage($backendUser, $size);
......@@ -125,7 +102,12 @@ class Avatar
return $avatarImage;
}
}
return null;
return GeneralUtility::makeInstance(
Image::class,
PathUtility::getPublicResourceWebPath('EXT:core/Resources/Public/Icons/T3Icons/svgs/avatar/avatar-default.svg'),
$size,
$size
);
}
/**
......@@ -133,9 +115,9 @@ class Avatar
*
* @throws \RuntimeException
*/
protected function validateSortAndInitiateAvatarProviders()
protected function validateSortAndInitiateAvatarProviders(): void
{
/** @var array<string,array<mixed>> $providers */
/** @var array<string,array> $providers */
$providers = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['avatarProviders'] ?? [];
if (empty($providers)) {
return;
......@@ -157,10 +139,7 @@ class Avatar
);
}
}
$orderedProviders = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($providers);
// Initializes providers
$orderedProviders = $this->dependencyOrderingService->orderByDependencies($providers);
foreach ($orderedProviders as $configuration) {
/** @var AvatarProviderInterface $avatarProvider */
$avatarProvider = GeneralUtility::makeInstance($configuration['provider']);
......@@ -168,44 +147,8 @@ class Avatar
}
}
/**
* Returns the current BE user.
*
* @return BackendUserAuthentication
*/
protected function getBackendUser()
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
/**
* Returns a new standalone view, shorthand function
*
* @param string $filename Which templateFile should be used.
* @return StandaloneView
*/
protected function getFluidTemplateObject(string $filename = null): StandaloneView
{
$view = GeneralUtility::makeInstance(StandaloneView::class);
$view->setLayoutRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts')]);
$view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
$view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]);
if ($filename === null) {
$filename = 'Main.html';
}
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/Avatar/' . $filename));
$view->getRequest()->setControllerExtensionName('Backend');
return $view;
}
/**
* @return FrontendInterface
*/
protected function getCache()
{
return GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
}
}
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
......@@ -18,15 +20,13 @@ namespace TYPO3\CMS\Backend\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Module\ModuleLoader;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Information\Typo3Information;
use TYPO3\CMS\Core\Information\Typo3Version;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
use TYPO3Fluid\Fluid\View\ViewInterface;
use TYPO3\CMS\Fluid\View\BackendTemplateView;
/**
* Module 'about' shows some standard information for TYPO3 CMS:
......@@ -36,18 +36,6 @@ use TYPO3Fluid\Fluid\View\ViewInterface;
*/
class AboutController
{
/**
* ModuleTemplate object
*
* @var ModuleTemplate
*/
protected $moduleTemplate;
/**
* @var ViewInterface
*/
protected $view;
protected Typo3Version $version;
protected Typo3Information $typo3Information;
protected ModuleLoader $moduleLoader;
......@@ -72,14 +60,9 @@ class AboutController
/**
* Main action: Show standard information
*
* @param ServerRequestInterface $request
* @return ResponseInterface the HTML output
*/
public function indexAction(ServerRequestInterface $request): ResponseInterface
public function handleRequest(ServerRequestInterface $request): ResponseInterface
{
$this->moduleTemplate = $this->moduleTemplateFactory->create($request);
$this->initializeView('index');
$warnings = [];
// Hook for additional warnings
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['displayWarningMessages'] ?? [] as $className) {
......@@ -89,7 +72,10 @@ class AboutController
}
}
$this->view->assignMultiple([
$view = GeneralUtility::makeInstance(BackendTemplateView::class);
$view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates']);
$view->setPartialRootPaths(['EXT:backend/Resources/Private/Partials']);
$view->assignMultiple([
'copyrightYear' => $this->typo3Information->getCopyrightYear(),
'donationUrl' => $this->typo3Information::URL_DONATE,
'currentVersion' => $this->version->getVersion(),
......@@ -98,16 +84,14 @@ class AboutController
'warnings' => $warnings,
'modules' => $this->getModulesData(),
]);
$this->moduleTemplate->setContent($this->view->render());
return new HtmlResponse($this->moduleTemplate->renderContent());
$moduleTemplate = $this->moduleTemplateFactory->create($request);
$moduleTemplate->setContent($view->render('About/Index'));
return new HtmlResponse($moduleTemplate->renderContent());
}
/**
* Create array with data of all main modules (Web, File, ...)
* and its nested sub modules
*
* @return array
* and its nested sub modules.
*/
protected function getModulesData(): array
{
......@@ -128,16 +112,12 @@ class AboutController
/**
* Create array with data of all subModules of a specific main module
*
* @param string $moduleName Name of the main module
* @return array
*/
protected function getSubModuleData(string $moduleName): array
{
if (empty($this->moduleLoader->getModules()[$moduleName]['sub'])) {
return [];
}
$subModulesData = [];
foreach ($this->moduleLoader->getModules()[$moduleName]['sub'] ?? [] as $subModuleName => $subModuleInfo) {
$moduleLabels = $this->moduleLoader->getLabelsForModule($moduleName . '_' . $subModuleName);
......@@ -155,8 +135,6 @@ class AboutController
/**
* Fetches a list of all active (loaded) extensions in the current system
*
* @return array
*/
protected function getLoadedExtensions(): array
{
......@@ -174,18 +152,4 @@ class AboutController
}
return $extensions;
}
/**
* Initializes the view by setting the templateName
*
* @param string $templateName
*/
protected function initializeView(string $templateName)
{
$this->view = GeneralUtility::makeInstance(StandaloneView::class);
$this->view->setTemplate($templateName);
$this->view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates/About']);
$this->view->setPartialRootPaths(['EXT:backend/Resources/Private/Partials/About']);
$this->view->setLayoutRootPaths(['EXT:backend/Resources/Private/Layouts']);
}
}
......@@ -38,7 +38,7 @@ use TYPO3\CMS\Core\Type\File\ImageInfo;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
use TYPO3\CMS\Fluid\View\BackendTemplateView;
/**
* Class for rendering the TYPO3 backend
......@@ -55,11 +55,6 @@ class BackendController
*/
protected $toolbarItems = [];
/**
* @var string
*/
protected $templatePath = 'EXT:backend/Resources/Private/Templates/';
protected BackendModuleRepository $backendModuleRepository;
protected PageRenderer $pageRenderer;
protected IconFactory $iconFactory;
......@@ -164,8 +159,7 @@ class BackendController
// Prepare the scaffolding, at this point extension may still add javascript and css
$moduleTemplate = $this->moduleTemplateFactory->create($request);
$view = $moduleTemplate->getView();
$view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName($this->templatePath . 'Backend/Main.html'));
$view->setTemplatePathAndFilename('EXT:backend/Resources/Private/Templates/Backend/Main.html');
$moduleTemplate->setBodyTag($bodyTag);
$view->assign('moduleMenu', $this->generateModuleMenu());
$view->assign('topbar', $this->renderTopbar());
......@@ -198,8 +192,6 @@ class BackendController
*/
protected function renderTopbar()
{
$view = $this->getFluidTemplateObject($this->templatePath . 'Backend/Topbar.html');
// Extension Configuration to find the TYPO3 logo in the left corner
$extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('backend');
$logoPath = '';
......@@ -227,6 +219,7 @@ class BackendController
}
}
$view = $this->getFluidTemplateObject();
$view->assign('hasModules', count($this->moduleStorage) > 0);
$view->assign('logoUrl', PathUtility::getAbsoluteWebPath($logoPath));
$view->assign('logoWidth', $logoWidth);
......@@ -234,8 +227,7 @@ class BackendController
$view->assign('applicationVersion', $this->typo3Version->getVersion());
$view->assign('siteName', $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']);
$view->assign('toolbar', $this->renderToolbar());
return $view->render();
return $view->render('Backend/Topbar.html');
}
/**
......@@ -437,9 +429,9 @@ class BackendController
*/
protected function generateModuleMenu()
{
$view = $this->getFluidTemplateObject($this->templatePath . 'Backend/ModuleMenu.html');
$view = $this->getFluidTemplateObject();
$view->assign('modules', $this->moduleStorage);
return $view->render();
return $view->render('Backend/ModuleMenu.html');
}
protected function getCollapseStateOfMenu(): bool
......@@ -470,19 +462,10 @@ class BackendController
return new JsonResponse(['topbar' => $this->renderTopbar()]);
}
/**
* returns a new standalone view, shorthand function
*
* @param string $templatePathAndFileName optional the path to set the template path and filename
* @return \TYPO3\CMS\Fluid\View\StandaloneView
*/
protected function getFluidTemplateObject($templatePathAndFileName = null)
protected function getFluidTemplateObject(): BackendTemplateView
{
$view = GeneralUtility::makeInstance(StandaloneView::class);
$view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
if ($templatePathAndFileName) {
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName($templatePathAndFileName));
}
$view = GeneralUtility::makeInstance(BackendTemplateView::class);
$view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates/']);
return $view;
}
......
......@@ -31,11 +31,11 @@ use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Information\Typo3Information;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
use TYPO3Fluid\Fluid\View\ViewInterface;
use TYPO3\CMS\Fluid\View\BackendTemplateView;
/**
* Main "CSH help" module controller
* Main "CSH help" module controller.
*
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
*/
class HelpController
......@@ -52,12 +52,6 @@ class HelpController
*/
const TOC_ONLY = 1;
/** @var ModuleTemplate */
protected $moduleTemplate;
/** @var ViewInterface */
protected $view;
protected Typo3Information $typo3Information;
protected TableManualRepository $tableManualRepository;
protected IconFactory $iconFactory;
......@@ -80,100 +74,75 @@ class HelpController
/**
* Injects the request object for the current request, and renders correct action
*
* @param ServerRequestInterface $request the current request
* @return ResponseInterface the response with the content
*/
public function handleRequest(ServerRequestInterface $request): ResponseInterface
{
$this->moduleTemplate = $this->moduleTemplateFactory->create($request);
$action = $request->getQueryParams()['action'] ?? $request->getParsedBody()['action'] ?? 'index';
$action = $request->getQueryParams()['action'] ?? 'index';
if ($action === 'detail') {
$table = $request->getQueryParams()['table'] ?? $request->getParsedBody()['table'];
$table = $request->getQueryParams()['table'] ?? '';
if (!$table) {
return new RedirectResponse((string)$this->uriBuilder->buildUriFromRoute('help_cshmanual', [
'action' => 'index',
]), 303);
}
}
if (!in_array($action, self::ALLOWED_ACTIONS, true)) {
return new HtmlResponse('Action not allowed', 400);
}
$this->initializeView($action);
$result = $this->{$action . 'Action'}($request);
if ($result instanceof ResponseInterface) {
return $result;
}
$this->registerDocHeaderButtons($request);
$this->moduleTemplate->setTitle($this->getShortcutTitle($request));
$this->moduleTemplate->setContent($this->view->render());
return new HtmlResponse($this->moduleTemplate->renderContent());
}
/**
* @param string $templateName
*/
protected function initializeView(string $templateName)
{
$this->view = GeneralUtility::makeInstance(StandaloneView::class);
$this->view->setTemplate($templateName);
$this->view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates/ContextSensitiveHelp']);
$this->view->setPartialRootPaths(['EXT:backend/Resources/Private/Partials']);
$this->view->setLayoutRootPaths(['EXT:backend/Resources/Private/Layouts']);
$this->view->getRequest()->setControllerExtensionName('Backend');
$this->view->assign('copyright', $this->typo3Information->getCopyrightNotice());
$moduleTemplate = $this->moduleTemplateFactory->create($request);
$moduleTemplate->setTitle($this->getShortcutTitle($request));
return $this->{$action . 'Action'}($moduleTemplate, $request);
}
/**
* Show table of contents
*/
public function indexAction()
protected function indexAction(ModuleTemplate $moduleTemplate, ServerRequestInterface $request): ResponseInterface
{
$this->view->assign('toc', $this->tableManualRepository->getSections(self::TOC_ONLY));
$view = $this->initializeView();
$view->assign('toc', $this->tableManualRepository->getSections(self::TOC_ONLY));
$moduleTemplate->setContent($view->render('ContextSensitiveHelp/Index'));
$this->addShortcutButton($moduleTemplate, $request);
return new HtmlResponse($moduleTemplate->renderContent());
}
/**
* Show the table of contents and all manuals
*/
public function allAction()
protected function allAction(ModuleTemplate $moduleTemplate, ServerRequestInterface $request): ResponseInterface
{
$this->view->assign('all', $this->tableManualRepository->getSections(self::FULL));
$view = $this->initializeView();
$view->assign('all', $this->tableManualRepository->getSections(self::FULL));
$moduleTemplate->setContent($view->render('ContextSensitiveHelp/All'));
$this->addShortcutButton($moduleTemplate, $request);
$this->addBackButton($moduleTemplate);
return new HtmlResponse($moduleTemplate->renderContent());
}
/**
* Show a single manual
*
* @param ServerRequestInterface $request
*/
public function detailAction(ServerRequestInterface $request)
protected function detailAction(ModuleTemplate $moduleTemplate, ServerRequestInterface $request): ResponseInterface
{
$view = $this->initializeView();
$table = $request->getQueryParams()['table'] ?? $request->getParsedBody()['table'];
$field = $request->getQueryParams()['field'] ?? $request->getParsedBody()['field'] ?? '*';
$this->view->assignMultiple([
$view->assignMultiple([
'table' => $table,
'key' => $table,
'field' => $field,
'manuals' => $this->getManuals($request),
]);
$moduleTemplate->setContent($view->render('ContextSensitiveHelp/Detail'));
$this->addShortcutButton($moduleTemplate, $request);
$this->addBackButton($moduleTemplate);
return new HtmlResponse($moduleTemplate->renderContent());
}