‪TYPO3CMS  10.4
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\EventDispatcher\EventDispatcherInterface;
38 
43 {
47  protected ‪$recoveryService;
48 
52  protected ‪$userRepository;
53 
57  protected ‪$eventDispatcher;
58 
59  public function ‪__construct(
60  EventDispatcherInterface ‪$eventDispatcher,
63  ) {
64  $this->eventDispatcher = ‪$eventDispatcher;
65  $this->recoveryService = ‪$recoveryService;
66  $this->userRepository = ‪$userRepository;
67  }
68 
77  public function ‪recoveryAction(string $userIdentifier = null): void
78  {
79  if (empty($userIdentifier)) {
80  return;
81  }
82 
83  $email = $this->userRepository->findEmailByUsernameOrEmailOnPages(
84  $userIdentifier,
85  $this->‪getStorageFolders()
86  );
87 
88  if ($email) {
89  $this->recoveryService->sendRecoveryEmail($email);
90  }
91 
92  if ($this->‪exposeNoneExistentUser($email)) {
93  $this->‪addFlashMessage(
94  $this->‪getTranslation('forgot_reset_message_error'),
95  '',
97  );
98  } else {
99  $this->‪addFlashMessage($this->‪getTranslation('forgot_reset_message_emailSent'));
100  }
101 
102  $this->‪redirect('login', 'Login', 'felogin');
103  }
104 
109  protected function ‪validateIfHashHasExpired(): void
110  {
111  $hash = $this->request->hasArgument('hash') ? $this->request->getArgument('hash') : '';
112  $hash = is_string($hash) ? $hash : '';
113 
114  if (!$this->‪hasValidHash($hash)) {
115  $this->‪redirect('recovery', 'PasswordRecovery', 'felogin');
116  }
117 
118  $timestamp = (int)‪GeneralUtility::trimExplode('|', $hash)[0];
119  $currentTimestamp = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('date', 'timestamp');
120 
121  // timestamp is expired or hash can not be assigned to a user
122  if ($currentTimestamp > $timestamp || !$this->userRepository->existsUserWithHash(GeneralUtility::hmac($hash))) {
123  $result = $this->request->getOriginalRequestMappingResults();
124  $result->addError(new ‪Error($this->‪getTranslation('change_password_notvalid_message'), 1554994253));
125  $this->request->setOriginalRequestMappingResults($result);
126  $this->‪forward('recovery', 'PasswordRecovery', 'felogin');
127  }
128  }
129 
138  public function ‪initializeShowChangePasswordAction(): void
139  {
141  }
142 
148  public function ‪showChangePasswordAction(string $hash): void
149  {
150  $this->view->assign('hash', $hash);
151  }
152 
162  public function ‪initializeChangePasswordAction(): void
163  {
164  // Re-validate the lifetime of the hash again (just as showChangePassword action)
166 
167  // Exit early if newPass or newPassRepeat is not set.
168  $originalResult = $this->request->getOriginalRequestMappingResults();
169  $argumentsExist = $this->request->hasArgument('newPass') && $this->request->hasArgument('newPassRepeat');
170  $argumentsEmpty = empty($this->request->getArgument('newPass')) || empty($this->request->getArgument('newPassRepeat'));
171 
172  if (!$argumentsExist || $argumentsEmpty) {
173  $originalResult->addError(new ‪Error(
174  $this->‪getTranslation('empty_password_and_password_repeat'),
175  1554971665
176  ));
177  $this->request->setOriginalRequestMappingResults($originalResult);
178  $this->‪forward(
179  'showChangePassword',
180  'PasswordRecovery',
181  'felogin',
182  ['hash' => $this->request->getArgument('hash')]
183  );
184  }
185 
186  $this->‪validateNewPassword($originalResult);
187 
188  // todo: check if calling $this->errorAction is necessary here
189  // if an error exists, forward with all messages to the change password form
190  if ($originalResult->hasErrors()) {
191  $this->‪forward(
192  'showChangePassword',
193  'PasswordRecovery',
194  'felogin',
195  ['hash' => $this->request->getArgument('hash')]
196  );
197  }
198  }
199 
211  public function ‪changePasswordAction(string $newPass, string $hash): void
212  {
213  $hashedPassword = GeneralUtility::makeInstance(PasswordHashFactory::class)
214  ->getDefaultHashInstance('FE')
215  ->getHashedPassword($newPass);
216 
217  $hashedPassword = $this->‪notifyPasswordChange($newPass, $hashedPassword, $hash);
218  $user = $this->userRepository->findOneByForgotPasswordHash(GeneralUtility::hmac($hash));
219  $this->userRepository->updatePasswordAndInvalidateHash(GeneralUtility::hmac($hash), $hashedPassword);
220  $this->‪invalidateUserSessions($user['uid']);
221 
222  $this->‪addFlashMessage($this->‪getTranslation('change_password_done_message'));
223 
224  $this->‪redirect('login', 'Login', 'felogin');
225  }
226 
232  protected function ‪validateNewPassword(‪Result $originalResult): void
233  {
234  $newPass = $this->request->getArgument('newPass');
235 
236  // make sure the user entered the password twice
237  if ($newPass !== $this->request->getArgument('newPassRepeat')) {
238  $originalResult->‪addError(new ‪Error($this->‪getTranslation('password_must_match_repeated'), 1554912163));
239  }
240 
241  // Resolve validators from TypoScript configuration
242  $validators = GeneralUtility::makeInstance(ValidatorResolverService::class)
243  ->resolve($this->settings['passwordValidators']);
244 
245  // Call each validator on new password
246  foreach ($validators ?? [] as ‪$validator) {
247  $result = ‪$validator->validate($newPass);
248  $originalResult->‪merge($result);
249  }
250 
251  //set the result from all validators
252  $this->request->setOriginalRequestMappingResults($originalResult);
253  }
254 
262  protected function ‪getTranslation(string $key): string
263  {
264  return (string)‪LocalizationUtility::translate($key, 'felogin');
265  }
266 
274  protected function ‪hasValidHash($hash): bool
275  {
276  return !empty($hash) && is_string($hash) && strpos($hash, '|') === 10;
277  }
278 
286  protected function ‪notifyPasswordChange(string $newPassword, string $hashedPassword, string $hash): string
287  {
288  $user = $this->userRepository->findOneByForgotPasswordHash(GeneralUtility::hmac($hash));
289  if (is_array($user)) {
290  $event = new ‪PasswordChangeEvent($user, $hashedPassword, $newPassword);
291  $this->eventDispatcher->dispatch($event);
292  $hashedPassword = $event->getHashedPassword();
293  if ($event->isPropagationStopped()) {
294  $requestResult = $this->request->getOriginalRequestMappingResults();
295  $requestResult->addError(new ‪Error($event->getErrorMessage() ?? '', 1562846833));
296  $this->request->setOriginalRequestMappingResults($requestResult);
297 
298  $this->‪forward(
299  'showChangePassword',
300  'PasswordRecovery',
301  'felogin',
302  ['hash' => $hash]
303  );
304  }
305  } else {
306  // No user found
307  $requestResult = $this->request->getOriginalRequestMappingResults();
308  $requestResult->addError(new ‪Error('Invalid hash', 1562846832));
309  $this->request->setOriginalRequestMappingResults($requestResult);
310  $this->‪forward(
311  'showChangePassword',
312  'PasswordRecovery',
313  'felogin',
314  ['hash' => $hash]
315  );
316  }
317  return $hashedPassword;
318  }
319 
324  protected function ‪exposeNoneExistentUser(?string $email): bool
325  {
326  $acceptedValues = ['1', 1, 'true'];
327 
328  return !$email && in_array(
329  $this->settings['exposeNonexistentUserInForgotPasswordDialog'] ?? null,
330  $acceptedValues,
331  true
332  );
333  }
334 
338  protected function ‪invalidateUserSessions(int $userId): void
339  {
340  $sessionManager = GeneralUtility::makeInstance(SessionManager::class);
341  $sessionBackend = $sessionManager->getSessionBackend('FE');
342  $sessionManager->invalidateAllSessionsByUserId($sessionBackend, $userId);
343  }
344 }
‪TYPO3\CMS\Extbase\Mvc\Exception\StopActionException
Definition: StopActionException.php:31
‪TYPO3\CMS\Extbase\Mvc\Controller\ActionController\forward
‪forward($actionName, $controllerName=null, $extensionName=null, array $arguments=null)
Definition: ActionController.php:815
‪TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory
Definition: PasswordHashFactory.php:27
‪TYPO3\CMS\Core\Messaging\AbstractMessage
Definition: AbstractMessage.php:26
‪TYPO3\CMS\Extbase\Mvc\Exception\UnsupportedRequestTypeException
Definition: UnsupportedRequestTypeException.php:26
‪TYPO3\CMS\Extbase\Utility\LocalizationUtility
Definition: LocalizationUtility.php:33
‪TYPO3\CMS\FrontendLogin\Controller\AbstractLoginFormController
Definition: AbstractLoginFormController.php:26
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\recoveryAction
‪recoveryAction(string $userIdentifier=null)
Definition: PasswordRecoveryController.php:74
‪TYPO3\CMS\FrontendLogin\Domain\Repository\FrontendUserRepository
Definition: FrontendUserRepository.php:31
‪TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException
Definition: InvalidPasswordHashException.php:26
‪TYPO3\CMS\FrontendLogin\Controller
Definition: AbstractLoginFormController.php:18
‪TYPO3\CMS\FrontendLogin\Event\PasswordChangeEvent
Definition: PasswordChangeEvent.php:28
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\showChangePasswordAction
‪showChangePasswordAction(string $hash)
Definition: PasswordRecoveryController.php:145
‪TYPO3\CMS\Core\Session\SessionManager
Definition: SessionManager.php:39
‪TYPO3\CMS\Extbase\Mvc\Controller\ActionController\addFlashMessage
‪addFlashMessage($messageBody, $messageTitle='', $severity=AbstractMessage::OK, $storeInSession=true)
Definition: ActionController.php:747
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\validateNewPassword
‪validateNewPassword(Result $originalResult)
Definition: PasswordRecoveryController.php:229
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:53
‪TYPO3\CMS\Extbase\Error\Result
Definition: Result.php:24
‪TYPO3\CMS\Extbase\Error\Result\merge
‪merge(Result $otherResult)
Definition: Result.php:445
‪TYPO3\CMS\Extbase\Error\Result\addError
‪addError(Error $error)
Definition: Result.php:89
‪TYPO3\CMS\FrontendLogin\Controller\AbstractLoginFormController\getStorageFolders
‪array getStorageFolders()
Definition: AbstractLoginFormController.php:32
‪TYPO3\CMS\Extbase\Mvc\Controller\ActionController\redirect
‪redirect($actionName, $controllerName=null, $extensionName=null, array $arguments=null, $pageUid=null, $delay=0, $statusCode=303)
Definition: ActionController.php:852
‪TYPO3\CMS\Extbase\Error\Error
Definition: Error.php:25
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\initializeChangePasswordAction
‪initializeChangePasswordAction()
Definition: PasswordRecoveryController.php:159
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\$recoveryService
‪RecoveryServiceInterface $recoveryService
Definition: PasswordRecoveryController.php:46
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\exposeNoneExistentUser
‪bool exposeNoneExistentUser(?string $email)
Definition: PasswordRecoveryController.php:321
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\notifyPasswordChange
‪string notifyPasswordChange(string $newPassword, string $hashedPassword, string $hash)
Definition: PasswordRecoveryController.php:283
‪$validator
‪if(isset($args['d'])) $validator
Definition: validateRstFiles.php:218
‪TYPO3\CMS\Extbase\Utility\LocalizationUtility\translate
‪static string null translate(string $key, ?string $extensionName=null, array $arguments=null, string $languageKey=null, array $alternativeLanguageKeys=null)
Definition: LocalizationUtility.php:67
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController
Definition: PasswordRecoveryController.php:43
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\validateIfHashHasExpired
‪validateIfHashHasExpired()
Definition: PasswordRecoveryController.php:106
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static string[] trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
Definition: GeneralUtility.php:1059
‪TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
Definition: AspectNotFoundException.php:26
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\__construct
‪__construct(EventDispatcherInterface $eventDispatcher, RecoveryServiceInterface $recoveryService, FrontendUserRepository $userRepository)
Definition: PasswordRecoveryController.php:56
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\$userRepository
‪FrontendUserRepository $userRepository
Definition: PasswordRecoveryController.php:50
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\initializeShowChangePasswordAction
‪initializeShowChangePasswordAction()
Definition: PasswordRecoveryController.php:135
‪TYPO3\CMS\FrontendLogin\Service\ValidatorResolverService
Definition: ValidatorResolverService.php:28
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\$eventDispatcher
‪EventDispatcherInterface $eventDispatcher
Definition: PasswordRecoveryController.php:54
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\getTranslation
‪string getTranslation(string $key)
Definition: PasswordRecoveryController.php:259
‪TYPO3\CMS\FrontendLogin\Service\RecoveryServiceInterface
Definition: RecoveryServiceInterface.php:23
‪TYPO3\CMS\Core\Messaging\AbstractMessage\ERROR
‪const ERROR
Definition: AbstractMessage.php:31
‪TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException
Definition: NoSuchArgumentException.php:26
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\hasValidHash
‪bool hasValidHash($hash)
Definition: PasswordRecoveryController.php:271
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\invalidateUserSessions
‪invalidateUserSessions(int $userId)
Definition: PasswordRecoveryController.php:335
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\changePasswordAction
‪changePasswordAction(string $newPass, string $hash)
Definition: PasswordRecoveryController.php:208