You are here

password_policy.module in Password Policy 5

File

password_policy.module
View source
<?php

/**
 * Help text for the password policy module.
 */
function password_policy_help($section = '') {
  $output = '';
  switch ($section) {
    case 'admin/user/password_policy':
      $output = '<p>' . t('The password policy module allows you to enforce a specific level of password complexity for the user passwords on the system.') . '</p>';
      if (password_policy_get_policy_count() != 0) {
        $output .= '<p>' . t('Listed below are the currently defined password policies. If no policy is set as the default, then any password will be accepted by the system (the Drupal default).') . '</p><p>' . t('To set a new default password policy, select the policy below and click the ') . '<em>' . t('Set default policy') . '</em>' . t(' button.');
      }
      else {
        $output .= '<p>' . t('No policies are currently defined. To add a new policy, click <a href="@url">add policy</a>.', array(
          "@url" => url('admin/user/password_policy/add'),
        ));
      }
      break;
    case 'admin/user/password_policy/add':
    case 'admin/user/password_policy/edit/' . arg(3):
      $output = '<p>';
      if (arg(2) == 'add') {
        $output .= t('Give a name and descriptive comment to your new password policy. ');
      }
      $output .= t("A specific level of required password complexity can be achieved by adding minimum requirements for the constraints listed below. ") . t("If no minimum requirements are specified for a constraint, then that constraint will be ignored. ") . t("Only the constraints given values will be used.") . '</p><p>' . t("Please note that it is very easy to specify a set of constraints which can NEVER be satisfied (eg. min length = 3, min uppercase = 3, min lowercase = 3).") . t("This module can not determine these situations automatically, so be careful during the definition of your policy.") . '</p>';
      break;
    case 'admin/user/password_policy/list_expired':
      $output = '<p>' . t('List of accounts which passwords have expired.') . '</p>';
  }
  return $output;
}

/**
 * Permissions for the password policy module.
 */
function password_policy_perm() {
  return array(
    'administer password policies',
  );
}

/**
 * Implementation of hook_menu().
 */
