commit 4c793071cb7658a1a522b4e87741316f03c62818 Author: Tim Plunkett Date: Sun Jul 13 14:00:03 2014 -0700 Issue #1998198: user reset. diff --git a/core/modules/user/src/Controller/UserController.php b/core/modules/user/src/Controller/UserController.php index c27e3c9..5cab518 100644 --- a/core/modules/user/src/Controller/UserController.php +++ b/core/modules/user/src/Controller/UserController.php @@ -11,6 +11,10 @@ use Drupal\Core\Controller\ControllerBase; use Drupal\user\UserInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Drupal\Core\Datetime\Date; +use Drupal\user\UserStorageInterface; /** * Controller routines for user routes. @@ -18,6 +22,111 @@ class UserController extends ControllerBase { /** + * The date formatting service. + * + * @var \Drupal\Core\Datetime\Date + */ + protected $date; + + /** + * The user storage. + * + * @var \Drupal\user\UserStorageInterface + */ + protected $userStorage; + + /** + * Constructs a UserController object. + * + * @param \Drupal\Core\Datetime\Date $date + * The date formatting service. + * @param \Drupal\user\UserStorageInterface $user_storage + * The user storage. + */ + public function __construct(Date $date, UserStorageInterface $user_storage) { + $this->date = $date; + $this->userStorage = $user_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('date'), + $container->get('entity.manager')->getStorage('user') + ); + } + + /** + * Returns the user password reset page. + * + * @param int $uid + * UID of user requesting reset. + * @param int $timestamp + * The current timestamp. + * @param string $hash + * Login link hash. + * + * @return array|\Symfony\Component\HttpFoundation\RedirectResponse + * The form structure or a redirect response. + * + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * If the login link is for a blocked user or invalid user ID. + */ + public function resetPass($uid, $timestamp, $hash) { + $account = $this->currentUser(); + $config = $this->config('user.settings'); + // When processing the one-time login link, we have to make sure that a user + // isn't already logged in. + if ($account->isAuthenticated()) { + // The current user is already logged in. + if ($account->id() == $uid) { + drupal_set_message($this->t('You are logged in as %user. Change your password.', array('%user' => $account->getUsername(), '!user_edit' => $this->url('user.edit', array('user' => $account->id()))))); + } + // A different user is already logged in on the computer. + else { + if ($reset_link_user = $this->userStorage->load($uid)) { + drupal_set_message($this->t('Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user. Please logout and try using the link again.', + array('%other_user' => $account->getUsername(), '%resetting_user' => $reset_link_user->getUsername(), '!logout' => $this->url('user.logout')))); + } + else { + // Invalid one-time link specifies an unknown user. + drupal_set_message($this->t('The one-time login link you clicked is invalid.')); + } + } + return $this->redirect(''); + } + else { + // The current user is not logged in, so check the parameters. + // Time out, in seconds, until login URL expires. + $timeout = $config->get('password_reset_timeout'); + $current = REQUEST_TIME; + /* @var \Drupal\user\UserInterface $user */ + $user = $this->userStorage->load($uid); + // Verify that the user exists and is active. + if ($user && $user->isActive()) { + // No time out for first time login. + if ($user->getLastLoginTime() && $current - $timestamp > $timeout) { + drupal_set_message($this->t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.')); + return $this->redirect('user.pass'); + } + elseif ($user->isAuthenticated() && ($timestamp >= $user->getLastLoginTime()) && ($timestamp <= $current) && ($hash === user_pass_rehash($user->getPassword(), $timestamp, $user->getLastLoginTime()))) { + $expiration_date = $user->getLastLoginTime() ? $this->date->format($timestamp + $timeout) : NULL; + return $this->formBuilder()->getForm('Drupal\user\Form\UserPasswordResetForm', $user, $expiration_date, $timestamp, $hash); + } + else { + drupal_set_message($this->t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.')); + return $this->redirect('user.pass'); + } + } + } + // Blocked or invalid user ID, so deny access. The parameters will be in the + // watchdog's URL for the administrator to check. + throw new AccessDeniedHttpException(); + } + + /** * Returns the user page. * * Displays user profile if user is logged in, or login form for anonymous diff --git a/core/modules/user/src/Form/UserForm.php b/core/modules/user/src/Form/UserForm.php deleted file mode 100644 index 919cb1e..0000000 --- a/core/modules/user/src/Form/UserForm.php +++ /dev/null @@ -1,24 +0,0 @@ -getForm('user_pass_reset', $uid, $timestamp, $hash, $operation); - } - -} diff --git a/core/modules/user/src/Form/UserPasswordResetForm.php b/core/modules/user/src/Form/UserPasswordResetForm.php new file mode 100644 index 0000000..ba7c121 --- /dev/null +++ b/core/modules/user/src/Form/UserPasswordResetForm.php @@ -0,0 +1,119 @@ +logger = $logger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('logger.factory')->get('user') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormID() { + return 'user_pass_reset'; + } + + /** + * {@inheritdoc} + * + * @param array $form + * An associative array containing the structure of the form. + * @param array $form_state + * An associative array containing the current state of the form. + * @param \Drupal\Core\Session\AccountInterface $user + * User requesting reset. + * @param string $expiration_date + * Formatted expiration date for the login link, or NULL if the link does + * not expire. + * @param int $timestamp + * The current timestamp. + * @param string $hash + * Login link hash. + */ + public function buildForm(array $form, array &$form_state, AccountInterface $user = NULL, $expiration_date = NULL, $timestamp = NULL, $hash = NULL) { + if ($expiration_date) { + $form['message'] = array('#markup' => $this->t('

