' . 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 embodies 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. Users with the \'skip captcha\' permission won\'t be offered a captcha. Be sure to grant this permission to the trusted users (e.g. site administrators).', array('@perm' => url('admin/user/access'))) . '

'; } 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, ); } 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 (isset($result)) { //&& is_array($result)) { foreach($result as $challenge) { $captcha_challenges["$module/$challenge"] = "$challenge ($module)"; } } } return $captcha_challenges; } /** * 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 helpfull to enable/disable captchas on forms. When enabled, users with the \'administer site configuration\' permission will see captcha adminstration links on all forms (except on administrative pages, which shouldn\'t be accessible to untrusted users in the first place).'), ); // 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 for setting the additional captcha description $form["captcha_description"] = array( '#type' => 'textfield', '#title' => t('Captcha description'), '#description' => t('With this description you can explain to the user why the captcha needs to be solved.'), '#default_value' => t(variable_get('captcha_description', CAPTCHA_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); } } 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 = variable_get('captcha_description', CAPTCHA_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' => t($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['value'], ); // The captcha token is an additional measure to make spam submission harder. // It is hidden field with a random number between 0 and RAND_MAX (typically // some billions), which will also be stored in $_SESSION in the pre_render // phase to be validated against. // Rationale: with the standard math captcha a spammer has a chance of 1/20 // on a correct guess (if he would just submit without parsing captcha challenge). // With the captcha token this chance of lucky submission is *much* smaller. // The spammer could of course parse the html of the form to get the token, // but then he could also invest in automating the captcha solving. $form['captcha']['captcha_token'] = array ( '#type' => 'hidden', '#value' => 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 site configuration') && 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_answer = $form_values['#post']['captcha_answer']; $validationdata = $form_values['validationdata']['#value']; if ($validationdata['preprocess']) { $captcha_answer = module_invoke($validationdata['module'], 'captcha', 'process', $validationdata['type'], $captcha_answer); } $form_id = $validationdata['form_id']; //Check captcha_token if ($form_values['#post']['captcha_token'] != $_SESSION['captcha'][$form_id]['captcha_token']) { form_set_error('captcha_token', t('Captcha token test failed.')); } // Check answer if ($captcha_answer === $_SESSION['captcha'][$form_id]['captcha_solution']) { $_SESSION['captcha'][$form_id]['success'] = TRUE; } else { form_set_error('captcha_answer', t('The answer you entered to the captcha challenge is 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_solution']); } /** * Implementation of form #pre_render. * * This function stores the solution of the captcha challenge in $_SESSION */ function captcha_pre_render($form_id, $form) { // store the captcha solution in $_SESSION $_SESSION['captcha'][$form_id]['captcha_solution'] = $form['captcha']['captcha_solution']['#value']; $_SESSION['captcha'][$form_id]['success'] = FALSE; // store the captcha_token in $_SESSION $_SESSION['captcha'][$form_id]['captcha_token'] = $form['captcha']['captcha_token']['#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']); } } /** * 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['value'] = "$answer"; $result['form']['captcha_answer'] = array ( '#type' => 'textfield', '#title' => t('Math Question: What is %problem?', array('%problem' => "$x + $y")), '#description' => t('Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.'), '#weight' => 0, '#required' => TRUE, ); return $result; } } }