function password_policy_menu($may_cache) {
  $items = array();
  if (!$may_cache) {
    $items[] = array(
      'path' => 'admin/settings/password_policy',
      'title' => t('Password policy'),
      'description' => t('Configures policies for user account passwords.'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'password_policy_admin_settings',
      ),
      'access' => user_access('administer site configuration'),
    );
  }
  $items[] = array(
    'path' => 'admin/user/password_policy',
    'title' => t('Password policy'),
    'description' => t('Configures policies for user account passwords.'),
    'callback' => 'password_policy_view',
    'access' => user_access('administer password policies'),
  );
  $items[] = array(
    'path' => 'admin/user/password_policy/add',
    'title' => t('Add policy'),
    'callback' => 'password_policy_form_policy',
    'access' => user_access('administer password policies'),
    'type' => MENU_LOCAL_TASK,
  );
  $arg4 = arg(4);
  if (!empty($arg4) && is_numeric($arg4)) {
    $items[] = array(
      'path' => 'admin/user/password_policy/edit/' . arg(4),
      'title' => t('Edit password policy'),
      'callback' => 'password_policy_form_policy',
      'callback arguments' => array(
        'id' => arg(4),
      ),
      'type' => MENU_CALLBACK,
      'access' => user_access('administer password policies'),
    );
    $items[] = array(
      'path' => 'admin/user/password_policy/delete/' . arg(4),
      'title' => t('Delete password policy'),
      'callback' => 'password_policy_delete',
      'callback arguments' => array(
        'id' => arg(4),
      ),
      'type' => MENU_CALLBACK,
      'access' => user_access('administer password policies'),
    );
  }

  // we display the name of the policy when viewing
  $arg3 = arg(3);
  if (!empty($arg3) && is_numeric($arg3)) {
    $policy = password_policy_load_policy_by_id(arg(3));
    $items[] = array(
      'path' => 'admin/user/password_policy/' . arg(3),
      'title' => $policy->name,
      'callback' => 'password_policy_view',
      'callback arguments' => array(
        'id' => arg(3),
      ),
      'type' => MENU_CALLBACK,
      'access' => user_access('administer password policies'),
    );
  }
  $items[] = array(
    'path' => 'admin/user/password_policy/list',
    'title' => t('List'),
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $items[] = array(
    'path' => 'admin/user/password_policy/list_expired',
    'title' => t('Expired accounts'),
    'callback' => 'password_policy_list_expired',
    'access' => user_access('administer password policies'),
    'type' => MENU_LOCAL_TASK,
  );
  $items[] = array(
    'path' => 'admin/user/password_policy/unblock/' . arg(4),
    'title' => t('Unblock'),
    'callback' => 'password_policy_unblock',
    'callback arguments' => array(
      'id' => arg(4),
    ),
    'type' => MENU_CALLBACK,
    'access' => user_access('administer password policies'),
  );
  return $items;
}

/**
 * Submit hook for the form on the default view for the password policy module.  From the
 * default view, the user can set a new default password policy or clear the default so
 * that no policy is active and the default drupal password mechanism takes affect.
 */
function password_policy_view_form_submit($form_id, $form_values) {
  $op = !empty($form_values['op']) ? $form_values['op'] : '';
  if ($op == t('Clear default')) {
    _password_policy_clear_default();
    drupal_set_message(t('No policy is active, all user passwords will be accepted (Drupal default).'));
  }
  else {
    if ($op == t('Set default policy')) {
      $pid = $form_values['default'];
      if ($pid) {
        $policy = password_policy_load_policy_by_id($pid);
        if ($policy) {
          _password_policy_clear_default();
          $time = time();
          db_query("UPDATE {password_policy} SET enabled = %d, created = %d WHERE id = %d", 1, $time, $pid);
          drupal_set_message(t('\'%name\' has been set as the default password policy.', array(
            '%name' => $policy->name,
          )));
        }
      }
    }
  }
}

/**
 * Resets the enabled flag for all policies in the database to 0.
 *
 */
function _password_policy_clear_default() {
  db_query("UPDATE {password_policy} SET enabled = 0");
}

/**
 * The default view for the password policy module.
 */
function password_policy_view($pid = NULL) {

  // If we have a pid, then display the details for that policy, else
  // display all policies.
  if ($pid) {
    $policy = password_policy_load_policy_by_id($pid);
    if (!$policy) {
      drupal_goto('admin/user/password_policy');
    }
    $edit_url = l(t('editing this policy'), 'admin/user/password_policy/edit/' . $pid);
    $constraints = $policy->constraints;
    $desc = !$constraints ? t('This policy has no constraints set.  You can add constraints by ') . $edit_url . '.' : t('This policy has the constraints listed below.  You can change the constraints by ') . $edit_url . '.<br />' . $policy
      ->getValidationErrorMessage();
    $output = "<p>{$desc}</p>";
    $expiration = $policy->expiration;
    $desc = $expiration > 0 ? t('The passwords expire after %number %days.', array(
      '%number' => $expiration,
      '%days' => format_plural($expiration, t('day'), t('days')),
    )) : t('The passwords never expire.');
    $output .= "<p>{$desc}</p>";
    return $output;
  }

  // load the summary policies (id->name)
  $summaries = _password_policy_load_policy_summaries();
  if ($summaries) {
    return drupal_get_form('password_policy_view_form', $summaries);
  }
  return '';
}
function password_policy_view_form($summaries) {
  $form = array();

  //$summaries = _password_policy_load_policy_summaries();
  if ($summaries) {
    foreach ($summaries as $summary) {

      //drupal_set_message('<pre>'.print_r($summary, 1).'</pre>');
      $id = $summary['id'];
      $name = $summary['name'];
      $row = array();
      $options[$id] = '';
      if ($summary['enabled']) {
        $default_id = $id;
        $form[$id]['created'] = array(
          '#value' => format_date($summary['created'], 'custom', 'm/d/y H:i:s'),
        );
      }
      $form[$id]['id'] = array(
        '#value' => $id,
      );
      $form[$id]['name'] = array(
        '#value' => $name,
      );
      $form[$id]['view'] = array(
        '#value' => l(t('view'), 'admin/user/password_policy/' . $id),
      );
      $form[$id]['edit'] = array(
        '#value' => l(t('edit'), 'admin/user/password_policy/edit/' . $id),
      );
      $form[$id]['delete'] = array(
        '#value' => l(t('delete'), 'admin/user/password_policy/delete/' . $id),
      );

      //$options[$id] = $name;

      //. l(t('view'), 'admin/password_policy/'. $id) . l(t('edit'), 'admin/password_policy/edit/'. $id) . l(t('delete'), 'admin/password_policy/delete/'. $id);
    }
    $form['default'] = array(
      '#type' => 'radios',
      '#options' => $options,
      '#default_value' => $default_id,
    );
    $form['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Set default policy'),
    );
    $form['clear'] = array(
      '#type' => 'submit',
      '#value' => t('Clear default'),
    );

    //drupal_set_message('<pre>'.print_r($form, 1).'</pre>');
    return $form;
  }
}

/**
 * Custom theme for rendering a checkbox list of defined policies.  With Drupal's form
 * API, it can be tricky to get a layout of the form elements which is different from
 * the default.  This layout is based on a similar layout found in the "input formats"
 * module.
 */
function theme_password_policy_view_form($form) {
  foreach ($form as $id => $element) {
    if (!empty($element['edit']) && is_array($element['edit'])) {
      $rows[] = array(
        drupal_render($form['default'][$element['id']['#value']]),
        check_plain($form[$id]['name']['#value']),
        $element['created']['#value'],
        drupal_render($form[$id]['view']),
        drupal_render($form[$id]['edit']),
        drupal_render($form[$id]['delete']),
      );
      unset($form[$id]);
    }
  }
  $header = array(
    t('Default'),
    t('Name'),
    t('Enabled'),
    array(
      'data' => t('Operations'),
      'colspan' => 3,
    ),
  );
  $output = theme('table', $header, $rows);

  //drupal_set_message('<pre>'.print_r($rows, 1).'</pre>');
  $output .= drupal_render($form);
  return $output;
}

