‪TYPO3CMS  11.5
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;
21 use Psr\Http\Message\ResponseInterface;
39 
44 {
48  protected ‪$recoveryService;
49 
53  protected ‪$userRepository;
54 
58  protected ‪$eventDispatcher;
59 
60  public function ‪__construct(
61  EventDispatcherInterface ‪$eventDispatcher,
64  ) {
65  $this->eventDispatcher = ‪$eventDispatcher;
66  $this->recoveryService = ‪$recoveryService;
67  $this->userRepository = ‪$userRepository;
68  }
69 
80  public function ‪recoveryAction(string $userIdentifier = null)
81  {
82  if (empty($userIdentifier)) {
83  return $this->‪htmlResponse();
84  }
85 
86  $email = $this->userRepository->findEmailByUsernameOrEmailOnPages(
87  $userIdentifier,
88  $this->‪getStorageFolders()
89  );
90 
91  if ($email) {
92  $this->recoveryService->sendRecoveryEmail($email);
93  }
94 
95  if ($this->‪exposeNoneExistentUser($email)) {
96  $this->‪addFlashMessage(
97  $this->‪getTranslation('forgot_reset_message_error'),
98  '',
100  );
101  } else {
102  $this->‪addFlashMessage($this->‪getTranslation('forgot_reset_message_emailSent'));
103  }
104 
105  $this->‪redirect('login', 'Login', 'felogin');
106  }
107 
112  protected function ‪validateIfHashHasExpired()
113  {
114  $hash = $this->request->hasArgument('hash') ? $this->request->getArgument('hash') : '';
115  $hash = is_string($hash) ? $hash : '';
116 
117  if (!$this->‪hasValidHash($hash)) {
118  $this->‪redirect('recovery', 'PasswordRecovery', 'felogin');
119  }
120 
121  $timestamp = (int)‪GeneralUtility::trimExplode('|', $hash)[0];
122  $currentTimestamp = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('date', 'timestamp');
123 
124  // timestamp is expired or hash can not be assigned to a user
125  if ($currentTimestamp > $timestamp || !$this->userRepository->existsUserWithHash(GeneralUtility::hmac($hash))) {
126  $result = $this->request->getOriginalRequestMappingResults();
127  $result->addError(new ‪Error($this->‪getTranslation('change_password_notvalid_message'), 1554994253));
128  $this->request->setOriginalRequestMappingResults($result);
129 
130  return (new ‪ForwardResponse('recovery'))
131  ->withControllerName('PasswordRecovery')
132  ->withExtensionName('felogin')
133  ->withArgumentsValidationResult($result);
134  }
135  }
136 
140  public function ‪showChangePasswordAction(string $hash = ''): ResponseInterface
141  {
142  // Validate the lifetime of the hash
143  if (($response = $this->‪validateIfHashHasExpired()) instanceof ResponseInterface) {
144  return $response;
145  }
146 
147  $this->view->assign('hash', $hash);
148 
149  return $this->‪htmlResponse();
150  }
151 
162  public function ‪validateHashAndPasswords()
163  {
164  // Validate the lifetime of the hash
165  if (($response = $this->‪validateIfHashHasExpired()) instanceof ResponseInterface) {
166  return $response;
167  }
168 
169  // Exit early if newPass or newPassRepeat is not set.
170  $originalResult = $this->request->getOriginalRequestMappingResults();
171  $argumentsExist = $this->request->hasArgument('newPass') && $this->request->hasArgument('newPassRepeat');
172  $argumentsEmpty = empty($this->request->getArgument('newPass')) || empty($this->request->getArgument('newPassRepeat'));
173 
174  if (!$argumentsExist || $argumentsEmpty) {
175  $originalResult->addError(new ‪Error(
176  $this->‪getTranslation('empty_password_and_password_repeat'),
177  1554971665
178  ));
179  $this->request->setOriginalRequestMappingResults($originalResult);
180 
181  return (new ‪ForwardResponse('showChangePassword'))
182  ->withControllerName('PasswordRecovery')
183  ->withExtensionName('felogin')
184  ->withArguments(['hash' => $this->request->getArgument('hash')])
185  ->withArgumentsValidationResult($originalResult);
186  }
187 
188  $this->‪validateNewPassword($originalResult);
189 
190  // todo: check if calling $this->errorAction is necessary here
191  // if an error exists, forward with all messages to the change password form
192  if ($originalResult->hasErrors()) {
193  return (new ‪ForwardResponse('showChangePassword'))
194  ->withControllerName('PasswordRecovery')
195  ->withExtensionName('felogin')
196  ->withArguments(['hash' => $this->request->getArgument('hash')])
197  ->withArgumentsValidationResult($originalResult);
198  }
199  }
200 
213  public function ‪changePasswordAction(string $newPass, string $hash)
214  {
215  if (($response = $this->‪validateHashAndPasswords()) instanceof ResponseInterface) {
216  return $response;
217  }
218 
219  $hashedPassword = GeneralUtility::makeInstance(PasswordHashFactory::class)
220  ->getDefaultHashInstance('FE')
221  ->getHashedPassword($newPass);
222 
223  if (($hashedPassword = $this->‪notifyPasswordChange(
224  $newPass,
225  $hashedPassword,
226  $hash
227  )) instanceof ‪ForwardResponse) {
228  return $hashedPassword;
229  }
230 
231  $user = $this->userRepository->findOneByForgotPasswordHash(GeneralUtility::hmac($hash));
232  $this->userRepository->updatePasswordAndInvalidateHash(GeneralUtility::hmac($hash), $hashedPassword);
233  $this->‪invalidateUserSessions($user['uid']);
234 
235  $this->‪addFlashMessage($this->‪getTranslation('change_password_done_message'));
236 
237  $this->‪redirect('login', 'Login', 'felogin');
238  }
239 
243  protected function ‪validateNewPassword(‪Result $originalResult): void
244  {
245  $newPass = $this->request->getArgument('newPass');
246 
247  // make sure the user entered the password twice
248  if ($newPass !== $this->request->getArgument('newPassRepeat')) {
249  $originalResult->‪addError(new ‪Error($this->‪getTranslation('password_must_match_repeated'), 1554912163));
250  }
251 
252  // Resolve validators from TypoScript configuration
253  $validators = GeneralUtility::makeInstance(ValidatorResolverService::class)
254  ->resolve($this->settings['passwordValidators']);
255 
256  // Call each validator on new password
257  foreach ($validators ?? [] as ‪$validator) {
258  $result = ‪$validator->validate($newPass);
259  $originalResult->‪merge($result);
260  }
261 
262  //set the result from all validators
263  $this->request->setOriginalRequestMappingResults($originalResult);
264  }
265 
269  protected function ‪getTranslation(string $key): string
270  {
271  return (string)‪LocalizationUtility::translate($key, 'felogin');
272  }
273 
281  protected function ‪hasValidHash($hash): bool
282  {
283  return !empty($hash) && is_string($hash) && strpos($hash, '|') === 10;
284  }
285 
292  protected function ‪notifyPasswordChange(string $newPassword, string $hashedPassword, string $hash)
293  {
294  $user = $this->userRepository->findOneByForgotPasswordHash(GeneralUtility::hmac($hash));
295  if (is_array($user)) {
296  $event = new ‪PasswordChangeEvent($user, $hashedPassword, $newPassword);
297  $this->eventDispatcher->dispatch($event);
298  $hashedPassword = $event->getHashedPassword();
299  if ($event->isPropagationStopped()) {
300  $requestResult = $this->request->getOriginalRequestMappingResults();
301  $requestResult->addError(new ‪Error($event->getErrorMessage() ?? '', 1562846833));
302  $this->request->setOriginalRequestMappingResults($requestResult);
303 
304  return (new ‪ForwardResponse('showChangePassword'))
305  ->withControllerName('PasswordRecovery')
306  ->withExtensionName('felogin')
307  ->withArguments(['hash' => $hash]);
308  }
309  } else {
310  // No user found
311  $requestResult = $this->request->getOriginalRequestMappingResults();
312  $requestResult->addError(new ‪Error('Invalid hash', 1562846832));
313  $this->request->setOriginalRequestMappingResults($requestResult);
314 
315  return (new ‪ForwardResponse('showChangePassword'))
316  ->withControllerName('PasswordRecovery')
317  ->withExtensionName('felogin')
318  ->withArguments(['hash' => $hash]);
319  }
320 
321  return $hashedPassword;
322  }
323 
327  protected function ‪exposeNoneExistentUser(?string $email): bool
328  {
329  $acceptedValues = ['1', 1, 'true'];
330 
331  return !$email && in_array(
332  $this->settings['exposeNonexistentUserInForgotPasswordDialog'] ?? null,
333  $acceptedValues,
334  true
335  );
336  }
337 
341  protected function ‪invalidateUserSessions(int $userId): void
342  {
343  $sessionManager = GeneralUtility::makeInstance(SessionManager::class);
344  $sessionBackend = $sessionManager->getSessionBackend('FE');
345  $sessionManager->invalidateAllSessionsByUserId($sessionBackend, $userId);
346  }
347 }
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\validateHashAndPasswords
‪validateHashAndPasswords()
Definition: PasswordRecoveryController.php:159
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
Definition: GeneralUtility.php:999
‪TYPO3\CMS\Extbase\Mvc\Exception\StopActionException
Definition: StopActionException.php:37
‪TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory
Definition: PasswordHashFactory.php:27
‪TYPO3\CMS\Core\Messaging\AbstractMessage
Definition: AbstractMessage.php:26
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\getTranslation
‪getTranslation(string $key)
Definition: PasswordRecoveryController.php:266
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\changePasswordAction
‪ResponseInterface string ForwardResponse null changePasswordAction(string $newPass, string $hash)
Definition: PasswordRecoveryController.php:210
‪TYPO3\CMS\Extbase\Mvc\Controller\ActionController\htmlResponse
‪ResponseInterface htmlResponse(string $html=null)
Definition: ActionController.php:1067
‪TYPO3\CMS\Extbase\Utility\LocalizationUtility
Definition: LocalizationUtility.php:33
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\showChangePasswordAction
‪showChangePasswordAction(string $hash='')
Definition: PasswordRecoveryController.php:137
‪TYPO3\CMS\FrontendLogin\Controller\AbstractLoginFormController
Definition: AbstractLoginFormController.php:26
‪TYPO3\CMS\FrontendLogin\Domain\Repository\FrontendUserRepository
Definition: FrontendUserRepository.php:30
‪TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException
Definition: InvalidPasswordHashException.php:25
‪TYPO3\CMS\FrontendLogin\Controller
Definition: AbstractLoginFormController.php:18
‪TYPO3\CMS\FrontendLogin\Event\PasswordChangeEvent
Definition: PasswordChangeEvent.php:28
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\exposeNoneExistentUser
‪exposeNoneExistentUser(?string $email)
Definition: PasswordRecoveryController.php:324
‪TYPO3\CMS\Core\Session\SessionManager
Definition: SessionManager.php:39
‪TYPO3\CMS\Extbase\Http\ForwardResponse
Definition: ForwardResponse.php:24
‪TYPO3\CMS\Extbase\Mvc\Controller\ActionController\addFlashMessage
‪addFlashMessage($messageBody, $messageTitle='', $severity=AbstractMessage::OK, $storeInSession=true)
Definition: ActionController.php:828
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\recoveryAction
‪ResponseInterface void recoveryAction(string $userIdentifier=null)
Definition: PasswordRecoveryController.php:77
‪TYPO3\CMS\FrontendLogin\Controller\AbstractLoginFormController\getStorageFolders
‪getStorageFolders()
Definition: AbstractLoginFormController.php:30
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\validateNewPassword
‪validateNewPassword(Result $originalResult)
Definition: PasswordRecoveryController.php:240
‪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\Extbase\Error\Error
Definition: Error.php:25
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\$recoveryService
‪RecoveryServiceInterface $recoveryService
Definition: PasswordRecoveryController.php:47
‪$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:44
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\validateIfHashHasExpired
‪validateIfHashHasExpired()
Definition: PasswordRecoveryController.php:109
‪TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
Definition: AspectNotFoundException.php:25
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\__construct
‪__construct(EventDispatcherInterface $eventDispatcher, RecoveryServiceInterface $recoveryService, FrontendUserRepository $userRepository)
Definition: PasswordRecoveryController.php:57
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\$userRepository
‪FrontendUserRepository $userRepository
Definition: PasswordRecoveryController.php:51
‪TYPO3\CMS\Extbase\Mvc\Controller\ActionController\redirect
‪never redirect($actionName, $controllerName=null, $extensionName=null, array $arguments=null, $pageUid=null, $_=null, $statusCode=303)
Definition: ActionController.php:940
‪TYPO3\CMS\FrontendLogin\Service\ValidatorResolverService
Definition: ValidatorResolverService.php:28
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\$eventDispatcher
‪EventDispatcherInterface $eventDispatcher
Definition: PasswordRecoveryController.php:55
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50
‪TYPO3\CMS\FrontendLogin\Service\RecoveryServiceInterface
Definition: RecoveryServiceInterface.php:21
‪TYPO3\CMS\Core\Messaging\AbstractMessage\ERROR
‪const ERROR
Definition: AbstractMessage.php:31
‪TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException
Definition: NoSuchArgumentException.php:25
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\hasValidHash
‪bool hasValidHash($hash)
Definition: PasswordRecoveryController.php:278
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\notifyPasswordChange
‪ForwardResponse string notifyPasswordChange(string $newPassword, string $hashedPassword, string $hash)
Definition: PasswordRecoveryController.php:289
‪TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController\invalidateUserSessions
‪invalidateUserSessions(int $userId)
Definition: PasswordRecoveryController.php:338