'Text CAPTCHA', 'description' => 'Builds CAPTCHAs that use text rather than images.', 'page callback' => 'drupal_get_form', 'page arguments' => array('textcaptcha_settings_form'), 'access arguments' => array('administer CAPTCHA settings'), 'type' => MENU_LOCAL_TASK ); return $items; } /** * Admin settings form callback. */ function textcaptcha_settings_form() { $form = array(); $form['instructions']['#value'] = t('To use TextCAPTCHA, you need to get an API key by !link', array( '!link' => l(t('registering at textcaptcha.com'), 'http://textcaptcha.com/register')) ); $form['textcaptcha_challenge_description'] = array( '#type' => 'textfield', '#title' => t('Text Captcha challenge description'), '#description' => t("Text inserted here will be prepended before the challenge question. This can be helpful for accessibility. Screen readers do not ready Drupal form element descriptions by default (which is where CAPTCHA module's default Challenge Description goes)."), '#default_value' => variable_get('textcaptcha_challenge_description', ''), '#required' => TRUE, ); $form['textcaptcha_api_key'] = array( '#type' => 'textfield', '#title' => t('Textcaptcha.com API key'), '#description' => t('Your API key from textcaptcha.com'), '#default_value' => variable_get('textcaptcha_api_key', ''), '#required' => TRUE, ); $form['textcaptcha_cron'] = array( '#type' => 'checkbox', '#title' => t('Fetch questions on cron'), '#description' => t('Enable or disable fetching and processing questions on cron.'), '#default_value' => variable_get('textcaptcha_cron', 1), ); $form['textcaptcha_question_limit'] = array( '#type' => 'textfield', '#title' => t('Local question limit'), '#description' => t('The number of questions to cache in the database. Set to 0 for unlimited.'), '#default_value' => variable_get('textcaptcha_question_limit', 1000), '#required' => TRUE, ); $form['actions']['trim_questions'] = array( '#type' => 'submit', '#value' => t('Trim local questions tables now'), '#submit' => array('textcaptcha_trim_tables'), ); $form['actions']['text_captcha'] = array( '#type' => 'submit', '#value' => t('Retrieve new captcha challenges'), '#submit' => array('textcaptcha_get_new_challenges'), ); return system_settings_form($form); } /** * Form validation for admin settings. Here we check that the API key works. */ function textcaptcha_settings_form_validate($form, $form_state) { $result = drupal_http_request(TEXTCAPTCHA_URL_PREFIX . trim($form_state['values']['textcaptcha_api_key'])); if ($result->code !== '200') { form_set_error('textcaptcha_api_key', t('The API Key did not work or the server cannot contact textcaptcha.com')); } $value = $form_state['values']['textcaptcha_question_limit']; if ($value !== '' && (!is_numeric($value) || intval($value) != $value || $value < 0)) { form_set_error('textcaptcha_question_limit', t('Local question limit must be an integer greater than or equal to 0.')); } } /** * Function to add new question to database. * * @param string $question * Question to enter into the database. * * @return int * Returns the id of the newly created question, or FALSE if error. */ function textcaptcha_question_create($question) { $record = new stdClass(); $record->question = $question; $record->created = time(); drupal_write_record('textcaptcha_questions', $record); if (!isset($record->qid)) { watchdog('textcaptcha', "There was an error writing a question to the database. Try clearing drupal's cache to update the schema registry."); return FALSE; } return $record->qid; } /** * Function to add new answer to database. * * @param int $qid * The database id of the question to which this answer is associated. * @param string $answer * Hashed answer for the question. * * @return int * Returns the id of the newly created question, or FALSE if error. */ function textcaptcha_answer_create($qid, $answer) { $record = new stdClass(); $record->qid = $qid; $record->answer = $answer; $record->created = time(); drupal_write_record('textcaptcha_answers', $record); if (!isset($record->aid)) { watchdog('textcaptcha', "There was an error writing the answer to the database. Try clearing drupal's cache to update the schema registry."); return FALSE; } return $record->aid; } /** * Save new question / answer combo to database. * * @param array $challenge * Challange retrieved from textcaptcha.org and parsed into array. * * @return bool * Returns TRUE on success or FALSE on failure */ function textcaptcha_challange_add($challenge) { if (!textcaptcha_verify_distinct_challenge($challenge['question'])) { return FALSE; } $qid = textcaptcha_question_create($challenge['question']); foreach ($challenge['answers'] as $answer) { if ($qid) { textcaptcha_answer_create($qid, $answer); } } return TRUE; } /** * Function to verify that new challenges are distinct. * * @param array $question * Question to verify if entry exists in database. * * @return bool * If an entry exists validation returns FAILURE, otherwise pass * validation with TRUE */ function textcaptcha_verify_distinct_challenge($question) { // Check to see if question is already in the system. $result = db_fetch_array(db_query("SELECT qid FROM {textcaptcha_questions} WHERE question = '%s'", $question)); return $result ? FALSE : TRUE; } /** * Trims the textcaptcha_questions and textcaptcha_answers tables. */ function textcaptcha_trim_tables() { $limit = variable_get('textcaptcha_question_limit', 1000); if (intval($limit) > 0) { $sql = 'DELETE FROM {textcaptcha_questions} WHERE qid NOT IN ( SELECT qid FROM ( SELECT qid FROM {textcaptcha_questions} ORDER BY qid DESC LIMIT %d ) intermediary )'; if (db_query($sql, $limit)) { watchdog('textcaptcha', '@table table trimmed.', array('@table' => 'textcaptcha_questions')); } else { watchdog('textcaptcha', 'Error trimming the @table table.', array('@table' => 'textcaptcha_questions'), WATCHDOG_WARNING); } $sql = 'DELETE FROM {textcaptcha_answers} WHERE qid NOT IN ( SELECT qid FROM {textcaptcha_questions} )'; if (db_query($sql)) { watchdog('textcaptcha', '@table table trimmed.', array('@table' => 'textcaptcha_answers')); } else { watchdog('textcaptcha', 'Error trimming the @table table', array('@table' => 'textcaptcha_answers'), WATCHDOG_WARNING); } } } /** * Get new challenge questions. */ function textcaptcha_get_new_challenges() { $success = 0; $total_successes = 0; for ($i = 0; $i < 10; $i++) { if ($challenge = textcaptcha_get_new_challenge()) { $new_challenge = textcaptcha_challange_add($challenge); if ($new_challenge) { $success++; } $total_successes++; } } if ($success < 1) { $text = t('Sorry. After trying !i times, I was unable to retrieve any new challenge questions. Either your API key is bad, you are not online, or the web service you are trying to reach is not responding.', array('!i' => $i)); drupal_set_message($text, 'warning'); } else { $text = t('Success! You have !success new challenge questions. (!total/!i attempts to retrieve new challenge questions were successful.)', array( '!success' => $success, '!total' => $total_successes, '!i' => $i, ) ); drupal_set_message($text, 'success'); } } /** * Retrieve a new challenge from the Text CAPTCHA web service. * * @return int * Returns a challenge array or FALSE if failed attempt to * reach textcaptcha.com */ function textcaptcha_get_new_challenge() { // Attempt to retrieve a new challenge from the web service. $success = FALSE; for ($i = 1; $i <= 10; $i++) { $response = textcaptcha_http_request(); if (!$response->code == 200) { // We didn't get a successful response. Log failed attempt. $text = t('Failed attempt !n to get textcaptcha question from !api.', array('!n' => $i, '!api' => TEXTCAPTCHA_URL_PREFIX)); watchdog('textcaptcha', $text); } else { // Log success. $success = TRUE; $text = t('Successfully retrieved new textcaptcha challenge question on attempt !n', array('!n' => $i)); watchdog('textcaptcha', $text); break; } } return ($success) ? textcaptcha_parse_data($response->data) : FALSE; } /** * Wrapper around textcaptcha API call. * * @return array * Response from drupal_http_request() to textcaptcha web service. */ function textcaptcha_http_request() { $response = drupal_http_request(TEXTCAPTCHA_URL_PREFIX . trim(variable_get('textcaptcha_api_key', ''))); return $response; } /** * Parse return from textcaptcha.com api calls. * * @param array $data * Data from a successful drupal_http_request() call to textcaptcha.com API * * @return array * Return array including challenge and md5 hash of valid answers. * e.g. $challenge( * 'question' => 'What color is the sky?', * 'answers' => array('c9f0f895f', '24d27c169'), * ); */ function textcaptcha_parse_data($data) { $parser = drupal_xml_parser_create($data); $captcha_xml = array(); xml_parse_into_struct($parser, $data, $captcha_xml); $challenge = array(); foreach ($captcha_xml as $element) { if ($element['tag'] == 'QUESTION' && $element['type'] == 'complete') { $challenge['question'] = $element['value']; } if ($element['tag'] == 'ANSWER' && $element['type'] == 'complete') { $challenge['answers'][] = $element['value']; } } return $challenge; } /** * Implements hook_captcha(). */ function textcaptcha_captcha($op, $captcha_type='') { switch ($op) { case 'list': return array('Text Captcha'); case 'generate': $result = array(); if ($captcha_type == 'Text Captcha') { $challenge = textcaptcha_get_captcha(); $result['form']['captcha_response'] = array( '#type' => 'textfield', '#title' => $challenge['question'], '#description' => t('Question text provided by !link', array('!link' => l(t('textcaptcha.com'), 'http://textcaptcha.com')) ), '#size' => 50, '#maxlength' => 50, '#required' => TRUE, // For accessibility put the challenge description in front of the // challenge. This way it will be read by screen readers. '#prefix' => '' . variable_get('textcaptcha_challenge_description', '') . '
', '#suffix' => '
', ); $result['solution'] = serialize($challenge['answers']); $result['captcha_validate'] = 'textcaptcha_captcha_validate'; return $result; } break; } } /** * Retrieve captcha from service. * * @return array * Question and appropriate answer hashes. */ function textcaptcha_get_captcha() { // Right now, just proxy textcaptcha_get_random_question(). // This should be able to grab a question from multiple sources. return textcaptcha_get_random_question(); } /** * Implements hook_cron(). * * Every time cron runs it fetches new challenge questions. */ function textcaptcha_cron() { if (variable_get('textcaptcha_cron', 1)) { textcaptcha_get_new_challenges(); } if (variable_get('textcaptcha_question_limit', 1000)) { textcaptcha_trim_tables(); } } /** * Retrieve random question/answer. * * @return array * Question and appropriate answer hashes from database. */ function textcaptcha_get_random_question() { $textcaptcha = array(); $result = db_fetch_array(db_query("SELECT * FROM {textcaptcha_questions} ORDER BY RAND() LIMIT 1")); $textcaptcha['question'] = $result['question']; $answers = db_query("SELECT * FROM {textcaptcha_answers} WHERE qid = %d", $result['qid']); $textcaptcha['answers'] = array(); while ($answer = db_fetch_array($answers)) { $textcaptcha['answers'][] = $answer['answer']; } return $textcaptcha; } /** * Custom CAPTCHA validation callback. */ function textcaptcha_captcha_validate($solution, $response) { if (!unserialize($solution)) { return TRUE; } return (in_array(md5(drupal_strtolower(trim($response))), unserialize($solution))); }