You are here

security_questions.module in Security Questions 6.2

Main module file for security_questions.

File

security_questions.module
View source
<?php

/**
 * @file
 * Main module file for security_questions.
 */

/**
 * Implements hook_perm().
 */
function security_questions_perm() {
  return array(
    'administer security questions',
    'bypass security questions',
  );
}

/**
 * Implements hook_menu().
 */
function security_questions_menu() {
  $items = array();
  $items['admin/user/security_questions'] = array(
    'title' => 'Security questions',
    'description' => 'Manage security questions.',
    'access arguments' => array(
      'administer security questions',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'security_questions_list_form',
    ),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'security_questions.admin.inc',
  );
  $items['admin/user/security_questions/questions'] = array(
    'title' => 'Questions',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $items['admin/user/security_questions/questions/delete/%security_questions_question'] = array(
    'title' => 'Delete question',
    'description' => 'Delete security question.',
    'access arguments' => array(
      'administer security questions',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'security_questions_delete_question_form',
      5,
    ),
    'file' => 'security_questions.admin.inc',
  );
  $items['admin/user/security_questions/settings'] = array(
    'title' => 'Settings',
    'description' => 'Manage the security questions module.',
    'access arguments' => array(
      'administer security questions',
    ),
    'page callback' => 'drupal_get_form',
    'type' => MENU_LOCAL_TASK,
    'page arguments' => array(
      'security_questions_settings_form',
    ),
    'file' => 'security_questions.admin.inc',
  );
  $items['user/%user/security_questions'] = array(
    'title' => 'Security questions',
    'description' => 'Select your security questions and answers.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'security_questions_user_form',
      1,
    ),
    'access callback' => 'security_questions_access',
    'access arguments' => array(
      1,
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'security_questions.pages.inc',
  );
  return $items;
}

/**
 * Access callback for viewing or editing a user's security questions.
 *
 * @param $account
 *   The user account whose security questions are being accessed.
 */
function security_questions_access($account) {
  global $user;
  return $user->uid == $account->uid || user_access('administer users', $user);
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Allows the user to select security questions during registration.
 */
function security_questions_form_user_register_alter(&$form, &$form_state) {

  // Hide the questions form when the user is being registered by an admin.
  if (!user_access('administer users')) {

    // Merge in our user form.
    module_load_include('inc', 'security_questions', 'security_questions.pages');
    $form += drupal_retrieve_form('security_questions_user_form', $form_state);
    array_unshift($form['#validate'], 'security_questions_user_register_form_validate');

    // We can't add our submit handler here, because the user account doesn't
    // exist yet. Instead, we will get the info during hook_user().
  }
}

/**
 * Validation handler for security_questions_form_user_register_form_alter().
 */
function security_questions_user_register_form_validate($form, &$form_state) {
  module_load_include('inc', 'security_questions', 'security_questions.pages');
  security_questions_user_form_validate($form, $form_state);
}

/**
 * Implements hook_user().
 */
function security_questions_user($op, &$edit, $account, $category) {
  switch ($op) {
    case 'insert':

      // During registration, save the user's answers.
      if (!empty($edit['questions'])) {
        security_questions_user_answers_save($account, $edit['questions']);
      }
      break;
    case 'login':

      // Upon successful login, set the "remember" cookie.
      if (!empty($edit['remember'])) {
        security_questions_set_cookie($account);
      }
      break;
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Adds a question challenge to the login form.
 */
function security_questions_form_user_login_alter(&$form, &$form_state) {
  _security_questions_user_login_form_alter($form, $form_state);
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Adds a question challenge to the login block.
 */
function security_questions_form_user_login_block_alter(&$form, &$form_state) {

  // Redirect login block submissions to the main login form.
  // @todo Is there a better way to hand off from the login block to /user?
  $form['form_id']['#value'] = 'user_login';

  // Preserve the login block's destination. Drupal 6 lacks Drupal 7's
  // drupal_parse_url(), so we need to borrow some of its logic here.
  $options = array();
  $parts = explode('?', $form['#action']);
  if (isset($parts[1])) {
    $query_variables = array();
    $query_parts = explode('#', $parts[1]);
    parse_str($query_parts[0], $query_variables);
    if (isset($query_variables['destination'])) {
      $options = array(
        'query' => array(
          'destination' => $query_variables['destination'],
        ),
      );
    }
  }
  $form['#action'] = url('user', $options);
  _security_questions_user_login_form_alter($form, $form_state);
}

/**
 * Helper function for altering the login forms.
 *
 * @see security_questions_form_user_login_alter()
 * @see security_questions_form_user_login_block_alter()
 */
function _security_questions_user_login_form_alter(&$form, &$form_state) {

  // Abort if the login forms are not protected.
  if (!variable_get('security_questions_user_login', FALSE)) {
    return;
  }
  $protection_mode = variable_get('security_questions_protection_mode', 'after');

  // We need our own submission handler to make this a multistep form.
  $form['#submit'] = array(
    'security_questions_user_login_submit',
  );

  // We can keep the core validator for a blocked username, but since
  // user_login_authenticate_validate() performs the actual login in D6, we
  // can't reuse it or user_login_final_validate().
  $form['#validate'] = array(
    'user_login_name_validate',
  );

  // Phase 1: Initial login prompt.
  if (empty($form_state['storage']['security_questions']['name'])) {

    // If the questions come before the password, hide the password field.
    if ($protection_mode == 'before') {
      unset($form['pass']);

      // Validate that the login name exists and is active.
      $form['#validate'][] = 'security_questions_user_login_name_validate';
    }
    else {
      $form['#validate'][] = 'security_questions_user_login_authenticate_validate';

      // It's possible that the user may be able to skip phase 2, so we need
      // our answer validator and final validator, too.
      $form['#validate'][] = 'security_questions_user_login_answer_validate';
      $form['#validate'][] = 'security_questions_user_login_final_validate';
    }
  }
  else {
    $name = $form_state['storage']['security_questions']['name'];
    $form['name'] = array(
      '#type' => 'value',
      '#value' => $name,
    );
    $account = user_load(array(
      'name' => $name,
    ));

    // If the user has already chosen "remember this computer" on a challenge,
    // remember that choice.
    if (!empty($form_state['storage']['security_questions']['remember'])) {
      $form['remember'] = array(
        '#type' => 'value',
        '#value' => TRUE,
      );
    }

    // What we do next depends on whether we have the user's password yet.
    if (empty($form_state['storage']['security_questions']['pass'])) {

      // If the user is allowed to bypass question challenges or has already
      // successfully answered one, then we just need to collect the password
      // and set our normal validators.
      if (!empty($form_state['storage']['security_questions']['passed_challenge']) || security_questions_bypass_challenge($account)) {
        $form['#validate'][] = 'security_questions_user_login_authenticate_validate';
        $form['#validate'][] = 'security_questions_user_login_answer_validate';
        $form['#validate'][] = 'security_questions_user_login_final_validate';
      }
      else {
        $form += security_questions_challenge($account);

        // Show the "remember this computer" option, if enabled.
        if (variable_get('security_questions_cookie', FALSE)) {
          $form['remember'] = array(
            '#title' => t('Remember this computer'),
            '#type' => 'checkbox',
          );
        }

        // We also don't want to get the password until after the challenge,
        // so we need to unset it and use the alternate validator.
        unset($form['pass']);
        $form['#validate'][] = 'security_questions_user_login_name_validate';
        $form['#validate'][] = 'security_questions_user_login_answer_validate';
      }
    }
    else {
      $form['pass'] = array(
        '#type' => 'value',
        '#value' => $form_state['storage']['security_questions']['pass'],
      );
      $form['#validate'][] = 'security_questions_user_login_authenticate_validate';
      $form['#validate'][] = 'security_questions_user_login_answer_validate';
      $form['#validate'][] = 'security_questions_user_login_final_validate';

      // If we're here, it means that both the user name and password have been
      // validated, but the user has not been logged in yet because either a
      // challenge is needed or the required number of answers is not on file.
      if (empty($form_state['storage']['security_questions']['passed_challenge'])) {
        $form += security_questions_challenge($account);

        // Show the "remember this computer" option, if enabled.
        if (variable_get('security_questions_cookie', FALSE)) {
          $form['remember'] = array(
            '#title' => t('Remember this computer'),
            '#type' => 'checkbox',
          );
        }
      }
      else {
        module_load_include('inc', 'security_questions', 'security_questions.pages');
        $form += security_questions_user_form($form_state, $account);
        array_unshift($form['#validate'], 'security_questions_user_login_user_form_validate');
      }
    }
  }
}

/**
 * Validation handler for _security_questions_user_login_form_alter().
 *
 * Validates the user name when the questions are asked before the password.
 */
function security_questions_user_login_name_validate($form, &$form_state) {

  // Without a password, we can't fully authenticate, so we don't need to save
  // the return value of this function.
  _security_questions_user_authenticate($form_state['values']['name']);
}

/**
 * Validation handler for _security_questions_user_login_form_alter().
 *
 * Validates the user login name and password.
 */
function security_questions_user_login_authenticate_validate($form, &$form_state) {

  // Set $form_state['uid'] as a flag for
  // security_questions_user_login_final_validate().
  $form_state['uid'] = _security_questions_user_authenticate($form_state['values']['name'], $form_state['values']['pass']);
}

/**
 * Helper function for security_questions_user_login_name_validate() and
 * security_questions_user_login_authenticate_validate().
 *
 * As in user_authenticate(), checks that the user exists (with the supplied
 * password, if given) and that the user's email is not reserved, but does not
 * trigger an actual login.
 *
 * @param $name
 *   A user name.
 * @param $pass
 *   A password. Will be FALSE when called from
 *   security_questions_user_login_name_validate().
 *
 * @return
 *   The user's ID, if authentication succeeds. Otherwise, FALSE.
 */
function _security_questions_user_authenticate($name, $pass = FALSE) {

  // Load the account with the information available.
  if ($pass === FALSE) {
    $account = user_load(array(
      'name' => $name,
      'status' => 1,
    ));
  }
  else {
    $account = user_load(array(
      'name' => $name,
      'pass' => trim($pass),
      'status' => 1,
    ));
  }

  // If no matching account was found, output an error similar to
  // user_login_final_validate().
  if (!$account) {
    if ($pass === FALSE) {
      form_set_error('name', t('Sorry, unrecognized username. <a href="@password">Have you forgotten your username?</a>', array(
        '@password' => url('user/password'),
      )));
    }
    else {
      form_set_error('name', t('Sorry, unrecognized username or password. <a href="@password">Have you forgotten your password?</a>', array(
        '@password' => url('user/password'),
      )));
    }
  }
  elseif (drupal_is_denied('mail', $account->mail)) {
    form_set_error('name', t('The name %name is registered using a reserved e-mail address and therefore could not be logged in.', array(
      '%name' => $account->name,
    )));
  }

  // Log an error and return FALSE if any of the above checks failed.
  if (form_get_errors()) {
    watchdog('user', 'Login attempt failed for %user.', array(
      '%user' => $name,
    ));
    return FALSE;
  }

  // Otherwise, return the authenticated user's ID.
  return $account->uid;
}

/**
 * Validation handler for _security_questions_user_login_form_alter().
 *
 * Validates the user's response to the security question challenge.
 */
function security_questions_user_login_answer_validate($form, &$form_state) {

  // Only check if the user hasn't passed a challenge yet.
  if (!form_get_errors() && empty($form_state['storage']['security_questions']['passed_challenge'])) {
    if ($account = user_load(array(
      'name' => $form_state['values']['name'],
    ))) {

      // If the user is allowed to bypass challenges, give them a pass.
      if (security_questions_bypass_challenge($account)) {
        $form_state['storage']['security_questions']['passed_challenge'] = TRUE;
      }
      elseif (!empty($form_state['values']['question'])) {
        $sqid = $form_state['values']['question'];
        $answer = _security_questions_clean_answer($form_state['values']['answer']);
        $valid_answer = _security_questions_clean_answer(security_questions_get_answer($account->uid, $sqid));
        $form_state['storage']['security_questions']['passed_challenge'] = $valid_answer == $answer;

        // If the answer was correct, flush the flood event.
        if ($form_state['storage']['security_questions']['passed_challenge']) {
          security_questions_flush_incorrect($account->uid);
        }
        else {
          drupal_set_message(t('That answer does not match the one on the user account. Please try again with a different question.'), 'error');
          security_questions_register_incorrect($sqid, $account->uid);
        }
      }
    }
  }
}

/**
 * Validation handler for _security_questions_user_login_form_alter().
 *
 * This should be the final validator. If the user name and password have been
 * authenticated and the user has successfully passed the security question
 * challenge, this function will call the core validation functions to complete
 * the login.
 */
function security_questions_user_login_final_validate($form, &$form_state) {

  // Were the user name and password valid?
  $is_authenticated = !empty($form_state['uid']);

  // Was the security question challenge answered correctly or not required?
  $passed_challenge = !empty($form_state['storage']['security_questions']['passed_challenge']);

  // Is the required number of security question answers on file?
  if ($is_authenticated) {
    $account = user_load($form_state['uid']);

    // Users with bypass permission aren't required to have answers on file.
    if (user_access('bypass security questions', $account)) {
      $has_required_number = TRUE;
    }
    else {
      $required = variable_get('security_questions_number_required', 3);
      $answers = count(security_questions_get_answer_list($account->uid));
      $has_required_number = $answers >= $required;
    }
  }
  else {
    $has_required_number = FALSE;
  }

  // If no form errors were found and all tests above passed, log in as normal.
  if (!form_get_errors() && $is_authenticated && $passed_challenge && $has_required_number) {
    if (!empty($form_state['storage']['security_questions']['remember'])) {
      $form_state['values']['remember'] = TRUE;
    }
    user_login_authenticate_validate($form, $form_state);
    user_login_final_validate($form, $form_state);
  }
}

/**
 * Validation handler for _security_questions_user_login_form_alter().
 *
 * Validates and saves security questions and answers created during login.
 */
function security_questions_user_login_user_form_validate($form, &$form_state) {
  module_load_include('inc', 'security_questions', 'security_questions.pages');
  security_questions_user_form_validate($form, $form_state);

  // Because D6 performs the login during validation, not submission, we need
  // to save the new questions/answers here.
  if (!form_get_errors()) {
    security_questions_user_form_submit($form, $form_state);
  }
}

/**
 * Submission handler for _security_questions_user_login_form_alter().
 */
function security_questions_user_login_submit($form, &$form_state) {

  // If the user has logged in, hand off to the core submission handler.
  global $user;
  if ($user->uid) {
    return user_login_submit($form, $form_state);
  }
  else {
    $form_state['storage']['security_questions'] = array(
      'name' => $form_state['values']['name'],
      'pass' => empty($form_state['values']['pass']) ? FALSE : $form_state['values']['pass'],
      'passed_challenge' => !empty($form_state['storage']['security_questions']['passed_challenge']),
      'remember' => !empty($form_state['values']['remember']),
    );
    $form_state['rebuild'] = TRUE;

    // Also, wipe the uid from the form state to ensure that the user name and
    // password are retested on the next run.
    if (isset($form_state['uid'])) {
      unset($form_state['uid']);
    }
  }
}

/**
 * Checks whether a user can bypass a security question challenge.
 *
 * @param $account
 *   The user account object.
 *
 * @return
 *   Boolean indicating whether the user can skip normally required challenges.
 */
function security_questions_bypass_challenge($account) {

  // Check for the bypass permission.
  if (user_access('bypass security questions', $account)) {
    return TRUE;
  }

  // If "remember me" cookies are allowed, check for one.
  // @todo Replace the cookies option with a more secure method.
  if (variable_get('security_questions_cookie', FALSE) && isset($_COOKIE['security_questions'])) {
    $cookie = $_COOKIE['security_questions'];
    $cookie = explode('-', $cookie);
    $cookie_uid = $cookie[3];
    if ($account->uid == $cookie_uid) {
      return TRUE;
    }
  }

  // Finally, if the user has no answers on file, we have to skip it.
  if (!security_questions_get_answer_list($account->uid)) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Adds a question challenge to the password reset form.
 */
function security_questions_form_user_pass_alter(&$form, &$form_state) {

  // Abort if the password form is not protected.
  if (!variable_get('security_questions_password_reset', FALSE)) {
    return;
  }

  // We need our own submission handler to make this a multistep form.
  $form['#submit'] = array(
    'security_questions_user_pass_submit',
  );

  // If our submit handler triggered a rebuild, we need to issue a challenge.
  $account = FALSE;
  if (!empty($form_state['storage']['security_questions']['uid'])) {
    $account = user_load($form_state['storage']['security_questions']['uid']);
  }
  if ($account) {
    $form += security_questions_challenge($account);
    $form['#validate'][] = 'security_questions_user_pass_answer_validate';
    $form['name']['#type'] = 'value';
    $form['name']['#value'] = $account->mail;
    $form['submit']['#weight'] = 10;
  }
}

/**
 * Validation handler for security_questions_form_user_pass_alter().
 */
function security_questions_user_pass_answer_validate($form, &$form_state) {

  // @todo Can security_questions_user_pass_answer_validate() be combined with security_questions_user_login_answer_validate()?
  // user_pass_validate() adds the 'account' value on a successful load.
  if (!empty($form_state['values']['account'])) {
    $account = $form_state['values']['account'];
    $sqid = $form_state['values']['question'];
    $answer = _security_questions_clean_answer($form_state['values']['answer']);
    $valid_answer = _security_questions_clean_answer(security_questions_get_answer($account->uid, $sqid));
    $form_state['storage']['security_questions']['passed_challenge'] = $valid_answer == $answer;

    // If the answer was correct, flush the flood event.
    if ($form_state['storage']['security_questions']['passed_challenge']) {
      security_questions_flush_incorrect($account->uid);
    }
    else {
      drupal_set_message(t('That answer does not match the one on the user account. Please try again with a different question.'), 'error');
      security_questions_register_incorrect($sqid, $account->uid);
    }
  }
}

/**
 * Submission handler for security_questions_form_user_pass_alter().
 */
function security_questions_user_pass_submit($form, &$form_state) {

  // If the user is allowed to bypass or has already answered the challenge,
  // hand off to the normal password reset submission handler.
  $account = $form_state['values']['account'];
  if (security_questions_bypass_challenge($account) || !empty($form_state['storage']['security_questions']['passed_challenge'])) {
    user_pass_submit($form, $form_state);
    unset($form_state['storage']['security_questions']);
  }
  else {
    $form_state['storage']['security_questions']['uid'] = $account->uid;
    $form_state['rebuild'] = TRUE;
  }
}

/**
 * Helper function to clean users answers.
 */
function _security_questions_clean_answer($answer) {

  // Clean the users answer so we have nice variables to compare.
  $answer = drupal_strtolower(trim($answer));
  return $answer;
}

/**
 * Generates the form elements for a security question challenge.
 *
 * @param $account
 *   The user being challenged.
 *
 * @return
 *   The form elements for the challenge.
 */
function security_questions_challenge($account) {
  $question = security_questions_require_question($account->uid);
  $form['question'] = array(
    '#type' => 'value',
    '#value' => $question->sqid,
  );
  $form['answer'] = array(
    '#title' => check_plain($question->question),
    '#type' => 'textfield',
  );
  return $form;
}

/**
 * Denies access if an answerable question cannot be found for a given user.
 *
 * This function should be used any time the user is required to answer a
 * security question in order to proceed.  It first checks for failed attempts
 * by the visitor (as identified by IP address) to answer questions as the given
 * user and removes those questions from consideration; this impedes brute force
 * attempts to guess a user's answers, and also prevents the same question from
 * being presented repeatedly.  It then randomly selects a question from those
 * that remain in consideration.  Finally, if no question can be found, it
 * aborts the request with an access denied message.
 *
 * @param $uid
 *   The user's ID.
 *
 * @return
 *   A security question.
 */
function security_questions_require_question($uid) {

  // Get a list of questions that this visitor has already failed to answer.
  $result = db_query("SELECT sqid FROM {security_questions_incorrect} WHERE uid = %d AND ip = '%s' AND expiration > %d", $uid, ip_address(), time());
  $failed = array();
  while ($row = db_fetch_object($result)) {
    $failed[] = $row->sqid;
  }

  // Get a random question from the database where the user has an answer,
  // excluding those where the visitor has logged a failed attempt.
  $query = "SELECT q.sqid FROM {security_questions} q";
  $query .= " INNER JOIN {security_questions_answers} a ON q.sqid = a.sqid AND a.uid = %d";
  $args = array(
    $uid,
  );
  if (!empty($failed)) {
    $query .= " WHERE q.sqid NOT IN (" . db_placeholders($failed) . ")";
    $args = array_merge($args, $failed);
  }
  $query .= " ORDER BY RAND() LIMIT 1";
  $qid = db_result(db_query($query, $args));
  $question = empty($qid) ? FALSE : security_questions_question_load($qid);

  // If no question could be selected, we need to abort.
  if (empty($question)) {

    // Rather than using drupal_set_message(), we overwrite the messages array
    // directly to prevent any other messages from being displayed.
    $_SESSION['messages'] = array();
    $_SESSION['messages']['error'][] = t('Too many failed attempts to answer security questions in @interval. Try again later.', array(
      '@interval' => format_interval(variable_get('security_questions_flood_expire', 3600)),
    ));
    drupal_access_denied();
    exit;
  }
  return $question;
}

/**
 * Set a cookie to bypass future challenges.
 *
 * @param $account
 *   The user account.
 *
 * @todo Upgrade the cookie to compare the uniqueness of the user's computer, so if someone steals this cookie, it will be harder to impersonate them.
 */
function security_questions_set_cookie($account) {
  if (variable_get('security_questions_cookie', FALSE)) {
    $expire = variable_get('security_questions_cookie_expire', 604800);
    setcookie('security_questions', 'do-not-challenge-' . $account->uid, time() + $expire, '/');
  }
}

/**
 * Saves a question to the database.
 *
 * In most cases, it is better to use security_questions_add_question().
 *
 * @param $question
 *   The question object to be saved. If $question->sqid is omitted, a new
 *   question will be added.
 *
 * @return
 *   SAVED_NEW or SAVED_UPDATED if successful, otherwise FALSE.
 *
 * @see security_questions_add_question()
 */
function security_questions_question_save($question) {

  // If a machine name was provided, make sure it is unique.
  if (!empty($question->machine_name)) {
    $existing_sqid = db_result(db_query("SELECT sqid FROM {security_questions} WHERE machine_name = '%s'", $question->machine_name));
    if ($existing_sqid && (empty($question->sqid) || $question->sqid != $existing_sqid)) {
      return FALSE;
    }
  }
  if (empty($question->sqid)) {
    return drupal_write_record('security_questions', $question);
  }
  else {
    return drupal_write_record('security_questions', $question, array(
      'sqid',
    ));
  }
}

/**
 * Utility function to add a question.
 *
 * @param $text
 *   The question text.
 * @param $uid
 *   (optional) If this is a per-user question, the ID of the user. Defaults to
 *   0 to indicate a system-wide question.
 * @param $machine_name
 *   (optional) A machine-readable name, to make the question exportable.
 *
 * @return
 *   The ID of the newly created question, or FALSE if failed to save.
 */
function security_questions_add_question($text, $uid = 0, $machine_name = NULL) {
  $question = new stdClass();
  $question->question = $text;
  $question->uid = $uid;
  $question->machine_name = $machine_name;
  if (security_questions_question_save($question)) {
    return $question->sqid;
  }
  return FALSE;
}

/**
 * Saves a user's answer to the database.
 *
 * @param $uid
 *   The user's ID.
 * @param $sqid
 *   The security question ID.
 * @param $answer
 *   The answer text.
 */
function security_questions_answer_save($uid, $sqid, $answer) {
  $existing = security_questions_get_answer_list($uid, $sqid);
  if (empty($existing)) {
    $query = "INSERT INTO {security_questions_answers} (answer, uid, sqid) VALUES ('%s', %d, %d)";
  }
  else {
    $query = "UPDATE {security_questions_answers} SET answer = '%s' WHERE uid = %d AND sqid = %d";
  }
  db_query($query, array(
    $answer,
    $uid,
    $sqid,
  ));
}

/**
 * Saves a full set of answers and custom questions for a user.
 *
 * @param $account
 *   The user account object.
 * @param &$edit
 *   The submitted question and answer values from the user form.
 *
 * @see security_questions_user_form()
 */
function security_questions_user_answers_save($account, &$edit) {

  // Delete all existing answers for this user.
  db_query('DELETE FROM {security_questions_answers} WHERE uid = %d', array(
    $account->uid,
  ));
  $chosen = array();
  foreach ($edit as $question) {

    // Save new custom questions.
    if ($question['question'] == 'other') {
      $sqid = security_questions_add_question($question['custom_question'], $account->uid);
      if ($sqid) {
        $question['question'] = $sqid;
      }
      else {

        // If the custom question could not be saved, skip.
        continue;
      }
    }
    $chosen[] = $question['question'];

    // Save new answers.
    security_questions_answer_save($account->uid, $question['question'], $question['answer']);
  }

  // Clean-up unused custom questions.
  db_query('DELETE FROM {security_questions} WHERE uid = %d AND sqid NOT IN (' . db_placeholders($chosen) . ')', array_merge(array(
    $account->uid,
  ), $chosen));
}

/**
 * Fetch a specific question from the database.
 *
 * @param $sqid
 *   The security question ID.
 *
 * @return
 *   A question object, or FALSE if no question was found.
 */
function security_questions_question_load($sqid) {
  $question = security_questions_question_load_multiple(array(
    'sqid' => $sqid,
  ));
  return isset($question[$sqid]) ? $question[$sqid] : FALSE;
}

/**
 * Fetch multiple questions from the database.
 *
 * @param $conditions
 *   (optional) An array of query conditions as field => value pairs. If
 *   omitted, all questions in the database will be returned.
 * @param $any
 *   (optional) By default, the result set includes only questions that match
 *   all of the $conditions. If this parameter is set to TRUE, the result set
 *   will instead include questions that match any of the $conditions.
 *
 * @return
 *   An empty array if no questions match the conditions; otherwise, an array
 *   of question objects indexed by sqid.
 */
function security_questions_question_load_multiple($conditions = array(), $any = FALSE) {
  $schema = drupal_get_schema('security_questions');
  $query = "SELECT * FROM {security_questions}";
  $clause = $values = $return = array();
  if (!empty($conditions)) {
    $separator = $any ? ' OR ' : ' AND ';
    foreach ($conditions as $field => $value) {
      $clause[] = "{$field} = " . db_type_placeholder($schema['fields'][$field]['type']);
      $values[] = $value;
    }
    $query .= " WHERE " . implode($separator, $clause);
  }
  $result = db_query($query, $values);
  while ($row = db_fetch_object($result)) {
    $return[$row->sqid] = $row;
  }
  return $return;
}

/**
 * Fetch a user's available questions from the database.
 *
 * @param $account
 *   (optional) A user object. If omitted, only global questions are fetched.
 *
 * @return
 *   An array suitable for use in a form select element's '#options' of
 *   questions that the user is permitted to choose.
 */
function security_questions_get_question_list($account = NULL) {

  // Cache the question set for the user, as this may be called repeatedly.
  static $options = array();
  $uid = isset($account) ? $account->uid : 0;
  if (!isset($options[$uid])) {
    $questions = security_questions_question_load_multiple(array(
      'uid' => 0,
    ));
    if ($uid && variable_get('security_questions_user_questions', FALSE)) {
      $questions += security_questions_question_load_multiple(array(
        'uid' => $uid,
      ));
    }
    foreach ($questions as $question) {
      $options[$uid][$question->sqid] = $question->question;
    }
  }
  return $options[$uid];
}

/**
 * Fetch a user's answer to a question from the database.
 *
 * @param $uid
 *   The user's ID.
 * @param $sqid
 *   The security question ID.
 *
 * @return
 *   FALSE if the user has not answered the question.  Otherwise, the answer.
 */
function security_questions_get_answer($uid, $sqid) {
  $list = security_questions_get_answer_list($uid, $sqid);
  $answer = reset($list);
  return empty($answer) ? FALSE : $answer->answer;
}

/**
 * Fetch a list of the user's answers from the database.
 *
 * @param $uid
 *   The user's ID.
 * @param $sqid
 *   (optional) A security question ID by which to filter the list.
 *
 * @return
 *   An empty array if no answers are found for the user.  Otherwise, an array
 *   of answer objects indexed by sqid.
 */
function security_questions_get_answer_list($uid, $sqid = NULL) {
  $query = "SELECT a.uid, a.sqid, a.answer, q.question\n    FROM {security_questions_answers} a\n    INNER JOIN {security_questions} q\n    ON a.sqid = q.sqid AND a.uid = %d";
  $values = array(
    $uid,
  );
  if (isset($sqid)) {
    $query .= " WHERE a.sqid = %d";
    $values[] = $sqid;
  }
  $result = db_query($query, $values);
  $return = array();
  while ($row = db_fetch_object($result)) {
    $return[$row->sqid] = $row;
  }
  return $return;
}

/**
 * Delete a question from the database.
 *
 * @param $sqid
 *   The unique security question identifier.
 */
function security_questions_question_delete($sqid) {
  foreach (array(
    'security_questions_answers',
    'security_questions',
  ) as $table) {
    db_query("DELETE FROM {$table} WHERE sqid = %d", array(
      $sqid,
    ));
  }
}

/**
 * Delete a user's answer from the database.
 *
 * @param $uid
 *   The user's ID.
 * @param $sqid
 *   The unique security question identifier.
 */
function security_questions_answer_delete($uid, $sqid) {
  db_query("DELETE FROM {security_questions_answers} WHERE uid = %d AND sqid = %d", array(
    $uid,
    $sqid,
  ));
}

/**
 * Registers an incorrect answer attempt by the current visitor.
 *
 * @param $sqid
 *   The ID of the question that the visitor attempted to answer.
 * @param $uid
 *   The ID of the user whom the visitor claims to be.
 */
function security_questions_register_incorrect($sqid, $uid) {
  $query = "INSERT INTO {security_questions_incorrect} (sqid, uid, ip, timestamp, expiration) VALUES (%d, %d, '%s', %d, %d)";
  db_query($query, $sqid, $uid, ip_address(), time(), time() + variable_get('security_questions_flood_expire', 3600));
}

/**
 * Flush incorrect answer attempts for the current visitor.
 *
 * @param $uid
 *   The ID of the user whom the visitor claims to be.
 */
function security_questions_flush_incorrect($uid) {
  db_query("DELETE FROM {security_questions_incorrect} WHERE uid = %d AND ip = '%s'", $uid, ip_address());
}

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

  // Delete all expired records from the incorrect answer attempt list.
  db_query("DELETE FROM {security_questions_incorrect} WHERE expiration < %d", time());
}

Functions

Namesort descending Description
security_questions_access Access callback for viewing or editing a user's security questions.
security_questions_add_question Utility function to add a question.
security_questions_answer_delete Delete a user's answer from the database.
security_questions_answer_save Saves a user's answer to the database.
security_questions_bypass_challenge Checks whether a user can bypass a security question challenge.
security_questions_challenge Generates the form elements for a security question challenge.
security_questions_cron Implements hook_cron().
security_questions_flush_incorrect Flush incorrect answer attempts for the current visitor.
security_questions_form_user_login_alter Implements hook_form_FORM_ID_alter().
security_questions_form_user_login_block_alter Implements hook_form_FORM_ID_alter().
security_questions_form_user_pass_alter Implements hook_form_FORM_ID_alter().
security_questions_form_user_register_alter Implements hook_form_FORM_ID_alter().
security_questions_get_answer Fetch a user's answer to a question from the database.
security_questions_get_answer_list Fetch a list of the user's answers from the database.
security_questions_get_question_list Fetch a user's available questions from the database.
security_questions_menu Implements hook_menu().
security_questions_perm Implements hook_perm().
security_questions_question_delete Delete a question from the database.
security_questions_question_load Fetch a specific question from the database.
security_questions_question_load_multiple Fetch multiple questions from the database.
security_questions_question_save Saves a question to the database.
security_questions_register_incorrect Registers an incorrect answer attempt by the current visitor.
security_questions_require_question Denies access if an answerable question cannot be found for a given user.
security_questions_set_cookie Set a cookie to bypass future challenges.
security_questions_user Implements hook_user().
security_questions_user_answers_save Saves a full set of answers and custom questions for a user.
security_questions_user_login_answer_validate Validation handler for _security_questions_user_login_form_alter().
security_questions_user_login_authenticate_validate Validation handler for _security_questions_user_login_form_alter().
security_questions_user_login_final_validate Validation handler for _security_questions_user_login_form_alter().
security_questions_user_login_name_validate Validation handler for _security_questions_user_login_form_alter().
security_questions_user_login_submit Submission handler for _security_questions_user_login_form_alter().
security_questions_user_login_user_form_validate Validation handler for _security_questions_user_login_form_alter().
security_questions_user_pass_answer_validate Validation handler for security_questions_form_user_pass_alter().
security_questions_user_pass_submit Submission handler for security_questions_form_user_pass_alter().
security_questions_user_register_form_validate Validation handler for security_questions_form_user_register_form_alter().
_security_questions_clean_answer Helper function to clean users answers.
_security_questions_user_authenticate Helper function for security_questions_user_login_name_validate() and security_questions_user_login_authenticate_validate().
_security_questions_user_login_form_alter Helper function for altering the login forms.