password_policy.module in Password Policy 7.2
Same filename and directory in other branches
Enforces password policies.
File
password_policy.moduleView 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;
}