‪TYPO3CMS  ‪main
PasswordRecoveryController.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Psr\Http\Message\ResponseInterface;
45 
50 {
51  public function ‪__construct(
52  protected ‪RecoveryService $recoveryService,
53  protected ‪FrontendUserRepository $userRepository,
54  protected ‪RecoveryConfiguration $recoveryConfiguration,
55  protected readonly ‪Features $features,
56  protected readonly ‪PageRepository $pageRepository
57  ) {}
58 
63  public function ‪recoveryAction(string $userIdentifier = null): ResponseInterface
64  {
65  if (empty($userIdentifier)) {
66  return $this->‪htmlResponse();
67  }
68 
69  $storagePageIds = (‪$GLOBALS['TYPO3_CONF_VARS']['FE']['checkFeUserPid'] ?? false)
70  ? $this->pageRepository->getPageIdsRecursive(‪GeneralUtility::intExplode(',', (string)($this->settings['pages'] ?? ''), true), (int)($this->settings['recursive'] ?? 0))
71  : [];
72 
73  $userData = $this->userRepository->findUserByUsernameOrEmailOnPages($userIdentifier, $storagePageIds);
74 
75  if ($userData && GeneralUtility::validEmail($userData['email'])) {
76  $hash = $this->recoveryConfiguration->getForgotHash();
77  $this->userRepository->updateForgotHashForUserByUid($userData['uid'], $this->hashService->hmac($hash, self::class));
78  $this->recoveryService->sendRecoveryEmail($this->request, $userData, $hash);
79  }
80 
81  if ($this->‪exposeNoneExistentUser($userData)) {
82  $this->‪addFlashMessage(
83  $this->‪getTranslation('forgot_reset_message_error'),
84  '',
85  ContextualFeedbackSeverity::ERROR
86  );
87  } else {
88  $this->‪addFlashMessage($this->‪getTranslation('forgot_reset_message_emailSent'));
89  }
90 
91  return $this->‪redirect('login', 'Login', 'felogin');
92  }
93 
103  protected function ‪validateHashArgument(): ?ResponseInterface
104  {
105  $hash = $this->request->hasArgument('hash') ? $this->request->getArgument('hash') : '';
106  $hash = is_string($hash) ? $hash : '';
107 
108  if (!$this->‪validateHashFormat($hash)) {
109  return $this->‪redirect('recovery', 'PasswordRecovery', 'felogin');
110  }
111 
112  $timestamp = (int)‪GeneralUtility::trimExplode('|', $hash)[0];
113  $currentTimestamp = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('date', 'timestamp');
114 
115  // timestamp is expired or hash can not be assigned to a user
116  if ($currentTimestamp > $timestamp || !$this->userRepository->existsUserWithHash($this->hashService->hmac($hash, self::class))) {
118  $extbaseRequestParameters = clone $this->request->getAttribute('extbase');
119  $originalResult = $extbaseRequestParameters->getOriginalRequestMappingResults();
120  $originalResult->addError(new ‪Error($this->‪getTranslation('change_password_notvalid_message'), 1554994253));
121  $extbaseRequestParameters->setOriginalRequestMappingResults($originalResult);
122  $this->request = $this->request->withAttribute('extbase', $extbaseRequestParameters);
123 
124  return (new ‪ForwardResponse('recovery'))
125  ->withControllerName('PasswordRecovery')
126  ->withExtensionName('felogin')
127  ->withArgumentsValidationResult($originalResult);
128  }
129 
130  return null;
131  }
132 
136  public function ‪showChangePasswordAction(string $hash = ''): ResponseInterface
137  {
138  // Validate hash (lifetime, format and fe_user with hash persistence)
139  if (($response = $this->‪validateHashArgument()) instanceof ResponseInterface) {
140  return $response;
141  }
142 
143  $this->view->assignMultiple([
144  'hash' => $hash,
145  'passwordRequirements' => $this->‪getPasswordPolicyValidator()->getRequirements(),
146  ]);
147 
148  return $this->‪htmlResponse();
149  }
150 
157  public function ‪validateHashAndPasswords()
158  {
159  // Validate hash (lifetime, format and fe_user with hash persistence)
160  if (($response = $this->‪validateHashArgument()) instanceof ResponseInterface) {
161  return $response;
162  }
163 
164  // Exit early if newPass or newPassRepeat is not set.
166  $extbaseRequestParameters = clone $this->request->getAttribute('extbase');
167  $originalResult = $extbaseRequestParameters->getOriginalRequestMappingResults();
168  $argumentsExist = $this->request->hasArgument('newPass') && $this->request->hasArgument('newPassRepeat');
169  $argumentsEmpty = empty($this->request->getArgument('newPass')) || empty($this->request->getArgument('newPassRepeat'));
170 
171  if (!$argumentsExist || $argumentsEmpty) {
172  $originalResult->addError(new ‪Error(
173  $this->‪getTranslation('empty_password_and_password_repeat'),
174  1554971665
175  ));
176 
177  return (new ‪ForwardResponse('showChangePassword'))
178  ->withControllerName('PasswordRecovery')
179  ->withExtensionName('felogin')
180  ->withArguments(['hash' => $this->request->getArgument('hash')])
181  ->withArgumentsValidationResult($originalResult);
182  }
183 
184  $this->‪validateNewPassword($originalResult);
185 
186  // if an error exists, forward with all messages to the change password form
187  if ($originalResult->hasErrors()) {
188  return (new ‪ForwardResponse('showChangePassword'))
189  ->withControllerName('PasswordRecovery')
190  ->withExtensionName('felogin')
191  ->withArguments(['hash' => $this->request->getArgument('hash')])
192  ->withArgumentsValidationResult($originalResult);
193  }
194  }
195 
202  public function ‪changePasswordAction(string $newPass, string $hash): ResponseInterface
203  {
204  if (($response = $this->‪validateHashAndPasswords()) instanceof ResponseInterface) {
205  return $response;
206  }
207 
208  $hashedPassword = GeneralUtility::makeInstance(PasswordHashFactory::class)
209  ->getDefaultHashInstance('FE')
210  ->getHashedPassword($newPass);
211 
212  $user = $this->userRepository->findOneByForgotPasswordHash($this->hashService->hmac($hash, self::class));
213  $event = new ‪PasswordChangeEvent($user, $hashedPassword, $newPass);
214  $this->eventDispatcher->dispatch($event);
215 
216  $this->userRepository->updatePasswordAndInvalidateHash($this->hashService->hmac($hash, self::class), $hashedPassword);
217  $this->‪invalidateUserSessions($user['uid']);
218 
219  $this->‪addFlashMessage($this->‪getTranslation('change_password_done_message'));
220 
221  return $this->‪redirect('login', 'Login', 'felogin');
222  }
223 
227  protected function ‪validateNewPassword(‪Result $originalResult): void
228  {
229  $newPass = $this->request->getArgument('newPass');
230 
231  // make sure the user entered the password twice
232  if ($newPass !== $this->request->getArgument('newPassRepeat')) {
233  $originalResult->‪addError(new ‪Error($this->‪getTranslation('password_must_match_repeated'), 1554912163));
234  }
235 
236  $hash = $this->request->getArgument('hash');
237  $userData = $this->userRepository->findOneByForgotPasswordHash($this->hashService->hmac($hash, self::class));
238 
239  // Validate against password policy
240  $passwordPolicyValidator = $this->‪getPasswordPolicyValidator();
241  $contextData = new ‪ContextData(
242  loginMode: 'FE',
243  currentPasswordHash: $userData['password']
244  );
245  $contextData->setData('currentUsername', $userData['username']);
246  $contextData->setData('currentFirstname', $userData['first_name']);
247  $contextData->setData('currentLastname', $userData['last_name']);
248  $event = $this->eventDispatcher->dispatch(
250  $contextData,
251  $userData,
252  self::class
253  )
254  );
255  $contextData = $event->getContextData();
256 
257  if (!$passwordPolicyValidator->isValidPassword($newPass, $contextData)) {
258  foreach ($passwordPolicyValidator->getValidationErrors() as $validationError) {
259  $validationResult = new ‪Result();
260  $validationResult->addError(new ‪Error($validationError, 1667647475));
261  $originalResult->‪merge($validationResult);
262  }
263  }
264  }
265 
269  protected function ‪getTranslation(string $key): string
270  {
271  return (string)‪LocalizationUtility::translate($key, 'felogin');
272  }
273 
277  protected function ‪validateHashFormat(string $hash): bool
278  {
279  return !empty($hash) && strpos($hash, '|') === 10;
280  }
281 
285  protected function ‪exposeNoneExistentUser(?array $user): bool
286  {
287  $acceptedValues = ['1', 1, 'true'];
288 
289  return !$user && in_array(
290  $this->settings['exposeNonexistentUserInForgotPasswordDialog'] ?? null,
291  $acceptedValues,
292  true
293  );
294  }
295 
299  protected function ‪invalidateUserSessions(int $userId): void
300  {
301  $sessionManager = GeneralUtility::makeInstance(SessionManager::class);
302  $sessionBackend = $sessionManager->getSessionBackend('FE');
303  $sessionManager->invalidateAllSessionsByUserId($sessionBackend, $userId);
304  }
305 
307  {
308  $passwordPolicy = ‪$GLOBALS['TYPO3_CONF_VARS']['FE']['passwordPolicy'] ?? 'default';
309  return GeneralUtility::makeInstance(
310  PasswordPolicyValidator::class,
311  PasswordPolicyAction::UPDATE_USER_PASSWORD,
312  is_string($passwordPolicy) ? $passwordPolicy : ''
313  );
314  }
315 }
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\validateHashAndPasswords
‪validateHashAndPasswords()
Definition: PasswordRecoveryController.php:157
‪TYPO3\CMS\Extbase\Mvc\Controller\ActionController\addFlashMessage
‪addFlashMessage(string $messageBody, string $messageTitle='', ContextualFeedbackSeverity $severity=ContextualFeedbackSeverity::OK, bool $storeInSession=true)
Definition: ActionController.php:637
‪TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory
Definition: PasswordHashFactory.php:27
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\getTranslation
‪getTranslation(string $key)
Definition: PasswordRecoveryController.php:269
‪TYPO3\CMS\Extbase\Utility\LocalizationUtility
Definition: LocalizationUtility.php:35
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\showChangePasswordAction
‪showChangePasswordAction(string $hash='')
Definition: PasswordRecoveryController.php:136
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\recoveryAction
‪recoveryAction(string $userIdentifier=null)
Definition: PasswordRecoveryController.php:63
‪TYPO3\CMS\FrontendLogin\Domain\Repository\FrontendUserRepository
Definition: FrontendUserRepository.php:28
‪TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException
Definition: InvalidPasswordHashException.php:25
‪TYPO3\CMS\FrontendLogin\Controller
Definition: LoginController.php:18
‪TYPO3\CMS\FrontendLogin\Event\PasswordChangeEvent
Definition: PasswordChangeEvent.php:24
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\getPasswordPolicyValidator
‪getPasswordPolicyValidator()
Definition: PasswordRecoveryController.php:306
‪TYPO3\CMS\Core\Session\SessionManager
Definition: SessionManager.php:41
‪TYPO3\CMS\Extbase\Http\ForwardResponse
Definition: ForwardResponse.php:24
‪TYPO3\CMS\FrontendLogin\Service\RecoveryService
Definition: RecoveryService.php:37
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\validateNewPassword
‪validateNewPassword(Result $originalResult)
Definition: PasswordRecoveryController.php:227
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:54
‪TYPO3\CMS\Extbase\Error\Result
Definition: Result.php:24
‪TYPO3\CMS\Extbase\Error\Result\merge
‪merge(Result $otherResult)
Definition: Result.php:409
‪TYPO3\CMS\Extbase\Error\Result\addError
‪addError(Error $error)
Definition: Result.php:85
‪TYPO3\CMS\Core\Type\ContextualFeedbackSeverity
‪ContextualFeedbackSeverity
Definition: ContextualFeedbackSeverity.php:25
‪TYPO3\CMS\Extbase\Mvc\Controller\ActionController\htmlResponse
‪htmlResponse(string $html=null)
Definition: ActionController.php:806
‪TYPO3\CMS\Extbase\Error\Error
Definition: Error.php:25
‪TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyValidator
Definition: PasswordPolicyValidator.php:27
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\__construct
‪__construct(protected RecoveryService $recoveryService, protected FrontendUserRepository $userRepository, protected RecoveryConfiguration $recoveryConfiguration, protected readonly Features $features, protected readonly PageRepository $pageRepository)
Definition: PasswordRecoveryController.php:51
‪TYPO3\CMS\Extbase\Utility\LocalizationUtility\translate
‪static string null translate(string $key, ?string $extensionName=null, array $arguments=null, Locale|string $languageKey=null)
Definition: LocalizationUtility.php:47
‪TYPO3\CMS\FrontendLogin\Configuration\RecoveryConfiguration
Definition: RecoveryConfiguration.php:33
‪TYPO3\CMS\Core\Configuration\Features
Definition: Features.php:56
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\validateHashArgument
‪validateHashArgument()
Definition: PasswordRecoveryController.php:103
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController
Definition: PasswordRecoveryController.php:50
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\validateHashFormat
‪validateHashFormat(string $hash)
Definition: PasswordRecoveryController.php:277
‪TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
Definition: AspectNotFoundException.php:25
‪TYPO3\CMS\Extbase\Mvc\Controller\ActionController\redirect
‪redirect(?string $actionName, ?string $controllerName=null, ?string $extensionName=null, ?array $arguments=null, ?int $pageUid=null, $_=null, int $statusCode=303)
Definition: ActionController.php:688
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\exposeNoneExistentUser
‪exposeNoneExistentUser(?array $user)
Definition: PasswordRecoveryController.php:285
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\PasswordPolicy\Event\EnrichPasswordValidationContextDataEvent
Definition: EnrichPasswordValidationContextDataEvent.php:30
‪TYPO3\CMS\Extbase\Mvc\Controller\ActionController
Definition: ActionController.php:63
‪TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyAction
‪PasswordPolicyAction
Definition: PasswordPolicyAction.php:24
‪TYPO3\CMS\Core\Domain\Repository\PageRepository
Definition: PageRepository.php:69
‪TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters
Definition: ExtbaseRequestParameters.php:35
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Utility\GeneralUtility\intExplode
‪static list< int > intExplode(string $delimiter, string $string, bool $removeEmptyValues=false)
Definition: GeneralUtility.php:751
‪TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException
Definition: NoSuchArgumentException.php:25
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:817
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\invalidateUserSessions
‪invalidateUserSessions(int $userId)
Definition: PasswordRecoveryController.php:299
‪TYPO3\CMS\Core\PasswordPolicy\Validator\Dto\ContextData
Definition: ContextData.php:28
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\changePasswordAction
‪changePasswordAction(string $newPass, string $hash)
Definition: PasswordRecoveryController.php:202