You are here

prlp.module in Password Reset Landing Page (PRLP) 7

Same filename and directory in other branches
  1. 8 prlp.module
  2. 6 prlp.module
  3. 7.x prlp.module

Password Reset Landing Page module.

File

prlp.module
View source
<?php

/**
 * @file
 * Password Reset Landing Page module.
 */

/**
 * Implements hook_menu().
 */
function prlp_menu() {
  $items = array();
  $items['admin/config/people/accounts/prlp'] = array(
    'title' => 'PRLP settings',
    'description' => 'Configure - Password Reset Landing Page - what happens when users use their one-time login links for resetting password.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'prlp_admin_settings',
    ),
    'access arguments' => array(
      'administer users',
    ),
    'file' => 'prlp.admin.inc',
    'type' => MENU_LOCAL_TASK,
  );
  return $items;
}

/**
 * Implements hook_drupal_goto_alter().
 */
function prlp_drupal_goto_alter(&$path, &$options, &$http_response_code) {
  if (isset($_SESSION['pass_reset_hash'])) {

    // This is likely Drupal 7.79+ redirecting the password reset confirm form
    // back to itself, after which it immediately unsets the session value.
    $parts = explode('/', $path);
    if (isset($parts[4]) && $parts[4] === 'confirm' && $parts[0] === 'user' && $parts[1] === 'reset') {
      $_SESSION['prlp_reset_hash'] = $_SESSION['pass_reset_hash'];
    }
  }
}

/**
 * Alters the user password reset landing page.
 *
 * Lets the user enter a new password directly on the landing page
 * and adds a submit handler to save it.
 */
function prlp_form_user_pass_reset_alter(&$form, &$form_state) {
  @(list($uid, $timestamp, $hashed_pass, $action) = $form_state['build_info']['args']);
  if ($uid) {
    $form['#user'] = user_load($uid);

    // Do this only if the URI didn't have an 'action' component.
    if (empty($action)) {

      // Inject the username/password change form into the current form.
      user_account_form($form, $form_state);
      $user_account_form_id = 'user_account_form';
      drupal_alter(array(
        'form',
        'form_user_account_form',
      ), $form, $form_state, $user_account_form_id);

      // ???
      $form['#user_category'] = 'account';

      // Hide certain fields in $form.
      foreach ($form as $key => &$element) {
        if (in_array($key, prlp_get_hidden_fields())) {
          $element['#access'] = FALSE;
        }
      }

      // Hide certain fields in $form['account']
      foreach ($form['account'] as $key => &$element) {
        if (in_array($key, prlp_get_hidden_account_fields())) {
          $element['#access'] = FALSE;
        }
      }

      // The form does not properly rebuild itself if #action is set (which
      // makes it redirect in the middle of form rebuild), or on D7.79+ if the
      // session value isn't set (which makes it error out).
      unset($form['#action']);
      if (isset($_SESSION['prlp_reset_hash']) && empty($form_state['input'])) {
        $_SESSION['pass_reset_hash'] = $_SESSION['prlp_reset_hash'];
      }

      // Remove the part of the message that talks about changing password
      // later, since they are changing it right here.
      if (!empty($form['message']['#markup'])) {
        $form['message']['#markup'] = str_replace('<p>Click on this button to log in to the site and change your password.</p>', '', $form['message']['#markup']);
      }
      if (is_array($form['account']['pass']) && variable_get('prlp_password_required', 1)) {
        $form['account']['pass']['#required'] = TRUE;
      }

      // Before sending the user to user profile edit form.
      $form['#submit'][] = 'prlp_user_pass_reset_submit';
    }
  }
}

/**
 * Default value of the 'prlp_destination' config varible.
 */
define('PRLP_DESTINATION_DEFAULT', 'user/%uid/edit');

/**
 * A copy of 'user_pass_reset' function, with a couple of significant changes.
 *
 * It saves the password or username changes. Also, Unlike that function,
 * this one is a form submit handler.
 */
