You are here

email_confirm.module in Email Change Confirmation 7

Same filename and directory in other branches
  1. 5 email_confirm.module
  2. 6 email_confirm.module

The Email Change Confirmation module.

File

email_confirm.module
View source
<?php

/**
 * @file
 * The Email Change Confirmation module.
 */

/**
 * Implements hook_help().
 */
function email_confirm_help($path, $arg) {
  switch ($path) {
    case 'admin/modules#description':
      return t('Configuration of confirmation email sent to users who attempt to change their email address.');
    case 'admin/help#email_confirm':
      return t('<p>The Email Change Confirmation module addresses missing functionality in the core distribution of Drupal. With this module enabled, a user who attempts to change the email address associated with their account must confirm that change by clicking a confirmation link that is sent to the new email address. The confirmation link must be clicked with a certain time period after which the pending update to their email address will expire and they will have to attempt to update their account again. This module was based on code from <a href="!url">this issue</a></p>', array(
        '!url' => 'http://drupal.org/node/85494',
      ));
    case 'admin/config/people/email_confirm':
      return t("When the Email Change Confirmation module is enabled, users who attempt to update their email address will be required to confirm their changes by clicking a confirmation link in an email sent to the new email address. The settings below determine the subject and body of the confirmation email sent to the user. A copy is sent to both the user's original email address and the new address.");
  }
}

/**
 * Implements hook_menu().
 */
function email_confirm_menu() {
  $items = array();
  $items['user/change-mail'] = array(
    'title' => 'Change e-mail',
    'page callback' => 'email_confirm_user_change_mail',
    'access callback' => 'user_is_logged_in',
    'type' => MENU_CALLBACK,
  );
  $items['admin/config/people/email_confirm'] = array(
    'title' => 'Email change confirmation settings',
    'description' => 'Configuration of confirmation email sent to users who attempt to change their email address.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'email_confirm_admin_settings',
    ),
    'access callback' => 'user_access',
    'access arguments' => array(
      'administer site configuration',
    ),
    'type' => MENU_NORMAL_ITEM,
  );
  return $items;
}

/**
 * Implements hook_permission().
 */
function email_confirm_permission() {
  $permissions = array(
    'bypass email confirmation' => array(
      'title' => t('Bypass Email Change Confirmation'),
      'description' => t('Allow user to change user emails without requiring a confirmation email.'),
    ),
  );
  return $permissions;
}

/**
 * Implements hook_settings().
 */
