You are here

password_policy.module in Password Policy 7.2

Enforces password policies.

File

password_policy.module
View source
<?php

/**
 * @file
 * Enforces password policies.
 */

/**
 * Implements hook_menu().
 */
function password_policy_menu() {
  $items['password_policy/check'] = array(
    'page callback' => 'password_policy_ajax_check',
    // Open callback allows anonymous users' passwords to be checked.
    // This may be needed when an anonymous user is setting a password on the
    // user registration form.
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * AJAX callback to check password against applicable policies.
 */
function password_policy_ajax_check() {

  // Decode password which javascript ran encodeURIComponent.
  // The password will not be displayed, so there is no need to filter it with
  // check_plain() or filter_xss() as suggested by Coder.
  // @ignore security_17
  if (isset($_POST['password'])) {
    $untrimmed_password = rawurldecode($_POST['password']);

    // Trim the password before checking against policies, since Drupal will
    // trim passwords before saving them.
    $password = trim($untrimmed_password);
    $is_trimmed = $password !== $untrimmed_password;

    // Determine whether password is all spaces.  If it is empty string after
    // trimming, it was all spaces.
    $is_all_spaces = $is_trimmed && $password === '';
    if ($is_all_spaces) {
      return drupal_json_output(array(
        'message' => t('Password is all spaces and will not be saved.'),
        'strength' => 0,
        'indicatorText' => '',
      ));
    }

    // Do not process overlong passwords to avoid potential DoS.
    // Drupal core does not allow passwords over a certain number of bytes, so
    // impose the same limitation.
    if (_password_policy_is_password_too_long($password)) {
      return drupal_json_output(array(
        'message' => t('Password exceeds maximum length. Please choose a shorter password.'),
        'strength' => 0,
        'indicatorText' => '',
      ));
    }

    // Using this user is not always going to work.
    global $user;
    $account = $user;
    password_policy_user_load(array(
      $account->uid => $account,
    ));
    $policies = PasswordPolicy::matchedPolicies($account);

    // Exit prematurely if no policies are usable.
    if (count($policies) == 0) {
      return;
    }
    $total = 0;
    $errors = array();
    foreach ($policies as $policy) {
      $total += count($policy
        ->messages());
      $errors = array_merge($errors, $policy
        ->check($password, $account));
    }
    $sus_count = $total - count($errors);
    $score = $sus_count / $total * 100;
    $msg = '';
    if (!empty($errors)) {
      $msg .= t('Password does not meet the following requirements:');
      $msg .= theme('item_list', array(
        'items' => $errors,
      ));
      if ($is_trimmed) {
        $msg .= t('Password has spaces at the beginning or end which are ignored.');
      }
    }
    $return = array(
      'message' => $msg,
      'strength' => $score,
      'indicatorText' => t('@sus_count of @total', array(
        '@sus_count' => $sus_count,
        '@total' => $total,
      )),
    );
    drupal_json_output($return);
  }
}

/**
 * Implements hook_ctools_plugin_api().
 */
function password_policy_ctools_plugin_api($module, $api) {
  if ($module == 'password_policy' && $api == 'default_password_policy') {
    return array(
      'version' => '1',
    );
  }
}

/**
 * Implements hook_ctools_plugin_type().
 */
function password_policy_ctools_plugin_type() {
  return array(
    'constraint' => array(
      'defaults' => array(
        'class' => 'PasswordPolicyConstraint',
      ),
    ),
    'condition' => array(
      'defaults' => array(
        'class' => 'PasswordPolicyCondition',
      ),
    ),
    'item' => array(
      'defaults' => array(
        'class' => 'PasswordPolicyItem',
      ),
    ),
  );
}

/**
 * Implements hook_ctools_plugin_directory().
 */
function password_policy_ctools_plugin_directory($module, $plugin) {
  if ($module == 'password_policy') {
    return "plugins/{$plugin}";
  }
  if ($module == 'ctools') {
    return "plugins/{$plugin}";
  }
}

/**
 * Ctools exportable 'settings' callback.
 *
 * @see plugins/export_ui/password_policy.inc
 */
function password_policy_admin_settings(&$form, &$form_state) {
  $item = $form_state['item'];
  $policy = new PasswordPolicy($item);
  $form = $policy
    ->adminForm($form, $form_state);
  $form['info']['name']['#description'] .= ' ' . t('This is not case sensitive, so "abc" is the same as "ABC" and "aBc".');
  $form_state['policy'] = $policy;
  return $form;
}

/**
 * Ctools exportable 'validate' callback.
 *
 * @see plugins/export_ui/password_policy.inc
 */
function password_policy_admin_validate(&$form, &$form_state) {
  if (empty($form_state['policy']->name)) {
    $name = $form_state['values']['name'];
    $dupe_name = db_query('SELECT 1 FROM {password_policy} WHERE LOWER(name) = :new', array(
      ':new' => drupal_strtolower($name),
    ))
      ->fetchField();
    if ($dupe_name) {
      form_set_error('name', t('Duplicate policy name.'));
    }
  }
  return TRUE;
}

/**
 * Ctools exportable 'submit' callback.
 *
 * @see plugins/export_ui/password_policy.inc
 */
function password_policy_admin_submit(&$form, &$form_state) {
  $policy = $form_state['policy'];
  $policy
    ->adminFormSubmit($form, $form_state);
}

/**
 * Implements hook_permission().
 */
function password_policy_permission() {
  return array(
    'administer password policy' => array(
      'title' => t('Administer password policy'),
    ),
  );
}

/**
 * Implements hook_cron().
 */
function password_policy_cron() {
  $policies = PasswordPolicy::enabledPolicies();
  foreach ($policies as $policy) {
    $policy
      ->cron();
  }
}

/**
 * Implements hook_init().
 */
function password_policy_init() {
  global $user;
  $policies = PasswordPolicy::matchedPolicies($user);
  foreach ($policies as $policy) {
    $policy
      ->init($user);
  }
}

/**
 * Alters the password element.
 *
 * @param array $element
 *   Password element.
 * @param object $account
 *   User object.
 */
function password_policy_password_element_alter(array &$element, $account) {
  $items = array();
  $policies = PasswordPolicy::matchedPolicies($account);
  foreach ($policies as $policy) {
    $items = array_merge($items, $policy
      ->messages());
  }

  // Only alter description if policy messages are present.
  if (count($items)) {
    $element['#description'] = (isset($element['#description']) ? $element['#description'] : '') . theme('item_list', array(
      'items' => $items,
      'title' => t('Passwords must meet the following requirements:'),
      'attributes' => array(
        'id' => 'password-policy-requirements',
      ),
    ));
  }

  // Attach password evaluation logic and ensure it's added after user.js.
  $element['#attached']['js'][] = array(
    'data' => drupal_get_path('module', 'password_policy') . '/password_policy.js',
    'weight' => 10,
  );

  // Add dependency of password_policy.js.
  $element['#attached']['library'][] = array(
    'system',
    'drupal.form',
  );

  // Add clean URL setting for use by password_policy.js.
  $clean_url = variable_get('clean_url', FALSE);
  $settings = array(
    'cleanUrl' => $clean_url,
  );
  $element['#attached']['js'][] = array(
    'data' => array(
      'passwordPolicy' => $settings,
    ),
    'type' => 'setting',
  );
}

/**
 * Implements hook_js_alter().
 */
function password_policy_js_alter(&$javascript) {

  // Because Drupal.settings cannot use a weight attribute, we must alter after
  // the page has been fully loaded.
  $javascript['settings']['data'][] = array(
    'password' => array(
      'strengthTitle' => t('Password compliance:'),
    ),
    'type' => 'setting',
  );
}

/**
 * Implements hook_form_alter().
 */
function password_policy_form_alter(&$form, &$form_state, $form_id) {
  if (_password_policy_has_account_password_element($form)) {
    password_policy_password_element_alter($form['account']['pass'], $form['#user']);
    $form['#validate'][] = 'password_policy_user_profile_form_validate';
  }
}

/**
 * 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';
}

/**
 * Form validation handler for user_profile_form().
 */
function password_policy_user_profile_form_validate($form, &$form_state) {
  if (!empty($form_state['values']['pass'])) {
    $pass = $form_state['values']['pass'];

    // Do not process overlong passwords to avoid potential DoS.
    // Drupal core does not allow passwords over a certain number of bytes, so
    // impose the same limitation.
    if (_password_policy_is_password_too_long($pass)) {
      form_set_error('pass', t('Password exceeds maximum length. Please choose a shorter password.'));
      return;
    }
    $account = $form['#user'];
    $policies = PasswordPolicy::matchedPolicies($account);
    $errors = array();
    foreach ($policies as $policy) {
      $errors = $errors + $policy
        ->check($pass, $account);
    }
    if (!empty($errors)) {
      form_set_error('pass', theme('item_list', array(
        'items' => $errors,
      )));
    }
  }
}

/**
 * Determines whether password exceeds Drupal maximum length.
 *
 * The maximum length is copied from includes/password.inc.
 *
 * @param string $password
 *   Password.
 *
 * @return bool
 *   TRUE if password exceeds Drupal maximum length, FALSE otherwise.
 *
 * @see _password_crypt()
 */
function _password_policy_is_password_too_long($password) {
  return strlen($password) > 512;
}

/**
 * Stores user password hash.
 *
 * @param int $uid
 *   User id.
 * @param string $pass
 *   Hashed password.
 * @param bool $is_generated
 *   Generated password indicator.
 */
function _password_policy_store_password($uid, $pass, $is_generated = FALSE) {
  $history = (object) array(
    'uid' => $uid,
    'pass' => $pass,
    'created' => REQUEST_TIME,
    'is_generated' => $is_generated,
    'data' => array(),
  );
  password_policy_update_password_history($history);
}

/**
 * Implements hook_user_insert().
 */
function password_policy_user_insert(&$edit, $account, $category) {
  if (!empty($edit['pass'])) {

    // New users do not yet have a uid during the validation step, but they do
    // have one at this insert step. Store their first password in the system
    // for use with the history constraint (if used).
    // A new user's first password can be system-generated. Store indicator of
    // system-generated password to bypass delay constraint (if used).
    if ($account->uid) {
      $is_password_generated = variable_get('user_email_verification', TRUE);
      _password_policy_store_password($account->uid, $edit['pass'], $is_password_generated);
    }
  }
}

/**
 * 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_load().
 *
 * Adds password history to user. Used by past_password and expire plugins.
 */
function password_policy_user_load($accounts) {

  // Insure all accounts have a history array.
  foreach ($accounts as $account) {
    $account->password_history = array();
  }
  $query = db_select('password_policy_history', 'p')
    ->condition('p.uid', array_keys($accounts))
    ->fields('p', array(
    'uid',
    'pass',
    'created',
    'is_generated',
  ))
    ->orderBy('hid', 'DESC');
  foreach ($query
    ->execute() as $record) {
    $accounts[$record->uid]->password_history[] = $record;
  }
}

/**
 * Adds the history record for this user/created time.
 *
 * @param object $history
 *   History record.
 */
function password_policy_update_password_history($history) {
  db_insert('password_policy_history')
    ->fields(array(
    'uid' => $history->uid,
    'created' => $history->created,
    'pass' => $history->pass,
    'is_generated' => $history->is_generated ? 1 : 0,
  ))
    ->execute();
}

/**
 * Implements hook_token_info().
 */
function password_policy_token_info() {
  $type = array(
    'name' => t('Password Expiration Date'),
    'description' => t('Tokens related to expired passwords.'),
    'needs-data' => 'password_expiration_date',
  );
  $formats = module_invoke_all('date_format_types');
  foreach ($formats as $name => $title) {
    $format[$name] = array(
      'name' => t('Expire Date @title Format', array(
        '@title' => $title,
      )),
      'description' => t('The Date the Password Expires in the @title Format.', array(
        '@title' => $title,
      )),
    );
  }
  $format['interval'] = array(
    'name' => t('Expire Date interval'),
    'description' => t('The Date the Password Expires in x days format'),
  );
  return array(
    'types' => array(
      'password_expiration_date' => $type,
    ),
    'tokens' => array(
      'password_expiration_date' => $format,
    ),
  );
}

/**
 * Implements hook_tokens().
 */
function password_policy_tokens($type, $tokens, $data, $options) {
  if ($type == 'password_expiration_date') {
    $replacements = array();
    foreach ($tokens as $key => $token) {
      if ($key == 'interval') {
        $replacements[$token] = format_interval($data['password_expiration_date'] - REQUEST_TIME);
      }
      else {
        $replacements[$token] = format_date($data['password_expiration_date'], $key);
      }
    }
    return $replacements;
  }
}

/**
 * Implements hook_mail().
 */
function password_policy_mail($key, &$message, $params) {
  $message['subject'] .= $params['subject'];
  $message['body'][] = $params['body'];
}

/**
 * Helper function to get number of seconds represented by relative time string.
 *
 * @param string $string
 *   The time interval string to parse - like 20 minutes or 4 weeks.
 * @param bool $report_errors
 *   Whether or not to set a message if the string can't be parsed.
 *
 * @return int
 *   Number of seconds in the interval
 */
function password_policy_parse_interval($string, $report_errors = FALSE) {
  $int = strtotime($string, 0);
  if ($report_errors && $int === FALSE) {
    drupal_set_message(t("Unable to parse time interval. Please use something like '1 day' or 2 weeks'."), 'error');
  }
  return $int;
}

/**
 * Implements hook_default_password_policy_alter().
 */
function password_policy_default_password_policy_alter(&$policies) {

  // Only display this policy if no other policies are defined.
  if (count($policies) > 0) {
    return;
  }
  $config = array(
    'alpha_count' => array(
      'alpha_count' => '1',
    ),
    'char_count' => array(
      'char_count' => '8',
    ),
    'int_count' => array(
      'int_count' => '1',
    ),
    'past_passwords' => array(
      'past_passwords' => '3',
    ),
    'special_count' => array(
      'special_count' => 1,
      'special_count_chars' => '`~!@#$%^&*()_+=-|}{"?:><,./;\'\\[]',
    ),
  );
  $password_policy = new stdClass();
  $password_policy->disabled = TRUE;

  /* Edit this to true to make a default password_policy disabled initially */
  $password_policy->api_version = 1;
  $password_policy->name = 'Example policy';
  $password_policy->config = serialize($config);
  $policies['Example policy'] = $password_policy;
}

/**
 * Implements hook_password_policy_expire_url_exclude().
 */
function password_policy_password_policy_expire_url_exclude($account) {

  // Do not redirect on AJAX requests.
  if ((arg(0) == 'system' || arg(0) == 'file') && arg(1) == 'ajax') {
    return FALSE;
  }

  // Allow users to log out!
  if (arg(0) == 'user' && arg(1) == 'logout') {
    return FALSE;
  }

  // Do not do anything if the user is doing the core e-mail validation step.
  if (arg(0) == 'user' && arg(1) == 'reset' && is_string(arg(2))) {
    return FALSE;
  }

  // Do not do anything if the user is doing the LoginToboggan validation
  // process.
  if (module_exists('logintoboggan')) {
    if (arg(0) == 'user' && arg(1) == 'validate' && is_string(arg(2))) {
      return FALSE;
    }
  }

  // Do not do anything if the user is using a urllogin link.
  if (module_exists('urllogin')) {
    if (arg(0) == 'l' && is_string(arg(1))) {
      return FALSE;
    }
  }
}

/**
 * Implements hook_user_delete().
 */
function password_policy_user_delete($account) {
  db_delete('password_policy_history')
    ->condition('uid', $account->uid)
    ->execute();
}

/**
 * Implements hook_help().
 */
function password_policy_help($path, $arg) {
  switch ($path) {
    case 'admin/help#password_policy':
      return _password_policy_get_help_output();
  }
}

/**
 * Gets help output for hook_help().
 *
 * The output consists of the README.txt contents, if they can be loaded.
 *
 * @return string
 *   README.txt contents within an HTML pre tag, or empty string if they could
 *   not be loaded.
 */
function _password_policy_get_help_output() {
  $readme = _password_policy_get_readme();
  if ($readme === FALSE) {
    $output = '';
  }
  else {
    $output = '<pre>' . $readme . '</pre>';
  }
  return $output;
}

/**
 * Gets README.txt.
 *
 * This code adapted from the Drupal module documentation guidelines.
 *
 * @see https://www.drupal.org/docs/develop/documenting-your-project/module-documentation-guidelines#hook_help
 *
 * @return string|false
 *   The contents of README.txt, or FALSE if they could not be loaded.
 */
function _password_policy_get_readme() {
  $filepath = dirname(__FILE__) . '/README.txt';
  if (file_exists($filepath)) {
    $readme = file_get_contents($filepath);
  }
  else {
    $readme = FALSE;
  }
  return $readme;
}

Functions

Namesort descending Description
password_policy_admin_settings Ctools exportable 'settings' callback.
password_policy_admin_submit Ctools exportable 'submit' callback.
password_policy_admin_validate Ctools exportable 'validate' callback.
password_policy_ajax_check AJAX callback to check password against applicable policies.
password_policy_cron Implements hook_cron().
password_policy_ctools_plugin_api Implements hook_ctools_plugin_api().
password_policy_ctools_plugin_directory Implements hook_ctools_plugin_directory().
password_policy_ctools_plugin_type Implements hook_ctools_plugin_type().
password_policy_default_password_policy_alter Implements hook_default_password_policy_alter().
password_policy_form_alter Implements hook_form_alter().
password_policy_help Implements hook_help().
password_policy_init Implements hook_init().
password_policy_js_alter Implements hook_js_alter().
password_policy_mail Implements hook_mail().
password_policy_menu Implements hook_menu().
password_policy_parse_interval Helper function to get number of seconds represented by relative time string.
password_policy_password_element_alter Alters the password element.
password_policy_password_policy_expire_url_exclude Implements hook_password_policy_expire_url_exclude().
password_policy_permission Implements hook_permission().
password_policy_tokens Implements hook_tokens().
password_policy_token_info Implements hook_token_info().
password_policy_update_password_history Adds the history record for this user/created time.
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_presave Implements hook_user_presave().
password_policy_user_profile_form_validate Form validation handler for user_profile_form().
_password_policy_get_help_output Gets help output for hook_help().
_password_policy_get_readme Gets README.txt.
_password_policy_has_account_password_element Determines whether form has an account password element.
_password_policy_is_password_too_long Determines whether password exceeds Drupal maximum length.
_password_policy_store_password Stores user password hash.