diff --git a/typo3/sysext/backend/Classes/Controller/LoginController.php b/typo3/sysext/backend/Classes/Controller/LoginController.php index 1f85da1d10ca4ac321df3b8fe52035c93f0dd701..befdba4a414d441d7d2602a410168faf146188e3 100644 --- a/typo3/sysext/backend/Classes/Controller/LoginController.php +++ b/typo3/sysext/backend/Classes/Controller/LoginController.php @@ -21,7 +21,6 @@ use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Symfony\Component\HttpFoundation\Cookie; -use TYPO3\CMS\Backend\Authentication\PasswordReset; use TYPO3\CMS\Backend\LoginProvider\Event\ModifyPageLayoutOnLoginProviderSelectionEvent; use TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface; use TYPO3\CMS\Backend\Routing\UriBuilder; @@ -157,122 +156,6 @@ class LoginController return new HtmlResponse($this->createLoginLogoutForm($request)); } - /** - * Show a form to enter an email address to request an email. - * - * @param ServerRequestInterface $request - * @return ResponseInterface - */ - public function forgetPasswordFormAction(ServerRequestInterface $request): ResponseInterface - { - // Only allow to execute this if not logged in as a user right now - if ($this->context->getAspect('backend.user')->isLoggedIn()) { - return $this->formAction($request); - } - $this->init($request); - // Enable the switch in the template - $this->view->assign('enablePasswordReset', GeneralUtility::makeInstance(PasswordReset::class)->isEnabled()); - $this->view->setTemplate('Login/ForgetPasswordForm'); - $this->moduleTemplate->setContent($this->view->render()); - return new HtmlResponse($this->moduleTemplate->renderContent()); - } - - /** - * Validate the email address. - * - * Restricted to POST method in Configuration/Backend/Routes.php - * - * @param ServerRequestInterface $request - * @return ResponseInterface - */ - public function initiatePasswordResetAction(ServerRequestInterface $request): ResponseInterface - { - // Only allow to execute this if not logged in as a user right now - if ($this->context->getAspect('backend.user')->isLoggedIn()) { - return $this->formAction($request); - } - $this->init($request); - $passwordReset = GeneralUtility::makeInstance(PasswordReset::class); - $this->view->assign('enablePasswordReset', $passwordReset->isEnabled()); - $this->view->setTemplate('Login/ForgetPasswordForm'); - - $emailAddress = $request->getParsedBody()['email'] ?? ''; - $this->view->assign('email', $emailAddress); - if (!GeneralUtility::validEmail($emailAddress)) { - $this->view->assign('invalidEmail', true); - } else { - $passwordReset->initiateReset($request, $this->context, $emailAddress); - $this->view->assign('resetInitiated', true); - } - $this->moduleTemplate->setContent($this->view->render()); - // Prevent time based information disclosure by waiting a random time - // before sending a response. This prevents that the reponse time - // can be an indicator if the used email exists or not. - // wait a random time between 200 milliseconds and 3 seconds. - usleep(random_int(200000, 3000000)); - return new HtmlResponse($this->moduleTemplate->renderContent()); - } - - /** - * Validates the link and show a form to enter the new password. - * - * @param ServerRequestInterface $request - * @return ResponseInterface - */ - public function passwordResetAction(ServerRequestInterface $request): ResponseInterface - { - // Only allow to execute this if not logged in as a user right now - if ($this->context->getAspect('backend.user')->isLoggedIn()) { - return $this->formAction($request); - } - $this->init($request); - $passwordReset = GeneralUtility::makeInstance(PasswordReset::class); - $this->view->setTemplate('Login/ResetPasswordForm'); - $this->view->assign('enablePasswordReset', $passwordReset->isEnabled()); - if (!$passwordReset->isValidResetTokenFromRequest($request)) { - $this->view->assign('invalidToken', true); - } - $this->view->assign('token', $request->getQueryParams()['t'] ?? ''); - $this->view->assign('identity', $request->getQueryParams()['i'] ?? ''); - $this->view->assign('expirationDate', $request->getQueryParams()['e'] ?? ''); - $this->moduleTemplate->setContent($this->view->render()); - return new HtmlResponse($this->moduleTemplate->renderContent()); - } - - /** - * Updates the password in the database. - * - * Restricted to POST method in Configuration/Backend/Routes.php - * - * @param ServerRequestInterface $request - * @return ResponseInterface - */ - public function passwordResetFinishAction(ServerRequestInterface $request): ResponseInterface - { - // Only allow to execute this if not logged in as a user right now - if ($this->context->getAspect('backend.user')->isLoggedIn()) { - return $this->formAction($request); - } - $passwordReset = GeneralUtility::makeInstance(PasswordReset::class); - // Token is invalid - if (!$passwordReset->isValidResetTokenFromRequest($request)) { - return $this->passwordResetAction($request); - } - $this->init($request); - $this->view->setTemplate('Login/ResetPasswordForm'); - $this->view->assign('enablePasswordReset', $passwordReset->isEnabled()); - $this->view->assign('token', $request->getQueryParams()['t'] ?? ''); - $this->view->assign('identity', $request->getQueryParams()['i'] ?? ''); - $this->view->assign('expirationDate', $request->getQueryParams()['e'] ?? ''); - if ($passwordReset->resetPassword($request, $this->context)) { - $this->view->assign('resetExecuted', true); - } else { - $this->view->assign('error', true); - } - $this->moduleTemplate->setContent($this->view->render()); - return new HtmlResponse($this->moduleTemplate->renderContent()); - } - /** * This can be called by single login providers, they receive an instance of $this * @@ -397,6 +280,11 @@ class LoginController 'hasLoginError' => $this->isLoginInProgress($request), 'action' => $action, 'formActionUrl' => $formActionUrl, + 'forgetPasswordUrl' => $this->uriBuilder->buildUriWithRedirectFromRequest( + 'password_forget', + ['loginProvider' => $this->loginProviderIdentifier], + $request + ), 'redirectUrl' => $this->redirectUrl, 'loginRefresh' => $this->loginRefresh, 'loginProviders' => $this->loginProviders, diff --git a/typo3/sysext/backend/Classes/Controller/ResetPasswordController.php b/typo3/sysext/backend/Classes/Controller/ResetPasswordController.php new file mode 100644 index 0000000000000000000000000000000000000000..dd045dde8ee334dffce186d2d3fb20d1d6c765b1 --- /dev/null +++ b/typo3/sysext/backend/Classes/Controller/ResetPasswordController.php @@ -0,0 +1,271 @@ +<?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\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Backend\Authentication\PasswordReset; +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\Configuration\Features; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Http\HtmlResponse; +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\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Mvc\View\ViewInterface; + +/** + * Controller responsible for rendering and processing password reset requests + * + * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API. + */ +class ResetPasswordController +{ + protected string $loginProvider = ''; + protected ?ViewInterface $view = null; + protected ?ModuleTemplate $moduleTemplate = null; + + protected Context $context; + protected Locales $locales; + protected Features $features; + protected UriBuilder $uriBuilder; + protected PageRenderer $pageRenderer; + protected PasswordReset $passwordReset; + protected Typo3Information $typo3Information; + protected ModuleTemplateFactory $moduleTemplateFactory; + protected AuthenticationStyleInformation $authenticationStyleInformation; + + public function __construct( + Context $context, + Locales $locales, + Features $features, + UriBuilder $uriBuilder, + PageRenderer $pageRenderer, + PasswordReset $passwordReset, + Typo3Information $typo3Information, + ModuleTemplateFactory $moduleTemplateFactory, + AuthenticationStyleInformation $authenticationStyleInformation + ) { + $this->context = $context; + $this->locales = $locales; + $this->features = $features; + $this->uriBuilder = $uriBuilder; + $this->pageRenderer = $pageRenderer; + $this->passwordReset = $passwordReset; + $this->typo3Information = $typo3Information; + $this->moduleTemplateFactory = $moduleTemplateFactory; + $this->authenticationStyleInformation = $authenticationStyleInformation; + } + + /** + * Show a form to enter an email address to request a password reset email. + * + * @param ServerRequestInterface $request + * @return ResponseInterface + */ + public function forgetPasswordFormAction(ServerRequestInterface $request): ResponseInterface + { + $this->initializeForgetPasswordView($request); + $this->moduleTemplate->setContent($this->view->render()); + return new HtmlResponse($this->moduleTemplate->renderContent()); + } + + /** + * Validate the email address. + * + * Restricted to POST method in Configuration/Backend/Routes.php + * + * @param ServerRequestInterface $request + * @return ResponseInterface + */ + public function initiatePasswordResetAction(ServerRequestInterface $request): ResponseInterface + { + $this->initializeForgetPasswordView($request); + $emailAddress = $request->getParsedBody()['email'] ?? ''; + $this->view->assign('email', $emailAddress); + if (!GeneralUtility::validEmail($emailAddress)) { + $this->view->assign('invalidEmail', true); + } else { + $this->passwordReset->initiateReset($request, $this->context, $emailAddress); + $this->view->assign('resetInitiated', true); + } + $this->moduleTemplate->setContent($this->view->render()); + // Prevent time based information disclosure by waiting a random time + // before sending a response. This prevents that the response time + // can be an indicator if the used email exists or not. Wait a random + // time between 200 milliseconds and 3 seconds. + usleep(random_int(200000, 3000000)); + return new HtmlResponse($this->moduleTemplate->renderContent()); + } + + /** + * Validates the link and show a form to enter the new password. + * + * @param ServerRequestInterface $request + * @return ResponseInterface + */ + public function passwordResetAction(ServerRequestInterface $request): ResponseInterface + { + $this->initializeResetPasswordView($request); + if (!$this->passwordReset->isValidResetTokenFromRequest($request)) { + $this->view->assign('invalidToken', true); + } + $this->moduleTemplate->setContent($this->view->render()); + return new HtmlResponse($this->moduleTemplate->renderContent()); + } + + /** + * Updates the password in the database. + * + * Restricted to POST method in Configuration/Backend/Routes.php + * + * @param ServerRequestInterface $request + * @return ResponseInterface + */ + public function passwordResetFinishAction(ServerRequestInterface $request): ResponseInterface + { + // Token is invalid + if (!$this->passwordReset->isValidResetTokenFromRequest($request)) { + return $this->passwordResetAction($request); + } + $this->initializeResetPasswordView($request); + if ($this->passwordReset->resetPassword($request, $this->context)) { + $this->view->assign('resetExecuted', true); + } else { + $this->view->assign('error', true); + } + $this->moduleTemplate->setContent($this->view->render()); + return new HtmlResponse($this->moduleTemplate->renderContent()); + } + + protected function initializeForgetPasswordView(ServerRequestInterface $request): void + { + $this->initialize($request); + $this->view->setTemplate('Login/ForgetPasswordForm'); + $parameters = array_filter(['loginProvider' => $this->loginProvider]); + $this->view->assignMultiple([ + 'formUrl' => $this->uriBuilder->buildUriWithRedirectFromRequest('password_forget_initiate_reset', $parameters, $request), + 'returnUrl' => $this->uriBuilder->buildUriWithRedirectFromRequest('login', $parameters, $request) + ]); + } + + protected function initializeResetPasswordView(ServerRequestInterface $request): void + { + $this->initialize($request); + $token = $request->getQueryParams()['t'] ?? ''; + $identity = $request->getQueryParams()['i'] ?? ''; + $expirationDate = $request->getQueryParams()['e'] ?? ''; + $parameters = array_filter(['loginProvider' => $this->loginProvider]); + $formUrl = $this->uriBuilder->buildUriWithRedirectFromRequest( + 'password_reset_finish', + array_filter(array_merge($parameters, [ + 't' => $token, + 'i' => $identity, + 'e' => $expirationDate + ])), + $request + ); + $this->view->setTemplate('Login/ResetPasswordForm'); + $this->view->assignMultiple([ + 'token' => $token, + 'identity' => $identity, + 'expirationDate' => $expirationDate, + 'formUrl' => $formUrl, + 'restartUrl' => $this->uriBuilder->buildUriWithRedirectFromRequest('password_forget', $parameters, $request) + ]); + } + + protected function initialize(ServerRequestInterface $request): void + { + // Only allow to execute this if not logged in as a user right now + if ($this->context->getAspect('backend.user')->isLoggedIn()) { + throw new PropagateResponseException( + new RedirectResponse($this->uriBuilder->buildUriFromRoute('login'), 303), + 1618342858 + ); + } + + // Fetch login provider from the request + $this->loginProvider = $request->getQueryParams()['loginProvider'] ?? ''; + + // Try to get the preferred browser language + $httpAcceptLanguage = $request->getServerParams()['HTTP_ACCEPT_LANGUAGE']; + $preferredBrowserLanguage = $this->locales->getPreferredClientLanguage($httpAcceptLanguage); + + // If we found a $preferredBrowserLanguage and it is not the default language + // initialize $this->getLanguageService() again with $preferredBrowserLanguage + if ($preferredBrowserLanguage !== 'default') { + $this->getLanguageService()->init($preferredBrowserLanguage); + $this->pageRenderer->setLanguage($preferredBrowserLanguage); + } + + $this->getLanguageService()->includeLLFile('EXT:backend/Resources/Private/Language/locallang_login.xlf'); + + $this->moduleTemplate = $this->moduleTemplateFactory->create($request); + $this->moduleTemplate->setTitle('TYPO3 CMS Login: ' . ($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '')); + + $this->view = $this->moduleTemplate->getView(); + $this->view->getRequest()->setControllerExtensionName('Backend'); + $this->view->assignMultiple([ + 'enablePasswordReset' => $this->passwordReset->isEnabled(), + 'referrerCheckEnabled' => $this->features->isFeatureEnabled('security.backend.enforceReferrer'), + 'loginUrl' => (string)$request->getUri(), + ]); + + $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Login'); + $this->provideCustomLoginStyling(); + } + + protected function provideCustomLoginStyling(): void + { + if (($backgroundImageStyles = $this->authenticationStyleInformation->getBackgroundImageStyles()) !== '') { + $this->pageRenderer->addCssInlineBlock('loginBackgroundImage', $backgroundImageStyles); + } + if (($footerNote = $this->authenticationStyleInformation->getFooterNote()) !== '') { + $this->view->assign('loginFootnote', $footerNote); + } + if (($highlightColorStyles = $this->authenticationStyleInformation->getHighlightColorStyles()) !== '') { + $this->pageRenderer->addCssInlineBlock('loginHighlightColor', $highlightColorStyles); + } + if (($logo = $this->authenticationStyleInformation->getLogo()) !== '') { + $logoAlt = $this->authenticationStyleInformation->getLogoAlt(); + } else { + $logo = $this->authenticationStyleInformation->getDefaultLogo(); + $logoAlt = $this->getLanguageService()->getLL('typo3.altText'); + $this->pageRenderer->addCssInlineBlock('loginLogo', $this->authenticationStyleInformation->getDefaultLogoStyles()); + } + $this->view->assignMultiple([ + 'logo' => $logo, + 'logoAlt' => $logoAlt, + 'images' => $this->authenticationStyleInformation->getSupportingImages(), + 'copyright' => $this->typo3Information->getCopyrightNotice(), + ]); + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/typo3/sysext/backend/Configuration/Backend/Routes.php b/typo3/sysext/backend/Configuration/Backend/Routes.php index fef158bda195d91a2a87d19d15c74d8874e61d8f..b91d0fb32c52421b38949c729c22cc9618af26ec 100644 --- a/typo3/sysext/backend/Configuration/Backend/Routes.php +++ b/typo3/sysext/backend/Configuration/Backend/Routes.php @@ -36,25 +36,25 @@ return [ 'password_forget' => [ 'path' => '/login/password-reset/forget', 'access' => 'public', - 'target' => Controller\LoginController::class . '::forgetPasswordFormAction' + 'target' => Controller\ResetPasswordController::class . '::forgetPasswordFormAction' ], // Send out the password reset email 'password_forget_initiate_reset' => [ 'path' => '/login/password-reset/initiate-reset', 'access' => 'public', 'methods' => ['POST'], - 'target' => Controller\LoginController::class . '::initiatePasswordResetAction' + 'target' => Controller\ResetPasswordController::class . '::initiatePasswordResetAction' ], 'password_reset_validate' => [ 'path' => '/login/password-reset/validate', 'access' => 'public', - 'target' => Controller\LoginController::class . '::passwordResetAction' + 'target' => Controller\ResetPasswordController::class . '::passwordResetAction' ], 'password_reset_finish' => [ 'path' => '/login/password-reset/finish', 'access' => 'public', 'methods' => ['POST'], - 'target' => Controller\LoginController::class . '::passwordResetFinishAction' + 'target' => Controller\ResetPasswordController::class . '::passwordResetFinishAction' ], // Register login frameset diff --git a/typo3/sysext/backend/Configuration/Services.yaml b/typo3/sysext/backend/Configuration/Services.yaml index 1a70905c40dbab47e68bf619fa327a4b7f3f1457..b212c1ce0af3e05f4e8a7ba2d80b604778276501 100644 --- a/typo3/sysext/backend/Configuration/Services.yaml +++ b/typo3/sysext/backend/Configuration/Services.yaml @@ -73,6 +73,9 @@ services: TYPO3\CMS\Backend\Controller\LoginController: tags: ['backend.controller'] + TYPO3\CMS\Backend\Controller\ResetPasswordController: + tags: ['backend.controller'] + TYPO3\CMS\Backend\Controller\HelpController: tags: ['backend.controller'] diff --git a/typo3/sysext/backend/Resources/Private/Templates/Login/ForgetPasswordForm.html b/typo3/sysext/backend/Resources/Private/Templates/Login/ForgetPasswordForm.html index 9c4829211f8cb4cd690cfda0f664dcc821dff214..48efe428a10abfc49d57e57374633c107b23e48d 100644 --- a/typo3/sysext/backend/Resources/Private/Templates/Login/ForgetPasswordForm.html +++ b/typo3/sysext/backend/Resources/Private/Templates/Login/ForgetPasswordForm.html @@ -11,7 +11,7 @@ <div class="callout-body"><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:email_sent.message" arguments="{0: email}" /></div> </div> <p class="pull-right"> - <f:be.link route="login"><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:button.back_to_login" /></f:be.link>. + <a href="{returnUrl}"><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:button.back_to_login" /></a>. </p> </f:then> <f:else> @@ -19,8 +19,7 @@ <f:be.infobox message="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:error.invalid_email')}" state="1" /> </f:if> <p><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:instructions.email" /></p> - <form action="{f:be.uri(route: 'password_forget_initiate_reset')}" method="post" name="forget-password-form" id="typo3-forget-password-form"> - <f:form.hidden name="loginProvider" value="{loginProviderIdentifier}" /> + <form action="{formUrl}" method="post" name="forget-password-form" id="typo3-forget-password-form"> <div class="form-group"> <div class="form-control-wrap"> <div class="form-control-holder"> @@ -41,7 +40,7 @@ </small> </p> <p class="pull-right"> - <f:be.link route="login"><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:button.back_to_login" /></f:be.link>. + <a href="{returnUrl}"><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:button.back_to_login" /></a>. </p> </f:else> </f:if> diff --git a/typo3/sysext/backend/Resources/Private/Templates/Login/ResetPasswordForm.html b/typo3/sysext/backend/Resources/Private/Templates/Login/ResetPasswordForm.html index 071d260877b5c7ee5c9cd29bd368bffbee50048e..226c50825c1e4d7ec98183020ecc5991e86bc4df 100644 --- a/typo3/sysext/backend/Resources/Private/Templates/Login/ResetPasswordForm.html +++ b/typo3/sysext/backend/Resources/Private/Templates/Login/ResetPasswordForm.html @@ -8,7 +8,9 @@ <f:then> <f:be.infobox message="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:error.token_expired')}" title="" state="1" /> <p> - <f:be.link class="btn btn-block btn-login" route="password_forget" parameters="{loginProvider: loginProviderIdentifier}"><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:button.restart" /></f:be.link> + <a class="btn btn-block btn-login" href="{restartUrl}"> + <f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:button.restart" /> + </a> </p> </f:then> <f:else if="{resetExecuted}"> @@ -26,8 +28,7 @@ <f:be.infobox message="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:error.password')}" state="1" /> </f:if> <p><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_reset_password.xlf:instructions.password" /></p> - <form action="{f:be.uri(route: 'password_reset_finish', parameters: '{i: identity, t: token, e: expirationDate}')}" method="post" name="forget-password-form" id="typo3-forget-password-form"> - <f:form.hidden name="loginProvider" value="{loginProviderIdentifier}" /> + <form action="{formUrl}" method="post" name="forget-password-form" id="typo3-forget-password-form"> <div class="form-group"> <div class="form-control-wrap"> <div class="form-control-holder"> diff --git a/typo3/sysext/backend/Resources/Private/Templates/UserPassLoginForm.html b/typo3/sysext/backend/Resources/Private/Templates/UserPassLoginForm.html index b47560516864d9e393470680819f995a7ab650fb..d803ee70a261372f02189e461ec3d82978de1d1d 100644 --- a/typo3/sysext/backend/Resources/Private/Templates/UserPassLoginForm.html +++ b/typo3/sysext/backend/Resources/Private/Templates/UserPassLoginForm.html @@ -29,7 +29,7 @@ <f:section name="ResetPassword"> <div class="forgot-password"> <div class="text-right"> - <f:be.link route="password_forget" parameters="{loginProvider: loginProviderIdentifier}"><f:translate key="login.password_forget" /></f:be.link> + <a href="{forgetPasswordUrl}"><f:translate key="login.password_forget" /></a> </div> </div> </f:section> diff --git a/typo3/sysext/backend/Tests/Functional/Controller/ResetPasswordControllerTest.php b/typo3/sysext/backend/Tests/Functional/Controller/ResetPasswordControllerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2b57f9e8816ddf4f0f8e3087ba51965cfe12d5f8 --- /dev/null +++ b/typo3/sysext/backend/Tests/Functional/Controller/ResetPasswordControllerTest.php @@ -0,0 +1,177 @@ +<?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\Tests\Functional\Controller; + +use Prophecy\Argument; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Backend\Authentication\PasswordReset; +use TYPO3\CMS\Backend\Controller\ResetPasswordController; +use TYPO3\CMS\Backend\Routing\UriBuilder; +use TYPO3\CMS\Backend\Template\ModuleTemplateFactory; +use TYPO3\CMS\Backend\View\AuthenticationStyleInformation; +use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Configuration\Features; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Context\UserAspect; +use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; +use TYPO3\CMS\Core\Http\PropagateResponseException; +use TYPO3\CMS\Core\Http\ServerRequest; +use TYPO3\CMS\Core\Information\Typo3Information; +use TYPO3\CMS\Core\Localization\LanguageServiceFactory; +use TYPO3\CMS\Core\Localization\Locales; +use TYPO3\CMS\Core\Page\PageRenderer; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +class ResetPasswordControllerTest extends FunctionalTestCase +{ + protected ResetPasswordController $subject; + protected ServerRequestInterface $request; + + protected $configurationToUseInTestInstance = [ + 'EXTENSIONS' => [ + 'backend' => [ + 'loginHighlightColor' => '#abcdef' + ] + ] + ]; + + protected function setUp(): void + { + parent::setUp(); + + $passwordResetProphecy = $this->prophesize(PasswordReset::class); + $passwordResetProphecy->isEnabled()->willReturn(true); + $passwordResetProphecy->isValidResetTokenFromRequest(Argument::any())->willReturn(true); + $passwordResetProphecy->resetPassword(Argument::any(), Argument::any())->willReturn(true); + + $this->subject = new ResetPasswordController( + $this->getService(Context::class), + $this->getService(Locales::class), + $this->getService(Features::class), + $this->getService(UriBuilder::class), + $this->getService(PageRenderer::class), + $passwordResetProphecy->reveal(), + $this->getService(Typo3Information::class), + $this->getService(ModuleTemplateFactory::class), + $this->getService(AuthenticationStyleInformation::class), + ); + + $this->request = (new ServerRequest()) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + + $GLOBALS['BE_USER'] = new BackendUserAuthentication(); + $GLOBALS['BE_USER']->initializeUserSessionManager(); + $GLOBALS['LANG'] = GeneralUtility::makeInstance(LanguageServiceFactory::class)->create('default'); + } + + /** + * @test + */ + public function throwsPropagateResponseExceptionOnLoggedInUser(): void + { + $backendUser = new BackendUserAuthentication(); + $backendUser->user['uid'] = 13; + GeneralUtility::makeInstance(Context::class)->setAspect('backend.user', new UserAspect($backendUser)); + + $this->expectExceptionCode(1618342858); + $this->expectException(PropagateResponseException::class); + $this->subject->forgetPasswordFormAction($this->request); + } + + /** + * @test + */ + public function customStylingIsApplied(): void + { + $response = $this->subject->forgetPasswordFormAction($this->request)->getBody()->getContents(); + self::assertStringContainsString('/*loginHighlightColor*/', $response); + self::assertRegExp('/\.btn-login { background-color: #abcdef; }.*\.card-login \.card-footer { border-color: #abcdef; }/s', $response); + } + + /** + * @test + */ + public function queryArgumentsAreKept(): void + { + $queryParams = [ + 'loginProvider' => '123456789', + 'redirect' => 'web_list', + 'redirectParams' => 'id=123' + ]; + $request = $this->request->withQueryParams($queryParams); + + // Both views supply "go back" links which should contain the defined queryParams + $expected = htmlspecialchars(http_build_query($queryParams)); + + self::assertStringContainsString($expected, $this->subject->forgetPasswordFormAction($request)->getBody()->getContents()); + self::assertStringContainsString($expected, $this->subject->initiatePasswordResetAction($request)->getBody()->getContents()); + self::assertStringContainsString($expected, $this->subject->passwordResetAction($request)->getBody()->getContents()); + self::assertStringContainsString($expected, $this->subject->passwordResetFinishAction($request)->getBody()->getContents()); + } + + /** + * @test + */ + public function initiatePasswordResetPreventsTimeBasedInformationDisclosure(): void + { + $start = microtime(true); + $this->subject->initiatePasswordResetAction($this->request); + self::assertGreaterThan(0.2, microtime(true) - $start); + } + + /** + * @test + */ + public function initiatePasswordResetValidatesGivenEmailAddress(): void + { + self::assertStringContainsString( + 'The entered email address is invalid. Please try again.', + $this->subject->initiatePasswordResetAction( + $this->request->withParsedBody(['email' =>'email..email@example.com']) + )->getBody()->getContents() + ); + } + + /** + * @test + */ + public function resetPasswordFormUrlContainsQueryParameters(): void + { + $queryParams = [ + 't' => 'some-token-123', + 'i' => 'some-identifier-456', + 'e' => '1618401660' + ]; + $request = $this->request->withQueryParams($queryParams); + + // Expect the form action to contain the necessary reset query params + $expected = '<form action="/typo3/login/password-reset/finish?' . htmlspecialchars(http_build_query($queryParams)); + + self::assertStringContainsString($expected, $this->subject->passwordResetAction($request)->getBody()->getContents()); + } + + protected function getService(string $service, array $constructorArguments = []) + { + $container = $this->getContainer(); + + return $container->has($service) + ? $container->get($service) + : GeneralUtility::makeInstance($service, ...$constructorArguments); + } +}