diff --git a/core/modules/user/config/install/user.mail.yml b/core/modules/user/config/install/user.mail.yml index 436ff53..1f1eaaf 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: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 to 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:name] at [site:name]' +mail_change_notification: + body: "[user:name],\n\nA request to change your e-mail address has been made at [site:name]. In order to complete the change you will need to follow the instructions sent to your new e-mail address within one day." + subject: 'E-mail change information for [user:name] at [site:name]' +mail_change_verification: + body: "[user:name],\n\nA request to change your e-mail address has been made at [site:name]. You need to verif 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 e-mail address at [site:name] will not change." + subject: 'E-mail change information for [user:name] at [site:name]' register_admin_created: body: "[user: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 to 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/src/AccountForm.php b/core/modules/user/src/AccountForm.php index 16ab69f..977885a 100644 --- a/core/modules/user/src/AccountForm.php +++ b/core/modules/user/src/AccountForm.php @@ -388,6 +388,25 @@ public function validate(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { + + $user = $this->getEntity($form_state); + $new_email = $form_state->getValue('mail'); + + if($user->getEmail() !== $new_email && !$user->access('administer users')) { + $old_email = $user->getEmail(); + + // Send a verification to the new email address. + $user->setEmail($new_email); + if(_user_mail_notify('mail_change_verification', $user, NULL)) { + // Send notification email to the old email address. + $user->setEmail($old_email); + _user_mail_notify('mail_change_notification', $user); + } + + // The user's email address will be updated after verification. + $user->setEmail($old_email); + } + parent::submitForm($form, $form_state); $user = $this->getEntity($form_state); diff --git a/core/modules/user/src/AccountSettingsForm.php b/core/modules/user/src/AccountSettingsForm.php index df65cb8..cf11268 100644 --- a/core/modules/user/src/AccountSettingsForm.php +++ b/core/modules/user/src/AccountSettingsForm.php @@ -312,6 +312,50 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#rows' => 12, ); + $form['email_mail_change_notification'] = array( + '#type' => 'details', + '#title' => t('Email Change Notification'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#description' => t('Edit the e-mail messages sent to users old mail address who change mail address.') . ' ' . $email_token_help, + '#group' => 'email', + '#weight' => 11, + ); + $form['email_mail_change_notification']['user_mail_mail_change_notification_subject'] = array( + '#type' => 'textfield', + '#title' => t('Subject'), + '#default_value' => $mail_config->get('mail_change_notification.subject'), + '#maxlength' => 180, + ); + $form['email_mail_change_notification']['user_mail_mail_change_notification_body'] = array( + '#type' => 'textarea', + '#title' => t('Body'), + '#default_value' => $mail_config->get('mail_change_notification.body'), + '#rows' => 12, + ); + + $form['email_mail_change_verification'] = array( + '#type' => 'details', + '#title' => t('Email Change Verification'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#description' => t('Edit the e-mail messages sent to users new mail address who change mail address.') . ' ' . $email_token_help, + '#group' => 'email', + '#weight' => 13, + ); + $form['email_mail_change_verification']['user_mail_mail_change_verification_subject'] = array( + '#type' => 'textfield', + '#title' => t('Subject'), + '#default_value' => $mail_config->get('mail_change_verification.subject'), + '#maxlength' => 180, + ); + $form['email_mail_change_verification']['user_mail_mail_change_verification_body'] = array( + '#type' => 'textarea', + '#title' => t('Body'), + '#default_value' => $mail_config->get('mail_change_verification.body'), + '#rows' => 12, + ); + $form['email_activated'] = array( '#type' => 'details', '#title' => $this->t('Account activation'), diff --git a/core/modules/user/src/Controller/UserController.php b/core/modules/user/src/Controller/UserController.php index 0243c33..d453634 100644 --- a/core/modules/user/src/Controller/UserController.php +++ b/core/modules/user/src/Controller/UserController.php @@ -138,6 +138,55 @@ public function resetPass($uid, $timestamp, $hash) { } /** + * Returens the user change email page. + * + * @param int $uid + * UID of user requesting reset. + * @param int $timestamp + * The current timestamp. + * @param string new_email + * The user's new email address. + * @param string $hash + * Login link hash. + * + * @return array|\Symfony\Component\HttpFoundation\RedirectResponse + * The form structure or a redirect response. + * + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * If the timestamp passed is in the future. + */ + public function changeEmail($uid, $timestamp, $new_email, $hash) { + $user = \Drupal::currentUser(); + $account = $this->userData->get('user', $uid); + + $timeout = 86400; + $current = $_SERVER['REQUEST_TIME']; + + if ($timestamp < $current) { + if($current - $timestamp > $timeout) { + drupal_set_message(t('You have tried to use a one-time e-mail change link for %account that has expired--your change of e-mail request was not completed. Please visit your account edit page if you wish to attempt the change again.', array('%account' => $account->name)), 'error'); + } + else if ($user->id() && $user->id() != $account->id()) { + drupal_set_message(t('You are currently logged in as %user, and are attempting to confirm an e-mail change for %account, which is not allowed. Please log in as %account and initiate a new change of e-mail request.', array('%user' => $user->name, '%account' => $account->name)), 'error'); + } + else if ($hash != user_pass_rehash($account->getPassword(), $timestamp, $new_email, $account->id())) { + drupal_set_message(t('There was a problem verifying your change of e-mail request. Please visit your account edit page and attempt the change again'), 'error'); + } + else if ($timestamp > $account->login && $timestamp < $current) { + $account->setEmail($new_email); + $account->save(); + drupal_set_message(t('Your e-mail address is now %mail.', array('%mail' => $new_email))); + } + } + else { + // Deny access if the timestamp passed is in the future. + throw new AccessDeniedHttpException(); + } + + $this->redirect('user'); + } + + /** * Redirects users to their profile page. * * This controller assumes that it is only invoked for authenticated users. diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 33ed45f..9a664a9 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -584,6 +584,32 @@ function user_pass_reset_url($account, $options = array()) { } /** + * Generates a unique URL for a one time email change confirmation. + * + * @param object $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. + * + * @return + * A unique URL that provides a one-time email change confirmation for the + * user. + */ +function user_change_mail_url($account, $options = array()) { + $timestamp = $_SERVER['REQUEST_TIME']; + $langcode = isset($options['langcode']) ? $options['langcode'] : $account->getPreferredLangcode(); + $url_options = array('absolute' => TRUE, 'language' => \Drupal::languageManager()->getLanguage($langcode)); + return \Drupal::url('user.change_email', [ + 'uid' => $account->id(), + 'timestamp' => $timestamp, + 'new_email' => $account->getEmail(), + 'hash' => user_pass_rehash($account->getPassword(), $timestamp, $account->getLastLoginTime(), $account->id()), + ], $url_options); +} + +/** * Generates a URL to confirm an account cancellation request. * * @param object $account @@ -931,12 +957,14 @@ 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 email address of the user. * @param $options * Unused parameter required by \Drupal\Core\Utility\Token::replace(). */ 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]'] = user_change_mail_url($data['user'], $options); $replacements['[user:cancel-url]'] = user_cancel_url($data['user'], $options); } } @@ -1179,6 +1207,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 6dc4fdb..fd8267d 100644 --- a/core/modules/user/user.routing.yml +++ b/core/modules/user/user.routing.yml @@ -152,3 +152,14 @@ user.reset: options: _maintenance_access: TRUE no_cache: TRUE + +user.change_email: + path: 'user/change-mail/{uid}/{timestamp}/{new_email}/{hash}' + defaults: + _controller: '\Drupal\user\Controller\UserController::changeEmail' + _title: 'Change email address' + requirements: + _access: 'TRUE' + options: + _maintenance_access: TRUE + no_cache: TRUE