Files
cms-backend/Classes/Controller/LoginController.php
T

299 lines
14 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\Controller;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\HttpFoundation\Cookie;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\LoginProvider\Event\ModifyPageLayoutOnLoginProviderSelectionEvent;
use TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface;
use TYPO3\CMS\Backend\LoginProvider\LoginProviderResolver;
use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
use TYPO3\CMS\Backend\Routing\RouteRedirect;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\PageRendererBackendSetupTrait;
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\Context\SecurityAspect;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\FormProtection\BackendFormProtection;
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Http\PropagateResponseException;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Information\Typo3Information;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Localization\Locales;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Routing\BackendEntryPointResolver;
use TYPO3\CMS\Core\Security\RequestToken;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\View\ViewFactoryData;
use TYPO3\CMS\Core\View\ViewFactoryInterface;
/**
* Controller responsible for rendering the TYPO3 Backend login form.
*
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
* @todo: The central template rendering magic needs an overhaul: Currently, LoginProviderInterface has to
* be implemented, which retrieves a "prepared" view with tons of variable used by the default Layout "Login.fluid.html".
* Single LoginProviderInterface then set their template path ("Login/UserPassLoginForm" in UsernamePasswordLoginProvider),
* which sets Login.fluid.html as layout in its template to then get its sections "loginFormFields" and "ResetPassword"
* rendered. This strategy is a major mess and needs to be turned around somehow.
* Note there is also this BE "relogin" and "login refresh" foo with lots of attached JS magic that
* should either be streamlined to actually work, or (preferred) be thrown away.
*/
#[AsController]
readonly class LoginController
{
use PageRendererBackendSetupTrait;
public function __construct(
protected Typo3Information $typo3Information,
protected EventDispatcherInterface $eventDispatcher,
protected PageRenderer $pageRenderer,
protected UriBuilder $uriBuilder,
protected Features $features,
protected Context $context,
protected LoginProviderResolver $loginProviderResolver,
protected ExtensionConfiguration $extensionConfiguration,
protected BackendEntryPointResolver $backendEntryPointResolver,
protected FormProtectionFactory $formProtectionFactory,
protected Locales $locales,
protected ConnectionPool $connectionPool,
protected AuthenticationStyleInformation $authenticationStyleInformation,
protected ViewFactoryInterface $viewFactory,
) {}
/**
* Injects the request and response objects for the current request or subrequest
* As this controller goes only through the main() method, it is rather simple for now
*/
public function formAction(ServerRequestInterface $request): ResponseInterface
{
return $this->createLoginLogout($request, (bool)($request->getParsedBody()['loginRefresh'] ?? $request->getQueryParams()['loginRefresh'] ?? false));
}
/**
* Calls the main function but with loginRefresh enabled at any time
*/
public function refreshAction(ServerRequestInterface $request): ResponseInterface
{
return $this->createLoginLogout($request, true);
}
/**
* @param bool $loginRefresh The backend triggers this with this value set when the login is
* close to being expired and the form needs to be redrawn.
* @throws PropagateResponseException
* @throws RouteNotFoundException
*/
protected function createLoginLogout(ServerRequestInterface $request, bool $loginRefresh): ResponseInterface
{
$backendUser = $this->getBackendUserAuthentication();
if (!empty($backendUser->user['uid'])) {
// If BE user is logged in, redirect to backend. Also handles "refresh" foo.
$this->checkRedirect($request, $loginRefresh);
}
$languageService = $this->getLanguageService();
if (empty($backendUser->user['uid'])) {
// If no user is logged in, initialize LanguageService with preferred browser language and set the
// language to the backend user object, so labels in fluid views are translated.
$httpAcceptLanguage = $request->getServerParams()['HTTP_ACCEPT_LANGUAGE'] ?? '';
$preferredBrowserLanguage = $this->locales->getPreferredClientLanguage($httpAcceptLanguage);
$languageService->init($this->locales->createLocale($preferredBrowserLanguage));
$backendUser->user['lang'] = $preferredBrowserLanguage;
}
if (($backgroundImageStyles = $this->authenticationStyleInformation->getBackgroundImageStyles($request)) !== '') {
$this->pageRenderer->addCssInlineBlock('loginBackgroundImage', $backgroundImageStyles, null, false, true);
}
if (($highlightColorStyles = $this->authenticationStyleInformation->getHighlightColorStyles()) !== '') {
$this->pageRenderer->addCssInlineBlock('loginHighlightColor', $highlightColorStyles, null, false, true);
}
$loginProviderIdentifier = $this->loginProviderResolver->resolveLoginProviderIdentifierFromRequest($request, 'be_lastLoginProvider');
if (empty($backendUser->user['uid'])) {
// Show login form
$action = 'login';
$formActionUrl = $this->uriBuilder->buildUriWithRedirect('login', ['loginProvider' => $loginProviderIdentifier], RouteRedirect::createFromRequest($request));
} else {
// Show logout form
$action = 'logout';
$formActionUrl = $this->uriBuilder->buildUriFromRoute('logout');
}
$forgotPasswordUrl = $this->uriBuilder->buildUriWithRedirect('password_forget', ['loginProvider' => $loginProviderIdentifier], RouteRedirect::createFromRequest($request));
$viewVariables = [
'copyright' => $this->typo3Information->getCopyrightNotice(),
'loginFootnote' => $this->authenticationStyleInformation->getFooterNote(),
'referrerCheckEnabled' => $this->features->isFeatureEnabled('security.backend.enforceReferrer'),
'loginUrl' => (string)$request->getUri(),
'loginProviderIdentifier' => $loginProviderIdentifier,
'backendUser' => $backendUser->user,
'hasLoginError' => $this->isLoginInProgress($request),
'action' => $action,
'formActionUrl' => $formActionUrl,
'requestTokenName' => RequestToken::PARAM_NAME,
'requestTokenValue' => $this->provideRequestTokenJwt(),
'forgetPasswordUrl' => $forgotPasswordUrl,
'loginRefresh' => $loginRefresh,
'loginProviders' => $this->loginProviderResolver->getLoginProviders(),
'loginNewsItems' => $this->getSystemNews(),
];
$this->setUpBasicPageRendererForBackend($this->pageRenderer, $this->extensionConfiguration, $request, $languageService);
$this->pageRenderer->setTitle('TYPO3 CMS Login: ' . ($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? ''));
$loginProviderConfiguration = $this->loginProviderResolver->getLoginProviderConfigurationByIdentifier($loginProviderIdentifier);
$loginProvider = GeneralUtility::makeInstance($loginProviderConfiguration['provider']);
if (!$loginProvider instanceof LoginProviderInterface) {
throw new \RuntimeException($loginProviderConfiguration['provider'] . ' must implement LoginProviderInterface', 1724772171);
}
$viewFactoryData = new ViewFactoryData(
templateRootPaths: ['EXT:backend/Resources/Private/Templates'],
partialRootPaths: ['EXT:backend/Resources/Private/Partials'],
layoutRootPaths: ['EXT:backend/Resources/Private/Layouts'],
request: $request,
);
$view = $this->viewFactory->create($viewFactoryData);
$view->assignMultiple($viewVariables);
$this->eventDispatcher->dispatch(new ModifyPageLayoutOnLoginProviderSelectionEvent($view, $request));
$templateFile = $loginProvider->modifyView($request, $view);
$content = $view->render($templateFile);
$this->pageRenderer->setBodyContent('<body>' . $content);
$response = $this->pageRenderer->renderResponse($request);
return $this->appendLoginProviderCookie($request, $response);
}
/**
* Returns a new request-token value, which is signed by a new nonce value (the nonce is sent
* as cookie automatically in `RequestTokenMiddleware` since it is created via the `NoncePool`).
*/
public function requestTokenAction(ServerRequestInterface $request): ResponseInterface
{
return new JsonResponse([
'headerName' => RequestToken::HEADER_NAME,
'requestToken' => $this->provideRequestTokenJwt(),
]);
}
/**
* @throws PropagateResponseException
*/
protected function checkRedirect(ServerRequestInterface $request, bool $loginRefresh): void
{
$formProtection = $this->formProtectionFactory->createFromRequest($request);
if (!$formProtection instanceof BackendFormProtection) {
throw new \RuntimeException('The Form Protection retrieved does not match the expected one.', 1432080411);
}
if ($loginRefresh) {
// Triggering `TYPO3/CMS/Backend/LoginRefresh` module happens in JS `TYPO3/CMS/Backend/Login`
$formProtection->setSessionTokenFromRegistry();
$formProtection->persistSessionToken();
} else {
$formProtection->storeSessionTokenInRegistry();
// @todo: Consolidate RouteDispatcher::evaluateReferrer() when changing 'main' to something different
$redirectToURL = (string)$this->uriBuilder->buildUriWithRedirect('main', [], RouteRedirect::createFromRequest($request));
throw new PropagateResponseException(new RedirectResponse($redirectToURL, 303), 1724705833);
}
}
/**
* If a login provider was chosen in the previous request, which is not the default provider,
* it is stored in a Cookie and appended to the HTTP Response.
*/
protected function appendLoginProviderCookie(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$normalizedParams = $request->getAttribute('normalizedParams');
$loginProviderIdentifier = $this->loginProviderResolver->resolveLoginProviderIdentifierFromRequest($request, 'be_lastLoginProvider');
if ($loginProviderIdentifier === $this->loginProviderResolver->getPrimaryLoginProviderIdentifier()) {
return $response;
}
$cookie = new Cookie(
'be_lastLoginProvider',
$loginProviderIdentifier,
$GLOBALS['EXEC_TIME'] + 7776000, // 90 days
$this->backendEntryPointResolver->getPathFromRequest($request),
'',
// Use the secure option when the current request is served by a secure connection
$normalizedParams->isHttps(),
true,
false,
Cookie::SAMESITE_STRICT
);
return $response->withAddedHeader('Set-Cookie', $cookie->__toString());
}
/**
* Gets news as array from sys_news and converts them into a
* format suitable for showing them at the login screen.
*/
protected function getSystemNews(): array
{
$systemNews = [];
$queryResult = $this->connectionPool
->getQueryBuilderForTable('sys_news')
->select('uid', 'title', 'content', 'crdate')
->from('sys_news')
->orderBy('crdate', 'DESC')
->executeQuery();
while ($row = $queryResult->fetchAssociative()) {
$systemNews[] = [
'uid' => $row['uid'],
'date' => $row['crdate'] ? date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], (int)$row['crdate']) : '',
'header' => $row['title'],
'content' => $row['content'],
];
}
return $systemNews;
}
/**
* Checks if login credentials have been submitted
*/
protected function isLoginInProgress(ServerRequestInterface $request): bool
{
// @todo: Restrict to POST?!
// Value of forms submit button for login. If set, the login button was pressed.
$submitValue = $request->getParsedBody()['commandLI'] ?? $request->getQueryParams()['commandLI'] ?? '';
$username = $request->getParsedBody()['username'] ?? $request->getQueryParams()['username'] ?? null;
return !empty($username) || !empty($submitValue);
}
protected function provideRequestTokenJwt(): string
{
$nonce = SecurityAspect::provideIn($this->context)->provideNonce();
return RequestToken::create('core/user-auth/be')->toHashSignedJwt($nonce);
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
protected function getBackendUserAuthentication(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}