Commit 23644892 authored by Oliver Bartsch's avatar Oliver Bartsch Committed by Benni Mack

[TASK] Unify authentication form styles

Using backend extension configuration, it's possible
to customize the authentication forms, e.g. login and
password recovery.

Since MFA was added as another part of this process,
the corresponding authentication forms must also
respect the customized styling.

This is achieved by extracting the logic form
LoginController and adding it to a dedicated
class `AuthenticationStyleInformation`.

Resolves: #93898
Releases: master
Change-Id: Ia0209017592ac9b168934df3ed3ab8bc08a1913d
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/68765Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Jochen's avatarJochen <rothjochen@gmail.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Jochen's avatarJochen <rothjochen@gmail.com>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent a560b9b0
......@@ -29,8 +29,8 @@ use TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Backend\View\AuthenticationStyleInformation;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Configuration\Features;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Database\ConnectionPool;
......@@ -45,7 +45,6 @@ use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Localization\Locales;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
/**
......@@ -343,86 +342,29 @@ class LoginController implements LoggerAwareInterface
$this->view->assign('loginProviderIdentifier', $this->loginProviderIdentifier);
}
protected function provideCustomLoginStyling()
protected function provideCustomLoginStyling(): void
{
// Extension Configuration
$extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('backend');
// Background Image
if (!empty($extConf['loginBackgroundImage'])) {
$backgroundImage = $this->getUriForFileName($extConf['loginBackgroundImage']);
if ($backgroundImage === '') {
$this->logger->warning(
'The configured TYPO3 backend login background image "' . htmlspecialchars($extConf['loginBackgroundImage']) .
'" can\'t be resolved. Please check if the file exists and the extension is activated.'
);
}
$this->pageRenderer->addCssInlineBlock('loginBackgroundImage', '
.typo3-login-carousel-control.right,
.typo3-login-carousel-control.left,
.panel-login { border: 0; }
.typo3-login { background-image: url("' . $backgroundImage . '"); }
.typo3-login-footnote { background-color: #000000; color: #ffffff; opacity: 0.5; }
');
$authenticationStyleInformation = GeneralUtility::makeInstance(AuthenticationStyleInformation::class);
if (($backgroundImageStyles = $authenticationStyleInformation->getBackgroundImageStyles()) !== '') {
$this->pageRenderer->addCssInlineBlock('loginBackgroundImage', $backgroundImageStyles);
}
// Login Footnote
if (!empty($extConf['loginFootnote'])) {
$this->view->assign('loginFootnote', strip_tags(trim($extConf['loginFootnote'])));
}
// Add additional css to use the highlight color in the login screen
if (!empty($extConf['loginHighlightColor'])) {
$this->pageRenderer->addCssInlineBlock('loginHighlightColor', '
.btn-login.disabled, .btn-login[disabled], fieldset[disabled] .btn-login,
.btn-login.disabled:hover, .btn-login[disabled]:hover, fieldset[disabled] .btn-login:hover,
.btn-login.disabled:focus, .btn-login[disabled]:focus, fieldset[disabled] .btn-login:focus,
.btn-login.disabled.focus, .btn-login[disabled].focus, fieldset[disabled] .btn-login.focus,
.btn-login.disabled:active, .btn-login[disabled]:active, fieldset[disabled] .btn-login:active,
.btn-login.disabled.active, .btn-login[disabled].active, fieldset[disabled] .btn-login.active,
.btn-login:hover, .btn-login:focus, .btn-login:active,
.btn-login:active:hover, .btn-login:active:focus,
.btn-login { background-color: ' . $extConf['loginHighlightColor'] . '; }
.panel-login .panel-body { border-color: ' . $extConf['loginHighlightColor'] . '; }
');
if (($footerNote = $authenticationStyleInformation->getFooterNote()) !== '') {
$this->view->assign('loginFootnote', $footerNote);
}
// Logo
$logo = '';
$logoAlt = '';
if (!empty($extConf['loginLogo'])) {
if ($this->getUriForFileName($extConf['loginLogo']) === '') {
$this->logger->warning(
'The configured TYPO3 backend login logo "' . htmlspecialchars($extConf['loginLogo']) .
'" can\'t be resolved. Please check if the file exists and the extension is activated.'
);
} else {
$logo = $extConf['loginLogo'];
$logoAlt = trim($extConf['loginLogoAlt'] ?? '');
if (empty($logoAlt)) {
trigger_error('Login logo without alt-text is not accessible and will fall back to "TYPO3 CMS logo" in v12. Configure alt-text in the backend extension.', E_USER_DEPRECATED);
}
}
if (($highlightColorStyles = $authenticationStyleInformation->getHighlightColorStyles()) !== '') {
$this->pageRenderer->addCssInlineBlock('loginHighlightColor', $highlightColorStyles);
}
if (!$logo) {
// Use TYPO3 logo depending on highlight color
if (!empty($extConf['loginHighlightColor'])) {
$logo = 'EXT:backend/Resources/Public/Images/typo3_black.svg';
} else {
$logo = 'EXT:backend/Resources/Public/Images/typo3_orange.svg';
}
if (($logo = $authenticationStyleInformation->getLogo()) !== '') {
$logoAlt = $authenticationStyleInformation->getLogoAlt();
} else {
$logo = $authenticationStyleInformation->getDefaultLogo();
$logoAlt = $this->getLanguageService()->getLL('typo3.altText');
$this->pageRenderer->addCssInlineBlock('loginLogo', '
.typo3-login-logo .typo3-login-image { max-width: 150px; height:100%;}
');
$this->pageRenderer->addCssInlineBlock('loginLogo', $authenticationStyleInformation->getDefaultLogoStyles());
}
$this->view->assignMultiple([
'logo' => $this->getUriForFileName($logo),
'logo' => $logo,
'logoAlt' => $logoAlt,
'images' => [
'capslock' => $this->getUriForFileName('EXT:backend/Resources/Public/Images/icon_capslock.svg'),
'typo3' => $this->getUriForFileName('EXT:backend/Resources/Public/Images/typo3_orange.svg'),
],
'images' => $authenticationStyleInformation->getSupportingImages(),
'copyright' => $this->typo3Information->getCopyrightNotice(),
]);
}
......@@ -623,29 +565,6 @@ class LoginController implements LoggerAwareInterface
return $systemNews;
}
/**
* Returns the uri of a relative reference, resolves the "EXT:" prefix
* (way of referring to files inside extensions) and checks that the file is inside
* the project root of the TYPO3 installation
*
* @param string $filename The input filename/filepath to evaluate
* @return string Returns the filename of $filename if valid, otherwise blank string.
* @internal
*/
private function getUriForFileName($filename): string
{
// Check if it's already a URL
if (preg_match('/^(https?:)?\/\//', $filename)) {
return $filename;
}
$absoluteFilename = GeneralUtility::getFileAbsFileName(ltrim($filename, '/'));
$filename = '';
if ($absoluteFilename !== '' && @is_file($absoluteFilename)) {
$filename = PathUtility::getAbsoluteWebPath($absoluteFilename);
}
return $filename;
}
/**
* Checks if login credentials are currently submitted
*
......
......@@ -22,11 +22,16 @@ use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Backend\View\AuthenticationStyleInformation;
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderManifestInterface;
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager;
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderRegistry;
use TYPO3\CMS\Core\Authentication\Mfa\MfaViewType;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Page\PageRenderer;
/**
* Controller to provide a multi-factor authentication endpoint
......@@ -37,6 +42,21 @@ class MfaController extends AbstractMfaController implements LoggerAwareInterfac
{
use LoggerAwareTrait;
protected AuthenticationStyleInformation $authenticationStyleInformation;
protected PageRenderer $pageRenderer;
public function __construct(
UriBuilder $uriBuilder,
MfaProviderRegistry $mfaProviderRegistry,
ModuleTemplateFactory $moduleTemplateFactory,
AuthenticationStyleInformation $authenticationStyleInformation,
PageRenderer $pageRenderer
) {
parent::__construct($uriBuilder, $mfaProviderRegistry, $moduleTemplateFactory);
$this->authenticationStyleInformation = $authenticationStyleInformation;
$this->pageRenderer = $pageRenderer;
}
/**
* Main entry point, checking prerequisite, initializing and setting
* up the view and finally dispatching to the requested action.
......@@ -87,9 +107,11 @@ class MfaController extends AbstractMfaController implements LoggerAwareInterfac
'provider' => $mfaProvider,
'alternativeProviders' => $this->getAlternativeProviders($mfaProvider),
'isLocked' => $mfaProvider->isLocked($propertyManager),
'providerContent' => $providerResponse->getBody()
'providerContent' => $providerResponse->getBody(),
'footerNote' => $this->authenticationStyleInformation->getFooterNote()
]);
$this->moduleTemplate->setTitle('TYPO3 CMS Login: ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']);
$this->addCustomAuthenticationFormStyles();
return new HtmlResponse($this->moduleTemplate->renderContent());
}
......@@ -188,4 +210,14 @@ class MfaController extends AbstractMfaController implements LoggerAwareInterfac
}
return null;
}
protected function addCustomAuthenticationFormStyles(): void
{
if (($backgroundImageStyles = $this->authenticationStyleInformation->getBackgroundImageStyles()) !== '') {
$this->pageRenderer->addCssInlineBlock('loginBackgroundImage', $backgroundImageStyles);
}
if (($highlightColorStyles = $this->authenticationStyleInformation->getHighlightColorStyles()) !== '') {
$this->pageRenderer->addCssInlineBlock('loginHighlightColor', $highlightColorStyles);
}
}
}
<?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\View;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
/**
* Provide styling for backend authentication forms, customized via extension configuration.
*
* @internal
*/
class AuthenticationStyleInformation implements LoggerAwareInterface
{
use LoggerAwareTrait;
protected array $backendExtensionConfiguration;
public function __construct(ExtensionConfiguration $extensionConfiguration)
{
$this->backendExtensionConfiguration = (array)$extensionConfiguration->get('backend');
}
public function getBackgroundImageStyles(): string
{
$backgroundImage = (string)($this->backendExtensionConfiguration['loginBackgroundImage'] ?? '');
if ($backgroundImage === '') {
return '';
}
$backgroundImageUri = $this->getUriForFileName($backgroundImage);
if ($backgroundImageUri === '') {
$this->logger->warning(
'The configured TYPO3 backend login background image "' . htmlspecialchars($backgroundImageUri) .
'" can\'t be resolved. Please check if the file exists and the extension is activated.'
);
return '';
}
return '
.typo3-login-carousel-control.right,
.typo3-login-carousel-control.left,
.panel-login { border: 0; }
.typo3-login { background-image: url("' . $backgroundImageUri . '"); }
.typo3-login-footnote { background-color: #000000; color: #ffffff; opacity: 0.5; }
';
}
public function getHighlightColorStyles(): string
{
$highlightColor = (string)($this->backendExtensionConfiguration['loginHighlightColor'] ?? '');
if ($highlightColor === '') {
return '';
}
return '
.btn-login.disabled, .btn-login[disabled], fieldset[disabled] .btn-login,
.btn-login.disabled:hover, .btn-login[disabled]:hover, fieldset[disabled] .btn-login:hover,
.btn-login.disabled:focus, .btn-login[disabled]:focus, fieldset[disabled] .btn-login:focus,
.btn-login.disabled.focus, .btn-login[disabled].focus, fieldset[disabled] .btn-login.focus,
.btn-login.disabled:active, .btn-login[disabled]:active, fieldset[disabled] .btn-login:active,
.btn-login.disabled.active, .btn-login[disabled].active, fieldset[disabled] .btn-login.active,
.btn-login:hover, .btn-login:focus, .btn-login:active,
.btn-login:active:hover, .btn-login:active:focus,
.btn-login { background-color: ' . $highlightColor . '; }
.panel-login .panel-body { border-color: ' . $highlightColor . '; }
';
}
public function getFooterNote(): string
{
$footerNote = (string)($this->backendExtensionConfiguration['loginFootnote'] ?? '');
if ($footerNote === '') {
return '';
}
return strip_tags(trim($footerNote));
}
public function getLogo(): string
{
$logo = ($this->backendExtensionConfiguration['loginLogo'] ?? '');
if ($logo === '') {
return '';
}
$logoUri = $this->getUriForFileName($logo);
if ($logoUri === '') {
$this->logger->warning(
'The configured TYPO3 backend login logo "' . htmlspecialchars($logoUri) .
'" can\'t be resolved. Please check if the file exists and the extension is activated.'
);
return '';
}
return $logoUri;
}
public function getLogoAlt(): string
{
$logoAlt = trim((string)($this->backendExtensionConfiguration['loginLogoAlt'] ?? ''));
if ($logoAlt === '') {
trigger_error(
'Login logo without alt-text is not accessible and will fall back to "TYPO3 CMS logo" in v12. Configure alt-text in the backend extension.',
E_USER_DEPRECATED
);
}
return $logoAlt;
}
public function getDefaultLogo(): string
{
// Use TYPO3 logo depending on highlight color
$logo = ((string)($this->backendExtensionConfiguration['loginHighlightColor'] ?? '') !== '')
? 'EXT:backend/Resources/Public/Images/typo3_black.svg'
: 'EXT:backend/Resources/Public/Images/typo3_orange.svg';
return $this->getUriForFileName($logo);
}
public function getDefaultLogoStyles(): string
{
return '.typo3-login-logo .typo3-login-image { max-width: 150px; height:100%;}';
}
public function getSupportingImages(): array
{
return [
'capslock' => $this->getUriForFileName('EXT:backend/Resources/Public/Images/icon_capslock.svg'),
'typo3' => $this->getUriForFileName('EXT:backend/Resources/Public/Images/typo3_orange.svg'),
];
}
/**
* Returns the uri of a relative reference, resolves the "EXT:" prefix
* (way of referring to files inside extensions) and checks that the file is inside
* the project root of the TYPO3 installation
*
* @param string $filename The input filename/filepath to evaluate
* @return string Returns the filename of $filename if valid, otherwise blank string.
* @internal
*/
protected function getUriForFileName(string $filename): string
{
// Check if it's already a URL
if (preg_match('/^(https?:)?\/\//', $filename)) {
return $filename;
}
$absoluteFilename = GeneralUtility::getFileAbsFileName(ltrim($filename, '/'));
$filename = '';
if ($absoluteFilename !== '' && @is_file($absoluteFilename)) {
$filename = PathUtility::getAbsoluteWebPath($absoluteFilename);
}
return $filename;
}
}
......@@ -49,6 +49,9 @@ services:
TYPO3\CMS\Backend\Backend\Shortcut\ShortcutRepository:
public: true
TYPO3\CMS\Backend\View\AuthenticationStyleInformation:
public: true
TYPO3\CMS\Backend\Controller\BackendController:
tags: ['backend.controller']
......
......@@ -63,6 +63,11 @@
</div>
</div>
</div>
<f:if condition="{footerNote}">
<aside class="typo3-login-footnote" aria-label="{f:translate(key: 'login.region.footnote')}">
<p>{footerNote}</p>
</aside>
</f:if>
</div>
</div>
......
......@@ -20,6 +20,7 @@ namespace TYPO3\CMS\Backend\Tests\Functional\Controller;
use TYPO3\CMS\Backend\Controller\MfaController;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Backend\View\AuthenticationStyleInformation;
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderRegistry;
use TYPO3\CMS\Core\Authentication\Mfa\Provider\Totp;
use TYPO3\CMS\Core\Context\Context;
......@@ -27,6 +28,7 @@ use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
use TYPO3\CMS\Core\Http\ServerRequest;
use TYPO3\CMS\Core\Log\Logger;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
......@@ -46,6 +48,8 @@ class MfaControllerTest extends FunctionalTestCase
$container->get(UriBuilder::class),
$container->get(MfaProviderRegistry::class),
$container->get(ModuleTemplateFactory::class),
$container->get(AuthenticationStyleInformation::class),
$container->get(PageRenderer::class),
);
$this->subject->setLogger($this->prophesize(Logger::class)->reveal());
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment