You are here

password_policy.module in Password Policy 7

Allows enforcing restrictions on user passwords by defining policies.

File

password_policy.module
View source
<?php

/**
 * @file
 * Allows enforcing restrictions on user passwords by defining policies.
 */

/****************************************************************************/
include_once 'password_policy.authmap.inc';
include_once 'password_policy.time.inc';
define('PASSWORD_POLICY_DEFAULT_WARNING_SUBJECT', 'Password expiration warning for [user:name] at [site:name]');
define('PASSWORD_POLICY_DEFAULT_WARNING_BODY', "[user:name],\n\nYour password at [site:name] will expire in less than [password-policy:days-left] day(s).\n\nPlease go to [password-policy:password-edit-url] to change your password.");

/****************************************************************************/

/* Core API hooks                                                           */

/****************************************************************************/

/**
 * Implements hook_help().
 */
function password_policy_help($path, $arg) {
  switch ($path) {
    case 'admin/help#password_policy':
      return '<p>' . t('The Password Policy module allows you to enforce a specific level of password complexity for the user passwords on the system.') . '</p>';
  }
}

/**
 * Implements hook_init().
 */
function password_policy_init() {
  global $user;

  // Check password reset status and force a reset if needed.
  if (_password_policy_is_password_change_forced($user->uid) && !_password_policy_is_path_allowed_when_password_change_forced()) {
    _password_policy_set_password_change_forced_message();
    _password_policy_go_to_password_change_page();
  }
}

/**
 * Implements hook_permission().
 */
function password_policy_permission() {
  return array(
    'administer password policies' => array(
      'title' => t('Administer policies'),
    ),
    'unblock expired accounts' => array(
      'title' => t('Unlock expired accounts'),
    ),
    'force password change' => array(
      'title' => t('Force password change'),
    ),
  );
}

/**
 * Implements hook_theme().
 */
function password_policy_theme() {
  return array(
    'password_policy_admin_list' => array(
      'render element' => 'form',
      'file' => 'password_policy.admin.inc',
    ),
  );
}

/**
 * Implements hook_menu().
 */
function password_policy_menu() {
  $items['admin/config/people/password_policy'] = array(
    'title' => 'Password policies',
    'description' => 'Configures policies for user account passwords.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'password_policy_admin_settings',
    ),
    'access arguments' => array(
      'administer password policies',
    ),
    'file' => 'password_policy.admin.inc',
  );
  $items['admin/config/people/password_policy/configure'] = array(
    'title' => 'Settings',
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/config/people/password_policy/list'] = array(
    'title' => 'List',
    'type' => MENU_LOCAL_TASK,
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'password_policy_admin_list',
    ),
    'access arguments' => array(
      'administer password policies',
    ),
    'weight' => 1,
    'file' => 'password_policy.admin.inc',
  );
  $items['admin/config/people/password_policy/add'] = array(
    'title' => 'Add',
    'type' => MENU_LOCAL_TASK,
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'password_policy_admin_form',
      NULL,
    ),
    'access arguments' => array(
      'administer password policies',
    ),
    'weight' => 2,
    'file' => 'password_policy.admin.inc',
  );
  $items['admin/config/people/password_policy/%password_policy_policy'] = array(
    'title' => 'Password policy',
    'title callback' => 'password_policy_format_title',
    'title arguments' => array(
      4,
    ),
    'page callback' => 'password_policy_admin_view',
    'page arguments' => array(
      4,
    ),
    'access arguments' => array(
      'administer password policies',
    ),
    'file' => 'password_policy.admin.inc',
  );
  $items['admin/config/people/password_policy/%password_policy_policy/view'] = array(
    'title' => 'View',
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/config/people/password_policy/%password_policy_policy/edit'] = array(
    'title' => 'Edit',
    'type' => MENU_LOCAL_TASK,
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'password_policy_admin_form',
      4,
    ),
    'access arguments' => array(
      'administer password policies',
    ),
    'weight' => 1,
    'file' => 'password_policy.admin.inc',
  );
  $items['admin/config/people/password_policy/%password_policy_policy/delete'] = array(
    'title' => 'Delete',
    'type' => MENU_CALLBACK,
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'password_policy_admin_delete',
      4,
    ),
    'access arguments' => array(
      'administer password policies',
    ),
    'file' => 'password_policy.admin.inc',
  );
  $items['admin/config/people/password_policy/password_change'] = array(
    'title' => 'Force password change',
    'description' => 'Force users to change their password',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'password_policy_password_change_settings',
    ),
    'access arguments' => array(
      'force password change',
    ),
    'file' => 'password_policy.admin.inc',
    'type' => MENU_LOCAL_TASK,
    'weight' => 10,
  );
  $items['admin/people/expired'] = array(
    'title' => 'Expired accounts',
    'type' => MENU_LOCAL_TASK,
    'description' => 'Lists all expired accounts.',
    'page callback' => 'password_policy_expired_list',
    'page arguments' => array(
      'password_policy_list_expired',
    ),
    'access arguments' => array(
      'unblock expired accounts',
    ),
  );
  $items['admin/people/expired/unblock/%password_policy_uid'] = array(
    'title' => 'Unblock',
    'type' => MENU_CALLBACK,
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'password_policy_expired_unblock_confirm',
      4,
    ),
    'access arguments' => array(
      'unblock expired accounts',
    ),
  );
  return $items;
}

/**
 * Implements hook_drupal_goto_alter().
 */
function password_policy_drupal_goto_alter(&$path, &$options) {
  global $user;

  // If user is using a one-time login link, force a password change
  // immediately.
  if ($user->uid && isset($options['query']['pass-reset-token']) && variable_get('password_policy_force_change_reset', 0) && isset($_SESSION['pass_reset_' . $user->uid])) {
    db_merge('password_policy_force_change')
      ->key(array(
      'uid' => $user->uid,
    ))
      ->fields(array(
      'force_change' => 1,
    ))
      ->execute();
  }
}

/**
 * Loads policy array from the database.
 *
 * @param int $pid
 *   The policy id.
 *
 * @return array|false
 *   A populated policy array or FALSE if not found.
 */
function password_policy_policy_load($pid) {
  return _password_policy_load_policy_by_pid($pid);
}

/**
 * Loads user object from the database.
 *
 * @param int $uid
 *   The user id.
 *
 * @return object|false
 *   A populated user object or FALSE if not found.
 */
function password_policy_uid_load($uid) {
  if (is_numeric($uid)) {
    $account = user_load($uid);
    if ($account) {
      return $account;
    }
  }
  return FALSE;
}

/**
 * Displays a password policy form title.
 *
 * @param array $policy
 *   Policy array.
 *
 * @return string
 *   A policy's title string
 */
function password_policy_format_title(array $policy) {
  return $policy['name'];
}

/**
 * Implements hook_user_load().
 */
function password_policy_user_load($users) {
  foreach ($users as $uid => $user) {
    $user->force_password_change = _password_policy_is_password_change_forced($uid);
    if (empty($user->force_password_change)) {
      $blocked = db_select('password_policy_expiration', 'p', array(
        'target' => 'slave',
      ))
        ->fields('p', array(
        'blocked',
      ))
        ->condition('uid', $user->uid)
        ->execute()
        ->fetchField();
      if (!empty($blocked)) {
        if ($blocked <= _password_policy_get_request_time()) {
          $user->force_password_change = 1;
        }
      }
    }
  }
}

/**
 * Implements hook_user_insert().
 */
function password_policy_user_insert(&$edit, $account, $category) {
  $force = isset($edit['force_password_change']) ? $edit['force_password_change'] : variable_get('password_policy_new_login_change', 0);
  db_insert('password_policy_force_change')
    ->fields(array(
    'uid' => $account->uid,
    'force_change' => $force,
  ))
    ->execute();
  if (!empty($edit['pass'])) {

    // New users do not yet have an uid during the validation step, but they do
    // have at this insert step.  Store their first password in the system for
    // use with the history constraint (if used).
    if ($account->uid) {
      _password_policy_store_password($account->uid, $edit['pass']);
    }
  }
}

/**
 * Implements hook_user_presave().
 *
 * Adds entry to password history when password is changed for a user. This
 * should work whether the password is changed via the User module forms or
 * programmatically via user_save().
 */
function password_policy_user_presave(&$edit, $account, $category) {

  // If there is a pass value...
  if (!empty($edit['pass'])) {

    // And if this is not a newly created user...
    if (!$account->is_new) {

      // And if the pass value is not the same as before...
      if ($edit['pass'] != $account->pass) {

        // Then store the password hash to history.
        _password_policy_store_password($account->uid, $edit['pass']);
      }
    }
  }
}

/**
 * Implements hook_user_update().
 */
function password_policy_user_update(&$edit, $account, $category) {
  global $user;

  // If the user is being forced to change their password and is changing their
  // password, toggle the force_change field off.
  if (isset($account->original->force_password_change) && $account->original->force_password_change && isset($edit['pass'])) {
    db_update('password_policy_force_change')
      ->fields(array(
      'force_change' => 0,
    ))
      ->condition('uid', $account->uid)
      ->execute();
    db_delete('password_policy_expiration')
      ->condition('uid', $account->uid)
      ->execute();
  }
  elseif (!empty($edit['force_password_change'])) {

    // Check if user already has a force change entry.
    // If any users were created after this module was enabled, they will not
    // yet have an entry in this table.
    $user_exists = db_select('password_policy_force_change')
      ->condition('uid', $account->uid)
      ->countQuery()
      ->execute()
      ->fetchField();
    if ($user_exists == 0) {
      db_insert('password_policy_force_change')
        ->fields(array(
        'uid' => $account->uid,
        'force_change' => 1,
      ))
        ->execute();
    }
    else {
      db_update('password_policy_force_change')
        ->fields(array(
        'force_change' => 1,
      ))
        ->condition('uid', $account->uid)
        ->execute();
    }
    if ($user->uid != $account->uid) {
      drupal_set_message(t('@user will be required to change their password the next time they log in.', array(
        '@user' => $account->name,
      )));
    }
    if ($user->uid) {
      watchdog('password_policy', '@user flagged to change password on next login by @admin.', array(
        '@user' => $account->name,
        '@admin' => $user->name,
      ), WATCHDOG_NOTICE);
    }
    else {

      // The user may be updated programmatically (e.g., via Drupal cron). In
      // this case, there is no administrator forcing the password change.
      watchdog('password_policy', '@user flagged to change password on next login.', array(
        '@user' => $account->name,
      ), WATCHDOG_NOTICE);
    }
  }
  elseif (isset($edit['force_password_change'])) {
    db_update('password_policy_force_change')
      ->fields(array(
      'force_change' => 0,
    ))
      ->condition('uid', $account->uid)
      ->execute();
  }
  if (_password_policy_was_updated_user_unblocked($account)) {
    _password_policy_handle_unblock($account);
  }
}

/**
 * Determines whether updated user was unblocked.
 *
 * @param object $account
 *   User object.
 */
function _password_policy_was_updated_user_unblocked($account) {
  return $account->status == 1 && isset($account->original) && $account->original->status == 0;
}

/**
 * Implements hook_user_login().
 */
function password_policy_user_login(&$edit, $account) {
  $roles = is_array($account->roles) ? array_keys($account->roles) : array();
  $policy = _password_policy_load_active_policy($roles, $account);

  // A value $edit['name'] is NULL for a one time login.
  if ($policy && (!empty($account->uid) && $account->uid > 1 || variable_get('password_policy_admin', 1)) && !empty($edit['values']['name'])) {

    // Calculate expiration and warning times.
    $expiration = $policy['expiration'];
    $warning = empty($policy['warning']) ? 0 : max(explode(',', $policy['warning']));
    $expiration_seconds = $expiration * (60 * 60 * 24);
    $warning_seconds = $warning * (60 * 60 * 24);

    // The policy was enabled.
    $policy_start = $policy['created'];
    if (variable_get('password_policy_begin', 0) == 1) {
      $policy_start -= $expiration_seconds;
    }
    if (!empty($expiration)) {

      // Account expiration is active.
      // Get the last password change time.
      $last_change = db_query_range('SELECT created FROM {password_policy_history} WHERE uid = :uid ORDER BY created DESC', 0, 1, array(
        ':uid' => $account->uid,
      ))
        ->fetchField();
      if (empty($last_change)) {

        // User has not changed their password since this module was enabled.
        $last_change = _password_policy_get_user_created_time($account);
      }
      $time = _password_policy_get_request_time();
      if ($time > max($policy_start, $last_change) + $expiration_seconds) {
        if (variable_get('password_policy_block', 0) == 0) {
          $cron_blocked = db_query_range('SELECT blocked FROM {password_policy_expiration} WHERE uid = :uid ORDER BY blocked DESC', 0, 1, array(
            ':uid' => $account->uid,
          ))
            ->fetchField();
          if ($cron_blocked > _password_policy_get_user_login_time($account)) {

            // User is blocked immediately and cannot change their password
            // after expiration.
            _password_policy_block_account($account);
          }
        }
        else {

          // Redirect user and let password force change handle.
          db_update('password_policy_force_change')
            ->fields(array(
            'force_change' => 1,
          ))
            ->condition('uid', $account->uid)
            ->execute();
          _password_policy_set_password_change_forced_message();
          _password_policy_go_to_password_change_page();
        }
      }
      elseif ($time > max($policy_start, $last_change) + $expiration_seconds - $warning_seconds) {

        // The warning is shown on login and the user is transferred to the
        // password change page.
        $days_left = ceil((max($policy_start, $last_change) + $expiration_seconds - $time) / (60 * 60 * 24));
        drupal_set_message(format_plural($days_left, 'Your password will expire in less than one day. Please change it.', 'Your password will expire in less than @count days. Please change it.'));
        _password_policy_go_to_password_change_page();
      }
    }
  }
}

/**
 * Implements hook_user_delete().
 */
function password_policy_user_delete($account) {
  $txn = db_transaction();

  // Ensure all deletes occur.
  try {
    db_delete('password_policy_history')
      ->condition('uid', $account->uid)
      ->execute();
    db_delete('password_policy_expiration')
      ->condition('uid', $account->uid)
      ->execute();
    db_delete('password_policy_force_change')
      ->condition('uid', $account->uid)
      ->execute();
  } catch (Exception $e) {

    // Something went wrong somewhere, so roll back now.
    $txn
      ->rollback();

    // Log the exception to watchdog.
    watchdog_exception('type', $e);
  }
}

/**
 * Implements hook_form_alter().
 */
function password_policy_form_alter(&$form, &$form_state, $form_id) {
  global $user;
  if (_password_policy_has_account_password_element($form)) {

    // Timing issues require reloading the user object to get the
    // password_change property set.
    $account = user_load($user->uid);

    // Force password change on user account.
    if (user_access('force password change')) {
      if (isset($form['#user_category']) && $form['#user_category'] == 'account') {
        $force_change = db_query_range('SELECT force_change FROM {password_policy_force_change} WHERE uid=:uid', 0, 1, array(
          ':uid' => $form['#user']->uid,
        ))
          ->fetchField();

        // If we didn't get a valid result, use the default.
        if (is_null($force_change) || $force_change === FALSE) {
          $force_change = variable_get('password_policy_new_login_change', 0);
        }
        $form['password_policy'] = array(
          '#type' => 'fieldset',
          '#title' => t('Password settings'),
        );
        $form['password_policy']['force_password_change'] = array(
          '#type' => 'checkbox',
          '#title' => t('Force password change on next login'),
          '#description' => t('If already logged in, the user will be forced to change their password upon their next page request.'),
          '#default_value' => $force_change,
        );
      }
    }

    // Password change form.
    $account = $form['#user'];
    $roles = isset($account->roles) ? array_keys($account->roles) : array(
      DRUPAL_AUTHENTICATED_RID,
    );
    if ($form_id == 'user_register_form') {
      $roles = array(
        DRUPAL_AUTHENTICATED_RID,
      );
    }
    $policy = _password_policy_load_active_policy($roles, $account);
    $translate = array();
    if (!empty($policy['constraints'])) {

      // Some policy constraints are active.
      password_policy_add_policy_js($policy, $account, $form);
      foreach ($policy['constraints'] as $key => $value) {
        if ($value) {
          $translate['constraint_' . $key] = _password_policy_constraint_error($key, $value);
        }
      }
    }

    // Printing out the restrictions.
    if (variable_get('password_policy_show_restrictions', 0) && isset($translate) && (isset($form['pass']) || isset($form['account']['pass']))) {
      $restriction_html = '<div id="account-pass-restrictions">' . theme('item_list', array(
        'items' => array_values($translate),
        'title' => t('Password Requirements'),
      )) . '</div>';
      if (isset($form['account']) && is_array($form['account'])) {
        $form['account']['pass']['#prefix'] = $restriction_html;
      }
      else {
        $form['pass']['#prefix'] = $restriction_html;
      }
    }

    // Set a custom form validate and submit handlers.
    $form['#validate'][] = 'password_policy_password_validate';
  }
  if ($form_id == 'password_policy_password_tab') {
    $form['submit']['#weight'] = 10;
  }
}

/**
 * Implements hook_cron().
 */
function password_policy_cron() {

  // Short circuit if no policies are active that use expiration.
  $expiration_policies = db_select('password_policy', 'p', array(
    'target' => 'slave',
  ))
    ->condition('enabled', 1)
    ->condition('expiration', 0, '>')
    ->countQuery()
    ->execute()
    ->fetchField();
  if ($expiration_policies == 0) {
    return;
  }
  $accounts = array();
  $warns = array();
  $unblocks = array();
  $pids = array();

  // Get all users' last password change time. Don't touch blocked accounts.
  $query = db_select('users', 'u', array(
    'target' => 'slave',
  ));
  $query
    ->leftJoin('password_policy_history', 'p', 'u.uid = p.uid');
  $query
    ->leftJoin('password_policy_expiration', 'e', 'u.uid = e.uid');
  $result = $query
    ->fields('u', array(
    'uid',
    'name',
    'created',
  ))
    ->fields('p', array(
    'created',
  ))
    ->fields('e', array(
    'pid',
    'unblocked',
    'warning',
  ))
    ->condition('u.uid', 0, '>')
    ->condition('u.status', 1)
    ->orderBy('p.created')
    ->orderBy('e.warning')
    ->execute();
  foreach ($result as $row) {
    if ($row->uid == 1 && !variable_get('password_policy_admin', 1)) {
      continue;
    }

    // Use account creation timestamp if there is no entry in password history
    // table.
    $accounts[$row->uid] = empty($row->p_created) ? $row->created : $row->p_created;

    // Last time a warning was mailed out (if was). We need it because we send
    // warnings only once a day, not on all cron runs.
    $warns[$row->uid] = $row->warning;

    // Last time user was unblocked (if was). We don't block this account again
    // for some period of time.
    $unblocks[$row->uid] = $row->unblocked;

    // Unique password policy expirations ID.
    $pids[$row->uid] = $row->pid;

    // Usernames.
    $names[$row->uid] = $row->name;
  }
  foreach ($accounts as $uid => $last_change) {
    $roles = array(
      DRUPAL_AUTHENTICATED_RID,
    );
    $result = db_select('users_roles', 'u', array(
      'target' => 'slave',
    ))
      ->fields('u', array(
      'rid',
    ))
      ->condition('uid', $uid)
      ->orderBy('u.rid')
      ->execute();
    foreach ($result as $row) {
      $roles[] = $row->rid;
    }
    $name = $names[$uid];
    $dummy_account = (object) array(
      'name' => $name,
      'uid' => $uid,
    );
    $policy = _password_policy_load_active_policy($roles, $dummy_account);
    if ($policy) {
      $expiration = $policy['expiration'];
      $warnings = !empty($policy['warning']) ? explode(',', $policy['warning']) : array();
      if (!empty($expiration)) {

        // Calculate expiration time.
        $expiration_seconds = $expiration * (60 * 60 * 24);
        $policy_start = $policy['created'];
        if (variable_get('password_policy_begin', 0) == 1) {
          $policy_start -= $expiration_seconds;
        }
        rsort($warnings, SORT_NUMERIC);
        $time = _password_policy_get_request_time();

        // Check expiration and warning days for each account.
        if (!empty($warnings)) {
          foreach ($warnings as $warning) {

            // Loop through all configured warning send-out days. If today is
            // the day, we send out the warning.
            $warning_seconds = $warning * (60 * 60 * 24);

            // Warning start time.
            $start_period = max($policy_start, $last_change) + $expiration_seconds - $warning_seconds;

            // Warning end time. We create a one day window for cron to run.
            $end_period = $start_period + 60 * 60 * 24;
            if ($warns[$uid] && $warns[$uid] > $start_period && $warns[$uid] < $end_period) {

              // A warning was already mailed out.
              continue;
            }
            if ($time > $start_period && $time < $end_period) {

              // A warning falls in the one day window, so we send out the
              // warning.
              $account = user_load($uid, TRUE);
              $message = drupal_mail('password_policy', 'warning', $account->mail, user_preferred_language($account), array(
                'account' => $account,
                'days_left' => $warning,
              ));
              if ($message['result']) {

                // The mail was sent out successfully.
                watchdog('password_policy', 'Password expiration warning mailed to %username at %email.', array(
                  '%username' => $account->name,
                  '%email' => $account->mail,
                ));
              }
              if ($pids[$uid]) {
                db_update('password_policy_expiration')
                  ->fields(array(
                  'warning' => $time,
                ))
                  ->condition('uid', $uid)
                  ->execute();
              }
              else {
                db_insert('password_policy_expiration')
                  ->fields(array(
                  'uid' => $uid,
                  'warning' => $time,
                ))
                  ->execute();
              }
            }
          }
        }
        if ($time > max($policy_start, $last_change) + $expiration_seconds && $time > $unblocks[$uid] + 60 * 60 * 24 && variable_get('password_policy_block', 0) == 0) {

          // Block expired accounts. Unblocked accounts are not blocked for 24h.
          // One time login lasts for a 24h.
          db_update('users')
            ->fields(array(
            'status' => 0,
          ))
            ->condition('uid', $uid)
            ->execute();
          if ($pids[$uid]) {
            db_update('password_policy_expiration')
              ->fields(array(
              'blocked' => $time,
            ))
              ->condition('uid', $uid)
              ->execute();
          }
          else {
            db_insert('password_policy_expiration')
              ->fields(array(
              'uid' => $uid,
              'blocked' => $time,
            ))
              ->execute();
          }
          $account = user_load($uid, TRUE);
          watchdog('password_policy', 'Password for user %name has expired.', array(
            '%name' => $account->name,
          ), WATCHDOG_NOTICE, l(t('edit'), 'user/' . $account->uid . '/edit'));
        }
      }
    }
  }
}

/**
 * Implements hook_mail().
 */
function password_policy_mail($key, &$message, $params) {
  $language = $message['language'];
  $variables = array(
    'user' => $params['account'],
    'days_left' => $params['days_left'],
  );
  $message['subject'] .= _password_policy_mail_text($key . '_subject', $language, $variables);
  $message['body'][] = _password_policy_mail_text($key . '_body', $language, $variables);
}

/**
 * Implements hook_field_extra_fields().
 */
function password_policy_field_extra_fields() {
  $extra['user']['user'] = array(
    'form' => array(
      'password_policy' => array(
        'label' => t('Password policy'),
        'description' => t('Password policy module settings form elements.'),
        'weight' => 0,
      ),
    ),
  );
  return $extra;
}

/****************************************************************************/

/* FAPI                                                                     */

/****************************************************************************/

/**
 * Determines whether form has an account password element.
 */
function _password_policy_has_account_password_element($form) {
  return isset($form['account']['pass']['#type']) && $form['account']['pass']['#type'] == 'password_confirm';
}

/**
 * Password save validate handler.
 */
function password_policy_password_validate($form, &$form_state) {
  $account = _password_policy_get_user_from_form($form, $form_state);
  if (_password_policy_is_force_password_change_set($account)) {

    // Admins can edit accounts without having to reset passwords.
    if (_password_policy_password_field_is_empty($form_state) && _password_policy_is_current_user($account)) {
      form_set_error('pass', t('Your password has expired. You must change your password to proceed on the site.'));
    }
  }

  // If a password is set and OpenID is not being used, validate password.
  $values = $form_state['values'];
  if (!empty($values['pass']) && !isset($values['auth_openid'])) {

    // Validate length.
    // Short-circuit validation if password exceeds Drupal maximum length as
    // safeguard against potential DoS attacks.
    if (_password_policy_is_form_password_too_long($form_state)) {
      form_set_error('pass', t('Password exceeds maximum length. Please choose a shorter password.'));
      return;
    }

    // Validate constraints.
    _password_policy_validate_constraints($form_state, $account);
  }
}

/**
 * Gets from form user for whom password is being validated.
 *
 * @return object
 *   Custom user object for validating constraints.
 */
function _password_policy_get_user_from_form($form, &$form_state) {
  $account = isset($form['#user']) ? $form['#user'] : (object) array(
    'uid' => 0,
  );
  if ($account->uid == 0) {
    $account->roles = array(
      DRUPAL_AUTHENTICATED_RID => DRUPAL_AUTHENTICATED_RID,
    );
  }
  $values = $form_state['values'];
  if (isset($values['name'])) {
    $account->name = $values['name'];
  }
  if (!_password_policy_is_current_user($account)) {

    // Administrator is changing password for another user. For validating
    // constraints, use roles selected on form for the user.
    _password_policy_add_selected_roles_to_account($form_state, $account);
  }
  return $account;
}

/**
 * Determines whether user has been flagged for forced password change.
 *
 * @param object $account
 *   Custom user object for validating constraints.
 */
function _password_policy_is_force_password_change_set($account) {
  return isset($account->uid) && isset($account->force_password_change) && $account->force_password_change == 1;
}

/**
 * Adds roles selected for user on form to the user object.
 *
 * This function has no effect if the form does not have a roles field. For
 * instance, the Password Change Tab password form has no roles field.
 *
 * @param array $form_state
 *   Form state.
 * @param object $account
 *   Custom user object for validating constraints.
 */
function _password_policy_add_selected_roles_to_account(array $form_state, &$account) {
  if (isset($form_state['values']['roles'])) {
    $rids = array_keys(array_filter($form_state['values']['roles']));
    $roles = array_combine($rids, $rids);
    $account->roles = $roles;
  }
}

/**
 * Determines whether password field is empty.
 *
 * @param array $form_state
 *   Form state.
 */
function _password_policy_password_field_is_empty(array $form_state) {
  $values = $form_state['values'];
  return isset($values['pass']) && $values['pass'] == '';
}

/**
 * Determines whether given user is the current user.
 *
 * @param object $account
 *   User object.
 */
function _password_policy_is_current_user($account) {
  global $user;
  return $user->uid == $account->uid;
}

/**
 * Determines whether password on form exceeds Drupal maximum length.
 *
 * The maximum length is copied from includes/password.inc.
 *
 * @param array $form_state
 *   Form state.
 *
 * @return bool
 *   TRUE if password exceeds Drupal maximum length, FALSE otherwise.
 *
 * @see _password_crypt()
 */
function _password_policy_is_form_password_too_long(array $form_state) {
  $pass = $form_state['values']['pass'];
  return strlen($pass) > 512;
}

/**
 * Validates constraints.
 *
 * Gets error message for each constraint that fails validation and displays
 * them all in list format as a form error.
 *
 * @param array $form_state
 *   Form state.
 * @param object $account
 *   Custom user object for validating constraints.
 */
function _password_policy_validate_constraints(array $form_state, $account) {
  $pass = $form_state['values']['pass'];
  $error = _password_policy_constraint_validate($pass, $account);
  if ($error) {
    form_set_error('pass', t('Your password has not met the following requirement(s):') . '<ul><li>' . implode('</li><li>', $error) . '</li></ul>');
  }
}

/****************************************************************************/

/* Force password change functions                                          */

/****************************************************************************/

/**
 * Determines whether user is to be forced to change their password.
 *
 * Uses static variable to avoid redundantly querying database in single
 * request, which can happen when both password_policy_init() and
 * password_policy_user_load() are called.
 *
 * @param int $uid
 *   User ID.
 *
 * @return bool
 *   TRUE if a password change is to be forced, FALSE otherwise.
 */
function _password_policy_is_password_change_forced($uid) {
  static $force_change = array();
  if ($uid == 0) {
    return FALSE;
  }
  if (!isset($force_change[$uid])) {
    $force_change[$uid] = db_select('password_policy_force_change', 'p', array(
      'target' => 'slave',
    ))
      ->fields('p', array(
      'force_change',
    ))
      ->condition('uid', $uid)
      ->execute()
      ->fetchField();
  }
  return $force_change[$uid];
}

/**
 * Determines whether access to path allowed when user password change forced.
 *
 * @return bool
 *   TRUE if the path is allowed, FALSE otherwise.
 */
function _password_policy_is_path_allowed_when_password_change_forced() {
  $allowed_paths = _password_policy_get_allowed_paths();
  $patterns = implode("\n", $allowed_paths);
  return drupal_match_path(current_path(), $patterns);
}

/**
 * Gets paths allowed when password change forced.
 *
 * The allowed paths comprise configurable and non-configurable paths.
 * Configurable paths can be changed by the administrator; non-configurable
 * paths are built into the module and cannot be changed.
 *
 * Modules can add, modify, or delete paths by implementing
 * hook_password_policy_force_change_allowed_paths_alter(). For instance, a
 * module could add the path of a JavaScript file needed for the page to behave
 * properly. Implementors should consider the security implications of altering
 * the paths. There is increased risk that allowed paths will be accessed by
 * attackers.
 *
 * @return string[]
 *   Array of path patterns in the form expected by the $patterns parameter of
 *   drupal_match_path().
 */
function _password_policy_get_allowed_paths() {
  $configurable_allowed_paths = _password_policy_get_configurable_allowed_paths();
  $nonconfigurable_allowed_paths = _password_policy_get_nonconfigurable_allowed_paths();
  $allowed_paths = array_merge($configurable_allowed_paths, $nonconfigurable_allowed_paths);
  drupal_alter('password_policy_force_change_allowed_paths', $allowed_paths);
  return array_unique($allowed_paths);
}

/**
 * Gets configurable paths allowed when password change forced.
 *
 * These are extra paths the administrator chooses to allow when a user is
 * forced to change their password.
 *
 * @return string[]
 *   Array of path patterns in the form expected by the $patterns parameter of
 *   drupal_match_path().
 */
function _password_policy_get_configurable_allowed_paths() {
  $unprocessed_paths = variable_get('password_policy_force_change_extra_allowed_paths', _password_policy_default_force_change_extra_allowed_paths());
  $paths = array_filter(preg_split("/(\n|\r)/", $unprocessed_paths));
  return $paths;
}

/**
 * Gets non-configurable paths allowed when password change forced.
 *
 * These paths are the ones users should be able to access at a minimum,
 * regardless of site configuration, when being forced to change their
 * password.
 *
 * @return string[]
 *   Array of path patterns in the form expected by the $patterns parameter of
 *   drupal_match_path().
 */
function _password_policy_get_nonconfigurable_allowed_paths() {
  $password_change_paths = _password_policy_get_password_change_paths();
  $logout_paths = array(
    'user/logout',
  );
  return array_merge($password_change_paths, $logout_paths);
}

/**
 * Sets message indicating a password change is forced.
 */
function _password_policy_set_password_change_forced_message() {
  drupal_set_message(t('Your password has expired. You must change your password to proceed on the site.'), 'error', FALSE);
}

/**
 * Redirects user to password change page.
 */
function _password_policy_go_to_password_change_page() {
  $password_change_path = _password_policy_get_preferred_password_change_path();

  // Set query to redirect user back to their original destination after
  // leaving password change page.
  $options = array(
    'query' => drupal_get_destination(),
  );
  unset($_GET['destination']);

  // Add password reset token, if it is available, to query so user is not
  // prompted for their current password on password change page unnecessarily.
  _password_policy_add_pass_reset_token_if_available($options);
  drupal_goto($password_change_path, $options);
}

/**
 * Gets preferred path for password change page.
 *
 * The password change page path depends on whether Password Policy Password
 * Tab is enabled.  When there are multiple paths, the first is assumed to be
 * preferred.
 */
function _password_policy_get_preferred_password_change_path() {
  $password_change_paths = _password_policy_get_password_change_paths();
  return $password_change_paths[0];
}

/**
 * Gets paths that allow user to change their password.
 */
function _password_policy_get_password_change_paths() {
  global $user;
  return _password_policy_get_password_edit_paths_for_user($user);
}

/**
 * Gets password edit paths for the given user.
 */
function _password_policy_get_password_edit_paths_for_user($account) {
  $password_change_path = variable_get('password_policy_force_change_path', NULL);
  if (!empty($password_change_path)) {

    // Support replacement patterns for password_policy_force_change_path.
    $password_change_path = str_replace('[user:uid]', $account->uid, $password_change_path);
    $password_change_path = str_replace('[user:name]', $account->name, $password_change_path);
    return array(
      $password_change_path,
    );
  }
  elseif (module_exists('password_policy_password_tab')) {
    return array(
      "user/{$account->uid}/password",
    );
  }
  else {
    return array(
      "user/{$account->uid}/edit/account",
      "user/{$account->uid}/edit",
    );
  }
}

/**
 * Returns default value for password_policy_force_change_extra_allowed_paths.
 *
 * It is not necessary to allow system/ajax when password changes are forced,
 * but historically this module has allowed this path by non-configurable
 * default. We keep this path as a default but it is now configurable.
 *
 * @return string
 *   Default value.
 */
function _password_policy_default_force_change_extra_allowed_paths() {
  return 'system/ajax';
}

/**
 * Adds password reset token, if available, to query.
 *
 * This is for the case that the user is forced to change their password on
 * reset, but attempts to browse away from the password change path before
 * changing their password.  Adding the reset token makes it so the user will
 * not be prompted for their current password.
 *
 * @param array $options
 *   Options to be passed to drupal_goto().
 */
function _password_policy_add_pass_reset_token_if_available(array &$options) {
  global $user;
  if (isset($_SESSION['pass_reset_' . $user->uid])) {
    $pass_reset_token = $_SESSION['pass_reset_' . $user->uid];
    $options['query']['pass-reset-token'] = $pass_reset_token;
  }
}

/****************************************************************************/

/* Expired accounts UI                                                      */

/****************************************************************************/

/**
 * Lists all expired accounts.
 */
function password_policy_expired_list() {
  $header[] = array(
    'data' => t('Username'),
    'field' => 'name',
  );
  $header[] = array(
    'data' => t('Blocked'),
    'field' => 'blocked',
    'sort' => 'desc',
  );
  $header[] = array(
    'data' => t('Unblocked'),
    'field' => 'unblocked',
  );
  $header[] = array(
    'data' => t('Action'),
  );
  $query = db_select('password_policy_expiration', 'p', array(
    'target' => 'slave',
  ));
  $query
    ->innerJoin('users', 'u', 'p.uid = u.uid');
  $result = $query
    ->fields('p')
    ->fields('u', array(
    'name',
  ))
    ->condition('p.blocked', 0, '>')
    ->extend('PagerDefault')
    ->extend('TableSort')
    ->limit(variable_get('password_policy_expired_account_entries', 10))
    ->orderByHeader($header)
    ->execute();
  foreach ($result as $row) {
    $entry[$row->uid]['name'] = l($row->name, 'user/' . $row->uid);
    $entry[$row->uid]['blocked'] = format_date($row->blocked, 'medium');
    $entry[$row->uid]['unblocked'] = $row->unblocked < $row->blocked ? '' : format_date($row->unblocked, 'medium');
    $entry[$row->uid]['action'] = $row->unblocked < $row->blocked ? l(t('unblock'), 'admin/people/expired/unblock/' . $row->uid, array(
      'query' => array(
        'destination' => 'admin/people/expired',
      ),
    )) : '';
  }
  if (!isset($entry)) {
    $colspan = '4';
    $entry[] = array(
      array(
        'data' => t('No entries'),
        'colspan' => $colspan,
      ),
    );
  }
  $page = theme('table', array(
    'header' => $header,
    'rows' => $entry,
  ));
  $page .= theme('pager');
  return $page;
}

/**
 * Confirms unblocking the expired account.
 */
function password_policy_expired_unblock_confirm($form_id, $form, $account) {
  return confirm_form(array(
    'account' => array(
      '#type' => 'value',
      '#value' => $account,
    ),
  ), t('Are you sure you would like to unblock the user %user?', array(
    '%user' => $account->name,
  )), 'admin/people/expired', t('This action cannot be undone.'), t('Unblock user'), t('Cancel'));
}

/**
 * Unblocks the expired account.
 */
function password_policy_expired_unblock_confirm_submit($form, &$form_state) {
  $account = $form_state['values']['account'];
  if (_password_policy_was_user_blocked_due_to_expiration($account)) {
    user_save($account, array(
      'status' => 1,
    ));
    drupal_set_message(t('The user %name has been unblocked.', array(
      '%name' => $account->name,
    )));
  }
  else {
    drupal_set_message(t('The user %name was not blocked using the Password Policy module. This account has not been unblocked.', array(
      '%name' => $account->name,
    )), 'warning');
  }
  drupal_goto('admin/people/expired');
}

/****************************************************************************/

/* Mail handling                                                            */

/****************************************************************************/

/**
 * Returns a mail string for a variable name.
 *
 * Used by password_policy_mail() and the settings forms to retrieve strings.
 */
function _password_policy_mail_text($key, $language = NULL, $variables = array(), $replace = TRUE) {
  $langcode = isset($language) ? $language->language : NULL;
  switch ($key) {
    case 'warning_subject':
      if (function_exists('i18n_variable_get')) {
        $text = i18n_variable_get('password_policy_warning_subject', $langcode, PASSWORD_POLICY_DEFAULT_WARNING_SUBJECT);
      }
      else {
        $text = variable_get('password_policy_warning_subject', PASSWORD_POLICY_DEFAULT_WARNING_SUBJECT);
      }
      break;
    case 'warning_body':
      if (function_exists('i18n_variable_get')) {
        $text = i18n_variable_get('password_policy_warning_body', $langcode, PASSWORD_POLICY_DEFAULT_WARNING_BODY);
      }
      else {
        $text = variable_get('password_policy_warning_body', PASSWORD_POLICY_DEFAULT_WARNING_BODY);
      }
      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' => 'password_policy_mail_tokens',
      'sanitize' => FALSE,
      'clear' => TRUE,
    ));
  }
  return $text;
}

/**
 * Token callback to add Password Policy tokens for user e-mails.
 *
 * This function is used by the token_replace() call at the end of
 * _password_policy_mail_text() to set up some additional tokens that can be
 * used in email messages generated by password_policy_mail().
 *
 * @param array $replacements
 *   An associative array variable containing mappings from token names to
 *   values (for use with strtr()).
 * @param array $data
 *   An associative array of token replacement values.
 * @param array $options
 *   Unused parameter required by the token_replace() function.
 */
function password_policy_mail_tokens(array &$replacements, array $data, array $options) {
  if (isset($data['days_left'])) {
    $replacements['[password-policy:days-left]'] = $data['days_left'];
  }
  $password_edit_url_token = '[password-policy:password-edit-url]';
  $account = $data['user'];
  $password_edit_url = _password_policy_get_preferred_password_edit_url_for_user($account);
  $replacements[$password_edit_url_token] = $password_edit_url;
}

/**
 * Gets preferred password edit URL for user.
 *
 * The password edit page URL depends on whether Password Policy Password Tab
 * is enabled. There can be multiple equivalent password edit URLs, and we have
 * to choose one. The one we choose is the "preferred" edit URL.
 *
 * Builds URL in the same way as the URL for [user:edit-url] in
 * user.tokens.inc.
 *
 * @param object $account
 *   User object.
 *
 * @return string
 *   Absolute URL for preferred password edit page.
 */
function _password_policy_get_preferred_password_edit_url_for_user($account) {
  global $language;
  $edit_path = _password_policy_get_preferred_password_edit_path_for_user($account);
  $url_options = array(
    'absolute' => TRUE,
    'language' => $language,
  );
  return url($edit_path, $url_options);
}

/**
 * Gets preferred password edit path for given user.
 */
function _password_policy_get_preferred_password_edit_path_for_user($account) {
  $edit_paths = _password_policy_get_password_edit_paths_for_user($account);
  return $edit_paths[0];
}

/****************************************************************************/

/* Constraints API                                                          */

/****************************************************************************/

/**
 * Validates user password.
 *
 * Returns NULL on success or array with error messages
 * from the constraints on failure.
 *
 * @param string $pass
 *   Clear text password.
 * @param object $account
 *   Populated user object.
 *
 * @return null|string[]
 *   NULL or array with error messages.
 */
function _password_policy_constraint_validate($pass, &$account) {
  _password_policy_constraints();
  $error = NULL;
  $roles = @is_array($account->roles) ? array_keys($account->roles) : array();
  $policy = _password_policy_load_active_policy($roles, $account);
  if (!empty($policy['constraints'])) {
    foreach ($policy['constraints'] as $key => $value) {
      if (!call_user_func('password_policy_constraint_' . $key . '_validate', $pass, $value, $account)) {
        $error[] = call_user_func('password_policy_constraint_' . $key . '_error', $value);
      }
    }
  }
  return $error;
}

/**
 * Gets the constraint's name and description.
 *
 * @param string $name
 *   Name of the constraint.
 *
 * @return array
 *   Array containing the name and description.
 */
function _password_policy_constraint_description($name) {
  _password_policy_constraints();
  return call_user_func('password_policy_constraint_' . $name . '_description');
}

/**
 * Gets the constraint's error message.
 *
 * @param string $name
 *   Name of the constraint.
 * @param string $constraint
 *   Constraint value.
 *
 * @return string
 *   Error message.
 */
function _password_policy_constraint_error($name, $constraint) {
  _password_policy_constraints();
  return call_user_func('password_policy_constraint_' . $name . '_error', $constraint);
}

/**
 * Gets JavaScript code from the constraint to be added to password validation.
 *
 * @param string $name
 *   Name of the constraint.
 * @param string $constraint
 *   Constraint value.
 * @param object $account
 *   User object.
 *
 * @return string
 *   JavaScript code snippet for the constraint.
 */
function _password_policy_constraint_js($name, $constraint, $account) {
  _password_policy_constraints();
  if (function_exists('password_policy_constraint_' . $name . '_js')) {
    return call_user_func('password_policy_constraint_' . $name . '_js', $constraint, $account);
  }
}

/****************************************************************************/

/* Features integration                                                     */

/****************************************************************************/

/**
 * Implements hook_features_api().
 */
function password_policy_features_api() {
  return array(
    'password_policy' => array(
      'name' => 'Password Policy',
      'file' => drupal_get_path('module', 'password_policy') . '/password_policy.features.inc',
      'default_hook' => 'password_policy_features_default_policies',
      'feature_source' => TRUE,
    ),
  );
}

/****************************************************************************/

/* Auxiliary functions                                                      */

/****************************************************************************/

/**
 * Loads contraints inc files.
 */
function _password_policy_constraints() {
  static $_password_policy;
  if (!isset($_password_policy)) {

    // Save all available constraints in a static variable.
    $dir = drupal_get_path('module', 'password_policy') . '/constraints';
    $constraints = file_scan_directory($dir, '/^constraint.*\\.inc$/');
    $_password_policy = array();
    foreach ($constraints as $file) {
      if (is_file($file->uri)) {
        include_once $file->uri;
        $_password_policy[] = drupal_substr($file->name, 11);
      }
    }
  }
  return $_password_policy;
}

/**
 * Loads the policy with the specified id.
 *
 * Attempts to load the policy from a static cache variable. If not found,
 * loads the policy from the database.
 *
 * @param int $pid
 *   The policy id.
 *
 * @return array|false
 *   A populated policy array or FALSE if not found.
 */
function _password_policy_load_policy_by_pid($pid) {
  static $policies = array();
  if (is_numeric($pid)) {
    if (isset($policies[$pid])) {
      return $policies[$pid];
    }
    else {
      $policy = _password_policy_load_policy_from_db(array(
        'pid' => $pid,
      ));
      if ($policy) {
        $policies[$pid] = $policy;
        return $policy;
      }
    }
  }
  return FALSE;
}

/**
 * Loads the policy with the specified name.
 *
 * Attempts to load the policy from a static cache variable. If not found,
 * loads the policy from the database.
 *
 * @param string $name
 *   The name of the policy.
 *
 * @return array|false
 *   A populated policy array or FALSE if not found.
 */
function password_policy_load_policy_by_name($name) {
  static $policies = array();
  if (isset($policies[$name])) {
    return $policies[$name];
  }
  else {
    $policy = _password_policy_load_policy_from_db(array(
      'name' => $name,
    ));
    if ($policy) {
      $policies[$name] = $policy;
      return $policy;
    }
  }
  return FALSE;
}

/**
 * Loads the policy that meets the specified conditions from the database.
 *
 * @param array $conditions
 *   Associative array of conditions where keys are field names and values are
 *   field values.
 *
 * @return array|false
 *   A policy array or FALSE if no policy was found.
 */
function _password_policy_load_policy_from_db(array $conditions) {
  $query = db_select('password_policy', 'p', array(
    'target' => 'slave',
  ))
    ->fields('p');
  foreach ($conditions as $field => $value) {
    $query
      ->condition($field, $value);
  }
  $row = $query
    ->execute()
    ->fetchAssoc();
  if ($row) {
    $row['constraints'] = unserialize($row['constraints']);

    // Fetch roles.
    $row['roles'] = array();
    $result = db_select('password_policy_role', 'p', array(
      'target' => 'slave',
    ))
      ->fields('p', array(
      'rid',
    ))
      ->condition('pid', $row['pid'])
      ->execute();
    foreach ($result as $role) {
      $row['roles'][$role->rid] = $role->rid;
    }

    // Fetch authentication modules.
    _password_policy_load_policy_excluded_authentication_modules($row);
    return $row;
  }
  return FALSE;
}

/**
 * Loads the first enabled policy that matches the specified roles.
 *
 * @param int[] $roles
 *   An array of role IDs.
 * @param object $account
 *   Populated user object.
 *
 * @return array|false
 *   A policy array, or FALSE if no active policy exists.
 */
function _password_policy_load_active_policy(array $roles, &$account) {
  static $cache = array();
  if (empty($roles)) {
    $roles = array(
      DRUPAL_ANONYMOUS_RID,
    );
  }

  // If the role is a name, not an ID, replace with the ID.
  for ($i = 0; $i < count($roles); $i++) {
    if (!is_numeric($roles[$i])) {
      $row = db_select('role', 'r', array(
        'target' => 'slave',
      ))
        ->fields('r', array(
        'rid',
      ))
        ->condition('r.name', $roles[$i])
        ->execute()
        ->fetchAssoc();
      if (!empty($row['rid'])) {
        $roles[$i] = $row['rid'];
      }
    }
  }
  $key = implode(',', $roles);

  // Use array_key_exists() instead of isset() as NULLs may be in the array.
  if (!array_key_exists($key, $cache)) {
    $query = db_select('password_policy', 'p', array(
      'target' => 'slave',
    ));
    $query
      ->innerJoin('password_policy_role', 'r', 'p.pid = r.pid');
    $row = $query
      ->fields('p')
      ->condition('p.enabled', 1)
      ->condition('r.rid', $roles, 'IN')
      ->orderBy('p.weight')
      ->range(0, 1)
      ->execute()
      ->fetchAssoc();
    if (is_array($row)) {
      $constraints = $row['constraints'];
      $constraints = unserialize($constraints);
      $row['constraints'] = $constraints;
      _password_policy_load_policy_excluded_authentication_modules($row);
      $cache[$key] = $row;
    }
    else {
      $cache[$key] = FALSE;
    }
  }
  $policy = $cache[$key];
  if ($policy && _password_policy_policy_excludes_authentication_module_of_user($policy, $account)) {
    $policy = FALSE;
  }
  return $policy;
}

/**
 * Saves a policy.
 *
 * @param array $policy
 *   A policy array.
 */
function password_policy_save_policy(array $policy) {
  if (isset($policy['pid']) && $policy['pid']) {
    $fields = array(
      'name' => $policy['name'],
      'description' => $policy['description'],
      'constraints' => serialize($policy['constraints']),
      'created' => isset($policy['created']) ? $policy['created'] : 0,
      'expiration' => !empty($policy['expiration']) ? $policy['expiration'] : 0,
      'warning' => str_replace(' ', '', $policy['warning']),
      'weight' => !empty($policy['weight']) ? $policy['weight'] : 0,
    );

    // On policy edit form we have no 'enabled' param, so modify update query.
    if (isset($policy['enabled'])) {
      $fields += array(
        'enabled' => $policy['enabled'],
      );
    }
    db_update('password_policy')
      ->fields($fields)
      ->condition('pid', $policy['pid'])
      ->execute();
    watchdog('password_policy', 'Policy %name updated.', array(
      '%name' => $policy['name'],
    ), WATCHDOG_NOTICE, l(t('edit'), 'admin/config/people/password_policy/' . $policy['pid'] . '/edit'));
    db_delete('password_policy_role')
      ->condition('pid', $policy['pid'])
      ->execute();
    db_delete('password_policy_excluded_authentication_modules')
      ->condition('pid', $policy['pid'])
      ->execute();
    $pid = $policy['pid'];
  }
  else {
    $pid = db_insert('password_policy')
      ->fields(array(
      'name' => $policy['name'],
      'description' => $policy['description'],
      'enabled' => $policy['enabled'],
      'constraints' => serialize($policy['constraints']),
      'created' => isset($policy['created']) ? $policy['created'] : 0,
      'expiration' => !empty($policy['expiration']) ? $policy['expiration'] : 0,
      'warning' => str_replace(' ', '', $policy['warning']),
      'weight' => !empty($policy['weight']) ? $policy['weight'] : 0,
    ))
      ->execute();
    watchdog('password_policy', 'New policy %name created.', array(
      '%name' => $policy['name'],
    ), WATCHDOG_NOTICE, l(t('edit'), 'admin/config/people/password_policy/' . $pid . '/edit'));
  }
  foreach (array_filter($policy['roles']) as $rid => $enabled) {
    db_insert('password_policy_role')
      ->fields(array(
      'pid' => $pid,
      'rid' => $rid,
    ))
      ->execute();
  }
  foreach (array_filter($policy['excluded_authentication_modules']) as $module => $excluded) {
    db_insert('password_policy_excluded_authentication_modules')
      ->fields(array(
      'pid' => $pid,
      'module' => $module,
    ))
      ->execute();
  }
}

/**
 * Stores user password hash.
 *
 * @param int $uid
 *   User id.
 * @param string $pass
 *   Password hash.
 */
function _password_policy_store_password($uid, $pass) {
  db_insert('password_policy_history')
    ->fields(array(
    'uid' => $uid,
    'pass' => $pass,
    'created' => _password_policy_get_request_time(),
  ))
    ->execute();
}

/**
 * Blocks the expired account.
 *
 * @param object $account
 *   User object.
 */
function _password_policy_block_account($account) {
  if ($account->uid > 1) {

    // We never block the superuser account.
    db_update('users')
      ->fields(array(
      'status' => 0,
    ))
      ->condition('uid', $account->uid)
      ->execute();

    // Check if user is already blocked.
    $blocked = db_select('password_policy_expiration', 'p', array(
      'target' => 'slave',
    ))
      ->fields('p', array(
      'pid',
    ))
      ->condition('uid', $account->uid)
      ->isNull('unblocked')
      ->execute()
      ->fetchField();
    if ($blocked) {
      db_update('password_policy_expiration')
        ->fields(array(
        'blocked' => _password_policy_get_request_time(),
      ))
        ->condition('uid', $account->uid)
        ->execute();
    }
    else {
      db_insert('password_policy_expiration')
        ->fields(array(
        'uid' => $account->uid,
        'blocked' => _password_policy_get_request_time(),
      ))
        ->execute();
    }
    watchdog('password_policy', 'Password for user %name has expired.', array(
      '%name' => $account->name,
    ), WATCHDOG_NOTICE, l(t('edit'), 'user/' . $account->uid . '/edit'));

    // Bypass logout process when executed via Drush.
    if (!function_exists('drush_verify_cli') || !drush_verify_cli()) {
      include_once drupal_get_path('module', 'user') . '/user.pages.inc';
      user_logout();
    }
  }
}

/**
 * Handles unblocking of an account.
 *
 * If the account was blocked by this module due to password expiration, we (1)
 * record the time it was unblocked and (2) force the user to change their
 * password upon next login.
 *
 * @param object $account
 *   User object.
 */
function _password_policy_handle_unblock($account) {

  // Check if user was blocked via this module.
  if (_password_policy_was_user_blocked_due_to_expiration($account)) {
    db_update('password_policy_expiration')
      ->fields(array(
      'unblocked' => _password_policy_get_request_time(),
    ))
      ->condition('uid', $account->uid)
      ->execute();
    db_update('password_policy_force_change')
      ->fields(array(
      'force_change' => 1,
    ))
      ->condition('uid', $account->uid)
      ->execute();
  }
}

/**
 * Determines whether user was blocked due to expiration.
 *
 * @param object $account
 *   User object.
 */
function _password_policy_was_user_blocked_due_to_expiration($account) {
  return db_select('password_policy_expiration', 'ppe')
    ->fields('ppe', array(
    'pid',
  ))
    ->condition(db_or()
    ->condition(db_and()
    ->isNull('unblocked')
    ->condition('blocked', '0', '<>'))
    ->condition(db_and()
    ->isNotNull('unblocked')
    ->where('blocked > unblocked')))
    ->condition('uid', $account->uid)
    ->execute()
    ->fetchField();
}

