diff --git a/core/modules/user/config/install/user.mail.yml b/core/modules/user/config/install/user.mail.yml
index f8e41ce..6b215b5 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-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."
+ 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..44e0ccf 100644
--- a/core/modules/user/config/install/user.settings.yml
+++ b/core/modules/user/config/install/user.settings.yml
@@ -3,6 +3,8 @@ 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
diff --git a/core/modules/user/config/schema/user.schema.yml b/core/modules/user/config/schema/user.schema.yml
index 627d8a6..d027c7b 100644
--- a/core/modules/user/config/schema/user.schema.yml
+++ b/core/modules/user/config/schema/user.schema.yml
@@ -20,6 +20,12 @@ 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'
@@ -61,6 +67,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 8b0149e..4881d9c 100644
--- a/core/modules/user/src/AccountForm.php
+++ b/core/modules/user/src/AccountForm.php
@@ -98,7 +98,8 @@ public function form(array $form, FormStateInterface $form_state) {
'#default_value' => (!$register ? $account->getEmail() : ''),
);
- // Only show name field on registration form or user can change own username.
+ // Only show name field on registration form or user can change own
+ // username.
$form['account']['name'] = array(
'#type' => 'textfield',
'#title' => $this->t('Username'),
@@ -313,10 +314,9 @@ public function syncUserLangcode($entity_type_id, UserInterface $user, array &$f
public function buildEntity(array $form, FormStateInterface $form_state) {
// Change the roles array to a list of enabled roles.
// @todo: Alter the form state as the form values are directly extracted and
- // set on the field, which throws an exception as the list requires
- // numeric keys. Allow to override this per field. As this function is
- // called twice, we have to prevent it from getting the array keys twice.
-
+ // set on the field, which throws an exception as the list requires
+ // numeric keys. Allow to override this per field. As this function is
+ // called twice, we have to prevent it from getting the array keys twice.
if (is_string(key($form_state->getValue('roles')))) {
$form_state->setValue('roles', array_keys(array_filter($form_state->getValue('roles'))));
}
@@ -355,7 +355,7 @@ protected function getEditedFieldNames(FormStateInterface $form_state) {
'timezone',
'langcode',
'preferred_langcode',
- 'preferred_admin_langcode'
+ 'preferred_admin_langcode',
), parent::getEditedFieldNames($form_state));
}
@@ -373,7 +373,7 @@ protected function flagViolations(EntityConstraintViolationListInterface $violat
'timezone',
'langcode',
'preferred_langcode',
- 'preferred_admin_langcode'
+ 'preferred_admin_langcode',
);
foreach ($violations->getByFields($field_names) as $violation) {
list($field_name) = explode('.', $violation->getPropertyPath(), 2);
@@ -386,13 +386,35 @@ 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.
+ $account->setEmail($old_mail);
+ _user_mail_notify('mail_change_notification', $account);
+ }
+
+ // The user's email address will be updated 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');
+ }
+
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 b76897d..46ad8d2 100644
--- a/core/modules/user/src/AccountSettingsForm.php
+++ b/core/modules/user/src/AccountSettingsForm.php
@@ -109,8 +109,8 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'#title' => $this->t('Administrator role'),
'#open' => TRUE,
);
- // Do not allow users to set the anonymous or authenticated user roles as the
- // administrator role.
+ // Do not allow users to set the anonymous or authenticated user roles as
+ // the administrator role.
$roles = user_role_names(TRUE);
unset($roles[RoleInterface::AUTHENTICATED_ID]);
@@ -157,13 +157,13 @@ public function buildForm(array $form, FormStateInterface $form_state) {
USER_REGISTER_ADMINISTRATORS_ONLY => $this->t('Administrators only'),
USER_REGISTER_VISITORS => $this->t('Visitors'),
USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL => $this->t('Visitors, but administrator approval is required'),
- )
+ ),
);
$form['registration_cancellation']['user_email_verification'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Require email verification when a visitor creates an account'),
'#default_value' => $config->get('verify_mail'),
- '#description' => $this->t('New users will be required to validate their email address prior to logging into the site, and will be assigned a system-generated password. With this setting disabled, users will be logged in immediately upon registering, and may select their own passwords during registration.')
+ '#description' => $this->t('New users will be required to validate their email address prior to logging into the site, and will be assigned a system-generated password. With this setting disabled, users will be logged in immediately upon registering, and may select their own passwords during registration.'),
);
$form['registration_cancellation']['user_password_strength'] = array(
'#type' => 'checkbox',
@@ -174,7 +174,13 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'#type' => 'radios',
'#title' => $this->t('When cancelling a user account'),
'#default_value' => $config->get('cancel_method'),
- '#description' => $this->t('Users with the %select-cancel-method or %administer-users permissions can override this default method.', array('%select-cancel-method' => $this->t('Select method for cancelling account'), '%administer-users' => $this->t('Administer users'), ':permissions-url' => $this->url('user.admin_permissions'))),
+ '#description' => $this->t('Users with the %select-cancel-method or %administer-users permissions can override this default method.',
+ array(
+ '%select-cancel-method' => $this->t('Select method for cancelling account'),
+ '%administer-users' => $this->t('Administer users'),
+ ':permissions-url' => $this->url('user.admin_permissions'),
+ )
+ ),
);
$form['registration_cancellation']['user_cancel_method'] += user_cancel_methods();
foreach (Element::children($form['registration_cancellation']['user_cancel_method']) as $key) {
@@ -201,7 +207,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-login-url], [user:cancel-url].');
$form['email_admin_created'] = array(
'#type' => 'details',
@@ -219,7 +225,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
$form['email_admin_created']['user_mail_register_admin_created_body'] = array(
'#type' => 'textarea',
'#title' => $this->t('Body'),
- '#default_value' => $mail_config->get('register_admin_created.body'),
+ '#default_value' => $mail_config->get('register_admin_created.body'),
'#rows' => 15,
);
@@ -303,6 +309,50 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'#rows' => 12,
);
+ $form['email_email_change_notification'] = array(
+ '#type' => 'details',
+ '#title' => $this->t('Email change notification'),
+ '#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(
+ '#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(
+ '#type' => 'textarea',
+ '#title' => $this->t('Body'),
+ '#default_value' => $mail_config->get('mail_change_notification.body'),
+ '#rows' => 12,
+ );
+
+ $form['email_email_change_verification'] = array(
+ '#type' => 'details',
+ '#title' => $this->t('Email change verification'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#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(
+ '#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(
+ '#type' => 'textarea',
+ '#title' => $this->t('Body'),
+ '#default_value' => $mail_config->get('mail_change_verification.body'),
+ '#rows' => 12,
+ );
+
$form['email_activated'] = array(
'#type' => 'details',
'#title' => $this->t('Account activation'),
@@ -445,6 +495,10 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
->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('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
new file mode 100644
index 0000000..fc59936
--- /dev/null
+++ b/core/modules/user/src/Controller/ChangeEmailController.php
@@ -0,0 +1,139 @@
+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 changeEmailUrl(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();
+ 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 changeEmailAccess($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/Tests/UserChangeEmailTest.php b/core/modules/user/src/Tests/UserChangeEmailTest.php
new file mode 100644
index 0000000..e30ba14
--- /dev/null
+++ b/core/modules/user/src/Tests/UserChangeEmailTest.php
@@ -0,0 +1,166 @@
+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->getEandomEmail();
+ $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 = $this->account;
+ $account_cloned->block();
+ $account_cloned->save();
+ $this->drupalGet(ChangeEmailController::changeEmailUrl($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::changeEmailUrl($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::changeEmailUrl($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::changeEmailUrl($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::changeEmailUrl($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.
+ *
+ * @return string
+ * Random email.
+ */
+ protected function getEandomEmail() {
+ 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 b7877d8..32cad7a 100644
--- a/core/modules/user/src/Tests/UserTokenReplaceTest.php
+++ b/core/modules/user/src/Tests/UserTokenReplaceTest.php
@@ -12,6 +12,7 @@
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\simpletest\WebTestBase;
use Drupal\user\Entity\User;
+use Drupal\user\Controller\ChangeEmailController;
/**
* Generates text using placeholders for dummy content to check user token
@@ -134,6 +135,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::changeEmailUrl($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 91a908e..ad523f8 100644
--- a/core/modules/user/user.install
+++ b/core/modules/user/user.install
@@ -1,5 +1,7 @@
save();
}
+
+
+/**
+ * Updates config for change mail notifications.
+ */
+function user_update_8002() {
+ $mail_settings = Yaml::parse(file_get_contents(__DIR__ . '/config/install/user.mail.yml'));
+
+ $mail_config = \Drupal::service('config.factory')->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 3d76074..f58b3d4 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -13,6 +13,7 @@
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
+use Drupal\user\Controller\ChangeEmailController;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
use Drupal\user\RoleInterface;
@@ -577,13 +578,12 @@ function user_user_logout($account) {
* they can change their password.
*/
function user_pass_reset_url($account, $options = array()) {
- $timestamp = REQUEST_TIME;
$langcode = isset($options['langcode']) ? $options['langcode'] : $account->getPreferredLangcode();
return \Drupal::url('user.reset',
array(
'uid' => $account->id(),
- 'timestamp' => $timestamp,
- 'hash' => user_pass_rehash($account, $timestamp),
+ 'timestamp' => REQUEST_TIME,
+ 'hash' => user_pass_rehash($account, REQUEST_TIME),
),
array(
'absolute' => TRUE,
@@ -940,6 +940,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().
@@ -947,6 +948,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::changeEmailUrl($data['user'], $options)->toString();
$replacements['[user:cancel-url]'] = user_cancel_url($data['user'], $options);
}
}
@@ -1176,6 +1178,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.
diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml
index 6eea7ec..2654a26 100644
--- a/core/modules/user/user.routing.yml
+++ b/core/modules/user/user.routing.yml
@@ -150,3 +150,14 @@ user.reset:
options:
_maintenance_access: TRUE
no_cache: TRUE
+
+user.change_email:
+ path: '/user/change-mail/{uid}/{timestamp}/{new_mail}/{hash}'
+ defaults:
+ _controller: '\Drupal\user\Controller\ChangeEmailController::changeEmailPage'
+ _title: 'Change email address'
+ requirements:
+ _custom_access: '\Drupal\user\Controller\ChangeEmailController::changeEmailAccess'
+ options:
+ _maintenance_access: TRUE
+ no_cache: TRUE