diff --git a/core/modules/user/config/install/user.mail.yml b/core/modules/user/config/install/user.mail.yml index 9f2ad67..48d458b 100644 --- a/core/modules/user/config/install/user.mail.yml +++ b/core/modules/user/config/install/user.mail.yml @@ -4,6 +4,12 @@ cancel_confirm: password_reset: body: "[user:display-name],\n\nA request to reset the password for your account has been made at [site:name].\n\nYou 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. It expires after one day and nothing will happen if it's not used.\n\n-- [site:name] team" subject: 'Replacement login information for [user:display-name] at [site:name]' +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-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" subject: 'An administrator created an account for you at [site:name]' diff --git a/core/modules/user/config/install/user.settings.yml b/core/modules/user/config/install/user.settings.yml index 8372ccd..ca46109 100644 --- a/core/modules/user/config/install/user.settings.yml +++ b/core/modules/user/config/install/user.settings.yml @@ -9,8 +9,11 @@ notify: 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 627d8a6..206a853 100644 --- a/core/modules/user/config/schema/user.schema.yml +++ b/core/modules/user/config/schema/user.schema.yml @@ -38,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?' @@ -47,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' @@ -61,6 +70,12 @@ user.mail: password_reset: type: mail label: 'Password recovery' + mail_change_notification: + type: mail + label: 'Change mail notification' + mail_change_verification: + type: mail + label: 'Change mail verification' register_admin_created: type: mail label: 'Account created by administrator' diff --git a/core/modules/user/src/AccountForm.php b/core/modules/user/src/AccountForm.php index 81a98ae..d38bbd0 100644 --- a/core/modules/user/src/AccountForm.php +++ b/core/modules/user/src/AccountForm.php @@ -382,13 +382,30 @@ protected function flagViolations(EntityConstraintViolationListInterface $violat * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { + /** @var \Drupal\user\UserInterface $account */ + $account = $this->getEntity(); + $new_mail = $form_state->getValue('mail'); + $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) !== NULL) { + // Send notification email to the old email address. + _user_mail_notify('mail_change_notification', $account); + // The user's mail address will be updated only after verification. + $form_state->setValue('mail', $old_mail); + drupal_set_message($this->t('Your updated email address needs to be validated. Further instructions have been sent to your new email address.'), 'warning'); + } + } + parent::submitForm($form, $form_state); - $user = $this->getEntity($form_state); // If there's a session set to the users id, remove the password reset tag // since a new password was saved. - if (isset($_SESSION['pass_reset_' . $user->id()])) { - unset($_SESSION['pass_reset_' . $user->id()]); + if (isset($_SESSION['pass_reset_' . $account->id()])) { + unset($_SESSION['pass_reset_' . $account->id()]); } } diff --git a/core/modules/user/src/AccountSettingsForm.php b/core/modules/user/src/AccountSettingsForm.php index 1e5cd49..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: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,7 +297,76 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#default_value' => $mail_config->get('password_reset.body'), '#rows' => 12, ); - + $form['mail_change'] = [ + '#type' => 'details', + '#title' => $this->t('Account email changing'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#group' => 'email', + '#weight' => 11, + ]; + $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, + '#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, + '#states' => $states, + ]; + $form['mail_change']['mail_change_verification'] = [ + '#type' => 'details', + '#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, + '#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, + '#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'), @@ -434,12 +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', $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/MailChangeController.php b/core/modules/user/src/Controller/MailChangeController.php new file mode 100644 index 0000000..f3fc66d --- /dev/null +++ b/core/modules/user/src/Controller/MailChangeController.php @@ -0,0 +1,114 @@ +config('user.settings')->get('mail_change_timeout'); + /** @var \Drupal\Core\Session\AccountProxyInterface $current_user */ + $current_user = $this->currentUser(); + $request_time = \Drupal::time()->getRequestTime(); + + // 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 another 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, user_pass_rehash($user, $timestamp, $new_mail))) { + // Save the new email but refresh also the last login time so that this + // mail change link gets 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 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 = NULL, $hash = NULL) { + $timestamp = $timestamp ?: \Drupal::time()->getRequestTime(); + $langcode = isset($options['langcode']) ? $options['langcode'] : $account->getPreferredLangcode(); + $hash = empty($hash) ? user_pass_rehash($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); + } + +} 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..2ae57f1 --- /dev/null +++ b/core/modules/user/src/Tests/Update/UpdateMailChangeTest.php @@ -0,0 +1,60 @@ +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 FALSE. + $this->assertFalse($user_settings->get('notify.mail_change_notification')); + $this->assertFalse($user_settings->get('notify.mail_change_verification')); + + $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']); + // Check that mail change timeout was set. + $this->assertIdentical($user_settings->get('mail_change_timeout'), 86400); + } + +} diff --git a/core/modules/user/src/Tests/UserTokenReplaceTest.php b/core/modules/user/src/Tests/UserTokenReplaceTest.php index b8cfde0..7344eb3 100644 --- a/core/modules/user/src/Tests/UserTokenReplaceTest.php +++ b/core/modules/user/src/Tests/UserTokenReplaceTest.php @@ -7,6 +7,7 @@ use Drupal\language\Entity\ConfigurableLanguage; use Drupal\simpletest\WebTestBase; use Drupal\user\Entity\User; +use Drupal\user\Controller\MailChangeController; /** * Generates text using placeholders for dummy content to check user token @@ -129,6 +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-url]'] = MailChangeController::getUrl($account)->toString(); $tests['[user:cancel-url]'] = user_cancel_url($account); // Generate tokens with interface language. diff --git a/core/modules/user/tests/src/Functional/UserMailChangeTest.php b/core/modules/user/tests/src/Functional/UserMailChangeTest.php new file mode 100644 index 0000000..1d376f5 --- /dev/null +++ b/core/modules/user/tests/src/Functional/UserMailChangeTest.php @@ -0,0 +1,211 @@ +account = $this->drupalCreateUser(); + $this->time = $this->container->get('datetime.time'); + } + + /** + * Tests email change functionality. + */ + public function testMailChange() { + $this->drupalLogin($this->account); + + // Change the user email address. + $new_mail = $this->getRandomEmailAddress(); + $edit = [ + 'mail' => $new_mail, + 'current_pass' => $this->account->pass_raw, + ]; + $this->drupalPostForm($this->account->toUrl('edit-form'), $edit, 'Save'); + + // Check that the validation status message has been displayed. + $this->assertSession()->pageTextContains('Your updated email address needs to be validated. Further instructions have been sent to your new email address.'); + + $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->assertSession()->responseContains(new FormattableMarkup('Your email address has been changed to %mail.', ['%mail' => $new_mail])); + + // Check that the change mail URL is not cached and expires after first use. + $this->drupalGet($sent_mail_change_url); + $this->assertNull($this->drupalGetHeader('X-Drupal-Cache')); + $this->assertSession()->responseContains('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. + self::assertSame(User::load($this->account->id())->getEmail(), $new_mail); + } + + /** + * Tests email change functionality when E-mail change verification is off. + */ + public function testMailChangeNoVerification() { + // Disable mail change verification. + $this->config('user.settings') + ->set('notify.mail_change_verification', FALSE) + ->save(); + $this->drupalLogin($this->account); + + // Change the user email address. + $new_mail = $this->getRandomEmailAddress(); + $edit = [ + 'mail' => $new_mail, + 'current_pass' => $this->account->pass_raw, + ]; + $this->drupalPostForm($this->account->toUrl('edit-form'), $edit, 'Save'); + + // Check that the validation status message has not been displayed. + $this->assertSession()->pageTextNotContains('Your updated email address needs to be validated. Further instructions have been sent to your new email address.'); + + // Check that no E-mail was sent to the old or to the new address. + $this->assertEmpty($this->getMails()); + + // Check that the user's E-mail was changed instantly. + self::assertSame($new_mail, User::load($this->account->id())->getEmail()); + } + + /** + * Tests change of email for blocked users. + */ + public function testBlockedUser() { + $timestamp = $this->time->getRequestTime() - 1; + $account_cloned = clone $this->account; + $account_cloned->block()->save(); + $this->drupalGet(MailChangeController::getUrl($account_cloned, [], $timestamp)->getInternalPath()); + $this->assertSession()->statusCodeEquals(403); + } + + /** + * Tests change of email for expired timestamp. + */ + public function testExpiredTimestamp() { + $timestamp = $this->time->getRequestTime() - (24 * 60 * 60 + 1); + $this->drupalGet(MailChangeController::getUrl($this->account, [], $timestamp)->getInternalPath()); + $this->assertSession()->responseContains('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 = $this->time->getRequestTime() - 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->assertSession()->responseContains(new FormattableMarkup('You are currently logged in as %user, and are attempting to confirm an email address change for another 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->assertSession()->responseContains(new FormattableMarkup('Your email address has been changed to %mail.', ['%mail' => $new_mail])); + } + + /** + * Tests change of email for timestamp in the future. + */ + public function testFutureTimestamp() { + $timestamp = $this->time->getRequestTime() + 60 * 60; + $this->drupalGet(MailChangeController::getUrl($this->account, [], $timestamp)->getInternalPath()); + $this->assertSession()->responseContains('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 = $this->time->getRequestTime() - 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->assertSession()->responseContains('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->getMails(['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/user.module b/core/modules/user/user.module index 90b813f..fb8da25 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -19,6 +19,7 @@ use Drupal\Core\Site\Settings; use Drupal\Core\Url; use Drupal\system\Entity\Action; +use Drupal\user\Controller\MailChangeController; use Drupal\user\Entity\Role; use Drupal\user\Entity\User; use Drupal\user\RoleInterface; @@ -638,15 +639,19 @@ function user_cancel_url(UserInterface $account, $options = array()) { * An object containing the user account. * @param int $timestamp * A UNIX timestamp, typically REQUEST_TIME. + * @param string $mail + * (optional) If passed, this value will be used to compute the hash instead + * of that stored into $account. * * @return string * A string that is safe for use in URLs and SQL statements. */ -function user_pass_rehash(UserInterface $account, $timestamp) { +function user_pass_rehash(UserInterface $account, $timestamp, $mail = NULL) { + $mail = $mail ?: $account->getEmail(); $data = $timestamp; $data .= $account->getLastLoginTime(); $data .= $account->id(); - $data .= $account->getEmail(); + $data .= $mail; return Crypt::hmacBase64($data, Settings::getHashSalt() . $account->getPassword()); } @@ -942,6 +947,7 @@ function user_mail($key, &$message, $params) { * properties: * - login: The UNIX timestamp of the user's last login. * - pass: The hashed account login password. + * - email: The user's email address. * @param array $options * A keyed array of settings and flags to control the token replacement * process. See \Drupal\Core\Utility\Token::replace(). @@ -949,6 +955,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-url]'] = MailChangeController::getUrl($data['user'], $options)->toString(); $replacements['[user:cancel-url]'] = user_cancel_url($data['user'], $options); } } @@ -1178,6 +1185,8 @@ function user_role_revoke_permissions($rid, array $permissions = array()) { * - 'register_pending_approval': Welcome message, user pending admin * approval. * - 'password_reset': Password recovery request. + * - 'mail_change_notification': Email change notification. + * - 'mail_change_verification': Email change verification. * - 'status_activated': Account activated. * - 'status_blocked': Account blocked. * - 'cancel_confirm': Account cancellation request. @@ -1190,11 +1199,13 @@ function user_role_revoke_permissions($rid, array $permissions = array()) { * (optional) Language code to use for the notification, overriding account * language. * - * @return array - * An array containing various information about the message. - * See \Drupal\Core\Mail\MailManagerInterface::mail() for details. + * @return array|null + * An array containing various information about the message, as they are + * returned by \Drupal\Core\Mail\MailManagerInterface::mail(), or NULL if the + * notifications for the required operation are disabled. * * @see user_mail_tokens() + * @see \Drupal\Core\Mail\MailManagerInterface::mail() */ function _user_mail_notify($op, $account, $langcode = NULL) { if (\Drupal::config('user.settings')->get('notify.' . $op)) { diff --git a/core/modules/user/user.post_update.php b/core/modules/user/user.post_update.php new file mode 100644 index 0000000..43461fb --- /dev/null +++ b/core/modules/user/user.post_update.php @@ -0,0 +1,42 @@ +getEditable('user.settings') + ->set('notify.mail_change_notification', FALSE) + ->set('notify.mail_change_verification', FALSE) + ->set('mail_change_timeout', 86400) + ->save(); + + $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-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]', + ]; + + $config_factory->getEditable('user.mail') + ->set('mail_change_notification', $mail_change_notification) + ->set('mail_change_verification', $mail_change_verification) + ->save(); +} + +/** + * @} End of "addtogroup updates-8.3.x". + */ diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml index 319f219..97e1e28 100644 --- a/core/modules/user/user.routing.yml +++ b/core/modules/user/user.routing.yml @@ -200,3 +200,16 @@ user.reset.form: options: _maintenance_access: TRUE no_cache: TRUE + +user.mail_change: + path: '/user/mail-change/{user}/{new_mail}/{timestamp}/{hash}' + defaults: + _controller: '\Drupal\user\Controller\MailChangeController::page' + _title: 'Change email address' + requirements: + _custom_access: '\Drupal\user\Controller\MailChangeController::access' + user: \d+ + timestamp: \d+ + options: + _maintenance_access: TRUE + no_cache: TRUE