diff --git a/core/modules/user/config/install/user.mail.yml b/core/modules/user/config/install/user.mail.yml index a1b9068..48d458b 100644 --- a/core/modules/user/config/install/user.mail.yml +++ b/core/modules/user/config/install/user.mail.yml @@ -8,7 +8,7 @@ mail_change_notification: body: "[user:display-name],\n\nA request to change your email address has been made at [site:name]. In order to complete the change you will need to follow the instructions sent to your new email address within one day." subject: 'Email change information for [user:display-name] at [site:name]' mail_change_verification: - body: "[user:display-name],\n\nA request to change your email address has been made at [site:name]. You need to verify the change by clicking on the link below or copying and pasting it in your browser:\n\n[user:mail-change-login-url]\n\nThis is a one-time URL, so it can be used only once. It expires after one day. If not used, your email address at [site:name] will not change." + body: "[user:display-name],\n\nA request to change your email address has been made at [site:name]. You need to verify the change by clicking on the link below or copying and pasting it in your browser:\n\n[user:mail-change-url]\n\nThis is a one-time URL, so it can be used only once. It expires after one day. If not used, your email address at [site:name] will not change." subject: 'Email change information for [user:display-name] at [site:name]' register_admin_created: body: "[user:display-name],\n\nA site administrator at [site:name] has created an account for you. You may now log in by clicking this link or copying and pasting it into your browser:\n\n[user:one-time-login-url]\n\nThis link can only be used once to log in and will lead you to a page where you can set your password.\n\nAfter setting your password, you will be able to log in at [site:login-url] in the future using:\n\nusername: [user:name]\npassword: Your password\n\n-- [site:name] team" diff --git a/core/modules/user/config/install/user.settings.yml b/core/modules/user/config/install/user.settings.yml index 44e0ccf..ca46109 100644 --- a/core/modules/user/config/install/user.settings.yml +++ b/core/modules/user/config/install/user.settings.yml @@ -3,16 +3,17 @@ verify_mail: true notify: cancel_confirm: true password_reset: true - mail_change_notification: true - mail_change_verification: true status_activated: true status_blocked: false status_canceled: false register_admin_created: true register_no_approval_required: true register_pending_approval: true + mail_change_notification: true + mail_change_verification: true register: visitors cancel_method: user_cancel_block password_reset_timeout: 86400 +mail_change_timeout: 86400 password_strength: true langcode: en diff --git a/core/modules/user/config/schema/user.schema.yml b/core/modules/user/config/schema/user.schema.yml index d027c7b..206a853 100644 --- a/core/modules/user/config/schema/user.schema.yml +++ b/core/modules/user/config/schema/user.schema.yml @@ -20,12 +20,6 @@ user.settings: password_reset: type: boolean label: 'Notify user when password reset' - mail_change_notification: - type: boolean - label: 'Notify user when email changes' - mail_change_verification: - type: boolean - label: 'Require email verification when a user changes their email address' status_activated: type: boolean label: 'Notify user when account is activated' @@ -44,6 +38,12 @@ user.settings: register_pending_approval: type: boolean label: 'Welcome (awaiting approval)' + mail_change_notification: + type: boolean + label: 'Notify user when email changes' + mail_change_verification: + type: boolean + label: 'Require email verification when a user changes their email address' register: type: string label: 'Who can register accounts?' @@ -53,6 +53,9 @@ user.settings: password_reset_timeout: type: integer label: 'Password reset timeout' + mail_change_timeout: + type: integer + label: 'Mail change timeout' password_strength: type: boolean label: 'Enable password strength indicator' diff --git a/core/modules/user/src/AccountForm.php b/core/modules/user/src/AccountForm.php index 4c97426..14272a2 100644 --- a/core/modules/user/src/AccountForm.php +++ b/core/modules/user/src/AccountForm.php @@ -388,17 +388,15 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $old_mail = $account->getEmail(); if (!$account->isNew() && ($old_mail !== $new_mail) && !$this->currentUser()->hasPermission('administer users')) { - // Send a verification to the new email address. $account_cloned = clone $account; $account_cloned->setEmail($new_mail); if (_user_mail_notify('mail_change_verification', $account_cloned)) { // Send notification email to the old email address. - $account->setEmail($old_mail); _user_mail_notify('mail_change_notification', $account); } - // The user's email address will be updated after verification. + // The user's mail address will be updated only after verification. $form_state->setValue('mail', $old_mail); drupal_set_message($this->t('Your new email address needs to be validated. Further instructions have been sent to your new email address.'), 'warning'); diff --git a/core/modules/user/src/AccountSettingsForm.php b/core/modules/user/src/AccountSettingsForm.php index b3c5821..f16c246 100644 --- a/core/modules/user/src/AccountSettingsForm.php +++ b/core/modules/user/src/AccountSettingsForm.php @@ -196,7 +196,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { ); // These email tokens are shared for all settings, so just define // the list once to help ensure they stay in sync. - $email_token_help = $this->t('Available variables are: [site:name], [site:url], [user:display-name], [user:account-name], [user:mail], [site:login-url], [site:url-brief], [user:edit-url], [user:one-time-login-url], [user:mail-change-login-url], [user:cancel-url].'); + $email_token_help = $this->t('Available variables are: [site:name], [site:url], [user:display-name], [user:account-name], [user:mail], [site:login-url], [site:url-brief], [user:edit-url], [user:one-time-login-url], [user:mail-change-url], [user:cancel-url].'); $form['email_admin_created'] = array( '#type' => 'details', @@ -297,51 +297,76 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#default_value' => $mail_config->get('password_reset.body'), '#rows' => 12, ); - - $form['email_email_change_notification'] = array( + $form['mail_change'] = [ '#type' => 'details', - '#title' => $this->t('Email change notification'), + '#title' => $this->t('Account email changing'), '#collapsible' => TRUE, '#collapsed' => TRUE, - '#description' => $this->t("Edit the email message sent to the user's old email address when the email address is changed.") . ' ' . $email_token_help, '#group' => 'email', '#weight' => 11, - ); - $form['email_email_change_notification']['user_email_change_notification_subject'] = array( + ]; + $form['mail_change']['mail_change_notification'] = [ + '#type' => 'details', + '#tree' => TRUE, + '#title' => $this->t('Notification of old email'), + '#description' => $this->t("Edit the email message sent to the user's old email address when the email address is changed.") . ' ' . $email_token_help, + '#open' => TRUE, + ]; + $form['mail_change']['mail_change_notification']['enabled'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Notify user when email changes'), + '#default_value' => $config->get('notify.mail_change_notification'), + ]; + $states = [ + 'invisible' => [ + 'input[name="mail_change_notification[enabled]"]' => ['checked' => FALSE], + ], + ]; + $form['mail_change']['mail_change_notification']['subject'] = [ '#type' => 'textfield', '#title' => $this->t('Subject'), '#default_value' => $mail_config->get('mail_change_notification.subject'), '#maxlength' => 180, - ); - $form['email_email_change_notification']['user_email_change_notification_body'] = array( + '#states' => $states, + ]; + $form['mail_change']['mail_change_notification']['body'] = [ '#type' => 'textarea', '#title' => $this->t('Body'), '#default_value' => $mail_config->get('mail_change_notification.body'), '#rows' => 12, - ); - - $form['email_email_change_verification'] = array( + '#states' => $states, + ]; + $form['mail_change']['mail_change_verification'] = [ '#type' => 'details', - '#title' => $this->t('Email change verification'), - '#collapsible' => TRUE, - '#collapsed' => TRUE, + '#tree' => TRUE, + '#title' => $this->t('Verification of new email'), '#description' => $this->t("Edit the email message sent to user's new email address when the email address is changed.") . ' ' . $email_token_help, - '#group' => 'email', - '#weight' => 13, - ); - $form['email_email_change_verification']['user_email_change_verification_subject'] = array( + '#open' => TRUE, + ]; + $form['mail_change']['mail_change_verification']['enabled'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Require email verification when a user changes their email address'), + '#default_value' => $config->get('notify.mail_change_verification'), + ]; + $states = [ + 'invisible' => [ + 'input[name="mail_change_verification[enabled]"]' => ['checked' => FALSE], + ], + ]; + $form['mail_change']['mail_change_verification']['subject'] = [ '#type' => 'textfield', '#title' => $this->t('Subject'), '#default_value' => $mail_config->get('mail_change_verification.subject'), '#maxlength' => 180, - ); - $form['email_email_change_verification']['user_email_change_verification_body'] = array( + '#states' => $states, + ]; + $form['mail_change']['mail_change_verification']['body'] = [ '#type' => 'textarea', '#title' => $this->t('Body'), '#default_value' => $mail_config->get('mail_change_verification.body'), '#rows' => 12, - ); - + '#states' => $states, + ]; $form['email_activated'] = array( '#type' => 'details', '#title' => $this->t('Account activation'), @@ -478,16 +503,20 @@ public function submitForm(array &$form, FormStateInterface $form_state) { ->set('notify.status_activated', $form_state->getValue('user_mail_status_activated_notify')) ->set('notify.status_blocked', $form_state->getValue('user_mail_status_blocked_notify')) ->set('notify.status_canceled', $form_state->getValue('user_mail_status_canceled_notify')) + ->set('notify.mail_change_notification', $form_state->getValue(['mail_change_notification', 'enabled'])) + ->set('notify.mail_change_verification', $form_state->getValue(['mail_change_verification', 'enabled'])) ->save(); + + $form_state->unsetValue(['mail_change_notification', 'enabled']); + $form_state->unsetValue(['mail_change_verification', 'enabled']); + $this->config('user.mail') ->set('cancel_confirm.body', $form_state->getValue('user_mail_cancel_confirm_body')) ->set('cancel_confirm.subject', $form_state->getValue('user_mail_cancel_confirm_subject')) ->set('password_reset.body', $form_state->getValue('user_mail_password_reset_body')) ->set('password_reset.subject', $form_state->getValue('user_mail_password_reset_subject')) - ->set('mail_change_notification.subject', $form_state->getValue('user_email_change_notification_subject')) - ->set('mail_change_notification.body', $form_state->getValue('user_email_change_notification_body')) - ->set('mail_change_verification.subject', $form_state->getValue('user_email_change_verification_subject')) - ->set('mail_change_verification.body', $form_state->getValue('user_email_change_verification_body')) + ->set('mail_change_notification', $form_state->getValue('mail_change_notification')) + ->set('mail_change_verification', $form_state->getValue('mail_change_verification')) ->set('register_admin_created.body', $form_state->getValue('user_mail_register_admin_created_body')) ->set('register_admin_created.subject', $form_state->getValue('user_mail_register_admin_created_subject')) ->set('register_no_approval_required.body', $form_state->getValue('user_mail_register_no_approval_required_body')) diff --git a/core/modules/user/src/Controller/ChangeEmailController.php b/core/modules/user/src/Controller/ChangeEmailController.php deleted file mode 100644 index 7c0e834..0000000 --- a/core/modules/user/src/Controller/ChangeEmailController.php +++ /dev/null @@ -1,141 +0,0 @@ -entityTypeManager()->getStorage('user')->load($uid); - - // We need to set the new email here to validate the hash correctly, which - // is created using the new mail adress. We only save the account if the - // hash matches and the user is active. - $timeout = 24 * 60 * 60; - - if ($account->isActive() && $timestamp < REQUEST_TIME) { - if (REQUEST_TIME - $timestamp > $timeout) { - drupal_set_message($this->t('You have tried to use a one-time email address change link for %account that has expired -- your change of email address was not completed. Please visit your account edit page if you wish to attempt the change again.', ['%account' => $account->getAccountName()]), 'error'); - } - elseif (!$this->currentUser()->isAnonymous() && ($this->currentUser()->id() != $account->id())) { - drupal_set_message($this->t('You are currently logged in as %user, and are attempting to confirm an email address change for %account, which is not allowed. Please log in as %account and initiate a new change of email request.', ['%user' => $this->currentUser()->getAccountName(), '%account' => $account->getAccountName()]), 'error'); - } - else { - $account->setEmail($new_mail); - $account->save(); - drupal_set_message($this->t('Your email address has been changed to %mail.', ['%mail' => $new_mail])); - } - } - else { - // Deny access if the timestamp passed is in the future. - throw new AccessDeniedHttpException(); - } - - return $this->redirect('user.page'); - } - - /** - * Generates a unique URL for a one time email change confirmation. - * - * @param \Drupal\user\UserInterface $account - * An object containing the user account. - * @param array $options - * (optional) A keyed array of settings. Supported options are: - * - langcode: A language code to be used when generating locale-sensitive - * URLs. If langcode is NULL the users preferred language is used. - * @param int $timestamp - * (optional) The timestamp when hash is created. - * @param string $hash - * (optional) Unique hash. - * - * @return \Drupal\Core\Url - * A unique URL that provides a one-time email change confirmation for the - * user. - */ - public static function getUrl(UserInterface $account, array $options = [], $timestamp = REQUEST_TIME, $hash = NULL) { - $langcode = isset($options['langcode']) ? $options['langcode'] : $account->getPreferredLangcode(); - $hash = empty($hash) ? self::getHash($account, $timestamp) : $hash; - $url_options = ['absolute' => TRUE, 'language' => \Drupal::getContainer()->get('language_manager')->getLanguage($langcode)]; - return Url::fromRoute('user.change_email', [ - 'uid' => $account->id(), - 'timestamp' => $timestamp, - 'new_mail' => $account->getEmail(), - 'hash' => $hash, - ], $url_options); - } - - /** - * Generates hash for change mail URL. - * - * @param \Drupal\user\UserInterface $account - * An object containing the user account. - * @param int $timestamp - * The timestamp when hash is created. - * - * @return string - * A new generated hash. - */ - public static function getHash(UserInterface $account, $timestamp) { - $data = $timestamp; - $data .= $account->id(); - $data .= $account->getEmail(); - $data .= $account->getCreatedTime(); - $data .= $account->getInitialEmail(); - return Crypt::hmacBase64($data, Settings::getHashSalt() . $account->getPassword()); - } - - /** - * Checks access to change email URL. - * - * @param int $uid - * The ID of the user requesting reset. - * @param int $timestamp - * The timestamp when hash is created. - * @param string $hash - * Unique hash. - * @param string $new_mail - * A new user email. - * - * @return \Drupal\Core\Access\AccessResultInterface - * An access result - */ - public function access($uid, $timestamp, $hash, $new_mail) { - /** @var \Drupal\user\UserInterface $account */ - $account = $this->entityTypeManager()->getStorage('user')->load($uid); - $account->setEmail($new_mail); - return AccessResult::allowedIf($hash == $this->getHash($account, $timestamp)); - } - -} diff --git a/core/modules/user/src/Controller/MailChangeController.php b/core/modules/user/src/Controller/MailChangeController.php new file mode 100644 index 0000000..3b3fa18 --- /dev/null +++ b/core/modules/user/src/Controller/MailChangeController.php @@ -0,0 +1,140 @@ +config('user.settings')->get('mail_change_timeout'); + /** @var \Drupal\Core\Session\AccountProxyInterface $current_user */ + $current_user = $this->currentUser(); + + // Other user is authenticated. + if ($current_user->isAuthenticated() && $current_user->id() != $user->id()) { + drupal_set_message($this->t('You are currently logged in as %user, and are attempting to confirm an email address change for other account. Please log out and try using the link again.', ['%user' => $current_user->getAccountName(), ':logout' => Url::fromRoute('user.logout')->toString()]), 'error'); + } + // The link has expired. + elseif (REQUEST_TIME - $timestamp > $timeout) { + drupal_set_message($this->t('You have tried to use an email address change link that has expired. Please visit your account and change your email again.'), 'error'); + } + // The link is valid. + elseif ($timestamp <= REQUEST_TIME && $timestamp >= $user->getLastLoginTime() && Crypt::hashEquals($hash, static::getHash($user, $timestamp, $new_mail))) { + // Save the new email but refresh also the last login time so that this + // mail change link get expired. + $user->setEmail($new_mail)->setLastLoginTime(REQUEST_TIME)->save(); + /** @var \Drupal\user\UserStorageInterface $user_storage */ + $user_storage = $this->entityTypeManager()->getStorage('user'); + $user_storage->updateLastLoginTimestamp($user); + // Reflect the changes in the session if the user is user is logged in. + if ($current_user->isAuthenticated() && $current_user->id() == $user->id()) { + $current_user->setAccount($user); + } + drupal_set_message($this->t("Your email address has been changed to %mail.", ['%mail' => $new_mail])); + } + // Timestamp from the link is abnormal (in the future) or user registered a + // new login in the meantime or the hash is not valid. + else { + drupal_set_message($this->t('You have tried to use an email address change link that has either been used or is no longer valid. Please visit your account and change your email again.'), 'error'); + } + + return $this->redirect(''); + } + + /** + * Checks access to change email url. + * + * @param \Drupal\user\UserInterface $user + * The user account requesting Email change. + * + * @return \Drupal\Core\Access\AccessResultInterface + * An access result + */ + public function access(UserInterface $user) { + return AccessResult::allowedIf($user->isActive()); + } + + /** + * Generates a unique url for a one time email change confirmation. + * + * @param \Drupal\user\UserInterface $account + * An object containing the user account. + * @param array $options + * (optional) A keyed array of settings. Supported options are: + * - langcode: A language code to be used when generating locale-sensitive + * URLs. If langcode is NULL the users preferred language is used. + * @param int $timestamp + * (optional) The timestamp when hash is created. If missed, the current + * request time is used. + * @param string $hash + * (optional) Unique hash. If missed, the hash is computed based on the + * account data and timestamp. + * + * @return \Drupal\Core\Url + * A unique url that provides a one-time email change confirmation. + */ + public static function getUrl(UserInterface $account, array $options = [], $timestamp = REQUEST_TIME, $hash = NULL) { + $langcode = isset($options['langcode']) ? $options['langcode'] : $account->getPreferredLangcode(); + $hash = empty($hash) ? static::getHash($account, $timestamp) : $hash; + $url_options = ['absolute' => TRUE, 'language' => \Drupal::service('language_manager')->getLanguage($langcode)]; + return Url::fromRoute('user.mail_change', [ + 'user' => $account->id(), + 'timestamp' => $timestamp, + 'new_mail' => $account->getEmail(), + 'hash' => $hash, + ], $url_options); + } + + /** + * Generates hash for change mail url. + * + * This method is a wrapper around user_pass_rehash(). Optionally, it allows + * an alternative mail address to be used in hash computing. + * + * @param \Drupal\user\UserInterface $user + * An object containing the user account. + * @param int $timestamp + * The timestamp when hash is created. + * @param string $mail + * (optional) If passed, this value will be used to compute the hash instead + * that stored into $account. + * + * @return string + * A new generated hash. + */ + protected static function getHash(UserInterface $user, $timestamp, $mail = NULL) { + if (!empty($mail)) { + // Clone to keep the original object unchanged. + $account = clone $user; + $account->setEmail($mail); + } + else { + $account = $user; + } + return user_pass_rehash($account, $timestamp); + } + +} diff --git a/core/modules/user/src/Tests/Update/UpdateMailChangeTest.php b/core/modules/user/src/Tests/Update/UpdateMailChangeTest.php new file mode 100644 index 0000000..8fde087 --- /dev/null +++ b/core/modules/user/src/Tests/Update/UpdateMailChangeTest.php @@ -0,0 +1,59 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8-rc1.bare.standard.php.gz', + ]; + } + + /** + * Tests user_post_update_mail_change(). + * + * @see user_post_update_mail_change() + */ + public function testMailChangeUpdate() { + $user_settings = $this->config('user.settings'); + $user_mail = $this->config('user.mail'); + + // Check that mail change notifications settings are not set. + $this->assertNull($user_settings->get('notify.mail_change_notification')); + $this->assertNull($user_settings->get('notify.mail_change_verification')); + $this->assertNull($user_settings->get('mail_change_timeout')); + + // Check that mail change configurations are not set. + $this->assertNull($user_mail->get('mail_change_notification')); + $this->assertNull($user_mail->get('mail_change_verification')); + + $this->runUpdates(); + + $user_settings = $this->config('user.settings'); + $user_mail = $this->config('user.mail'); + + // Check that mail change notifications were set to TRUE. + $this->assertTrue($user_settings->get('notify.mail_change_notification')); + $this->assertTrue($user_settings->get('notify.mail_change_verification')); + $this->assertIdentical($user_settings->get('mail_change_timeout'), 86400); + + $config = Yaml::parse(file_get_contents(__DIR__ . '/../../../config/install/user.mail.yml')); + + // Check that mail change configurations were set to default values. + $this->assertIdentical($user_mail->get('mail_change_notification'), $config['mail_change_notification']); + $this->assertIdentical($user_mail->get('mail_change_verification'), $config['mail_change_verification']); + } + +} diff --git a/core/modules/user/src/Tests/UserChangeEmailTest.php b/core/modules/user/src/Tests/UserChangeEmailTest.php deleted file mode 100644 index b54fd31..0000000 --- a/core/modules/user/src/Tests/UserChangeEmailTest.php +++ /dev/null @@ -1,166 +0,0 @@ -account = $this->drupalCreateUser(); - } - - /** - * Tests email change functionality. - */ - public function testEmailChange() { - // Create a user. - $this->drupalLogin($this->account); - - // Save a new email and verify that the user was sent an email. - $new_mail = $this->getRandomEmailAddress(); - $edit = [ - 'mail' => $new_mail, - 'current_pass' => $this->account->pass_raw, - ]; - $this->drupalPostForm('user/' . $this->account->id() . '/edit', $edit, t('Save')); - - // Take email settings from user.mail.yml. - $mail_settings = $this->config('user.mail'); - - /** @var \Drupal\Core\Utility\Token $token_service */ - $token_service = $this->container->get('token'); - // Verify that the user was sent a notification email. - $this->assertMail('to', $this->account->getEmail()); - $subject = $token_service->replace($mail_settings->get('mail_change_notification.subject'), ['user' => $this->account]); - $this->assertMail('subject', $subject); - - // Verify that the user was sent a verification email. - $this->assertMailString('to', $new_mail, 2); - $subject = $token_service->replace($mail_settings->get('mail_change_verification.subject'), ['user' => $this->account]); - $this->assertMailString('subject', $subject, 2); - - $change_mail_url = $this->gerUrlEmail('user_mail_change_verification'); - - // Check that the email has been successfully updated. - $this->drupalGet($change_mail_url); - $this->assertRaw(t('Your email address has been changed to %mail.', ['%mail' => $new_mail])); - - $this->drupalGet($change_mail_url); - $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache')); - - // Ensure the change email URL is not cached. - $this->drupalGet($change_mail_url); - $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache')); - } - - /** - * Tests change of email for blocked users. - */ - public function testEmailChangeForBlockedUser() { - $timestamp = REQUEST_TIME - 1; - $account_cloned = clone $this->account; - $account_cloned->block(); - $account_cloned->save(); - $this->drupalGet(ChangeEmailController::getUrl($account_cloned, [], $timestamp)->getInternalPath()); - $this->assertResponse(403); - } - - /** - * Tests change of email for expired timestamp. - */ - public function testEmailChangeExpiredTimestamp() { - $timestamp = REQUEST_TIME - 30 * 60 * 60 * 10; - $this->drupalGet(ChangeEmailController::getUrl($this->account, [], $timestamp)->getInternalPath()); - $this->assertRaw(t('You have tried to use a one-time email address change link for %account that has expired -- your change of email address was not completed. Please visit your account edit page if you wish to attempt the change again.', ['%account' => $this->account->getAccountName()])); - } - - /** - * Tests change of email when other user is logged in. - */ - public function testEmailImpersonation() { - $timestamp = REQUEST_TIME - 1; - // Create other account and login with it. - $current_account = $this->drupalCreateUser(); - $this->drupalLogin($current_account); - // Try to change the email for the first account when the other account is - // logged in. - $this->drupalGet(ChangeEmailController::getUrl($this->account, [], $timestamp)->getInternalPath()); - $this->assertRaw(t('You are currently logged in as %user, and are attempting to confirm an email address change for %account, which is not allowed. Please log in as %account and initiate a new change of email request.', ['%user' => $current_account->getAccountName(), '%account' => $this->account->getAccountName()])); - } - - /** - * Tests change of email for timestamp in the future. - */ - public function testEmailChangeForFutureTimestamp() { - $timestamp = REQUEST_TIME + 30 * 60 * 60; - $this->drupalGet(ChangeEmailController::getUrl($this->account, [], $timestamp)->getInternalPath()); - $this->assertResponse(403); - } - - /** - * Tests change of email with the wrong hash. - */ - public function testEmailChangeWithWrongHash() { - $timestamp = REQUEST_TIME - 1; - // Generate the hash for other user. - $other_account = $this->drupalCreateUser(); - $hash = ChangeEmailController::getHash($other_account, $timestamp); - $this->drupalGet(ChangeEmailController::getUrl($this->account, [], $timestamp, $hash)->getInternalPath()); - $this->assertResponse(403); - } - - /** - * Retrieves the change email and extracts the link. - * - * @param string $mail_id - * Unique mail ID. - * - * @return string - * An URL. - */ - protected function gerUrlEmail($mail_id) { - // Assume the most recent email. - $_emails = $this->drupalGetMails(['id' => $mail_id]); - $email = end($_emails); - $urls = []; - preg_match('#.+user/change-mail/.+#', $email['body'], $urls); - return $urls[0]; - } - - /** - * Generates a random email address. - * - * @return string - * A random email address. - */ - protected function getRandomEmailAddress() { - return Unicode::strtolower($this->randomMachineName()) . '@example.com'; - } - -} diff --git a/core/modules/user/src/Tests/UserMailChangeTest.php b/core/modules/user/src/Tests/UserMailChangeTest.php new file mode 100644 index 0000000..ef42e1e --- /dev/null +++ b/core/modules/user/src/Tests/UserMailChangeTest.php @@ -0,0 +1,168 @@ +account = $this->drupalCreateUser(); + } + + /** + * Tests email change functionality. + */ + public function testMailChange() { + $this->drupalLogin($this->account); + + // Save a new email and verify that the user has sent an email. + $new_mail = $this->getRandomEmailAddress(); + $edit = [ + 'mail' => $new_mail, + 'current_pass' => $this->account->pass_raw, + ]; + $this->drupalPostForm($this->account->toUrl('edit-form'), $edit, t('Save')); + + $user_mail = $this->config('user.mail'); + + /** @var \Drupal\Core\Utility\Token $token_service */ + $token_service = $this->container->get('token'); + + // Check that a notification mail has been sent. + $this->assertMail('to', $this->account->getEmail()); + $subject = $token_service->replace($user_mail->get('mail_change_notification.subject'), ['user' => $this->account]); + $this->assertMail('subject', $subject); + + // Check that a verification mail has been sent. + $this->assertMailString('to', $new_mail, 2); + $subject = $token_service->replace($user_mail->get('mail_change_verification.subject'), ['user' => $this->account]); + $this->assertMailString('subject', $subject, 2); + + $sent_mail_change_url = $this->extractUrlFromMail('user_mail_change_verification'); + + // Check that the email has been successfully updated. + $this->drupalGet($sent_mail_change_url); + $this->assertRaw(t('Your email address has been changed to %mail.', ['%mail' => $new_mail])); + + // Check the change email URL is not cached and expires after first use. + $this->drupalGet($sent_mail_change_url); + $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache')); + $this->assertRaw(t('You have tried to use an email address change link that has either been used or is no longer valid. Please visit your account and change your email again.')); + + // Check that the user mail has been changed. + $this->assertIdentical(User::load($this->account->id())->getEmail(), $new_mail); + } + + /** + * Tests change of email for blocked users. + */ + public function testBlockedUser() { + $timestamp = REQUEST_TIME - 1; + $account_cloned = clone $this->account; + $account_cloned->block()->save(); + $this->drupalGet(MailChangeController::getUrl($account_cloned, [], $timestamp)->getInternalPath()); + $this->assertResponse(403); + } + + /** + * Tests change of email for expired timestamp. + */ + public function testExpiredTimestamp() { + $timestamp = REQUEST_TIME - (24 * 60 * 60 + 1); + $this->drupalGet(MailChangeController::getUrl($this->account, [], $timestamp)->getInternalPath()); + $this->assertRaw(t('You have tried to use an email address change link that has expired. Please visit your account and change your email again.')); + } + + /** + * Tests change of email when other user is logged in. + */ + public function testOtherUserLoggedIn() { + $timestamp = REQUEST_TIME - 1; + // Create other account and login with it. + $current_account = $this->drupalCreateUser(); + $this->drupalLogin($current_account); + // Try to change the email for the first account when the other account is + // logged in. + $new_mail = $this->getRandomEmailAddress(); + $this->account->setEmail($new_mail); + $path = MailChangeController::getUrl($this->account, [], $timestamp)->getInternalPath(); + $this->drupalGet($path); + $this->assertRaw(t('You are currently logged in as %user, and are attempting to confirm an email address change for other account. Please log out and try using the link again.', ['%user' => $current_account->getAccountName(), ':logout' => Url::fromRoute('user.logout')->toString()])); + + // Retry as anonymous. + $this->drupalLogout(); + $this->drupalGet($path); + $this->assertRaw(t('Your email address has been changed to %mail.', ['%mail' => $new_mail])); + } + + /** + * Tests change of email for timestamp in the future. + */ + public function testFutureTimestamp() { + $timestamp = REQUEST_TIME + 60 * 60; + $this->drupalGet(MailChangeController::getUrl($this->account, [], $timestamp)->getInternalPath()); + $this->assertRaw(t('You have tried to use an email address change link that has either been used or is no longer valid. Please visit your account and change your email again.')); + } + + /** + * Tests change of email with the wrong hash. + */ + public function testWrongHash() { + $timestamp = REQUEST_TIME - 1; + // Generate the hash for other user. + $other_account = $this->drupalCreateUser(); + $hash = user_pass_rehash($other_account, $timestamp); + $this->drupalGet(MailChangeController::getUrl($this->account, [], $timestamp, $hash)->getInternalPath()); + $this->assertRaw(t('You have tried to use an email address change link that has either been used or is no longer valid. Please visit your account and change your email again.')); + } + + /** + * Retrieves the change email and extracts the link. + * + * @param string $mail_id + * Unique mail ID. + * + * @return string + * An URL. + */ + protected function extractUrlFromMail($mail_id) { + // Assume the most recent email. + $email = $this->drupalGetMails(['id' => $mail_id]); + $email = end($email); + preg_match('#.+user\/mail\-change\/.+#', $email['body'], $urls); + return $urls[0]; + } + + /** + * Generates a random email address. + * + * @return string + * A random email address. + */ + protected function getRandomEmailAddress() { + return Unicode::strtolower($this->randomMachineName()) . '@example.com'; + } + +} diff --git a/core/modules/user/src/Tests/UserTokenReplaceTest.php b/core/modules/user/src/Tests/UserTokenReplaceTest.php index 0675269..7344eb3 100644 --- a/core/modules/user/src/Tests/UserTokenReplaceTest.php +++ b/core/modules/user/src/Tests/UserTokenReplaceTest.php @@ -7,7 +7,7 @@ use Drupal\language\Entity\ConfigurableLanguage; use Drupal\simpletest\WebTestBase; use Drupal\user\Entity\User; -use Drupal\user\Controller\ChangeEmailController; +use Drupal\user\Controller\MailChangeController; /** * Generates text using placeholders for dummy content to check user token @@ -130,7 +130,7 @@ function testUserTokenReplacement() { // Generate login and cancel link. $tests = array(); $tests['[user:one-time-login-url]'] = user_pass_reset_url($account); - $tests['[user:mail-change-login-url]'] = ChangeEmailController::getUrl($account)->toString(); + $tests['[user:mail-change-url]'] = MailChangeController::getUrl($account)->toString(); $tests['[user:cancel-url]'] = user_cancel_url($account); // Generate tokens with interface language. diff --git a/core/modules/user/user.install b/core/modules/user/user.install index 2519918..7cc46ef 100644 --- a/core/modules/user/user.install +++ b/core/modules/user/user.install @@ -1,7 +1,5 @@ getEditable('user.mail'); - $mail_config->set('mail_change_notification.body', $mail_settings['mail_change_notification']['body']); - $mail_config->set('mail_change_notification.subject', $mail_settings['mail_change_notification']['subject']); - $mail_config->set('mail_change_verification.body', $mail_settings['mail_change_verification']['body']); - $mail_config->set('mail_change_verification.subject', $mail_settings['mail_change_verification']['body']); - $mail_config->save(); -} diff --git a/core/modules/user/user.module b/core/modules/user/user.module index a1de45e..8d8f359 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -19,7 +19,7 @@ use Drupal\Core\Site\Settings; use Drupal\Core\Url; use Drupal\system\Entity\Action; -use Drupal\user\Controller\ChangeEmailController; +use Drupal\user\Controller\MailChangeController; use Drupal\user\Entity\Role; use Drupal\user\Entity\User; use Drupal\user\RoleInterface; @@ -951,7 +951,7 @@ function user_mail($key, &$message, $params) { function user_mail_tokens(&$replacements, $data, $options) { if (isset($data['user'])) { $replacements['[user:one-time-login-url]'] = user_pass_reset_url($data['user'], $options); - $replacements['[user:mail-change-login-url]'] = ChangeEmailController::getUrl($data['user'], $options)->toString(); + $replacements['[user:mail-change-url]'] = MailChangeController::getUrl($data['user'], $options)->toString(); $replacements['[user:cancel-url]'] = user_cancel_url($data['user'], $options); } } diff --git a/core/modules/user/user.post_update.php b/core/modules/user/user.post_update.php new file mode 100644 index 0000000..790e5dd --- /dev/null +++ b/core/modules/user/user.post_update.php @@ -0,0 +1,37 @@ +getEditable('user.settings') + ->set('notify.mail_change_notification', $config['notify']['mail_change_notification']) + ->set('notify.mail_change_verification', $config['notify']['mail_change_verification']) + ->set('mail_change_timeout', 86400) + ->save(); + + $config = Yaml::parse(file_get_contents(__DIR__ . '/config/install/user.mail.yml')); + $config_factory->getEditable('user.mail') + ->set('mail_change_notification', $config['mail_change_notification']) + ->set('mail_change_verification', $config['mail_change_verification']) + ->save(); +} + +/** + * @} End of "addtogroup updates-8.2.x". + */ diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml index 25b27d2..1170912 100644 --- a/core/modules/user/user.routing.yml +++ b/core/modules/user/user.routing.yml @@ -151,13 +151,15 @@ user.reset: _maintenance_access: TRUE no_cache: TRUE -user.change_email: - path: '/user/change-mail/{uid}/{timestamp}/{new_mail}/{hash}' +user.mail_change: + path: '/user/mail-change/{user}/{new_mail}/{timestamp}/{hash}' defaults: - _controller: '\Drupal\user\Controller\ChangeEmailController::page' + _controller: '\Drupal\user\Controller\MailChangeController::page' _title: 'Change email address' requirements: - _custom_access: '\Drupal\user\Controller\ChangeEmailController::access' + _custom_access: '\Drupal\user\Controller\MailChangeController::access' + user: \d+ + timestamp: \d+ options: _maintenance_access: TRUE no_cache: TRUE