' . t('The password policy module allows you to enforce a specific level of password complexity for the user passwords on the system.') . '

'; } } /** * 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(); } } /** * 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($op='load') are called. * * @param $uid * User ID. * * @return * 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_result(db_query('SELECT force_change FROM {password_policy_force_change} WHERE uid=%d', $uid)); } return $force_change[$uid]; } /** * Determines whether access to path allowed when user password change forced. * * @return * TRUE if the path is allowed, FALSE otherwise. */ function _password_policy_is_path_allowed_when_password_change_forced() { $current_path = $_GET['q']; $excluded_paths = _password_policy_get_excluded_page_paths(); $password_change_paths = _password_policy_get_password_change_paths(); $allowed_paths = array_merge($excluded_paths, $password_change_paths); return in_array($current_path, $allowed_paths); } /** * Gets paths of pages excluded from redirection to change password. */ function _password_policy_get_excluded_page_paths() { $excluded_paths = variable_get('password_policy_exclude_pages', 'logout'); $excluded_paths = explode("\n", $excluded_paths); return array_map('trim', $excluded_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. * * 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_go_to_password_change_page() { $password_change_paths = _password_policy_get_password_change_paths(); $preferred_password_change_path = $password_change_paths[0]; $destination = drupal_get_destination(); unset($_GET['destination']); drupal_goto($preferred_password_change_path, $destination); } /** * Gets paths that allow user to change their password. */ function _password_policy_get_password_change_paths() { global $user; $custom_path = _password_policy_get_custom_password_change_path(); if (isset($custom_path)) { return array($custom_path); } else { return array( "user/{$user->uid}/edit/account", "user/{$user->uid}/edit", ); } } /** * Gets custom password change path. * * This is used in particular by the Password Policy Password Tab module to set * an alternate password change page path. * * @return * The custom path if a custom path is set, NULL otherwise. */ function _password_policy_get_custom_password_change_path() { global $user; $subpath = variable_get('password_policy_change_url', NULL); if (isset($subpath)) { return "user/{$user->uid}/edit/$subpath"; } else { return NULL; } } /** * Implements hook_theme(). */ function password_policy_theme() { return array( 'password_policy_admin_list' => array( 'arguments' => array('form' => NULL), 'file' => 'password_policy.theme.inc', ), ); } /** * Implements hook_perm(). */ function password_policy_perm() { return array('unblock expired accounts', 'force password change'); } /** * Implements hook_menu(). */ function password_policy_menu() { $items['admin/settings/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 site configuration'), 'file' => 'password_policy.admin.inc', ); $items['admin/settings/password_policy/configure'] = array( 'title' => 'Settings', 'type' => MENU_DEFAULT_LOCAL_TASK, ); $items['admin/settings/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 site configuration'), 'weight' => 1, 'file' => 'password_policy.admin.inc', ); $items['admin/settings/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 site configuration'), 'weight' => 2, 'file' => 'password_policy.admin.inc', ); $items['admin/settings/password_policy/%password_policy_policy'] = array( 'title callback' => 'password_policy_format_title', 'title arguments' => array(3), 'type' => MENU_CALLBACK, 'page callback' => 'password_policy_admin_view', 'page arguments' => array(3), 'access arguments' => array('administer site configuration'), 'file' => 'password_policy.admin.inc', ); $items['admin/settings/password_policy/%password_policy_policy/view'] = array( 'title' => 'View', 'type' => MENU_DEFAULT_LOCAL_TASK, ); $items['admin/settings/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', 3), 'access arguments' => array('administer site configuration'), 'weight' => 1, 'file' => 'password_policy.admin.inc', ); $items['admin/settings/password_policy/delete'] = array( 'title' => 'Delete', 'type' => MENU_CALLBACK, 'page callback' => 'drupal_get_form', 'page arguments' => array('password_policy_admin_delete'), 'access arguments' => array('administer site configuration'), 'file' => 'password_policy.admin.inc', ); $items['admin/settings/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/user/expired'] = array( 'title' => 'Expired accounts', '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/user/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; } /** * Load policy array from the database. * * @param $pid * The policy id * * @return * A populated policy array or NULL if not found. */ function password_policy_policy_load($pid) { static $policies = array(); if (is_numeric($pid)) { if (isset($policies[$pid])) { return $policies[$pid]; } else { $policy = _password_policy_load_policy_by_pid($pid); if ($policy) { $policies[$pid] = $policy; return $policy; } } } return FALSE; } /** * Load user object from the database. * * @param $id * The user id * * @return * A populated user object or NULL if not found. */ function password_policy_uid_load($id) { if (is_numeric($id)) { $account = user_load(array('uid' => $id)); if ($account) { return $account; } } return FALSE; } /** * Display a password policy form title. * * @param $policy * Policy array * * @return * A policy's title string */ function password_policy_format_title($policy) { return $policy['name']; } /** * Implements hook_user(). */ function password_policy_user($op, &$edit, &$account, $category = NULL) { global $user; switch ($op) { case 'load': $account->force_password_change = _password_policy_is_password_change_forced($account->uid); break; case 'validate': global $user; if (isset($account->uid) && $account->force_password_change == 1) { // Admins can edit accounts without having to reset passwords. if ($edit['pass'] == '' && $user->uid == $account->uid) { form_set_error('pass', t('Your password has expired. You must change your password to proceed on the site.')); } } break; case 'insert': $force = isset($edit['force_password_change']) ? $edit['force_password_change'] : variable_get('password_policy_new_login_change', 0); db_query('INSERT INTO {password_policy_force_change} VALUES(%d, %d)', $account->uid, $force); if (array_key_exists('pass', $edit)) { // 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']); } } break; case 'update': if (!empty($account->force_password_change) && array_key_exists('pass', $edit) && $user->uid == $account->uid) { db_query('UPDATE {password_policy_force_change} SET force_change = 0 WHERE uid = %d', $account->uid); } elseif (!empty($edit['force_password_change'])) { db_query('UPDATE {password_policy_force_change} SET force_change = 1 WHERE uid = %d', $account->uid); 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))); } watchdog('password policy', '@user flagged to change password on next login by @admin', array('@user' => $account->name, '@admin' => $user->name), WATCHDOG_NOTICE); } elseif (isset($edit['force_password_change'])) { db_query('UPDATE {password_policy_force_change} SET force_change = 0 WHERE uid = %d', $account->uid); } if (isset($edit['status']) && $edit['status'] != $account->status && $edit['status'] == 1) { // Account is being unblocked. db_query('UPDATE {password_policy_expiration} SET unblocked = %d WHERE uid = %d', time(), $account->uid); } break; case 'after_update': // Store hash of newly set password if (array_key_exists('pass', $edit)) { _password_policy_store_password($account->uid, $edit['pass']); } break; case 'login': $roles = is_array($account->roles) ? array_keys($account->roles) : array(); $policy = _password_policy_load_active_policy($roles); // A value $edit['name'] is NULL for a one time login if ($policy && ((!empty($account->uid) && $account->uid > 1) || variable_get('password_policy_admin', 0)) && !empty($edit['name'])) { // Calculate expiration and warning times. $expiration = $policy['expiration']; $warning = 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. $result = db_query_range("SELECT * FROM {password_policy_history} WHERE uid = %d ORDER BY created DESC", $account->uid, 0, 1); if ($row = db_fetch_object($result)) { $last_change = $row->created; } else { // User has not changed their password since this module was // enabled. $last_change = $account->created; } $time = time(); if ($time > max($policy_start, $last_change) + $expiration_seconds) { if (variable_get('password_policy_block', 0) == 0) { // User is blocked immediately and cannot change his password // after expiration. _password_policy_block_account($account); } else { // Redirect user and let password force change handle. db_query('UPDATE {password_policy_force_change} SET force_change=%d WHERE uid=%d', 1, $account->uid); _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.')); $destination = drupal_get_destination(); unset($_REQUEST['destination']); drupal_goto('user/' . $account->uid . '/edit' . (module_exists('password_policy_password_tab') ? '/password' : ''), $destination); } } } break; case 'delete': db_query("DELETE FROM {password_policy_history} WHERE uid = %d", $account->uid); db_query("DELETE FROM {password_policy_expiration} WHERE uid = %d", $account->uid); db_query("DELETE FROM {password_policy_force_change} WHERE uid = %d", $account->uid); break; case 'register': case 'form': // Force Password Change on user account. if (($category == 'account' || $op == 'register') && user_access('force password change')) { if ($category == 'account') { $force_change = db_result(db_query_range('SELECT force_change FROM {password_policy_force_change} WHERE uid=%d', $account->uid, 0, 1)); } else { $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 ); return $form; } break; } } /** * Implements hook_form_alter(). */ function password_policy_form_alter(&$form, $form_state, $form_id) { switch ($form_id) { case "user_profile_form": case "user_register": // Password change form. $uid = isset($form['#uid']) ? $form['#uid'] : NULL; //if ($uid == 1 && !variable_get('password_policy_admin', 0)) { break; } $roles = isset($form['_account']['#value']) ? array_keys($form['_account']['#value']->roles) : array(); $policy = _password_policy_load_active_policy($roles); $translate = array(); if (!empty($policy['policy'])) { // Some policy constraints are active. password_policy_add_policy_js($policy, $uid); foreach ($policy['policy'] as $key => $value) { $translate['constraint_' . $key] = _password_policy_constraint_error($key, $value); } } // Printing out the restrictions. if (variable_get('password_policy_show_restrictions', 0) && !empty($translate)) { $restriction_html = '
' . theme('item_list', $translate, t('Password Requirements')) . '
'; 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'; break; case 'password_policy_password_tab': $form['submit']['#weight'] = 10; break; } } /** * Implements hook_cron(). */ function password_policy_cron() { // Short circuit if no policies are active that use expiration. if (!db_result(db_query("SELECT COUNT(*) FROM {password_policy} WHERE enabled = 1 AND expiration > 0"))) { return; } // Get all users' last password change time. We don't touch blocked accounts $result = db_query(' SELECT u.uid AS uid, u.created AS created_u, p.created AS created_p, e.pid AS pid, e.warning AS warning, e.unblocked AS unblocked FROM {users} u LEFT JOIN {password_policy_history} p ON u.uid = p.uid LEFT JOIN {password_policy_expiration} e ON u.uid = e.uid WHERE u.uid > 0 AND u.status = 1 ORDER BY p.created ASC '); while ($row = db_fetch_object($result)) { if ($row->uid == 1 && !variable_get('password_policy_admin', 0)) continue; // Use account creation timestamp if there is no entry in password history // table. $accounts[$row->uid] = empty($row->created_p) ? $row->created_u : $row->created_p; // 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; // The user was last time unblocked (if was). We don't block this account // again for some period of time. $unblocks[$row->uid] = $row->unblocked; // The user was last time unblocked (if was). We don't block this account // again for some period of time. $pids[$row->uid] = $row->pid; } if ($accounts) { foreach ($accounts as $uid => $last_change) { /* Alternative: $result = db_query("SELECT p.* FROM {password_policy} p INNER JOIN {password_policy_role} r ON p.pid = r.pid INNER JOIN {users_roles} u ON r.rid = u.rid WHERE p.enabled = 1 AND u.uid = %d ORDER BY p.weight LIMIT 1", $uid); */ $roles = array(DRUPAL_AUTHENTICATED_RID); $result = db_query("SELECT rid FROM {users_roles} WHERE uid = %d ORDER BY rid", $uid); while ($row = db_fetch_object($result)) { $roles[] = $row->rid; } $policy = _password_policy_load_active_policy($roles); 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 = 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(array('uid' => $uid)); $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_query("UPDATE {password_policy_expiration} SET warning = %d WHERE uid = %d", $time, $uid); } else { db_query("INSERT INTO {password_policy_expiration} (uid, warning) VALUES (%d, %d)", $uid, $time); } } } } 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_query("UPDATE {users} SET status = 0 WHERE uid = %d", $uid); if ($pids[$uid]) { db_query("UPDATE {password_policy_expiration} SET blocked = %d WHERE uid = %d", $time, $uid); } else { db_query("INSERT INTO {password_policy_expiration} (uid, blocked) VALUES (%d, %d)", $uid, $time); } $account = user_load(array('uid' => $uid)); 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 = password_policy_mail_tokens($params, $language); $message['subject'] .= _password_policy_mail_text($key . '_subject', $language, $variables); $message['body'][] = _password_policy_mail_text($key . '_body', $language, $variables); } /** * Implements hook_simpletest(). */ function password_policy_simpletest() { // Scan through mymodule/tests directory for any .test files to tell // SimpleTest module. $tests = file_scan_directory(drupal_get_path('module', 'password_policy') . '/tests', '\.test'); return array_keys($tests); } /****************************************************************************/ /* FAPI */ /****************************************************************************/ /** * Password save validate handler. */ function password_policy_password_validate($form, &$form_state) { $values = $form_state['values']; $account = isset($form['_account']['#value']) ? $form['_account']['#value'] : (object)array('uid' => 0); if (!empty($values['pass']) && !isset($values['auth_openid'])) { $error = _password_policy_constraint_validate($values['pass'], $account); if ($error) { form_set_error('pass', t('Your password has not met the following requirement(s):') . ''); } } } /****************************************************************************/ /* 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')); $result = pager_query("SELECT p.*, u.name FROM {password_policy_expiration} p INNER JOIN {users} u ON p.uid = u.uid WHERE p.blocked > 0" . tablesort_sql($header), PASSWORD_POLICY_ENTRIES_PER_PAGE, 0, NULL); while ($row = db_fetch_object($result)) { $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/user/expired/unblock/' . $row->uid, array('query' => array('destination' => 'admin/user/expired'))) : ''; } if (!isset($entry)) { $colspan = '4'; $entry[] = array(array('data' => t('No entries'), 'colspan' => $colspan)); } $page = theme('table', $header, $entry); $page .= theme('pager', NULL, PASSWORD_POLICY_ENTRIES_PER_PAGE, 0); return $page; } /** * Confirm unblocking the expired account. */ function password_policy_expired_unblock_confirm($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/user/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) { // Unblock the user _password_policy_unblock($form_state['values']['account']); drupal_goto('admin/user/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()) { $langcode = isset($language) ? $language->language : NULL; if ($admin_setting = variable_get('password_policy_' . $key, FALSE)) { // An admin setting overrides the default string. return strtr($admin_setting, $variables); } else { // No override, return with default strings. switch ($key) { case 'warning_subject': return t('Password expiration warning for !username at !site', $variables, $langcode); case 'warning_body': return t("!username,\n\nYour password at !site will expire in less than !days_left day(s).\n\nPlease go to !edit_uri to change your password.", $variables, $langcode); } } } /** * Return an array of token to value mappings for user e-mail messages. * * @param $params * Structured array with the parameters. * @param $language * Language object to generate the tokens with. * * @return * Array of mappings from token names to values (for use with strtr()). */ function password_policy_mail_tokens($params, $language) { global $base_url; $account = $params['account']; $tokens = array( '!username' => $account->name, '!mailto' => $account->mail, '!site' => variable_get('site_name', 'SuiteDesk'), '!uri' => $base_url, '!uri_brief' => drupal_substr($base_url, drupal_strlen('http://')), '!date' => format_date(time(), 'medium', '', NULL, $language->language), '!login_uri' => url('user', array('absolute' => TRUE)), '!edit_uri' => url('user/' . $account->uid . '/edit' . (module_exists('password_policy_password_tab') ? '/password' : ''), array('absolute' => TRUE)), '!days_left' => isset($params['days_left']) ? $params['days_left'] : NULL, '!login_url' => isset($params['login_url']) ? $params['login_url'] : NULL, ); return $tokens; } /****************************************************************************/ /* Constraints API */ /****************************************************************************/ /** * Validates user password. Returns NULL on success or array with error messages * from the constraints on failure. * * @param $pass * Clear text password. * @param &$account * Populated user object. * * @return * NULL or array with error messages. */ function _password_policy_constraint_validate($pass, &$account) { _password_policy_constraints(); $error = NULL; $roles = (isset($account->roles) && is_array($account->roles)) ? array_keys($account->roles) : array(); $policy = _password_policy_load_active_policy($roles); if (!empty($policy['policy'])) { foreach ($policy['policy'] as $key => $value) { if (!call_user_func('password_policy_constraint_' . $key . '_validate', $pass, $value, $account->uid)) { $error[] = call_user_func('password_policy_constraint_' . $key . '_error', $value); } } } return $error; } /** * Gets the constraint's name and description. * * @param $name * Name of the constraint. * * @return * 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 $name * Name of the constraint. * @param $constraint * Constraint value. * * @return * Error message. */ function _password_policy_constraint_error($name, $constraint) { _password_policy_constraints(); return call_user_func('password_policy_constraint_' . $name . '_error', $constraint); } /** * Gets the javascript code from the constraint to be added to the password * validation. * * @param $name * Name of the constraint. * @param $constraint * Constraint value. * @param $uid * User's id. * * @return * Javascript code snippet for the constraint. */ function _password_policy_constraint_js($name, $constraint, $uid) { _password_policy_constraints(); if (function_exists('password_policy_constraint_' . $name . '_js')) { return call_user_func('password_policy_constraint_' . $name . '_js', $constraint, $uid); } } /****************************************************************************/ /* Auxiliary functions */ /****************************************************************************/ /** * Load 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->filename)) { include_once($file->filename); $_password_policy[] = drupal_substr($file->name, 11); } } } return $_password_policy; } /** * Loads the policy with the specified id. * * @param $pid * The policy id. * * @return * A policy array, or NULL if no policy was found. */ function _password_policy_load_policy_by_pid($pid) { $result = db_query('SELECT * FROM {password_policy} WHERE pid = %d', $pid); $row = db_fetch_array($result); if (is_array($row)) { // fetch and unserialize the serialized policy $row['policy'] = unserialize($row['policy']); // Fetch roles $row['roles'] = array(); $result = db_query('SELECT rid FROM {password_policy_role} WHERE pid = %d', $pid); while ($role = db_fetch_object($result)) { $row['roles'][$role->rid] = $role->rid; } return $row; } return NULL; } /** * Loads the first enabled policy that matches the specified roles. * * @param $roles * An array of role IDs. * * @return * A policy array, or NULL if no active policy exists. */ function _password_policy_load_active_policy($roles) { static $cache = array(); if (empty($roles)) { $roles = array(DRUPAL_AUTHENTICATED_RID); } $key = implode(',', $roles); // Use array_key_exists() instead of isset() as NULLs may be in the array. if (!array_key_exists($key, $cache)) { $result = db_query('SELECT p.* FROM {password_policy} p INNER JOIN {password_policy_role} r ON p.pid = r.pid WHERE p.enabled = 1 AND r.rid IN (' . db_placeholders($roles) . ') ORDER BY p.weight LIMIT 1', $roles); $row = db_fetch_array($result); if (is_array($row)) { // fetch and unserialize the serialized policy $row['policy'] = unserialize($row['policy']); $cache[$key] = $row; } else { $cache[$key] = NULL; } } return $cache[$key]; } /** * Stores user password hash. * * @param $uid * User id. * @param $pass * Clear text password. */ function _password_policy_store_password($uid, $pass) { if (module_exists('phpass')) { // Get password hash already saved by phpass. This is only certain to work // if this module's weight is less than that of phpass, as it should be. $hash = db_result(db_query("SELECT pass FROM {users} WHERE uid = %d", $uid)); } else { $hash = md5($pass); } db_query("INSERT INTO {password_policy_history} (uid, pass, created) VALUES (%d, '%s', %d)", $uid, $hash, time()); } /** * Block the expired account. * * @param $account * User object. */ function _password_policy_block_account($account) { if ($account->uid > 1) { // We never block the superuser account. db_query("UPDATE {users} SET status = 0 WHERE uid = %d", $account->uid); if (db_result(db_query("SELECT pid FROM {password_policy_expiration} WHERE uid = %d", $account->uid))) { db_query("UPDATE {password_policy_expiration} SET blocked = %d WHERE uid = %d", time(), $account->uid); } else { db_query("INSERT INTO {password_policy_expiration} (uid, blocked) VALUES (%d, %d)", $account->uid, time()); } watchdog('password_policy', 'Password for user %name has expired.', array('%name' => $account->name), WATCHDOG_NOTICE, l(t('edit'), 'user/' . $account->uid . '/edit')); include_once(drupal_get_path('module', 'user') . '/user.pages.inc'); user_logout(); } } /** * Unblocks the expired account. * * @param $account * User object. */ function _password_policy_unblock($account) { // Check if user was blocked via this module. $pp_blocked = db_result(db_query( 'SELECT pid FROM {password_policy_expiration} ppe WHERE blocked <> 0 AND unblocked IS NULL AND uid = %d', $account->uid )); if ($pp_blocked) { db_query('UPDATE {password_policy_expiration} ppe SET unblocked = %d WHERE uid = %d AND unblocked IS NULL', time(), $account->uid); // Unblock the user. user_save($account, array('status' => 1)); drupal_set_message(t('The user %name has been unblocked.', array('%name' => $account->name))); } } /** * Add password policy JS * * @param $policy * A policy array. * @param $uid * A user ID for which the policy is applied. */ function password_policy_add_policy_js($policy, $uid) { // Print out the javascript which checks the strength of the password. // It overwrites the default core javascript function. drupal_add_js(drupal_get_path('module', 'password_policy') . '/password_policy.js', 'module'); $s = "/**\n"; $s .= " * Evaluate the strength of a user's password.\n"; $s .= " *\n"; $s .= " * Returns the estimated strength and the relevant output message.\n"; $s .= " */\n"; $s .= "Drupal.evaluatePasswordStrength = function(value) {\n"; $s .= " var strength = \"high\", msg = [], translate = Drupal.settings.password_policy;\n"; $s .= " var trimmedSpaces = /^\s+|\s+$/.test(value);\n"; $s .= " if (/^\s+$/.test(value)) {\n"; $s .= " return { strength: \"low\", message: translate.allSpaces };\n"; $s .= " }\n"; $s .= " value = value.replace(/^\s+|\s+$/g,'')\n"; // Print out each constraint's javascript password strength evaluation. foreach ($policy['policy'] as $key => $value) { $s .= _password_policy_constraint_js($key, $value, $uid); // Constraints' error messages are used in javascript. $translate['constraint_' . $key] = _password_policy_constraint_error($key, $value); } $s .= " msg = msg.length > 0 ? translate.needsMoreVariation +\"\" : \"\";\n"; $s .= " if (trimmedSpaces && strength != \"high\") {\n"; $s .= " msg = msg.concat(translate.trimmedSpaces);\n"; $s .= " }\n"; $s .= " return { strength: strength, message: msg };\n"; $s .= "};\n"; drupal_add_js($s, 'inline'); drupal_add_js(array( 'password_policy' => array_merge(array( 'strengthTitle' => t('Password quality:'), 'lowStrength' => t('Bad'), 'mediumStrength' => t('Good'), 'highStrength' => t('Good'), '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.'), 'confirmSuccess' => t('Yes'), 'confirmFailure' => t('No'), 'confirmTitle' => t('Passwords match:')), $translate)), 'setting'); }