' . t('A captcha is a tool to fight automated spam submission of forms (e.g. user registration forms, comment forms, guestbook forms, etc.) by malicious users. A captcha is an extra field (or several fields) on a form presented to the user. It represents a challenge, which should be easy for a normal human to solve (e.g. a simple math problem), but hard enough to keep automated scripts and spam bots out.') . '

'; case 'admin/user/captcha': case 'admin/user/captcha/captcha': case 'admin/user/captcha/captcha/settings': return t('

A captcha can be added to virtually each drupal form (which is identified by a form_id). The captcha module comes with some default forms to add a captcha to, but arbitrary forms can be easily added and managed when the option "%adminlinks" is enabled.

Users with the "%skipcaptcha" permission won\'t be offered a captcha. Be sure to grant this permission to the trusted users (e.g. site administrators). If you want to test the captcha challenges, be sure to do it as a user without the "%skipcaptcha" permission (e.g. as unauthenticated user).

', array( '@perm' => url('admin/user/access'), '%adminlinks' => t('Add captcha adminstration links to forms'), '%skipcaptcha' => 'skip captcha challenges', )); } return $output; } /** * Implementation of hook_menu(). */ function captcha_menu($may_cache) { $items = array(); if ($may_cache) { // main configuration page of the basic captcha module $items[] = array( 'path' => 'admin/user/captcha', 'title' => t('Captcha'), 'description' => t('Administer how and where Captchas are used.'), 'callback' => 'captcha_admin', 'access' => user_access('administer captcha'), 'type' => MENU_NORMAL_ITEM, ); // the default local task (needed when other modules want to offer alternative // captcha challenges and their own configuration page as local task) $items[] = array( 'path' => 'admin/user/captcha/captcha', 'title' => t('Captcha'), 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -20, ); $items[] = array( 'path' => 'admin/user/captcha/captcha/settings', 'title' => t('General settings'), 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => 0, ); $items[] = array( 'path' => 'admin/user/captcha/captcha/examples', 'title' => t('Examples'), 'description' => t('An overview of the available captcha types with examples.'), 'callback' => 'captcha_examples', 'type' => MENU_LOCAL_TASK, 'weight' => 5, ); } return $items; } /** * Implementation of hook_perm(). */ function captcha_perm() { return array('administer captcha', 'skip captcha challenges'); } /** * Return an array with the available captcha types, for use as options array * for a select form elements. * The array is an associative array mapping "$module/$challenge" to * "$module/$challenge" with $module the module name implementing the captcha * challenge and $challenge the name of the challenge type. * (It also includes a 'none' => 'none' option) */ function _captcha_available_challenge_types(){ $captcha_challenges['none'] = 'none'; foreach(module_implements('captcha') as $module) { $result = call_user_func_array($module .'_captcha', 'list'); if (is_array($result)) { foreach($result as $challenge) { $captcha_challenges["$module/$challenge"] = "$challenge ($module)"; } } } return $captcha_challenges; } /** * Get the description which appears above the captcha in forms. * If the locale module is enabled, an optional language code can be given */ function _captcha_get_description($lang_code=NULL) { if (module_exists('locale')) { if ($lang_code == NULL) { global $locale; $lang_code = $locale; } $description = variable_get("captcha_description_$lang_code", t('This question is for testing whether you are a human visitor and to prevent automated spam submissions.')); } else { $description = variable_get('captcha_description', t('This question is for testing whether you are a human visitor and to prevent automated spam submissions.')); } return $description; } /** * General Captcha settings handler. Main entry point for captcha management * * If arguments are given: first argument is used as form_id, the second one * is interpreted as action (such as disable, delete and enable) to execute on * the form_id. * Otherwise: returns the general captcha configuration form. */ function captcha_admin($form_id='', $op='') { // if $form_id and action $op given: do the action if ($form_id) { switch ($op) { case 'disable': // disable the captcha for the form: set the module and type to NULL db_query("UPDATE {captcha_points} SET module = NULL, type = NULL WHERE form_id = '%s'", $form_id); drupal_set_message(t('Disabled captcha for form %form_id.', array('%form_id'=>$form_id))); // goto the captcha adminstration or alternative destination if present in URI drupal_goto('admin/user/captcha'); break; case 'delete': db_query("DELETE FROM {captcha_points} WHERE form_id = '%s'", $form_id); drupal_set_message(t('Deleted captcha for form %form_id.', array('%form_id'=>$form_id))); // goto the captcha adminstration or alternative destination if present in URI drupal_goto('admin/user/captcha'); break; case 'enable': db_query("DELETE FROM {captcha_points} WHERE form_id = '%s'", $form_id); db_query("INSERT INTO {captcha_points} (form_id, module, type) VALUES ('%s', NULL, NULL)", $form_id); // No drupal_goto() call because we have to go to the captcha adminstration // form and not a different destination if that would be present in the // URI. So we call this form explicitly. The destination will be preserved // so after completing the form, the user will still be redirected. drupal_get_form('captcha_admin_settings', $form_id); break; } } // no $form_id or legal action given: generate general captcha settings form return drupal_get_form('captcha_admin_settings', $form_id); } /** * Form builder function for the general captcha configuration */ function captcha_admin_settings($form_id='') { // field for the captcha adminstration mode $form['captcha_administration_mode'] = array( '#type' => 'checkbox', '#title' => t('Add captcha adminstration links to forms'), '#default_value' => variable_get('captcha_administration_mode', FALSE), '#description' => t('This option is very helpful to enable/disable captchas on forms. When enabled, users with the "%admincaptcha" permission will see captcha administration links on all forms (except on administrative pages, which shouldn\'t be accessible to untrusted users in the first place).', array('%admincaptcha' => 'administer captcha')), ); // field set with form_id -> captcha type configuration $form['captcha_types'] = array( '#type' => 'fieldset', '#title' => t('Select captcha types'), '#description' => t('Select what kind of captcha challenge you want for each form.'), '#tree' => TRUE, '#collapsible' => TRUE, '#collapsed' => FALSE, '#theme' => 'captcha_admin_settings_captcha_points', ); // list all possible form_id's $captcha_challenges = _captcha_available_challenge_types(); $result = db_query("SELECT * FROM {captcha_points} ORDER BY form_id"); while ($captcha_point = db_fetch_object($result)) { $form['captcha_types'][$captcha_point->form_id]['form_id'] = array( '#value' => $captcha_point->form_id, ); // select widget for captcha type $form['captcha_types'][$captcha_point->form_id]['captcha_type'] = array( '#type' => 'select', '#default_value' => "{$captcha_point->module}/{$captcha_point->type}", '#options' => $captcha_challenges, ); // if a form_id was given as argument of this form builder, highlight the captcha type select widget if ($form_id == $captcha_point->form_id) { $form['captcha_types'][$captcha_point->form_id]['captcha_type']['#attributes'] = array('class'=>'error'); } // additional operations $form['captcha_types'][$captcha_point->form_id]['operations'] = array( '#value' => implode(", ", array( l(t('delete'), "admin/user/captcha/{$captcha_point->form_id}/delete"), )) ); } // field(s) for setting the additional captcha description if (module_exists('locale')) { global $locale; $langs = locale_supported_languages(); $form['captcha_descriptions'] = array( '#type' => 'fieldset', '#title' => t('Captcha description'), '#description' => t('With this description you can explain the purpose of the captcha challenge to the user.'), ); foreach ($langs['name'] as $lang_code => $lang_name) { $form['captcha_descriptions']["captcha_description_$lang_code"] = array( '#type' => 'textfield', '#title' => t('Captcha description for %lang_name (locale %lang_code)', array('%lang_name'=>$lang_name, '%lang_code'=>$lang_code)), '#default_value' => _captcha_get_description($lang_code), ); } } else { $form['captcha_description'] = array( '#type' => 'textfield', '#title' => t('Captcha description'), '#description' => t('With this description you can explain the purpose of the captcha challenge to the user.'), '#default_value' => _captcha_get_description(), ); } // field for captcha persistence $form['captcha_persistence'] = array( '#type' => 'checkbox', '#title' => t('Persistent captchas'), '#description' => t('If checked, the user will always have to solve a captcha. If not checked, the captcha check for a form will be omitted during the rest of the session once the user has successfully solved a captcha for that form.'), '#default_value' => variable_get('captcha_persistence', TRUE), ); // submit button $form['submit'] = array( '#type' => 'submit', '#value' => t('Submit'), ); return $form; } /** * Custom theme function for a table of (form_id -> captcha type) settings */ function theme_captcha_admin_settings_captcha_points($form) { foreach (element_children($form) as $key) { $row = array(); $row[] = drupal_render($form[$key]['form_id']); $row[] = drupal_render($form[$key]['captcha_type']); $row[] = drupal_render($form[$key]['operations']); $rows[] = $row; } $header = array('form_id', t('Captcha type (module)'), t('Operations')); $output = theme('table', $header, $rows); return $output; } /** * Submission function for captcha_admin_settings form */ function captcha_admin_settings_submit($form_id, $form_values) { if ($form_id == 'captcha_admin_settings') { variable_set('captcha_administration_mode', $form_values['captcha_administration_mode']); foreach ($form_values['captcha_types'] as $form_id => $data) { if ($data['captcha_type'] == 'none') { db_query("UPDATE {captcha_points} SET module = NULL, type = NULL WHERE form_id = '%s'", $form_id); } else { list($module, $type) = explode('/', $data['captcha_type']); db_query("UPDATE {captcha_points} SET module = '%s', type = '%s' WHERE form_id = '%s'", $module, $type, $form_id); } } // description stuff if (module_exists('locale')) { $langs = locale_supported_languages(); foreach ($langs['name'] as $lang_code => $lang_name) { variable_set("captcha_description_$lang_code", $form_values["captcha_description_$lang_code"]); } } else { variable_set('captcha_description', $form_values['captcha_description']); } variable_set('captcha_persistence', $form_values['captcha_persistence']); drupal_set_message(t('Your captcha settings were saved.'), 'status'); } } /** * Implementation of hook_form_alter(). * * This function adds a captcha to forms for untrusted users if needed and adds * captcha adminstration links for site adminstrators if this option is enabled. */ function captcha_form_alter($form_id, &$form) { global $user; if (!user_access('skip captcha challenges')) { // Visitor does not have permission to skip the captcha challenge // Do not present captcha if not captcha-persistent and user has already solved a captcha for this form if(!variable_get('captcha_persistence', TRUE) && ($_SESSION['captcha'][$form_id]['success'] === TRUE)) { return; } // Get captcha type and module for this form. Return if no captcha was set. $result = db_query("SELECT module, type FROM {captcha_points} WHERE form_id = '%s'", $form_id); if (!$result) { return; } $captcha_point = db_fetch_object($result); if (!$captcha_point->type) { return; } // Generate a captcha challenge and its solution $captcha = module_invoke($captcha_point->module, 'captcha', 'generate', $captcha_point->type); if (!$captcha) { //The selected module returned nothing, maybe it is disabled or it's wrong, we should watchdog that and then quit. watchdog('captcha', t('Captcha problem: hook_captcha() of module %module returned nothing when trying to retrieve captcha type %type for form %form_id.', array('%type' => $captcha_point->type, '%module' => $captcha_point->module, '%form_id'=> $form_id)), WATCHDOG_ERROR); return; } // Add a captcha part to the form (depends on value of captcha_description) $captcha_description = _captcha_get_description(); if ($captcha_description) { // $captcha_description is not empty: captcha part is a fieldset with description $form['captcha'] = array( '#type' => 'fieldset', '#title' => t('Captcha'), '#description' => $captcha_description, ); } else { // $captcha_description is empty: captcha part is an empty markup form element $form['captcha'] = array(); } // Add the form elements of the generated captcha challenge to the form $form['captcha'] = array_merge($form['captcha'], $captcha['form']); // Store the solution of the generated captcha challenge as an internal form value. // This will be stored later in $_SESSION during the pre_render phase. // It can't be saved at this point because hook_form_alter is not only run // before form rendering, but also before form validation (which happens // in a new (POST) request. Consequently the right captcha solution would be // overwritten just before validation. The pre_render functions are not run // before validation and are the right place to store the solution in $_SESSION. $form['captcha']['captcha_solution'] = array ( '#type' => 'value', '#value' => $captcha['solution'], ); // The captcha token is used to differentiate between different instances // of the same form. This makes it possible to request the same form a // couple of times before submitting them. The solution of the captcha of // each of these form instances will be stored at the pre_render phase in // $_SESSION['captcha'][$form_id][$captcha_token] $form['captcha']['captcha_token'] = array ( '#type' => 'hidden', '#value' => md5(mt_rand()), ); // other internal values needed for the validation phase $form['captcha']['validationdata'] = array( '#type' => 'value', '#value' => array( 'form_id' => $form_id, 'preprocess' => isset($captcha['preprocess'])? $captcha['preprocess'] : FALSE, 'module' => $captcha_point->module, 'type' => $captcha_point->type, ), ); // handle the pre_render functions $form['#pre_render'] = ((array) $form['#pre_render']) + array('captcha_pre_render', 'captcha_pre_render_place_captcha'); // Add a validation function for the captcha part of the form $form['captcha']['#validate'] = ((array) $form['captcha']['#validate']) + array('captcha_validate' => array()); // prevent caching of the page with this captcha enabled form global $conf; $conf['cache'] = FALSE; } elseif (user_access('administer captcha') && variable_get('captcha_administration_mode', FALSE) && arg(0) != 'admin') { // For administrators: show captcha info and offer link to configure it $result = db_query("SELECT module, type FROM {captcha_points} WHERE form_id = '%s'", $form_id); if (!$result) { return; } $captcha_point = db_fetch_object($result); if ($captcha_point->type) { $form['captcha'] = array( '#type' => 'item', '#title' => t('Captcha administration'), '#description' => t('The captcha challenge "@type" (by module "@module") is enabled here for untrusted users: !edit or !disable.', array( '@type' => $captcha_point->type, '@module' => $captcha_point->module, '!edit' => l(t('edit captcha type'), "admin/user/captcha/$form_id", array(), drupal_get_destination()), '!disable' => l(t('disable captcha'), "admin/user/captcha/$form_id/disable", array(), drupal_get_destination()), )) ); } else { $form['captcha'] = array( '#type' => 'item', '#title' => t('Captcha administration'), '#description' => l(t('Place a captcha challenge here for untrusted users.'), "admin/user/captcha/$form_id/enable", array(), drupal_get_destination()), ); } // Add pre_render function for placing the captcha just above the submit button $form['#pre_render'] = ((array) $form['#pre_render']) + array('captcha_pre_render_place_captcha'); } } /** * Implementation of form #validate. */ function captcha_validate($form_values) { // Get answer and preprocess if needed $captcha_response = $form_values['#post']['captcha_response']; $validationdata = $form_values['validationdata']['#value']; if ($validationdata['preprocess']) { $captcha_response = module_invoke($validationdata['module'], 'captcha', 'preprocess', $validationdata['type'], $captcha_response); } $form_id = $validationdata['form_id']; $captcha_token = $form_values['#post']['captcha_token']; // Check if captcha_token exists if (!isset($_SESSION['captcha'][$form_id][$captcha_token])) { form_set_error('captcha_token', t('Invalid captcha token.')); } // Check answer if ($captcha_response === $_SESSION['captcha'][$form_id][$captcha_token]) { $_SESSION['captcha'][$form_id]['success'] = TRUE; } else { form_set_error('captcha_response', t('The answer you entered for the captcha challenge was not correct.')); } // Unset the solution to prevent reuse of the same captcha solution // by a spammer that repeats posting a form without requesting // (and thus rendering) a new form. Note that a new captcha solution is only // set at the pre_render phase. unset($_SESSION['captcha'][$form_id][$captcha_token]); } /** * Implementation of form #pre_render. * * The main purpose of this function is to store the solution of the captcha * challenge in the $_SESSION variable. */ function captcha_pre_render($form_id, &$form) { // Unset the captcha challenge if non-captcha persistent and the captcha has // already been successfully solved for this form. // This needs to be done in this pre_render phase when previewing for example // nodes and comments before submission. // On submission of such a forms for preview, captcha_form_alter() is called // *before* the captcha validation function (which sets // $_SESSION['captcha'][$form_id]['success'] to TRUE on a correctly answered // captcha). After this the form_values are entered in the generated form // and this form is presented with the preview. // This means that captcha_form_alter() can't know if the captcha was // correctly answered and consequently adds a captcha challenge to the form. // The pre_render phase happens after the validation phase and makes it // possible to remove the captcha from the form after all. if (!variable_get('captcha_persistence', TRUE) && ($_SESSION['captcha'][$form_id]['success'] === TRUE)) { unset($form['captcha']); return; } // count the number of unsolved captcha challenges and flush those if too many // minus 1 is needed because 'success' is also an item of $_SESSION['captcha'][$form_id] if (count($_SESSION['captcha'][$form_id]) - 1 > CAPTCHA_UNSOLVED_CHALLENGES_MAX) { unset($_SESSION['captcha'][$form_id]); drupal_set_message(t('You can\'t request more than @num captcha challenges without solving them. Your previous captcha challenges were flushed.', array('@num' => CAPTCHA_UNSOLVED_CHALLENGES_MAX))); } // store the current captcha solution in $_SESSION $captcha_token = $form['captcha']['captcha_token']['#value']; $_SESSION['captcha'][$form_id][$captcha_token] = $form['captcha']['captcha_solution']['#value']; $_SESSION['captcha'][$form_id]['success'] = FALSE; // empty the value of the captcha_response form item before rendering $form['captcha']['captcha_response']['#value'] = ''; } /** * Pre_render function to place the captcha form element just above the last submit button */ function captcha_pre_render_place_captcha($form_id, &$form) { // search the weights of the buttons in the form $button_weights = array(); foreach(element_children($form) as $key) { if ($form[$key]['#type'] == 'submit' || $form[$key]['#type'] == 'button') { $button_weights[] = $form[$key]['#weight']; } } if ($button_weights) { // set the weight of the captcha element a tiny bit smaller than the lightest button weight // (note that the default resolution of #weight values is 1/1000 (see drupal/includes/form.inc)) $first_button_weigth = min($button_weights); $form['captcha']['#weight'] = $first_button_weigth - 0.5/1000.0; // make sure the form gets sorted before rendering unset($form['#sorted']); } } /** * Funtion for generating a page with captcha challenge examples * If the arguments $module and $challenge are not set, generate a list with * examples of the available captcha challenge types. * If $module and $challenge are set, generate 10 examples of the concerning * challenge. */ function captcha_examples($module=NULL, $challenge=NULL) { if ($module && $challenge) { // generate 10 examples $output = ''; for ($i=0; $i<10; $i++) { // generate captcha challenge $captcha = call_user_func_array($module .'_captcha', array('generate', $challenge)); $form = $captcha['form']; $id = "captcha_examples_$module_$challenge_$i"; drupal_process_form($id, $form); $output .= drupal_render_form($id, $form); } } else { // generate a list with examples of the available challenges $output = t('This page gives an overview of all available captcha types, generated with their current settings.'); foreach(module_implements('captcha') as $module) { $challenges = call_user_func_array($module .'_captcha', 'list'); if ($challenges) { foreach($challenges as $challenge) { // generate captcha challenge $captcha = call_user_func_array($module .'_captcha', array('generate', $challenge)); // build form $form = array(); $form['captcha'] = array( '#type' => 'fieldset', '#title' => t('Challenge "%challenge" by module "%module"', array('%challenge' => $challenge, '%module' => $module)), ); $form['captcha'] = array_merge($form['captcha'], $captcha['form']); $form['captcha']['more_examples'] = array( '#type' => 'markup', '#value' => l(t('10 more examples of this captcha type.'), "admin/user/captcha/captcha/examples/$module/$challenge"), ); // return rendered form $id = "captcha_examples_$module_$challenge"; drupal_process_form($id, $form); $output .= drupal_render_form($id, $form); } } } } return $output; } /** * Default implementation of hook_captcha */ function captcha_captcha($op, $captcha_type='') { switch($op) { case 'list': return array('Math'); case 'generate': if ($captcha_type == 'Math') { $result = array(); $answer = mt_rand(1, 20); $x = mt_rand(1, $answer); $y = $answer - $x; $result['solution'] = "$answer"; $result['form']['captcha_response'] = array ( '#type' => 'textfield', '#title' => t('Math Question'), '#description' => t('Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.'), '#field_prefix' => t('@x + @y = ', array('@x' => $x, '@y' => $y)), '#size' => 4, '#maxlength' => 2, '#required' => TRUE, ); return $result; } } }