1<?php
2
3namespace Drupal\user\Form;
4
5use Drupal\Core\Config\ConfigFactory;
6use Drupal\Core\Flood\FloodInterface;
7use Drupal\Core\Form\FormBase;
8use Drupal\Core\Form\FormStateInterface;
9use Drupal\Core\Language\LanguageManagerInterface;
10use Drupal\Core\Render\Element\Email;
11use Drupal\user\UserInterface;
12use Drupal\user\UserStorageInterface;
13use Symfony\Component\DependencyInjection\ContainerInterface;
14
15/**
16 * Provides a user password reset form.
17 *
18 * Send the user an email to reset their password.
19 *
20 * @internal
21 */
22class UserPasswordForm extends FormBase {
23
24  /**
25   * The user storage.
26   *
27   * @var \Drupal\user\UserStorageInterface
28   */
29  protected $userStorage;
30
31  /**
32   * The language manager.
33   *
34   * @var \Drupal\Core\Language\LanguageManagerInterface
35   */
36  protected $languageManager;
37
38  /**
39   * The flood service.
40   *
41   * @var \Drupal\Core\Flood\FloodInterface
42   */
43  protected $flood;
44
45  /**
46   * Constructs a UserPasswordForm object.
47   *
48   * @param \Drupal\user\UserStorageInterface $user_storage
49   *   The user storage.
50   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
51   *   The language manager.
52   * @param \Drupal\Core\Config\ConfigFactory $config_factory
53   *   The config factory.
54   * @param \Drupal\Core\Flood\FloodInterface $flood
55   *   The flood service.
56   */
57  public function __construct(UserStorageInterface $user_storage, LanguageManagerInterface $language_manager, ConfigFactory $config_factory = NULL, FloodInterface $flood = NULL) {
58    $this->userStorage = $user_storage;
59    $this->languageManager = $language_manager;
60    if (!$config_factory) {
61      @trigger_error('Calling ' . __METHOD__ . ' without the $config_factory is deprecated in drupal:8.8.0 and is required before drupal:9.0.0. See https://www.drupal.org/node/1681832', E_USER_DEPRECATED);
62      $config_factory = \Drupal::configFactory();
63    }
64    $this->configFactory = $config_factory;
65    if (!$flood) {
66      @trigger_error('Calling ' . __METHOD__ . ' without the $flood parameter is deprecated in drupal:8.8.0 and is required before drupal:9.0.0. See https://www.drupal.org/node/1681832', E_USER_DEPRECATED);
67      $flood = \Drupal::service('flood');
68    }
69    $this->flood = $flood;
70  }
71
72  /**
73   * {@inheritdoc}
74   */
75  public static function create(ContainerInterface $container) {
76    return new static(
77      $container->get('entity_type.manager')->getStorage('user'),
78      $container->get('language_manager'),
79      $container->get('config.factory'),
80      $container->get('flood')
81    );
82  }
83
84  /**
85   * {@inheritdoc}
86   */
87  public function getFormId() {
88    return 'user_pass';
89  }
90
91  /**
92   * {@inheritdoc}
93   */
94  public function buildForm(array $form, FormStateInterface $form_state) {
95    $form['name'] = [
96      '#type' => 'textfield',
97      '#title' => $this->t('Username or email address'),
98      '#size' => 60,
99      '#maxlength' => max(UserInterface::USERNAME_MAX_LENGTH, Email::EMAIL_MAX_LENGTH),
100      '#required' => TRUE,
101      '#attributes' => [
102        'autocorrect' => 'off',
103        'autocapitalize' => 'off',
104        'spellcheck' => 'false',
105        'autofocus' => 'autofocus',
106      ],
107    ];
108    // Allow logged in users to request this also.
109    $user = $this->currentUser();
110    if ($user->isAuthenticated()) {
111      $form['name']['#type'] = 'value';
112      $form['name']['#value'] = $user->getEmail();
113      $form['mail'] = [
114        '#prefix' => '<p>',
115        '#markup' => $this->t('Password reset instructions will be mailed to %email. You must log out to use the password reset link in the email.', ['%email' => $user->getEmail()]),
116        '#suffix' => '</p>',
117      ];
118    }
119    else {
120      $form['mail'] = [
121        '#prefix' => '<p>',
122        '#markup' => $this->t('Password reset instructions will be sent to your registered email address.'),
123        '#suffix' => '</p>',
124      ];
125      $form['name']['#default_value'] = $this->getRequest()->query->get('name');
126    }
127    $form['actions'] = ['#type' => 'actions'];
128    $form['actions']['submit'] = ['#type' => 'submit', '#value' => $this->t('Submit')];
129    $form['#cache']['contexts'][] = 'url.query_args';
130
131    return $form;
132  }
133
134  /**
135   * {@inheritdoc}
136   */
137  public function validateForm(array &$form, FormStateInterface $form_state) {
138    $flood_config = $this->configFactory->get('user.flood');
139    if (!$this->flood->isAllowed('user.password_request_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
140      $form_state->setErrorByName('name', $this->t('Too many password recovery requests from your IP address. It is temporarily blocked. Try again later or contact the site administrator.'));
141      return;
142    }
143    $this->flood->register('user.password_request_ip', $flood_config->get('ip_window'));
144    $name = trim($form_state->getValue('name'));
145    // Try to load by email.
146    $users = $this->userStorage->loadByProperties(['mail' => $name]);
147    if (empty($users)) {
148      // No success, try to load by name.
149      $users = $this->userStorage->loadByProperties(['name' => $name]);
150    }
151    $account = reset($users);
152    if ($account && $account->id()) {
153      // Blocked accounts cannot request a new password.
154      if (!$account->isActive()) {
155        $form_state->setErrorByName('name', $this->t('%name is blocked or has not been activated yet.', ['%name' => $name]));
156      }
157      else {
158        // Register flood events based on the uid only, so they apply for any
159        // IP address. This allows them to be cleared on successful reset (from
160        // any IP).
161        $identifier = $account->id();
162        if (!$this->flood->isAllowed('user.password_request_user', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
163          $form_state->setErrorByName('name', $this->t('Too many password recovery requests for this account. It is temporarily blocked. Try again later or contact the site administrator.'));
164          return;
165        }
166        $this->flood->register('user.password_request_user', $flood_config->get('user_window'), $identifier);
167        $form_state->setValueForElement(['#parents' => ['account']], $account);
168      }
169    }
170    else {
171      $form_state->setErrorByName('name', $this->t('%name is not recognized as a username or an email address.', ['%name' => $name]));
172    }
173  }
174
175  /**
176   * {@inheritdoc}
177   */
178  public function submitForm(array &$form, FormStateInterface $form_state) {
179    $langcode = $this->languageManager->getCurrentLanguage()->getId();
180
181    $account = $form_state->getValue('account');
182    // Mail one time login URL and instructions using current language.
183    $mail = _user_mail_notify('password_reset', $account, $langcode);
184    if (!empty($mail)) {
185      $this->logger('user')->notice('Password reset instructions mailed to %name at %email.', ['%name' => $account->getAccountName(), '%email' => $account->getEmail()]);
186      $this->messenger()->addStatus($this->t('Further instructions have been sent to your email address.'));
187    }
188
189    $form_state->setRedirect('user.page');
190  }
191
192}
193