/**
 * Adds password policy JS.
 *
 * @param array $policy
 *   A policy array.
 * @param object $account
 *   User object of user for which the policy is applied.
 * @param array $render_array
 *   (Optional) A renderable array to attach the JavaScript to. If not
 *   provided, the JavaScript will be added to the page directly.
 */
function password_policy_add_policy_js(array $policy, $account, array &$render_array = NULL) {
  $s = <<<JS
  /**
   * Evaluates the strength of a user's password.
   *
   * Returns the estimated strength and the relevant output message.
   */
  Drupal.evaluatePasswordStrength = function (value) {
    var strength = 'high';
    var msg = [];
    var translate = Drupal.settings.password;
    // Merge Password Policy translations.
    for (var setting in Drupal.settings.passwordPolicy) {
      translate[setting] = Drupal.settings.passwordPolicy[setting];
    }
    var trimmedSpaces = /^\\s+|\\s+\$/.test(value);
    if (/^\\s+\$/.test(value)) {
      return {
        strength: 10,
        indicatorText: translate.lowStrength,
        message: translate.allSpaces
      };
    }
    value = value.replace(/^\\s+|\\s+\$/g, '');
JS;

  // Print out each constraint's javascript password strength evaluation.
  foreach ($policy['constraints'] as $key => $value) {
    $s .= _password_policy_constraint_js($key, $value, $account);

    // Constraints' error messages are used in javascript.
    $translate['constraint_' . $key] = _password_policy_constraint_error($key, $value);
  }
  $s .= <<<JS

    if (msg.length > 0) {
      msg = translate.needsMoreVariation + '<ul><li>' + msg.join('</li><li>') + '</li></ul>';
    }
    else {
      msg = '';
    }
    if (trimmedSpaces) {
      msg = msg.concat(translate.trimmedSpaces);
    }
    var level = '';
    if (strength === 'high') {
      level = 100;
    }
    else {
      level = 10;
    }
    if (strength === 'high') {
      strength = translate.highStrength;
    }
    if (strength === 'medium') {
      strength = translate.mediumStrength;
    }
    if (strength === 'low') {
      strength = translate.lowStrength;
    }
    return {
      strength: level,
      indicatorText: strength,
      message: msg
    };
  }
JS;
  $options = array(
    'scope' => 'header',
    'type' => 'inline',
    'weight' => 10,
  );
  if (isset($render_array)) {
    $options['data'] = $s;
    $render_array['#attached']['js'][] = $options;
  }
  else {
    drupal_add_js($s, $options);
  }
  $data = array(
    // Override some core 'password' settings.
    // Drupal by default rates passwords in terms of strength. However, a
    // password that meets Password Policy constraints is not necessarily a
    // strong password. So we rate the password in terms of "quality". A
    // password is "bad" if it does not meet constraints, "good" if it does.
    'password' => array(
      'strengthTitle' => t('Password quality:'),
      'lowStrength' => t('Bad'),
      'mediumStrength' => t('Good'),
      'highStrength' => t('Good'),
    ),
    // Add new settings for this module.
    'passwordPolicy' => array_merge(array(
      'trimmedSpaces' => t('The password has spaces at the beginning or end which are ignored.'),
      'allSpaces' => t('The password is all spaces and will not be saved.'),
      'needsMoreVariation' => t('The password does not include enough variation to be secure.'),
    ), $translate),
  );
  if (isset($render_array)) {
    $options = array(
      'data' => $data,
      'type' => 'setting',
    );
    $render_array['#attached']['js'][] = $options;
  }
  else {
    drupal_add_js($data, 'setting');
  }
}

Functions

Namesort descending Description
password_policy_add_policy_js Adds password policy JS.
password_policy_cron Implements hook_cron().
password_policy_drupal_goto_alter Implements hook_drupal_goto_alter().
password_policy_expired_list Lists all expired accounts.
password_policy_expired_unblock_confirm Confirms unblocking the expired account.
password_policy_expired_unblock_confirm_submit Unblocks the expired account.
password_policy_features_api Implements hook_features_api().
password_policy_field_extra_fields Implements hook_field_extra_fields().
password_policy_format_title Displays a password policy form title.
password_policy_form_alter Implements hook_form_alter().
password_policy_help Implements hook_help().
password_policy_init Implements hook_init().
password_policy_load_policy_by_name Loads the policy with the specified name.
password_policy_mail Implements hook_mail().
password_policy_mail_tokens Token callback to add Password Policy tokens for user e-mails.
password_policy_menu Implements hook_menu().
password_policy_password_validate Password save validate handler.
password_policy_permission Implements hook_permission().
password_policy_policy_load Loads policy array from the database.
password_policy_save_policy Saves a policy.
password_policy_theme Implements hook_theme().
password_policy_uid_load Loads user object from the database.
password_policy_user_delete Implements hook_user_delete().
password_policy_user_insert Implements hook_user_insert().
password_policy_user_load Implements hook_user_load().
password_policy_user_login Implements hook_user_login().
password_policy_user_presave Implements hook_user_presave().
password_policy_user_update Implements hook_user_update().
_password_policy_add_pass_reset_token_if_available Adds password reset token, if available, to query.
_password_policy_add_selected_roles_to_account Adds roles selected for user on form to the user object.
_password_policy_block_account Blocks the expired account.
_password_policy_constraints Loads contraints inc files.
_password_policy_constraint_description Gets the constraint's name and description.
_password_policy_constraint_error Gets the constraint's error message.
_password_policy_constraint_js Gets JavaScript code from the constraint to be added to password validation.
_password_policy_constraint_validate Validates user password.
_password_policy_default_force_change_extra_allowed_paths Returns default value for password_policy_force_change_extra_allowed_paths.
_password_policy_get_allowed_paths Gets paths allowed when password change forced.
_password_policy_get_configurable_allowed_paths Gets configurable paths allowed when password change forced.
_password_policy_get_nonconfigurable_allowed_paths Gets non-configurable paths allowed when password change forced.
_password_policy_get_password_change_paths Gets paths that allow user to change their password.
_password_policy_get_password_edit_paths_for_user Gets password edit paths for the given user.
_password_policy_get_preferred_password_change_path Gets preferred path for password change page.
_password_policy_get_preferred_password_edit_path_for_user Gets preferred password edit path for given user.
_password_policy_get_preferred_password_edit_url_for_user Gets preferred password edit URL for user.
_password_policy_get_user_from_form Gets from form user for whom password is being validated.
_password_policy_go_to_password_change_page Redirects user to password change page.
_password_policy_handle_unblock Handles unblocking of an account.
_password_policy_has_account_password_element Determines whether form has an account password element.
_password_policy_is_current_user Determines whether given user is the current user.
_password_policy_is_force_password_change_set Determines whether user has been flagged for forced password change.
_password_policy_is_form_password_too_long Determines whether password on form exceeds Drupal maximum length.
_password_policy_is_password_change_forced Determines whether user is to be forced to change their password.
_password_policy_is_path_allowed_when_password_change_forced Determines whether access to path allowed when user password change forced.
_password_policy_load_active_policy Loads the first enabled policy that matches the specified roles.
_password_policy_load_policy_by_pid Loads the policy with the specified id.
_password_policy_load_policy_from_db Loads the policy that meets the specified conditions from the database.
_password_policy_mail_text Returns a mail string for a variable name.
_password_policy_password_field_is_empty Determines whether password field is empty.
_password_policy_set_password_change_forced_message Sets message indicating a password change is forced.
_password_policy_store_password Stores user password hash.
_password_policy_validate_constraints Validates constraints.
_password_policy_was_updated_user_unblocked Determines whether updated user was unblocked.
_password_policy_was_user_blocked_due_to_expiration Determines whether user was blocked due to expiration.

Constants