/**
 * Confirmation form for the deletion of a password policy.  Deletion takes place
 * in password_policy_delete_submit().
 */
function password_policy_delete() {
  $pid = arg(4);
  if (!$pid) {
    drupal_not_found();
  }
  $policy = password_policy_load_policy_by_id($pid);
  if ($policy) {
    return drupal_get_form('password_policy_delete_confirm', $pid, $policy);
  }
  else {
    drupal_not_found();
  }
}
function password_policy_delete_confirm($pid, $policy) {
  $form = array();
  $form['pid'] = array(
    '#type' => 'hidden',
    '#value' => $pid,
  );
  $form['name'] = array(
    '#type' => 'hidden',
    '#value' => $policy->name,
  );
  $description = count($policy->constraints) ? t('This policy has the following constraints:') . '<br />' . $policy
    ->getValidationErrorMessage() : t('There are no constraints specified for this policy.');
  return confirm_form($form, t('Are you sure you want to delete the policy \'%name\'?', array(
    '%name' => $policy->name,
  )), 'admin/user/password_policy', $description, t('Delete'));
}

/**
 * Submit hook for the delete policy operation.
 */
function password_policy_delete_confirm_submit($form_id, $form_values) {
  $pid = $form_values['pid'];
  $policy = password_policy_load_policy_by_id($pid);
  db_query("DELETE FROM {password_policy} WHERE id = %d", $pid);
  if (db_affected_rows()) {
    drupal_set_message(t('Password policy \'%policy\' was deleted.', array(
      '%policy' => $policy->name,
    )));
    watchdog('password_policy', t('Policy \'%name\' was deleted.', array(
      '%name' => $policy->name,
    )), WATCHDOG_NOTICE);
  }
  return 'admin/user/password_policy';
}

/**
 * Returns an array of constraint instances which the user should be able to
 * have access to in their password policy.  Only the ones which the user
 * sets a value for will be used. See password_policy_form_policy_submit().
 *
 * @return unknown
 */
function _password_policy_get_valid_constraints() {

  // NOTE: TO ADD A NEW CONSTRAINT
  // Create a new constraint object defined in a constraint_XXX.php file
  // in the constraints directory.  The object should extend the base
  // Constraint object class.  Then add a valid instance to the array
  // below.
  // TODO the constraint objects and the way the UI work only permit
  // one parameter to be passed to the constraint object, which
  // in all cases so far is an integer value representing the minimum
  // number of X that the password is being constrained to.  The values for
  // the constructors do not matter as long as they are valid since they will
  // be overwritten on the form submit.  Ideally we should have a separate
  // UI for each constraint type which can then allow for more complex
  // parameterization of the constraint objects, but so far this hasn't
  // been needed.
  _password_policy_load_constraint_definitions();
  return array(
    new Length_Constraint(1),
    new Letter_Constraint(1),
    new Digit_Constraint(1),
    new Letter_Digit_Constraint(1),
    new Digit_Placement_Constraint(1),
    new Lowercase_Constraint(1),
    new Uppercase_Constraint(1),
    new Punctuation_Constraint(1),
    new History_Constraint(),
    new Character_Types_Constraint(1),
    new Delay_Constraint(1),
    new Username_Constraint(),
  );
}

/**
 * Form display for new or to be edited password policies.
 */
function password_policy_form_policy($pid = NULL) {
  return drupal_get_form('password_policy_form_policy_form', $pid);
}
function password_policy_form_policy_form($pid) {
  $form = array();
  if ($pid) {
    $policy = password_policy_load_policy_by_id($pid);
  }
  $form['general'] = array(
    '#type' => 'fieldset',
    '#title' => t('General Settings'),
    '#collapsible' => FALSE,
    '#collapsed' => FALSE,
  );
  $form['general']['name'] = array(
    '#type' => 'textfield',
    '#title' => t('Name'),
    '#default_value' => $policy->name,
    '#maxlength' => 64,
    '#required' => TRUE,
  );
  $form['general']['description'] = array(
    '#type' => 'textarea',
    '#title' => t('Description'),
    '#default_value' => $policy->description,
  );
  $form['general']['expiration'] = array(
    '#type' => 'textfield',
    '#title' => t('Password Expiration'),
    '#default_value' => $policy->expiration,
    '#size' => 5,
    '#maxlength' => 5,
    '#description' => t('The passwords will expire after this number of days. The users with expired passwords will be blocked. Leaving this field empty won\'t put any password expiration constraints.'),
  );
  $form['general']['warning'] = array(
    '#type' => 'textfield',
    '#title' => t('Password Expiration Warning'),
    '#default_value' => $policy->warning,
    '#size' => 10,
    '#description' => t('The comma separated list of days. The warning about expiration of the password will be sent out on those days before the expiration. Leaving this field empty won\'t send out or display any warnings.'),
  );
  $form['constraints'] = array(
    '#type' => 'fieldset',
    '#title' => t('Password Constraints'),
    '#collapsible' => FALSE,
    '#collapsed' => FALSE,
  );
  $count = 1;
  foreach (_password_policy_get_valid_constraints() as $constraint) {
    $default_value = NULL;

    // if we are editing, then see if this constraint already has a value,
    // if so, then set that as the default value for the form
    if ($policy) {
      foreach ($policy->constraints as $policy_constraint) {
        if (get_class($policy_constraint) == get_class($constraint)) {
          $default_value = $policy_constraint
            ->getMinimumConstraintValue();
        }
      }
    }
    $form['constraints'][get_class($constraint)] = array(
      '#type' => 'textfield',
      '#size' => 5,
      '#default_value' => $default_value,
      '#maxlength' => 2,
      '#title' => $constraint
        ->getName(),
      '#description' => $constraint
        ->getDescription(),
    );
    $count++;
  }
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => $policy ? t('Edit policy') : t('Create policy'),
  );
  if ($policy) {
    $form['pid'] = array(
      '#type' => 'hidden',
      '#value' => $pid,
    );
  }
  return $form;
}