function email_confirm_admin_settings() {
  $form = array();
  $form['email_confirm_confirmation_email_subject'] = array(
    '#type' => 'textfield',
    '#title' => t('Email address change request email subject'),
    '#description' => t('The above text will be the subject for the email sent to a user that is attempting to update their email address. The placeholders [user:name] and [site:name] will be replaced by the username and the site name.'),
    '#default_value' => email_confirm_mail_text('confirmation_email_subject', NULL, array(), FALSE),
    '#size' => 60,
    '#maxlength' => 256,
    '#required' => TRUE,
  );
  $form['email_confirm_confirmation_email_author'] = array(
    '#type' => 'textfield',
    '#title' => t('Email address change request email author'),
    '#description' => t('The above address will be the \'From\' email address for the confirmation email for an email address change request. The default value is the site email address set on the <a href="@site-info">Site information</a> admin page. If using the SMTP Authentication Support module, check that module\'s configuration page for setting the default site email address.', array(
      '@site-info' => url('admin/config/system/site-information'),
    )),
    '#default_value' => variable_get('email_confirm_confirmation_email_author', email_confirm_default_confirmation_email_author()),
    '#size' => 60,
  );
  $form['email_confirm_confirmation_email_bcc'] = array(
    '#type' => 'textfield',
    '#title' => t('Email address change request email BCC email address'),
    '#description' => t('The above address will receive a BCC email copy of the confirmation email for an email address change request.'),
    '#default_value' => variable_get('email_confirm_confirmation_email_bcc', ''),
    '#size' => 60,
  );
  $form['email_confirm_confirmation_email_body'] = array(
    '#type' => 'textarea',
    '#title' => t('Email address change request email body'),
    '#description' => t("The above text will be the body for the email sent to a user that is attempting to update their email address. The text here will be sent to the user's new email address. The placeholders [user:name] and [site:name] will be replaced by the username and the site name."),
    '#default_value' => email_confirm_mail_text('confirmation_email_body', NULL, array(), FALSE),
    '#cols' => 80,
    '#rows' => 10,
    '#required' => TRUE,
  );
  $form['email_confirm_confirmation_original_email_body'] = array(
    '#type' => 'textarea',
    '#title' => t('Email address change request email body (Original)'),
    '#description' => t("The above text will be the body for the email sent to a user that is attempting to update their email address. The text here will be sent to the user's original email address. The placeholders [user:name] and [site:name] will be replaced by the username and the site name."),
    '#default_value' => email_confirm_mail_text('confirmation_original_email_body', NULL, array(), FALSE),
    '#cols' => 80,
    '#rows' => 10,
    '#required' => TRUE,
  );
  return system_settings_form($form);
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function email_confirm_form_email_confirm_admin_settings_alter(&$form, &$form_state) {
  if (module_exists('token')) {
    foreach (element_children($form) as $key) {
      $element =& $form[$key];
      switch ($key) {
        case 'email_confirm_confirmation_email_body':
        case 'email_confirm_confirmation_original_email_body':
          $element['#description'] = trim(str_replace('The placeholders [user:name] and [site:name] will be replaced by the username and the site name.', t('The list of available tokens that can be used in e-mails is provided below.'), $element['#description']));
          $element += array(
            '#token_types' => array(
              'user',
            ),
          );
          break;
      }
    }

    // Add the token tree UI.
    $form['token_tree'] = array(
      '#theme' => 'token_tree',
      '#token_types' => array(
        'user',
      ),
      '#show_restricted' => TRUE,
      '#weight' => 90,
    );
  }
}

/**
 * Validate the admin settings.
 */
function email_confirm_admin_settings_validate($form, $form_state) {
  if (!empty($form_state['values']['email_confirm_confirmation_email_author']) && !valid_email_address($form_state['values']['email_confirm_confirmation_email_author'])) {
    form_set_error('email_confirm_confirmation_email_author', t('You must enter a valid email address for the "Email address change request email author" setting.'));
  }
  if (!empty($form_state['values']['email_confirm_confirmation_email_bcc']) && !valid_email_address($form_state['values']['email_confirm_confirmation_email_bcc'])) {
    form_set_error('email_confirm_confirmation_email_bcc', t('You must enter a valid email address for the "Email address change request email BCC email address" setting.'));
  }
}

/**
 * Implements hook_variable_info().
 */
function email_confirm_variable_info($options) {
  $type['email_confirm_confirmation_email_subject'] = array(
    'type' => 'string',
    'title' => t('Email address change request email subject', array(), $options),
    'default' => email_confirm_mail_text('confirmation_email_subject', NULL, array(), FALSE),
    'description' => t('The above text will be the subject for the email sent to a user that is attempting to update their email address. The placeholders [user:name] and [site:name] will be replaced by the username and the site name.', array(), $options),
    'localize' => TRUE,
  );
  $type['email_confirm_confirmation_email_body'] = array(
    'type' => 'string',
    'title' => t('Email address change request email body', array(), $options),
    'default' => email_confirm_mail_text('confirmation_email_body', NULL, array(), FALSE),
    'description' => t("The above text will be the body for the email sent to a user that is attempting to update their email address. The text here will be sent to the user's new email address. The placeholders [user:name] and [site:name] will be replaced by the username and the site name.", array(), $options),
    'localize' => TRUE,
  );
  $type['email_confirm_confirmation_original_email_body'] = array(
    'type' => 'string',
    'title' => t('Email address change request email body (Original)', array(), $options),
    'default' => email_confirm_mail_text('confirmation_original_email_body', NULL, array(), FALSE),
    'description' => t("The above text will be the body for the email sent to a user that is attempting to update their email address. The text here will be sent to the user's original email address. The placeholders [user:name] and [site:name] will be replaced by the username and the site name.", array(), $options),
    'localize' => TRUE,
  );
  return $type;
}

/**
 * Implements hook_user_presave().
 */
function email_confirm_user_presave(&$edit, $account, $category) {
  if (!empty($edit['mail']) && (!empty($account->mail) && drupal_strtolower($account->mail) != drupal_strtolower($edit['mail'])) && !user_access('bypass email confirmation') && !drupal_installation_attempted() && (!isset($edit['email_confirmed']) || $edit['email_confirmed'] === FALSE)) {

    // Set a temporary session variable to indicate that the email was
    // changed. Is used in hook_exit to clear out a Drupal message set
    // by the user module after user_save() is called.
    $_SESSION['email_changed'] = TRUE;
    email_confirm_build_mail($edit, $account);
    $edit['data']['email_confirm']['pending_email'] = $edit['mail'];
    $edit['data']['email_confirm']['expiration_time'] = REQUEST_TIME + 86400;
    module_invoke_all('email_confirm', 'email change', $account->uid, $account->mail, $edit['mail']);
    if (module_exists('rules')) {
      rules_invoke_event('email_confirm_email_change_request', $account, $account->mail, $edit['mail']);
    }
    unset($edit['mail']);
    unset($edit['email_confirmed']);
  }
  if (isset($edit['email_confirmed']) && $edit['email_confirmed'] === TRUE || !empty($edit['data']) && isset($edit['data']['email_confirm']) && $edit['data']['email_confirm']['expiration_time'] < REQUEST_TIME) {
    unset($edit['data']['email_confirm']);
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function email_confirm_form_user_profile_form_alter(&$form, &$form_state, $form_id) {
  global $user;

  // Check if user profile form is on the Account section. If the Profile2
  // module is in use, the user profile form may be on a subform.
  if ($user->uid && isset($user->data['email_confirm']) && isset($form['account'])) {
    if (isset($user->data['email_confirm']['pending_email']) && $user->data['email_confirm']['expiration_time'] > REQUEST_TIME) {
      $pending_email_notice = '<div class="messages warning">' . t('You currently have a pending change of your e-mail address to <strong>%email</strong>', array(
        '%email' => $user->data['email_confirm']['pending_email'],
      )) . '</div>';
      $form['account']['mail']['#description'] = $form['account']['mail']['#description'] . $pending_email_notice;
    }
  }
}

/**
 * Menu callback; process one time email change confirm.
 *
 * @param int $uid
 *   Their uid.
 * @param int $timestamp
 *   Timestamp the hash was generated.
 * @param string $hash
 *   A hash, to validate the change being taken.
 *
 * @return int
 *   Returns MENU_ACCESS_DENIED if there is a problem or redirects on success.
 */
function email_confirm_user_change_mail($uid = NULL, $timestamp = NULL, $hash = NULL) {
  global $user;

  // Check if all required parameters are present.
  if (!isset($uid) || !is_numeric($uid) || !isset($timestamp) || !is_numeric($timestamp) || !isset($hash)) {
    return MENU_ACCESS_DENIED;
  }

  // Time out, in seconds, until login URL expires. 24 hours = 86400 seconds.
  $timeout = variable_get('email_confirm_timeout', 86400);
  $current = REQUEST_TIME;

  // Timestamps in the future are invalid.
  if ($timestamp > $current) {
    drupal_set_message(t('There was a problem with your one-time e-mail change link. Please attempt the change again.'), 'error');
    drupal_goto('user/' . $uid . '/edit');
  }

  // Ensure URL is for current user.
  if (!$user->uid || $user->uid != $uid) {
    drupal_set_message(t('You must be logged in to the same account that requested this e-mail change to proceed.'), 'error');
    if (!$user->uid) {
      drupal_goto('user/login');
    }
    else {
      return MENU_ACCESS_DENIED;
    }
  }

  // Load pending email change address.
  if (isset($user->data['email_confirm']['pending_email']) && $user->data['email_confirm']['expiration_time'] > $current) {
    $new_mail = $user->data['email_confirm']['pending_email'];
  }
  else {
    drupal_set_message(t('There was a problem with your one-time e-mail change link. Please attempt the change again.'), 'error');
    drupal_goto('user/' . $uid . '/edit');
  }

  // Check if timestamp provided is too old.
  if ($current - $timestamp > $timeout) {
    drupal_set_message(t('You have tried to use a one-time e-mail change link that has expired. Please attempt the change again.'), 'error');
    drupal_goto('user/' . $uid . '/edit');
  }

  // Ensure no new logins have occurred since the change was made.
  $account = user_load($uid);
  if ($timestamp < $account->login) {
    drupal_set_message(t('There was a problem with your one-time e-mail change link. Please attempt the change again.'), 'error');
    drupal_goto('user/' . $uid . '/edit');
  }

  // Continue with email change if URL hash validates.
  if ($hash === email_confirm_user_email_rehash($new_mail, $timestamp, $uid)) {
    watchdog('user', 'User %name used one-time e-mail change link at time %timestamp.', array(
      '%name' => $user->name,
      '%timestamp' => $timestamp,
    ));
    $old_mail = $user->mail;
    user_save($account, array(
      'mail' => $new_mail,
      'login' => REQUEST_TIME,
      'email_confirmed' => TRUE,
    ));
    module_invoke_all('email_confirm', 'email confirmation', $uid, $old_mail, $new_mail);
    if (module_exists('rules')) {
      rules_invoke_event('email_confirm_email_change_confirmation', $account, $old_mail, $new_mail);
    }
    drupal_set_message(t('Your e-mail address is now %mail.', array(
      '%mail' => $new_mail,
    )));

    // We already validated this uid, so just redirect.
    drupal_goto('user/' . $uid);
  }
  else {
    drupal_set_message(t('There was a problem with your one-time e-mail change link. Please attempt the change again.'), 'error');
    drupal_goto('user/' . $uid . '/edit');
  }
}

/**
 * Implements hook_mail().
 */
function email_confirm_mail($key, &$message, $params) {
  $language = $message['language'];
  $account = $params['account'];
  $context = $params['context'];
  $email_confirm = new stdClass();
  $email_confirm->url = $context['url'];
  $variables = array(
    'email_confirm' => $email_confirm,
    'user' => $account,
  );
  $message['subject'] = email_confirm_mail_text('confirmation_email_subject', $language, $variables);
  $message['body'][] = email_confirm_mail_text($key . '_body', $language, $variables);
  if (isset($params['headers']['Bcc'])) {
    $message['headers']['Bcc'] = $params['headers']['Bcc'];
  }
}

/**
 * Build and send out the confirmation emails.
 */
function email_confirm_build_mail($edit, $account) {
  $params = array();
  $params['account'] = $account;
  $params['new_mail'] = $edit['mail'];
  $params['old_mail'] = $account->mail;
  $params['context']['url'] = url(email_confirm_confirmation_email_url_path($edit['mail'], $account->uid), array(
    'absolute' => TRUE,
  ));
  $default_from = email_confirm_default_confirmation_email_author();
  $from = variable_get('email_confirm_confirmation_email_author', $default_from);

  // To account for case where the variable was stored with an empty string.
  // Make sure a from email was set. Otherwise, use the default.
  if (empty($from)) {
    $from = $default_from;
  }
  $bcc = variable_get('email_confirm_confirmation_email_bcc', '');
  $params['headers'] = array();
  if ($bcc) {
    $params['headers']['Bcc'] = $bcc;
  }
  if ($message['result'] = drupal_mail('email_confirm', 'confirmation_email', $edit['mail'], user_preferred_language($account), $params, $from)) {
    drupal_mail('email_confirm', 'confirmation_original_email', $account->mail, user_preferred_language($account), $params, $from);
    drupal_set_message(t('A confirmation email has been sent to your new email address. You must follow the link provided in that email within 24 hours in order to confirm the change to your account email address.'));
  }
}

/**
 * Determines the default from email address.
 *
 * @return string
 *   Default from email address.
 */
function email_confirm_default_confirmation_email_author() {
  $default_from =& drupal_static(__FUNCTION__);
  if (empty($default_from)) {
    if (module_exists('smtp') && variable_get('smtp_from', '') != '') {
      $default_from = variable_get('smtp_from', ini_get('sendmail_from'));
    }
    else {
      $default_from = variable_get('site_mail', ini_get('sendmail_from'));
    }
  }
  return $default_from;
}

/**
 * Returns a mail string for a variable name.
 */
function email_confirm_mail_text($key, $language = NULL, $variables = array(), $replace = TRUE) {
  $langcode = isset($language) ? $language->language : NULL;
  if (module_exists('i18n_variable') && $langcode) {
    $admin_setting = i18n_variable_get('email_confirm_' . $key, $langcode);
  }
  else {
    $admin_setting = variable_get('email_confirm_' . $key, FALSE);
  }
  if ($admin_setting) {

    // An admin setting overrides the default string.
    $text = $admin_setting;
  }
  else {

    // No override, return default string.
    switch ($key) {
      case 'confirmation_email_subject':
        $text = t('Email address change request for [user:name] at [site:name]', array(), array(
          'langcode' => $langcode,
        ));
        break;
      case 'confirmation_email_body':
        $text = t("Hello [user:name],\n\nA request to change your email address has been made at [site:name].\nYou need to verify the change by clicking on the link below or by\ncopying and pasting it in your browser:\n\n[email_confirm:email_url]\n\nThis is a one-time URL - it can be used only once. It expires after\n24 hours. If you do not click the link to confirm, your email address\nat [site:name] will not be updated.\n", array(), array(
          'langcode' => $langcode,
        ));
        break;
      case 'confirmation_original_email_body':
        $text = t("Hello [user:name],\n\nA request to change your email address has been made at [site:name].\nIn order to confirm the update of your email address you will\nneed to follow the instructions sent to your new email address\nwithin 24 hours.\n", array(), array(
          'langcode' => $langcode,
        ));
        break;
    }
  }
  if ($replace) {

    // We do not sanitize the token replacement, since the output of this
    // replacement is intended for an e-mail message, not a web browser.
    return token_replace($text, $variables, array(
      'language' => $language,
      'callback' => 'email_confirm_mail_tokens',
      'sanitize' => FALSE,
    ));
  }
  return $text;
}

/**
 * Generate the tokens for the confirmation emails.
 */
function email_confirm_mail_tokens(&$replacements, $data, $options) {

  // Add any core mail tokens.
  user_mail_tokens($replacements, $data, $options);
  if (isset($data['email_confirm'])) {
    $replacements['[email_confirm:email_url]'] = $data['email_confirm']->url;
  }
}

/**
 * Generate the path part of a URL to confirm an email address change request.
 *
 * @param string $mail
 *   The new email address.
 * @param int $uid
 *   The uid of the account changing their email.
 * @param int $timestamp
 *   Unix timestamp (e.g. from time()).
 * @return string
 *   Path to wrap in `url($return, ['absolute' => TRUE])` to get a full url.
 */
function email_confirm_confirmation_email_url_path($mail, $uid, $timestamp = NULL) {

  // Use time() here instead of REQUEST_TIME as it allows tests to work, the
  // performance impact is minimal, there's no benefit to avoiding time drift
  // during a page request in this code.
  $timestamp = !empty($timestamp) ? $timestamp : time();
  $hash = email_confirm_user_email_rehash($mail, $timestamp, $uid);
  return 'user/change-mail/' . $uid . '/' . $timestamp . '/' . $hash;
}

/**
 * Generate a hash of URL paramaters and pending changed email address.
 */
function email_confirm_user_email_rehash($mail, $timestamp, $uid) {

  // @see user_pass_rehash().
  // The email_confirm_hash_salt supports automated testing.
  // Not a one-time login link, so use 0 instead of last login.
  return drupal_hmac_base64($timestamp . 0 . $uid, variable_get('email_confirm_hash_salt', drupal_get_hash_salt()) . $mail);
}

/**
 * Implements hook_exit().
 */
function email_confirm_exit() {
  if (isset($_SESSION['email_changed'])) {
    $pruned_messages = array();
    $status_messages = drupal_get_messages('status', TRUE);
    if (count($status_messages)) {
      foreach ($status_messages['status'] as $message) {
        if (strcmp($message, t('The changes have been saved.')) != 0) {
          $pruned_messages[] = $message;
        }
      }
      if (count($pruned_messages)) {
        foreach ($pruned_messages as $message) {
          drupal_set_message(filter_xss($message), 'status');
        }
      }
    }
    unset($_SESSION['email_changed']);
  }
}

Functions

Namesort descending Description
email_confirm_admin_settings Implements hook_settings().
email_confirm_admin_settings_validate Validate the admin settings.
email_confirm_build_mail Build and send out the confirmation emails.
email_confirm_confirmation_email_url_path Generate the path part of a URL to confirm an email address change request.
email_confirm_default_confirmation_email_author Determines the default from email address.
email_confirm_exit Implements hook_exit().
email_confirm_form_email_confirm_admin_settings_alter Implements hook_form_FORM_ID_alter().
email_confirm_form_user_profile_form_alter Implements hook_form_FORM_ID_alter().
email_confirm_help Implements hook_help().
email_confirm_mail Implements hook_mail().
email_confirm_mail_text Returns a mail string for a variable name.
email_confirm_mail_tokens Generate the tokens for the confirmation emails.
email_confirm_menu Implements hook_menu().
email_confirm_permission Implements hook_permission().
email_confirm_user_change_mail Menu callback; process one time email change confirm.
email_confirm_user_email_rehash Generate a hash of URL paramaters and pending changed email address.
email_confirm_user_presave Implements hook_user_presave().
email_confirm_variable_info Implements hook_variable_info().