security_questions.module in Security Questions 6.2
Same filename and directory in other branches
Main module file for security_questions.
File
security_questions.moduleView 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());
}