/**
 * Form submission hook for new or edited password policies.
 */
function password_policy_form_policy_form_submit($form_id, $form_values) {

  // create the policy
  _password_policy_load_constraint_definitions();

  // Here we build/re-build a new password policy using an
  // And_Constraint object instance.  We add a constraint for
  // each type where the user entered a number for the minimum value.
  $policy = new And_Constraint();
  $policy
    ->setName($form_values['name']);
  $policy
    ->setDescription($form_values['description']);
  $policy
    ->setExpiration(trim($form_values['expiration']));
  $policy
    ->setWarning(str_replace(' ', '', $form_values['warning']));
  foreach ($form_values as $key => $value) {

    // if we have no form value, then we have no constraint to set.
    $value = trim($value);
    if ($value != '' && preg_match("/^.*constraint\$/i", $key)) {
      $class_name = $key;
      if (class_exists($class_name)) {
        $constraint = new $class_name($value);
        $constraint->minimumConstraintValue = $value;
        $policy
          ->addConstraint($constraint);
      }
    }
  }

  // if we have an id, update, else save.
  if ($form_values['pid']) {
    password_policy_update_policy($form_values['pid'], $policy);
    drupal_set_message(t('Policy \'%name\' has been updated.', array(
      '%name' => $policy->name,
    )));
    watchdog('password_policy', t('Policy \'%name\' updated.', array(
      '%name' => $policy->name,
    )), WATCHDOG_NOTICE, l(t('view'), 'admin/user/password_policy/' . $form_values['pid']));
  }
  else {
    password_policy_save_policy($policy);
    watchdog('password_policy', t('New policy \'%name\' added.', array(
      '%name' => $policy->name,
    )), WATCHDOG_NOTICE, l(t('view'), 'admin/user/password_policy'));
  }
  return "admin/user/password_policy";
}

/**
 * The implementation of hook_user().  Used to trap the validation step so
 * we can test any currently enabled password policies.
 *
 */
function password_policy_user($type, &$edit, &$user, $category = NULL) {
  if ($category == 'account' && !empty($edit['pass'])) {
    if ($type == 'validate') {
      $constraint = password_policy_load_active_policy();
      if ($constraint && $constraint
        ->validate($edit['pass'], $user) == FALSE) {
        form_set_error('pass', t('Your password must meet the following requirements:') . $constraint
          ->getValidationErrorMessage($edit['pass'], $user));
      }
      else {

        // as long as the password policy module is enabled, we will track the hashed password values which
        // can then be used in the history constraint.
        if ($user->uid) {
          _password_policy_store_password($user->uid, $edit['pass']);
        }

        // if user successfully changed his password we will unblock the account
        db_query("UPDATE {users} SET status = 1 WHERE uid = %d", $user->uid);
        db_query("DELETE FROM {password_policy_expiration} WHERE uid = %d", $user->uid);
      }
    }
    else {
      if ($type == 'insert' && !empty($edit['pass'])) {

        // new users will not yet have a uid during the validation step, but they will at this
        // insert step.  Here we store record their first password in the system for use
        // with the history constraint (if used).
        if ($user->uid) {
          _password_policy_store_password($user->uid, $edit['pass']);
        }
      }
    }
  }
  if ($type == 'login') {
    $constraint = password_policy_load_active_policy();

    // $edit['name'] is NULL for a one time login
    if ($constraint && ($user->uid > 1 || variable_get('password_policy_admin', false)) && !empty($edit['name'])) {
      $expiration = $constraint
        ->getExpiration();
      $warning = max(explode(',', $constraint
        ->getWarning()));
      $expiration_seconds = $expiration * 60 * 60 * 24;
      $warning_seconds = $warning * 60 * 60 * 24;
      $policy_enabled = _password_policy_enabled($expiration_seconds);
    }
    if (!empty($expiration)) {
      $result = db_query_range("SELECT * FROM {password_policy_users} WHERE uid = %d ORDER BY created DESC", $user->uid, 0, 1);
      if ($row = db_fetch_object($result)) {
        $last_change = $row->created;
      }
      else {

        // user has not changed his pwd after this module had been enabled
        $last_change = $user->created;
      }
      $time = time();
      if ($time > max($policy_enabled, $last_change) + $expiration_seconds) {
        db_query("UPDATE {users} SET status = 0 WHERE uid = %d", $user->uid);
        $result = db_query("SELECT * FROM {password_policy_expiration} WHERE uid = %d", $user->uid);
        if ($row = db_fetch_array($result)) {
          db_query("UPDATE {password_policy_expiration} SET blocked = %d WHERE uid = %d", $time, $user->uid);
        }
        else {
          db_query("INSERT INTO {password_policy_expiration} (uid, blocked) VALUES (%d, %d)", $user->uid, $time);
        }
        watchdog('password_policy', t('Password for user %name has expired.', array(
          '%name' => $user->name,
        )), WATCHDOG_NOTICE, l(t('edit'), "user/{$user->uid}/edit"));
        if (variable_get('password_policy_block', 0) == 0) {
          user_logout();
        }
        else {
          drupal_set_message(t('Your password has expired. You have to change it now or you won\'t be able to login again.'), 'error');
          unset($_REQUEST['destination']);
          drupal_goto("user/{$user->uid}/edit");
        }
      }
      elseif ($time > max($policy_enabled, $last_change) + $expiration_seconds - $warning_seconds) {
        $days_left = ceil((max($policy_enabled, $last_change) + $expiration_seconds - $time) / (60 * 60 * 24));
        drupal_set_message(t('Your password will expire in less than %number %days. Please change it.', array(
          '%number' => $days_left,
          '%days' => format_plural($days_left, t('day'), t('days')),
        )));
        unset($_REQUEST['destination']);
        drupal_goto("user/{$user->uid}/edit");
      }
    }
  }
  if ($type == 'delete') {
    db_query("DELETE FROM {password_policy_users} WHERE uid = %d", $user->uid);
    db_query("DELETE FROM {password_policy_expiration} WHERE uid = %d", $user->uid);
  }
}

/**
 * Stores the given password associated with the user with the specified id in the database.  This
 * data is used with the history constraint to prevent users from using a password they have
 * used previously.
 */
function _password_policy_store_password($uid, $pass) {
  db_query("INSERT INTO {password_policy_users} SET uid = %d, pass = '%s', created = %d", $uid, md5($pass), time());
}

/**
 * Returns the number of unique policies defined and stored in the database.
 *
 * @return
 *     the number of defined policies
 */
function password_policy_get_policy_count() {
  $result = db_result(db_query("SELECT COUNT(id) FROM {password_policy}"));
  return $result[0];
}

/**
 * Saves the policy instance in the database as a new policy.
 *
 * @param $policy
 *     The policy object instance (of type And_Constraint) to save.
 */
function password_policy_save_policy($policy) {
  $cid = db_next_id('{password_policy}_id');
  db_query("INSERT INTO {password_policy} SET id = %d, name = '%s', description = '%s', enabled = %d, serialized_policy = '%s'", $cid, $policy->name, $policy->description, 0, serialize($policy));
  drupal_set_message(t('Policy \'%name\' has been updated.', array(
    '%name' => $policy->name,
  )));
}

/**
 * Updates the given policy instance with the given policy id in the database.
 *
 * @param $pid
 *     The id of the policy to update.
 * @param $policy
 *     An object instance of type And_Constraint
 */
function password_policy_update_policy($pid, $policy) {
  db_query("UPDATE {password_policy} SET name = '%s', description = '%s', serialized_policy = '%s' WHERE id = %d", $policy->name, $policy->description, serialize($policy), $pid);
}

/**
 * Returns an array of associative arrays containing the keys id, name, enable and
 * description for all the password policies defined in the database ordered by name.
 *
 * @return
 *     An array of associative arrays.
 */
function _password_policy_load_policy_summaries() {
  $result = db_query('SELECT id, name, enabled, description, created FROM {password_policy} ORDER BY name');
  while ($ary = db_fetch_array($result)) {
    $summaries[] = $ary;
  }
  return $summaries;
}

/**
 * Loads the default (enabled and active) policy or NULL if there
 * are no active policies.
 *
 * @return
 *     An And_Constraint object instance, or NULL if no active policy exists.
 */
function password_policy_load_active_policy() {
  _password_policy_load_constraint_definitions();
  $result = db_query('SELECT * FROM {password_policy} p WHERE p.enabled = 1');
  if (!$result || !db_num_rows($result)) {
    return NULL;
  }
  $values = db_fetch_array($result);

  // fetch and unserialize the serialized policy
  return unserialize($values['serialized_policy']);
}