function prlp_user_pass_reset_submit($form, &$form_state) {
  global $user;
  @(list($uid, $timestamp, $hashed_pass, $action) = $form_state['build_info']['args']);
  if ($hashed_pass === 'confirm') {

    // Drupal 7.79+: the form redirected to a 'confirm' URL so the hash is not
    // in the URL anymore.
    if (isset($_SESSION['prlp_reset_hash'])) {
      $hashed_pass = $_SESSION['prlp_reset_hash'];
      unset($_SESSION['prlp_reset_hash']);
    }
    else {
      drupal_set_message(t('Could not find password hash; login will likely fail.'), 'warning');
    }
  }

  // When processing the one-time login link, we have to make sure that a user
  // isn't already logged in.
  if ($user->uid) {

    // The existing user is already logged in.
    if ($user->uid == $uid) {
      drupal_set_message(t('You are logged in as %user. <a href="!user_edit">Change your password.</a>', array(
        '%user' => $user->name,
        '!user_edit' => url("user/{$user->uid}/edit"),
      )));
    }
    else {
      $reset_link_account = user_load($uid);
      if (!empty($reset_link_account)) {
        drupal_set_message(t('Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user. Please <a href="!logout">logout</a> and try using the link again.', array(
          '%other_user' => $user->name,
          '%resetting_user' => $reset_link_account->name,
          '!logout' => url('user/logout'),
        )));
      }
      else {

        // Invalid one-time link specifies an unknown user.
        drupal_set_message(t('The one-time login link you clicked is invalid.'));
      }
    }
    $form_state['redirect'] = '';
  }
  else {

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

    // Run $form_state['values']['roles'] through array filter to remove
    // roles that a user doesn't actually have.
    $form_state['values']['roles'] = array_filter($form_state['values']['roles']);

    // Some redundant checks for extra security ?
    $users = user_load_multiple(array(
      $uid,
    ), array(
      'status' => '1',
    ));
    if ($timestamp <= $current && ($account = reset($users))) {

      // No time out for first time login.
      if ($account->login && $current - $timestamp > $timeout) {
        drupal_set_message(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'));
        $form_state['redirect'] = 'user/password';
      }
      elseif ($account->uid && $timestamp >= $account->login && $timestamp <= $current && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) {

        /*
         * BEGIN: CHANGED from user_pass_reset.
         */
        $form_state['user'] = $account;

        // Prevent a notice from line 324 in /modules/user/user.pages.inc.
        if (!isset($_SESSION)) {
          $_SESSION = array();
        }
        user_profile_form_submit($form, $form_state);

        // Get the index of the 'The changes have been saved.' message which was
        // added by user_profile_form_submit().
        $message_index = array_search(t('The changes have been saved.'), $_SESSION['messages']['status']);

        // Replace the message with a custom message.
        $_SESSION['messages']['status'][$message_index] = variable_get($account->access ? 'prlp_confirmation_message_existing_users' : 'prlp_confirmation_message_new_users');

        // Remove empty values.
        $_SESSION['messages']['status'] = array_filter($_SESSION['messages']['status']);

        // If status messages array is empty, remove it completely to prevent
        // an empty message from being displayed.
        if (empty($_SESSION['messages']['status'])) {
          unset($_SESSION['messages']['status']);
        }

        /*
         * END: CHANGED from user_pass_reset.
         */

        // First stage is a confirmation form, then login.
        watchdog('user', 'User %name used one-time login link at time %timestamp.', array(
          '%name' => $account->name,
          '%timestamp' => $timestamp,
        ));

        // Set the new user.
        // Reload the user instead of using $account because the plain text password is in $account->pass at this point.
        $user = user_load($account->uid);

        // user_login_finalize() also updates the login timestamp of the
        // user, which invalidates further use of the one-time login link.
        user_login_finalize();

        // Users have requested removal of the next message.
        // drupal_set_message(t('You have just used your one-time login link. It is no longer necessary to use this link to log in.'));
        // Let the user's password be changed without the current password check.
        $token = drupal_hash_base64(drupal_random_bytes(55));
        $_SESSION['pass_reset_' . $user->uid] = $token;

        // If necessary, replace '%uid' token with uid of the current user.
        $destination = variable_get('prlp_destination', PRLP_DESTINATION_DEFAULT);
        if (empty($destination) || $destination == PRLP_DESTINATION_DEFAULT) {
          $form_state['redirect'] = array(
            'user/' . $user->uid . '/edit',
            array(
              'query' => array(
                'pass-reset-token' => $token,
              ),
            ),
          );
        }
        else {
          $destination = str_replace('%uid', $user->uid, $destination);

          // Handle URL parameters in destination.
          $destination = drupal_parse_url($destination);
          $form_state['redirect'] = array(
            $destination['path'],
            $destination,
          );
        }
      }
      else {
        drupal_set_message(t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'));
        $form_state['redirect'] = 'user/password';
      }
    }
    else {

      // Deny access, no more clues.
      // Everything will be in the watchdog's URL for the administrator to check.
      drupal_access_denied();
    }
  }
}
define('PRLP_HIDDEN_FIELDS_DEFAULT', "signature_settings\npicture");

/**
 * Returns an array of fields under $form that should be hidden in PRLP form.
 */
function prlp_get_hidden_fields() {
  $fields =& drupal_static(__FUNCTION__);
  if (!isset($fields)) {

    // Break the variable value by newline separator.
    $fields = explode("\n", variable_get('prlp_hidden_fields', PRLP_HIDDEN_FIELDS_DEFAULT));
    foreach ($fields as &$field) {
      $field = trim($field);
    }
  }
  return $fields;
}
define('PRLP_HIDDEN_ACCOUNT_FIELDS_DEFAULT', "mail");

/**
 * Returns an array of fields on $form['account'] that should be hidden by PRLP.
 */
function prlp_get_hidden_account_fields() {
  $fields =& drupal_static(__FUNCTION__);
  if (!isset($fields)) {

    // Break the variable value by newline separator.
    $fields = explode("\n", variable_get('prlp_hidden_account_fields', PRLP_HIDDEN_ACCOUNT_FIELDS_DEFAULT));
  }
  return $fields;
}

/**
 * Implements hook_module_implements_alter().
 */
function prlp_module_implements_alter(&$implementations, $hook) {

  // Ensure Password Policy form alteration occurs after PRLP form alteration.
  // This allows PRLP to add the password element before Password Policy
  // modifies it.
  if (module_exists("password_policy") && $hook == 'form_alter') {
    $group = $implementations['password_policy'];
    unset($implementations['password_policy']);
    $implementations['password_policy'] = $group;
  }
}

/**
 * Implements hook_variable_info().
 *
 * Allow to translate variable values.
 */
function prlp_variable_info($options) {
  $variables['prlp_confirmation_message_existing_users'] = array(
    'type' => 'string',
    'title' => t('Confirmation message for existing users', array(), $options),
    'default' => 'Your new password has been saved.',
    'description' => t('Confirmation message that will be shown to existing users after they changed their password.', array(), $options),
    'group' => 'prlp_settings',
    'required' => FALSE,
  );
  $variables['prlp_confirmation_message_new_users'] = array(
    'type' => 'string',
    'title' => t('Confirmation message for new users', array(), $options),
    'default' => 'Thank you for setting your password, the registration process has completed.',
    'description' => t('Confirmation message that will be shown to new users after they set their password.', array(), $options),
    'group' => 'prlp_settings',
    'required' => FALSE,
  );
  return $variables;
}

/**
 * Implements hook_variable_group_info().
 */
function prlp_variable_group_info() {
  $groups['prlp_settings'] = array(
    'title' => t('Password reset settings'),
    'description' => t('Settings of the prlp module.'),
  );
  return $groups;
}

Functions

Namesort descending Description
prlp_drupal_goto_alter Implements hook_drupal_goto_alter().
prlp_form_user_pass_reset_alter Alters the user password reset landing page.
prlp_get_hidden_account_fields Returns an array of fields on $form['account'] that should be hidden by PRLP.
prlp_get_hidden_fields Returns an array of fields under $form that should be hidden in PRLP form.
prlp_menu Implements hook_menu().
prlp_module_implements_alter Implements hook_module_implements_alter().
prlp_user_pass_reset_submit A copy of 'user_pass_reset' function, with a couple of significant changes.
prlp_variable_group_info Implements hook_variable_group_info().
prlp_variable_info Implements hook_variable_info().

Constants

Namesort descending Description
PRLP_DESTINATION_DEFAULT Default value of the 'prlp_destination' config varible.
PRLP_HIDDEN_ACCOUNT_FIELDS_DEFAULT
PRLP_HIDDEN_FIELDS_DEFAULT