‪TYPO3CMS  ‪main
PasswordReset.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 Doctrine\DBAL\Platforms\MySQLPlatform;
21 use Psr\EventDispatcher\EventDispatcherInterface;
22 use Psr\Http\Message\ServerRequestInterface;
23 use Psr\Http\Message\UriInterface;
24 use Psr\Log\LoggerAwareInterface;
25 use Psr\Log\LoggerAwareTrait;
26 use Symfony\Component\Mime\Address;
35 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
49 use ‪TYPO3\CMS\Core\SysLog\Action\Login as SystemLogLoginAction;
50 use ‪TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
51 use ‪TYPO3\CMS\Core\SysLog\Type as SystemLogType;
53 
64 class ‪PasswordReset implements LoggerAwareInterface
65 {
66  use LoggerAwareTrait;
67 
68  protected const ‪TOKEN_VALID_UNTIL = '+2 hours';
69  protected const ‪MAXIMUM_RESET_ATTEMPTS = 3;
70  protected const ‪MAXIMUM_RESET_ATTEMPTS_SINCE = '-30 minutes';
71 
72  public function ‪__construct(
73  private readonly ‪MailerInterface $mailer,
74  private readonly ‪HashService $hashService,
75  ) {}
76 
80  public function ‪isEnabled(): bool
81  {
82  // Option not explicitly enabled
83  if (!(‪$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordReset'] ?? false)) {
84  return false;
85  }
86  $queryBuilder = $this->‪getPreparedQueryBuilder();
87  $statement = $queryBuilder
88  ->select('uid')
89  ->from('be_users')
90  ->setMaxResults(1)
91  ->executeQuery();
92  return (int)$statement->fetchOne() > 0;
93  }
94 
98  public function ‪isEnabledForUser(int $userId): bool
99  {
100  $queryBuilder = $this->‪getPreparedQueryBuilder();
101  $statement = $queryBuilder
102  ->select('uid')
103  ->from('be_users')
104  ->andWhere(
105  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($userId, ‪Connection::PARAM_INT))
106  )
107  ->setMaxResults(1)
108  ->executeQuery();
109  return $statement->fetchOne() > 0;
110  }
111 
124  public function ‪initiateReset(ServerRequestInterface $request, ‪Context $context, string $emailAddress): void
125  {
126  if (!GeneralUtility::validEmail($emailAddress)) {
127  return;
128  }
129  if ($this->‪hasExceededMaximumAttemptsForReset($context, $emailAddress)) {
130  $this->logger->alert('Password reset requested for email {email} but was requested too many times.', ['email' => $emailAddress]);
131  return;
132  }
133  $queryBuilder = $this->‪getPreparedQueryBuilder();
134  $users = $queryBuilder
135  ->select('*')
136  ->from('be_users')
137  ->andWhere(
138  $queryBuilder->expr()->eq('email', $queryBuilder->createNamedParameter($emailAddress))
139  )
140  ->executeQuery()
141  ->fetchAllAssociative();
142  if ($users === []) {
143  // No user found, do nothing, also no log to sys_log in order avoid log flooding
144  $this->logger->warning('Password reset requested for email but no valid users');
145  } elseif (count($users) > 1) {
146  // More than one user with the same email address found, send out the email that one cannot send out a reset link
147  $this->‪sendAmbiguousEmail($request, $context, $emailAddress);
148  } else {
149  $user = reset($users);
150  unset($user['password']);
151  $this->‪sendResetEmail($request, $context, $user);
152  }
153  }
154 
159  protected function ‪sendAmbiguousEmail(ServerRequestInterface $request, ‪Context $context, string $emailAddress): void
160  {
161  $emailObject = GeneralUtility::makeInstance(FluidEmail::class);
162  $emailObject
163  ->to(new Address($emailAddress))
164  ->setRequest($request)
165  ->assign('email', $emailAddress)
166  ->setTemplate('PasswordReset/AmbiguousResetRequested');
167 
168  $this->mailer->send($emailObject);
169  $this->logger->warning('Password reset sent to email address {email} but multiple accounts found', ['email' => $emailAddress]);
170  $this->‪log(
171  'Sent password reset email to email address %s but with multiple accounts attached.',
172  SystemLogLoginAction::PASSWORD_RESET_REQUEST,
173  SystemLogErrorClassification::WARNING,
174  0,
175  [
176  'email' => $emailAddress,
177  ],
178  ‪NormalizedParams::createFromRequest($request)->getRemoteAddress(),
179  $context
180  );
181  }
182 
186  protected function ‪sendResetEmail(ServerRequestInterface $request, ‪Context $context, array $user): void
187  {
188  $resetLink = $this->‪generateResetLinkForUser($context, (int)$user['uid'], (string)$user['email']);
189  $emailObject = GeneralUtility::makeInstance(FluidEmail::class);
190  $emailObject
191  ->to(new Address((string)$user['email'], $user['realName']))
192  ->setRequest($request)
193  ->assign('name', $user['realName'])
194  ->assign('email', $user['email'])
195  ->assign('language', $user['lang'] ?: 'default')
196  ->assign('resetLink', $resetLink)
197  ->assign('username', $user['username'])
198  ->assign('userData', $user)
199  ->setTemplate('PasswordReset/ResetRequested');
200 
201  $this->mailer->send($emailObject);
202 
203  $this->logger->info('Sent password reset email to email address {email} for user {username}', [
204  'email' => $user['email'],
205  'username' => $user['username'],
206  ]);
207  $this->‪log(
208  'Sent password reset email to email address %s',
209  SystemLogLoginAction::PASSWORD_RESET_REQUEST,
210  SystemLogErrorClassification::SECURITY_NOTICE,
211  (int)$user['uid'],
212  [
213  'email' => $user['email'],
214  ],
215  ‪NormalizedParams::createFromRequest($request)->getRemoteAddress(),
216  $context
217  );
218  }
219 
232  protected function ‪generateResetLinkForUser(‪Context $context, int $userId, string $emailAddress): UriInterface
233  {
234  $token = GeneralUtility::makeInstance(Random::class)->generateRandomHexString(96);
235  $currentTime = $context->‪getAspect('date')->getDateTime();
236  $expiresOn = $currentTime->modify(self::TOKEN_VALID_UNTIL);
237  // Create a hash ("one time password") out of the token including the timestamp of the expiration date
238  $hash = $this->hashService->hmac($token . '|' . $expiresOn->getTimestamp() . '|' . $emailAddress . '|' . $userId, 'password-reset');
239 
240  // Set the token in the database, which is hashed
241  GeneralUtility::makeInstance(ConnectionPool::class)
242  ->getConnectionForTable('be_users')
243  ->update('be_users', ['password_reset_token' => $this->‪getHasher()->getHashedPassword($hash)], ['uid' => $userId]);
244 
245  return GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute(
246  'password_reset_validate',
247  [
248  // "token"
249  't' => $token,
250  // "expiration date"
251  'e' => $expiresOn->getTimestamp(),
252  // "identity"
253  'i' => hash('sha1', $emailAddress . (string)$userId),
254  ],
256  );
257  }
258 
262  public function ‪isValidResetTokenFromRequest(ServerRequestInterface $request): bool
263  {
264  $user = $this->‪findValidUserForToken(
265  (string)($request->getQueryParams()['t'] ?? ''),
266  (string)($request->getQueryParams()['i'] ?? ''),
267  (int)($request->getQueryParams()['e'] ?? 0)
268  );
269  return $user !== null;
270  }
271 
277  protected function ‪findValidUserForToken(string $token, string $identity, int $expirationTimestamp): ?array
278  {
279  // Early return if token expired
280  if ($expirationTimestamp < time()) {
281  return null;
282  }
283 
284  $user = null;
285  // Find the token in the database
286  $queryBuilder = $this->‪getPreparedQueryBuilder();
287 
288  $queryBuilder
289  ->select('uid', 'username', 'realName', 'email', 'password_reset_token', 'password')
290  ->from('be_users');
291  if ($queryBuilder->getConnection()->getDatabasePlatform() instanceof MySQLPlatform) {
292  $queryBuilder->andWhere(
293  $queryBuilder->expr()->comparison('SHA1(CONCAT(' . $queryBuilder->quoteIdentifier('email') . ', ' . $queryBuilder->quoteIdentifier('uid') . '))', $queryBuilder->expr()::EQ, $queryBuilder->createNamedParameter($identity))
294  );
295  $user = $queryBuilder->executeQuery()->fetchAssociative();
296  } else {
297  // no native SHA1/ CONCAT functionality, has to be done in PHP
298  $stmt = $queryBuilder->executeQuery();
299  while ($row = $stmt->fetchAssociative()) {
300  if (hash_equals(hash('sha1', $row['email'] . (string)$row['uid']), $identity)) {
301  $user = $row;
302  break;
303  }
304  }
305  }
306 
307  if (!is_array($user) || empty($user)) {
308  return null;
309  }
310 
311  // Validate hash by rebuilding the hash from the parameters and the URL and see if this matches against the stored password_reset_token
312  $hash = $this->hashService->hmac($token . '|' . $expirationTimestamp . '|' . $user['email'] . '|' . $user['uid'], 'password-reset');
313  if (!$this->‪getHasher()->checkPassword($hash, $user['password_reset_token'] ?? '')) {
314  return null;
315  }
316  return $user;
317  }
318 
324  public function ‪resetPassword(ServerRequestInterface $request, ‪Context $context): bool
325  {
326  $expirationTimestamp = (int)($request->getQueryParams()['e'] ?? '');
327  $identityHash = (string)($request->getQueryParams()['i'] ?? '');
328  $token = (string)($request->getQueryParams()['t'] ?? '');
329  $newPassword = (string)($request->getParsedBody()['password'] ?? '');
330  $newPasswordRepeat = (string)($request->getParsedBody()['passwordrepeat'] ?? '');
331 
332  $user = $this->‪findValidUserForToken($token, $identityHash, $expirationTimestamp);
333  if ($user === null) {
334  $this->logger->warning('Password reset not possible. Valid user for token not found.');
335  return false;
336  }
337  $userId = (int)$user['uid'];
338 
339  if ($newPassword === '') {
340  $this->logger->debug('Password reset not possible because an empty password was provided.');
341  return false;
342  }
343 
344  if ($newPassword !== $newPasswordRepeat) {
345  $this->logger->debug('Password reset not possible because new password and new password repeat do not match.');
346  return false;
347  }
348 
349  if (!$this->‪isValidPassword($newPassword, $user)) {
350  $this->logger->debug('The new password does not match all requirements of the password policy.');
351  return false;
352  }
353 
354  GeneralUtility::makeInstance(ConnectionPool::class)
355  ->getConnectionForTable('be_users')
356  ->update('be_users', ['password_reset_token' => '', 'password' => $this->‪getHasher()->getHashedPassword($newPassword)], ['uid' => $userId]);
357 
358  $this->‪invalidateUserSessions($userId);
359 
360  $this->logger->info('Password reset successful for user \'{username}\'', ['username' => $user['username'], 'user_id' => $userId]);
361  $this->‪log(
362  'Password reset successful for user %s',
363  SystemLogLoginAction::PASSWORD_RESET_ACCOMPLISHED,
364  SystemLogErrorClassification::SECURITY_NOTICE,
365  $userId,
366  [
367  'email' => $user['email'],
368  'user' => $userId,
369  ],
370  ‪NormalizedParams::createFromRequest($request)->getRemoteAddress(),
371  $context
372  );
373  return true;
374  }
375 
385  protected function ‪getPreparedQueryBuilder(): QueryBuilder
386  {
387  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
388  $queryBuilder->getRestrictions()
389  ->removeAll()
390  ->add(GeneralUtility::makeInstance(RootLevelRestriction::class))
391  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
392  ->add(GeneralUtility::makeInstance(StartTimeRestriction::class))
393  ->add(GeneralUtility::makeInstance(EndTimeRestriction::class))
394  ->add(GeneralUtility::makeInstance(HiddenRestriction::class));
395  $queryBuilder->where(
396  $queryBuilder->expr()->neq('username', $queryBuilder->createNamedParameter('')),
397  $queryBuilder->expr()->neq('username', $queryBuilder->createNamedParameter('_cli_')),
398  $queryBuilder->expr()->neq('password', $queryBuilder->createNamedParameter('')),
399  $queryBuilder->expr()->neq('email', $queryBuilder->createNamedParameter(''))
400  );
401  if (!(‪$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordResetForAdmins'] ?? false)) {
402  $queryBuilder->andWhere(
403  $queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT))
404  );
405  }
406  return $queryBuilder;
407  }
408 
410  {
411  return GeneralUtility::makeInstance(PasswordHashFactory::class)->getDefaultHashInstance('BE');
412  }
413 
423  protected function ‪log(string $message, int $action, int $error, int $userId, array $data, $ipAddress, ‪Context $context): void
424  {
425  ‪$fields = [
426  'userid' => $userId,
427  'type' => SystemLogType::LOGIN,
428  'channel' => SystemLogType::toChannel(SystemLogType::LOGIN),
429  'level' => SystemLogType::toLevel(SystemLogType::LOGIN),
430  'action' => $action,
431  'error' => $error,
432  'details_nr' => 1,
433  'details' => $message,
434  'log_data' => json_encode($data),
435  'tablename' => 'be_users',
436  'recuid' => $userId,
437  'IP' => (string)$ipAddress,
438  'tstamp' => $context->‪getAspect('date')->get('timestamp'),
439  'event_pid' => 0,
440  'NEWid' => '',
441  'workspace' => 0,
442  ];
443 
444  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_log');
445  $connection->insert(
446  'sys_log',
447  ‪$fields,
448  [
465  ]
466  );
467  }
468 
473  protected function ‪hasExceededMaximumAttemptsForReset(‪Context $context, string $email): bool
474  {
475  $now = $context->‪getAspect('date')->getDateTime();
476  $numberOfAttempts = $this->‪getNumberOfInitiatedResetsForEmail($now->modify(self::MAXIMUM_RESET_ATTEMPTS_SINCE), $email);
477  return $numberOfAttempts > ‪self::MAXIMUM_RESET_ATTEMPTS;
478  }
479 
483  protected function ‪getNumberOfInitiatedResetsForEmail(\DateTimeInterface $since, string $email): int
484  {
485  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log');
486  return (int)$queryBuilder
487  ->count('uid')
488  ->from('sys_log')
489  ->where(
490  $queryBuilder->expr()->eq('type', $queryBuilder->createNamedParameter(SystemLogType::LOGIN)),
491  $queryBuilder->expr()->eq('action', $queryBuilder->createNamedParameter(SystemLogLoginAction::PASSWORD_RESET_REQUEST)),
492  $queryBuilder->expr()->eq('log_data', $queryBuilder->createNamedParameter(json_encode(['email' => $email]))),
493  $queryBuilder->expr()->gte('tstamp', $queryBuilder->createNamedParameter($since->getTimestamp(), ‪Connection::PARAM_INT))
494  )
495  ->executeQuery()
496  ->fetchOne();
497  }
498 
502  protected function ‪isValidPassword(string $password, array $user): bool
503  {
504  $passwordPolicy = ‪$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy'] ?? 'default';
505  $passwordPolicyValidator = GeneralUtility::makeInstance(
506  PasswordPolicyValidator::class,
507  PasswordPolicyAction::UPDATE_USER_PASSWORD,
508  is_string($passwordPolicy) ? $passwordPolicy : ''
509  );
510  $contextData = new ‪ContextData(currentPasswordHash: $user['password']);
511  $contextData->setData('currentUsername', $user['username']);
512  $contextData->setData('currentFullname', $user['realName']);
513  $event = GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch(
515  $contextData,
516  $user,
517  self::class
518  )
519  );
520  $contextData = $event->getContextData();
521 
522  return $passwordPolicyValidator->isValidPassword($password, $contextData);
523  }
524 
528  protected function ‪invalidateUserSessions(int $userId): void
529  {
530  $sessionManager = GeneralUtility::makeInstance(SessionManager::class);
531  $sessionBackend = $sessionManager->getSessionBackend('BE');
532  $sessionManager->invalidateAllSessionsByUserId($sessionBackend, $userId);
533  }
534 }
‪TYPO3\CMS\Backend\Authentication\PasswordReset\log
‪log(string $message, int $action, int $error, int $userId, array $data, $ipAddress, Context $context)
Definition: PasswordReset.php:423
‪TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction
Definition: HiddenRestriction.php:27
‪TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory
Definition: PasswordHashFactory.php:27
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:52
‪TYPO3\CMS\Backend\Authentication\PasswordReset\resetPassword
‪bool resetPassword(ServerRequestInterface $request, Context $context)
Definition: PasswordReset.php:324
‪TYPO3\CMS\Backend\Authentication
‪TYPO3\CMS\Backend\Authentication\PasswordReset\getHasher
‪getHasher()
Definition: PasswordReset.php:409
‪TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction
Definition: EndTimeRestriction.php:27
‪TYPO3\CMS\Core\Context\Context\getAspect
‪getAspect(string $name)
Definition: Context.php:76
‪TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction
Definition: StartTimeRestriction.php:27
‪TYPO3\CMS\Backend\Authentication\PasswordReset
Definition: PasswordReset.php:65
‪TYPO3\CMS\Backend\Authentication\PasswordReset\initiateReset
‪initiateReset(ServerRequestInterface $request, Context $context, string $emailAddress)
Definition: PasswordReset.php:124
‪TYPO3\CMS\Backend\Authentication\PasswordReset\MAXIMUM_RESET_ATTEMPTS
‪const MAXIMUM_RESET_ATTEMPTS
Definition: PasswordReset.php:69
‪TYPO3\CMS\Backend\Authentication\PasswordReset\__construct
‪__construct(private readonly MailerInterface $mailer, private readonly HashService $hashService,)
Definition: PasswordReset.php:72
‪TYPO3\CMS\Core\Mail\MailerInterface
Definition: MailerInterface.php:28
‪TYPO3\CMS\Core\Session\SessionManager
Definition: SessionManager.php:41
‪TYPO3\CMS\Backend\Authentication\PasswordReset\isEnabledForUser
‪isEnabledForUser(int $userId)
Definition: PasswordReset.php:98
‪$fields
‪$fields
Definition: pages.php:5
‪TYPO3\CMS\Core\Database\Connection\PARAM_STR
‪const PARAM_STR
Definition: Connection.php:57
‪TYPO3\CMS\Core\SysLog\Action\Login
Definition: Login.php:24
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:54
‪TYPO3\CMS\Backend\Authentication\PasswordReset\TOKEN_VALID_UNTIL
‪const TOKEN_VALID_UNTIL
Definition: PasswordReset.php:68
‪TYPO3\CMS\Backend\Authentication\PasswordReset\isValidResetTokenFromRequest
‪isValidResetTokenFromRequest(ServerRequestInterface $request)
Definition: PasswordReset.php:262
‪TYPO3\CMS\Core\Mail\FluidEmail
Definition: FluidEmail.php:35
‪TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction
Definition: RootLevelRestriction.php:27
‪TYPO3\CMS\Backend\Routing\UriBuilder
Definition: UriBuilder.php:44
‪TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyValidator
Definition: PasswordPolicyValidator.php:27
‪TYPO3\CMS\Backend\Authentication\PasswordReset\hasExceededMaximumAttemptsForReset
‪hasExceededMaximumAttemptsForReset(Context $context, string $email)
Definition: PasswordReset.php:473
‪TYPO3\CMS\Core\SysLog\Error
Definition: Error.php:24
‪TYPO3\CMS\Backend\Authentication\PasswordReset\generateResetLinkForUser
‪generateResetLinkForUser(Context $context, int $userId, string $emailAddress)
Definition: PasswordReset.php:232
‪TYPO3\CMS\Backend\Authentication\PasswordReset\findValidUserForToken
‪array null findValidUserForToken(string $token, string $identity, int $expirationTimestamp)
Definition: PasswordReset.php:277
‪TYPO3\CMS\Backend\Authentication\PasswordReset\isValidPassword
‪isValidPassword(string $password, array $user)
Definition: PasswordReset.php:502
‪TYPO3\CMS\Backend\Authentication\PasswordReset\sendAmbiguousEmail
‪sendAmbiguousEmail(ServerRequestInterface $request, Context $context, string $emailAddress)
Definition: PasswordReset.php:159
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:41
‪TYPO3\CMS\Backend\Authentication\PasswordReset\getPreparedQueryBuilder
‪getPreparedQueryBuilder()
Definition: PasswordReset.php:385
‪$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\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:28
‪TYPO3\CMS\Backend\Authentication\PasswordReset\invalidateUserSessions
‪invalidateUserSessions(int $userId)
Definition: PasswordReset.php:528
‪TYPO3\CMS\Backend\Authentication\PasswordReset\MAXIMUM_RESET_ATTEMPTS_SINCE
‪const MAXIMUM_RESET_ATTEMPTS_SINCE
Definition: PasswordReset.php:70
‪TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyAction
‪PasswordPolicyAction
Definition: PasswordPolicyAction.php:24
‪TYPO3\CMS\Core\Crypto\Random
Definition: Random.php:27
‪TYPO3\CMS\Backend\Authentication\PasswordReset\getNumberOfInitiatedResetsForEmail
‪getNumberOfInitiatedResetsForEmail(\DateTimeInterface $since, string $email)
Definition: PasswordReset.php:483
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Backend\Routing\UriBuilder\ABSOLUTE_URL
‪const ABSOLUTE_URL
Definition: UriBuilder.php:48
‪TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashInterface
Definition: PasswordHashInterface.php:25
‪TYPO3\CMS\Core\Http\NormalizedParams\createFromRequest
‪static static createFromRequest(ServerRequestInterface $request, array $systemConfiguration=null)
Definition: NormalizedParams.php:840
‪TYPO3\CMS\Core\Crypto\HashService
Definition: HashService.php:27
‪TYPO3\CMS\Backend\Authentication\PasswordReset\isEnabled
‪isEnabled()
Definition: PasswordReset.php:80
‪TYPO3\CMS\Backend\Authentication\PasswordReset\sendResetEmail
‪sendResetEmail(ServerRequestInterface $request, Context $context, array $user)
Definition: PasswordReset.php:186
‪TYPO3\CMS\Core\Http\NormalizedParams
Definition: NormalizedParams.php:38
‪TYPO3\CMS\Core\SysLog\Type
Definition: Type.php:28
‪TYPO3\CMS\Core\PasswordPolicy\Validator\Dto\ContextData
Definition: ContextData.php:28