/**
 * Loads the policy with the specified id or NULL if not found.
 *
 * @param $id
 *     The policy id.
 * @return
 *     An And_Constraint object instance, or NULL if no policy was found.
 */
function password_policy_load_policy_by_id($id) {
  _password_policy_load_constraint_definitions();
  $result = db_query('SELECT * FROM {password_policy} p WHERE p.id = %d', $id);
  if (!$result || !db_num_rows($result)) {
    return NULL;
  }
  $values = db_fetch_array($result);

  // fetch and unserialize the serialized policy
  return unserialize($values['serialized_policy']);
}

/**
 * Loads all the constraint object definitions.
 *
 */
function _password_policy_load_constraint_definitions() {
  $dir = dirname(__FILE__) . '/constraints';
  $constraints = file_scan_directory($dir, '^constraint.*\\.php$');
  foreach ($constraints as $c_file) {
    if (is_file($c_file->filename)) {
      include_once $c_file->filename;
    }
  }
}

/**
 * Implementation of hook_simpletest() for use with the
 * simpletest module.
 */
function password_policy_simpletest() {
  $dir = drupal_get_path('module', 'password_policy');
  $tests = file_scan_directory($dir, '\\.test$');
  return array_keys($tests);
}
function password_policy_admin_settings() {
  $form = array();
  $form['expiration'] = array(
    '#type' => 'fieldset',
    '#title' => t('Expiration Settings'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  $form['expiration']['password_policy_admin'] = array(
    '#type' => 'checkbox',
    '#title' => t('Admin (UID=1) password expires.'),
    '#default_value' => variable_get('password_policy_admin', false),
    '#description' => t('Admin account password will obey expiration policy.'),
  );
  $form['expiration']['password_policy_begin'] = array(
    '#type' => 'radios',
    '#title' => t('Beginning of password expirations'),
    '#default_value' => variable_get('password_policy_begin', 0),
    '#options' => array(
      '0' => t('After expiration time from setting a default policy (all passwords are valid during the expiration time from setting the default policy, and after that older than expiration time passwords expire).'),
      '1' => t('Setting a default policy (passwords older than expiration time expire after setting the default policy, retroactive behaviour).'),
    ),
  );
  $form['expiration']['password_policy_block'] = array(
    '#type' => 'radios',
    '#title' => t('Blocking expired accounts'),
    '#default_value' => variable_get('password_policy_block', 0),
    '#options' => array(
      '0' => t('Expired accounts are blocked. Only administrators can unblock them.'),
      '1' => t('The user with expired account is not blocked, but sent to a change password page. If the password is not changed, the account is blocked and the user cannot login again.'),
    ),
  );

  // E-mail notification settings.
  $form['email'] = array(
    '#type' => 'fieldset',
    '#title' => t('E-mail notification settings'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $form['email']['password_policy_mail_warning_subject'] = array(
    '#type' => 'textfield',
    '#title' => t('Subject of warning e-mail'),
    '#default_value' => _password_policy_mail_text('warning_subject'),
    '#maxlength' => 180,
    '#description' => t('Customize the subject of the warning e-mail message, which is sent to remind of password expiration.') . ' ' . t('Available variables are:') . ' %username, %site, %uri, %uri_brief, %mailto, %date, %login_uri, %edit_uri, %days.',
  );
  $form['email']['password_policy_mail_warning_body'] = array(
    '#type' => 'textarea',
    '#title' => t('Body of warning e-mail'),
    '#default_value' => _password_policy_mail_text('warning_body'),
    '#rows' => 15,
    '#description' => t('Customize the body of the warning e-mail message, which is sent to remind of password expiration.') . ' ' . t('Available variables are:') . ' %username, %site, %uri, %uri_brief, %mailto, %date, %login_uri, %edit_uri, %days.',
  );
  return system_settings_form($form);
}

/**
 * List all expired accounts
 */
function password_policy_list_expired() {
  $header[] = array(
    'data' => t('Blocked'),
    'field' => 'blocked',
    'sort' => 'desc',
  );
  $header[] = array(
    'data' => t('Username'),
    'field' => 'name',
  );
  $header[] = array(
    'data' => t('Unblocked'),
    'field' => 'unblocked',
  );
  $header[] = array(
    'data' => t('Action'),
  );
  $max_pages = 20;
  $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), $max_pages, 0, NULL);
  while ($row = db_fetch_object($result)) {
    $entry[$row->uid]['blocked'] = format_date($row->blocked, 'custom', 'm/d/y H:i:s');
    $entry[$row->uid]['name'] = l($row->name, 'user/' . $row->uid);
    $entry[$row->uid]['unblocked'] = $row->unblocked < $row->blocked ? '' : format_date($row->unblocked, 'custom', 'm/d/y H:i:s');
    $entry[$row->uid]['action'] = $row->unblocked < $row->blocked ? l(t('unblock'), 'admin/user/password_policy/unblock/' . $row->uid) : '';
  }
  if (!isset($entry)) {
    $colspan = '4';
    $entry[] = array(
      array(
        'data' => t('No entries'),
        'colspan' => $colspan,
      ),
    );
  }
  $page = theme_table($header, $entry);
  $page .= theme_pager(array(), $max_pages, 0);
  return $page;
}

/**
 * Unblocks the expired account
 */
function password_policy_unblock($uid = NULL) {
  if ($uid) {
    db_query("UPDATE {users} SET status = 1 WHERE uid = %d", $uid);
    db_query("UPDATE {password_policy_expiration} SET unblocked = %d WHERE uid = %d", time(), $uid);
    if ($account = user_load(array(
      'uid' => $uid,
      'status' => 1,
    ))) {
      password_policy_send_login($account);
      drupal_set_message(t('The user %name has been unblocked.', array(
        '%name' => $account->name,
      )));
    }
  }
  drupal_goto('admin/user/password_policy/list_expired');
}

/**
 * Sends one time login url to the user
 * based on 'user_pass_submit'
 */
function password_policy_send_login($account = NUL) {
  global $base_url;
  $from = variable_get('site_mail', ini_get('sendmail_from'));

  // Mail one time login URL and instructions.
  $variables = array(
    '!username' => $account->name,
    '!site' => variable_get('site_name', 'drupal'),
    '!login_url' => user_pass_reset_url($account),
    '!uri' => $base_url,
    '!uri_brief' => substr($base_url, strlen('http://')),
    '%mailto' => $account->mail,
    '%date' => format_date(time()),
    '%login_uri' => url('user', NULL, NULL, TRUE),
    '!edit_uri' => url('user/' . $account->uid . '/edit', NULL, NULL, TRUE),
  );
  $subject = _user_mail_text('pass_subject', $variables);
  $body = _user_mail_text('pass_body', $variables);
  $headers = array(
    'From' => $from,
    'Reply-to' => $from,
    'X-Mailer' => 'Drupal',
    'Return-path' => $from,
    'Errors-to' => $from,
  );
  $mail_success = drupal_mail('password-policy-send-login', $account->mail, $subject, $body, $from, $headers);
  if ($mail_success) {
    watchdog('password_policy', t('Password reset instructions mailed to %name at %email.', array(
      '%name' => $account->name,
      '%email' => $account->mail,
    )));
    drupal_set_message(t('Further instructions have been sent to %name e-mail address.', array(
      '%name' => $account->name,
    )));
  }
  else {
    watchdog('password_policy', t('Error mailing password reset instructions to %name at %email.', array(
      '%name' => $account->name,
      '%email' => $account->mail,
    )), WATCHDOG_ERROR);
    drupal_set_message(t('Unable to send mail. Please contact the site admin.'));
  }
}

/**
 * Implementation of hook_cron
 *
 */
function password_policy_cron() {
  $constraint = password_policy_load_active_policy();
  if ($constraint) {
    $expiration = $constraint
      ->getExpiration();
    $warnings = explode(',', $constraint
      ->getWarning());
    if (!empty($expiration)) {
      $accounts = array();

      // Get all users' last password change time. We don't touch blocked accounts
      $result = db_query("SELECT u.*, u.created created_u, p.created created_p, e.warning warning, e.unblocked unblocked FROM {users} u LEFT JOIN {password_policy_users} 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', false)) {
          continue;
        }
        $accounts[$row->uid] = empty($row->created_p) ? $row->created_u : $row->created_p;
        $warns[$row->uid] = $row->warning;
        $unblocks[$row->uid] = $row->unblocked;
      }
      $expiration_seconds = $expiration * 60 * 60 * 24;
      $policy_enabled = _password_policy_enabled($expiration_seconds);
      rsort($warnings, SORT_NUMERIC);
      $time = time();
      foreach ($accounts as $uid => $last_change) {
        foreach ($warnings as $warning) {
          if (!empty($warning)) {
            $warning_seconds = $warning * 60 * 60 * 24;
            $start_period = max($policy_enabled, $last_change) + $expiration_seconds - $warning_seconds;
            $end_period = $start_period + 60 * 60 * 24;
            if ($warns[$uid] > $start_period && $warns[$uid] < $end_period) {

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

              // we're sending a warning
              global $base_url;
              $from = variable_get('site_mail', ini_get('sendmail_from'));
              $account = user_load(array(
                'uid' => $uid,
              ));
              $variables = array(
                '%username' => $account->name,
                '%site' => variable_get('site_name', 'drupal'),
                '%uri' => $base_url,
                '%uri_brief' => substr($base_url, strlen('http://')),
                '%mailto' => $account->mail,
                '%date' => format_date(time()),
                '%login_uri' => url('user', NULL, NULL, TRUE),
                '%edit_uri' => url('user/' . $account->uid . '/edit', NULL, NULL, TRUE),
                '%days' => $warning,
              );
              $subject = _password_policy_mail_text('warning_subject', $variables);
              $body = _password_policy_mail_text('warning_body', $variables);
              $headers = array(
                'From' => $from,
                'Reply-to' => $from,
                'X-Mailer' => 'Drupal',
                'Return-path' => $from,
                'Errors-to' => $from,
              );
              $mail_success = drupal_mail('password-policy-cron-warning', $account->mail, $subject, $body, $from, $headers);
              if ($mail_success) {
                watchdog('password_policy', t('Password expiration warning mailed to %username at %email.', array(
                  '%username' => $account->name,
                  '%email' => $account->mail,
                )));
              }
              else {
                watchdog('password_policy', t('Error mailing password expiration warning to %username at %email.', array(
                  '%username' => $account->name,
                  '%email' => $account->mail,
                )));
              }
              if (!empty($warns[$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);
              }
            }
          }
        }

        // Block expired accounts. Unblocked accounts are not blocked for 24h.
        if ($time > max($policy_enabled, $last_change) + $expiration_seconds && $time > $unblocks[$uid] + 60 * 60 * 24 && variable_get('password_policy_block', 0) == 0) {
          db_query("UPDATE {users} SET status = '0' WHERE uid = '%d'", $uid);
          if (!empty($warns[$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', t('Password for user %name has expired.', array(
            '%name' => $account->name,
          )), WATCHDOG_NOTICE, l(t('edit'), "user/{$account->uid}/edit"));
        }
      }
    }
  }
}

/**
 * Loads default or saved mail text
 */
function _password_policy_mail_text($messageid, $variables = array()) {

  // Check if an admin setting overrides the default string.
  if ($admin_setting = variable_get("password_policy_mail_{$messageid}", '')) {
    return strtr($admin_setting, $variables);
  }
  else {
    switch ($messageid) {
      case 'warning_subject':
        return t('Password expiration warning for %username at %site', $variables);
      case 'warning_body':
        return t("%username,\n\nYour password at %site will expire in less than %days day(s).\n\nPlease go to %edit_uri to change your password.", $variables);
    }
  }
}

/**
 * Returns starting point of active policy
 */
function _password_policy_enabled($expiration_seconds = 0) {
  $result = db_query_range("SELECT * FROM {password_policy} WHERE enabled = 1 ORDER BY enabled DESC", 0, 1);
  if ($row = db_fetch_object($result)) {
    $policy_enabled = $row->created;
  }
  if (variable_get('password_policy_begin', 0) == 1) {

    // password older than expiration time expires starting from setting the policy
    $policy_enabled -= $expiration_seconds;
  }
  return $policy_enabled;
}

Functions

Namesort descending Description
password_policy_admin_settings
password_policy_cron Implementation of hook_cron
password_policy_delete Confirmation form for the deletion of a password policy. Deletion takes place in password_policy_delete_submit().
password_policy_delete_confirm
password_policy_delete_confirm_submit Submit hook for the delete policy operation.
password_policy_form_policy Form display for new or to be edited password policies.
password_policy_form_policy_form
password_policy_form_policy_form_submit Form submission hook for new or edited password policies.
password_policy_get_policy_count Returns the number of unique policies defined and stored in the database.
password_policy_help Help text for the password policy module.
password_policy_list_expired List all expired accounts
password_policy_load_active_policy Loads the default (enabled and active) policy or NULL if there are no active policies.
password_policy_load_policy_by_id Loads the policy with the specified id or NULL if not found.
password_policy_menu Implementation of hook_menu().
password_policy_perm Permissions for the password policy module.
password_policy_save_policy Saves the policy instance in the database as a new policy.
password_policy_send_login Sends one time login url to the user based on 'user_pass_submit'
password_policy_simpletest Implementation of hook_simpletest() for use with the simpletest module.
password_policy_unblock Unblocks the expired account
password_policy_update_policy Updates the given policy instance with the given policy id in the database.
password_policy_user The implementation of hook_user(). Used to trap the validation step so we can test any currently enabled password policies.
password_policy_view The default view for the password policy module.
password_policy_view_form
password_policy_view_form_submit Submit hook for the form on the default view for the password policy module. From the default view, the user can set a new default password policy or clear the default so that no policy is active and the default drupal password mechanism takes affect.
theme_password_policy_view_form Custom theme for rendering a checkbox list of defined policies. With Drupal's form API, it can be tricky to get a layout of the form elements which is different from the default. This layout is based on a similar layout found in the "input…
_password_policy_clear_default Resets the enabled flag for all policies in the database to 0.
_password_policy_enabled Returns starting point of active policy
_password_policy_get_valid_constraints Returns an array of constraint instances which the user should be able to have access to in their password policy. Only the ones which the user sets a value for will be used. See password_policy_form_policy_submit().
_password_policy_load_constraint_definitions Loads all the constraint object definitions.
_password_policy_load_policy_summaries Returns an array of associative arrays containing the keys id, name, enable and description for all the password policies defined in the database ordered by name.
_password_policy_mail_text Loads default or saved mail text
_password_policy_store_password Stores the given password associated with the user with the specified id in the database. This data is used with the history constraint to prevent users from using a password they have used previously.