This is a one-time login for %user_name and will expire on %expiration_date.

Click on this button to log in to the site and change your password.

', array('%user_name' => $user->getUsername(), '%expiration_date' => $expiration_date))); + } + else { + // No expiration for first time login. + $form['message'] = array('#markup' => $this->t('

This is a one-time login for %user_name.

Click on this button to log in to the site and change your password.

', array('%user_name' => $user->getUsername()))); + } + + $form['#title'] = 'Reset Password'; + $form['user'] = array( + '#type' => 'value', + '#value' => $user, + ); + $form['timestamp'] = array( + '#type' => 'value', + '#value' => $timestamp, + ); + $form['help'] = array('#markup' => '

' . $this->t('This login can be used only once.') . '

'); + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => $this->t('Log in'), + ); + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + /** @var $user \Drupal\user\UserInterface */ + $user = $form_state['values']['user']; + user_login_finalize($user); + $this->logger->notice('User %name used one-time login link at time %timestamp.', array('%name' => $user->getUsername(), '%timestamp' => $form_state['values']['timestamp'])); + drupal_set_message($this->t('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.')); + // Let the user's password be changed without the current password check. + $token = Crypt::randomBytesBase64(55); + $_SESSION['pass_reset_' . $user->id()] = $token; + $form_state['redirect_route']['route_name'] = 'user.edit'; + $form_state['redirect_route']['route_parameters'] = array('user' => $user->id()); + $form_state['redirect_route']['options'] = array( + 'query' => array('pass-reset-token' => $token), + 'absolute' => TRUE, + ); + } + +} + diff --git a/core/modules/user/src/Tests/UserPasswordResetTest.php b/core/modules/user/src/Tests/UserPasswordResetTest.php index 41adecc..1045e2c 100644 --- a/core/modules/user/src/Tests/UserPasswordResetTest.php +++ b/core/modules/user/src/Tests/UserPasswordResetTest.php @@ -106,6 +106,13 @@ function testUserPasswordReset() { $_uid = $this->account->id(); $this->drupalGet("user/reset/$_uid/$bogus_timestamp/" . user_pass_rehash($this->account->getPassword(), $bogus_timestamp, $this->account->getLastLoginTime())); $this->assertText(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'Expired password reset request rejected.'); + + // Create a user, block the account, and verify that a login link is denied. + $timestamp = REQUEST_TIME - 1; + $blocked_account = $this->drupalCreateUser()->block(); + $blocked_account->save(); + $this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account->getPassword(), $timestamp, $blocked_account->getLastLoginTime())); + $this->assertResponse(403); } /** diff --git a/core/modules/user/user.pages.inc b/core/modules/user/user.pages.inc index 4b6a0ae..4a09d7b 100644 --- a/core/modules/user/user.pages.inc +++ b/core/modules/user/user.pages.inc @@ -6,97 +6,8 @@ */ use Drupal\Core\Render\Element; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; -use Symfony\Component\HttpKernel\HttpKernelInterface; -use Drupal\Component\Utility\Crypt; - -/** - * Menu callback; process one time login link and redirects to the user page on success. - * - * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. - * Use \Drupal\user\Form\UserForm::resetPass(). - */ -function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $action = NULL) { - global $user; - - // When processing the one-time login link, we have to make sure that a user - // isn't already logged in. - if ($user->isAuthenticated()) { - // The existing user is already logged in. - if ($user->id() == $uid) { - drupal_set_message(t('You are logged in as %user. Change your password.', array('%user' => $user->getUsername(), '!user_edit' => url("user/" . $user->id() . "/edit")))); - } - // A different user is already logged in on the computer. - else { - $reset_link_account = user_load($uid); - if (!empty($reset_link_account)) { - drupal_set_message(t('Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user. Please logout and try using the link again.', - array('%other_user' => $user->getUsername(), '%resetting_user' => $reset_link_account->getUsername(), '!logout' => url('user/logout')))); - } else { - // Invalid one-time link specifies an unknown user. - drupal_set_message(t('The one-time login link you clicked is invalid.')); - } - } - return new RedirectResponse(url('', array('absolute' => TRUE))); - } - else { - // Time out, in seconds, until login URL expires. - $timeout = \Drupal::config('user.settings')->get('password_reset_timeout'); - $current = REQUEST_TIME; - $account = user_load($uid); - // Verify that the user exists and is active. - if ($timestamp <= $current && $account && $account->isActive()) { - // No time out for first time login. - if ($account->getLastLoginTime() && $current - $timestamp > $timeout) { - drupal_set_message(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.')); - return new RedirectResponse(url('user/password', array('absolute' => TRUE))); - } - elseif ($account->isAuthenticated() && $timestamp >= $account->getLastLoginTime() && $timestamp <= $current && $hashed_pass == user_pass_rehash($account->getPassword(), $timestamp, $account->getLastLoginTime())) { - // First stage is a confirmation form, then login - if ($action == 'login') { - // Set the new user. - // user_login_finalize() also updates the login timestamp of the - // user, which invalidates further use of the one-time login link. - user_login_finalize($account); - \Drupal::logger('user')->notice('User %name used one-time login link at time %timestamp.', array('%name' => $account->getUsername(), '%timestamp' => $timestamp)); - drupal_set_message(t('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.')); - // Let the user's password be changed without the current password check. - $token = Crypt::randomBytesBase64(55); - $_SESSION['pass_reset_' . $user->id()] = $token; - return new RedirectResponse(url('user/' . $user->id() . '/edit', array( - 'query' => array('pass-reset-token' => $token), - 'absolute' => TRUE, - ))); - } - else { - if (!$account->getLastLoginTime()) { - // No expiration for first time login. - $form['message'] = array('#markup' => t('

This is a one-time login for %user_name.

Click on this button to log in to the site and change your password.

', array('%user_name' => $account->getUsername()))); - } - else { - $form['message'] = array('#markup' => t('

This is a one-time login for %user_name and will expire on %expiration_date.

Click on this button to log in to the site and change your password.

', array('%user_name' => $account->getUsername(), '%expiration_date' => format_date($timestamp + $timeout)))); - } - $form['help'] = array('#markup' => '

' . t('This login can be used only once.') . '

'); - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Log in')); - $form['#action'] = url("user/reset/$uid/$timestamp/$hashed_pass/login"); - return $form; - } - } - else { - drupal_set_message(t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.')); - return new RedirectResponse(url('user/password', array('absolute' => TRUE))); - } - } - else { - // Deny access, no more clues. - // Everything will be in the watchdog's URL for the administrator to check. - throw new AccessDeniedHttpException(); - } - } -} /** * Prepares variables for user templates. diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml index 0a7b842..087ffec 100644 --- a/core/modules/user/user.routing.yml +++ b/core/modules/user/user.routing.yml @@ -182,11 +182,10 @@ user.cancel_confirm: _entity_access: 'user.delete' user.reset: - path: '/user/reset/{uid}/{timestamp}/{hash}/{operation}' + path: '/user/reset/{uid}/{timestamp}/{hash}' defaults: - _content: '\Drupal\user\Form\UserForm::resetPass' + _content: '\Drupal\user\Controller\UserController::resetPass' _title: 'Reset password' - operation: NULL requirements: _access: 'TRUE' options: