You are here

botcha.module in BOTCHA Spam Prevention 7

BOTCHA - Spam Prevention It modifies forms by adding various botcha's.

File

botcha.module
View source
<?php

/**
 * @file
 * BOTCHA - Spam Prevention
 * It modifies forms by adding various botcha's.
 */
define('BOTCHA_LOG', 'BOTCHA');
define('BOTCHA_LOGLEVEL', variable_get('botcha_loglevel', 1));

/** BOTCHA_LOGLEVEL:
 *  0 - no log
 *  1 - log blocked/bad submissions only
 *  2 - also log why blocked
 *  3 - also log good submissions
 *  4 - also log when preparing forms
 *  5 - log extra submission details
 *  6 - misc development items
 * Please note!: Level 5 and 6 could cause putting vulnerable data into logs.
 *   We have some basic escaping (e.g., for password field) - but any other data
 *   could be found in raw format. Please be careful with logging level setting!
 */

/**
 * Implements hook_help().
 */
function botcha_help($path, $arg) {
  switch ($path) {
    case 'admin/help#botcha':
      $output = '<p>' . t('"BOTCHA" is an acronym for "BOT Computerized Heuristic Analysis". It is a method of protection from automated form submissions by performing analysis of submitted data that determines whether the user is a bot. The BOTCHA module is a tool to fight automated submission by malicious users that utilize automated form submission (e.g. for spamming) of for example comments forms, user registration forms, guestbook forms, etc. BOTCHA inserts elements into the desired forms that will not be shown to normal users. These elements have no impact on humans and require no puzzles to solve, but they are easy enough for automated scripts and spam bots to trip on.') . '</p>';

      // UNUSED
      //      $output .= '<p>' . t('Note that BOTCHA module interacts with page caching (see <a href="!performancesettings">performance settings</a>). Because BOTCHA elements should be unique for each generated form, the caching of the page it appears on is prevented. Make sure that these forms do not appear on too many pages or you will lose much caching efficiency. For example, if you put a BOTCHA on the user login block, which typically appears on each page for anonymous visitors, caching will practically be disabled. The comment submission forms are another example. In this case you should set the "%commentlocation" to "%separatepage" in the comment settings of the relevant <a href="!contenttypes">content types</a> for better caching efficiency.',
      //        array(
      //          '!performancesettings' => url('admin/config/development/performance'),
      //          '%commentlocation' => t('Location of comment submission form'),
      //          '%separatepage' => t('Display on separate page'),
      //          '!contenttypes' => url('admin/structure/types'),
      //        )
      //      ) .'</p>';
      //      $output .= '<p>'. t('BOTCHA is a trademark of IVA2K.') .'</p>';
      return $output;
    case 'admin/config/people/botcha':
      module_load_include('inc', 'botcha');
      $output = '<p>' . t('A BOTCHA protection can be added to virtually each Drupal form. Some default forms are already provided in the form list and more can be added using form internal name.') . '</p>';
      $output .= '<p>' . t('All existing forms can be easily added and managed when the option "%adminlinks" is enabled.', array(
        '%adminlinks' => t('Add BOTCHA administration links to forms'),
      )) . '</p>';
      if (botcha_is_captcha_installed()) {
        $output .= '<p>' . t('Other forms will be added automatically based on CAPTCHA settings when the option "%usecaptcha" is enabled.', array(
          '%usecaptcha' => t('Add BOTCHA to forms selected for CAPTCHA'),
        )) . '</p>';
      }
      $output .= '<p>' . t('Forms served to users with the "%skipbotcha" <a href="@perm">permission</a> won\'t be protected. Be sure to grant this permission to the trusted users (e.g. site administrators). If you want to test a protected form, be sure to do it as a user without the "%skipbotcha" permission (e.g. as anonymous user).', array(
        '%skipbotcha' => t('skip BOTCHA'),
        '@perm' => url('admin/people/permissions/list', array(
          'fragment' => 'module-' . 'botcha',
        )),
      )) . '</p>';
      return $output;
  }
}

/**
 * Implements hook_menu().
 */
function botcha_menu() {
  $items = array();

  // main configuration page of BOTCHA module
  $items['admin/config/people/botcha'] = array(
    'title' => 'BOTCHA',
    'description' => 'Administer how and where BOTCHAs are used.',
    'file' => 'botcha.pages.inc',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'botcha_admin_settings',
    ),
    'access arguments' => array(
      'administer BOTCHA settings',
    ),
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/config/people/botcha/botcha_point'] = array(
    'title' => 'BOTCHA point administration',
    'file' => 'botcha.pages.inc',
    'page callback' => 'botcha_point_admin',
    'page arguments' => array(
      5,
      6,
    ),
    'access arguments' => array(
      'administer BOTCHA settings',
    ),
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Implements hook_permission().
 */
function botcha_permission() {
  return array(
    'administer BOTCHA settings' => array(
      'title' => t('Administer BOTCHA settings'),
    ),
    'skip BOTCHA' => array(
      'title' => t('Skip BOTCHA'),
      'description' => t('Users with this permission will not be subjected to BOTCHA.'),
    ),
  );
}

/**
 * Implements hook_theme().
 */
function botcha_theme() {
  return array(
    'botcha_admin_settings_botcha_points' => array(
      'render element' => 'form',
    ),
  );
}

/**
 * Implements hook_form_alter().
 *
 * This function adds BOTCHA protection to forms for untrusted users if needed and adds
 * BOTCHA administration links for site administrators if this option is enabled.
 */
function botcha_form_alter(&$form, &$form_state, $form_id) {
  $botcha = FALSE;
  if (!user_access('skip BOTCHA')) {

    // Visitor does not have permission to skip the BOTCHA
    module_load_include('inc', 'botcha');

    // Get BOTCHA type for given form_id.
    $botcha_point = botcha_get_form_id_setting($form_id);
    if ($botcha_point) {
      $botcha = !$botcha_point->botcha_type || $botcha_point->botcha_type == 'none' ? FALSE : $botcha_point->botcha_type;
    }
    elseif (variable_get('botcha_on_captcha_forms', TRUE) && botcha_is_captcha_installed()) {

      // Check captcha.module settings. If there is a captcha, there is a botcha.
      // Botcha's don't hurt humans, so we don't implement any logic to bypass
      // the Botcha.
      $captcha_point = botcha_get_captcha_point($form_id, TRUE);
      if ($captcha_point && $captcha_point != 'none') {
        $botcha = 'default';
      }
    }
  }
  if (variable_get('botcha_administration_mode', FALSE) && user_access('administer BOTCHA settings') && (arg(0) != 'admin' || variable_get('botcha_allow_on_admin_pages', FALSE) || $form_id == 'user_register')) {

    // Add BOTCHA administration tools.
    module_load_include('inc', 'botcha');
    $botcha_point = botcha_get_form_id_setting($form_id);

    // For administrators: show BOTCHA info and offer link to configure it
    $botcha_element = array(
      '#type' => 'fieldset',
      '#title' => t('BOTCHA'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#attributes' => array(
        'class' => array(
          'botcha-admin-links',
        ),
      ),
    );
    if ($botcha_point !== NULL && $botcha_point->botcha_type && $botcha_point->botcha_type != 'none') {
      $botcha_element['#title'] = t('BOTCHA: protection enabled (@type cookbook)', array(
        '@type' => $botcha_point->botcha_type,
      ));
      $botcha_element['#description'] = t('Untrusted users will have form %form_id protected by BOTCHA (!settings).', array(
        '%form_id' => $form_id,
        '!settings' => l(t('general BOTCHA settings'), 'admin/config/people/botcha'),
      ));
      $botcha_element['protection'] = array(
        '#type' => 'item',
        '#title' => t('Enabled protection'),
        '#markup' => t('"@type" cookbook (!change, !disable)', array(
          '@type' => $botcha_point->botcha_type,
          '!change' => l(t('change'), "admin/config/people/botcha/botcha_point/{$form_id}", array(
            'query' => drupal_get_destination(),
            'html' => TRUE,
          )),
          '!disable' => l(t('disable'), "admin/config/people/botcha/botcha_point/{$form_id}/disable", array(
            'query' => drupal_get_destination(),
            'html' => TRUE,
          )),
        )),
      );
    }
    else {
      $botcha_element['#title'] = t('BOTCHA: no protection enabled');
      $botcha_element['add_botcha'] = array(
        '#markup' => l(t('Add BOTCHA protection on form %form_id for untrusted users.', array(
          '%form_id' => $form_id,
        )), "admin/config/people/botcha/botcha_point/{$form_id}", array(
          'query' => drupal_get_destination(),
          'html' => TRUE,
        )),
      );
    }

    // Get placement in form and insert in form.
    $botcha_placement = _botcha_get_botcha_placement($form_id, $form);
    _botcha_insert_botcha_element($form, $botcha_placement, $botcha_element);
  }
  switch ($form_id) {

    /* UNUSED
        case 'user_admin_settings':
          // Insert BOTCHA settings into admin/user/settings
          module_load_include('inc', 'botcha', 'botcha.pages');
          $myform = _botcha_admin_settings($form_state);
          $form['register'] = array(
            '#type' => 'fieldset',
            '#title' => t('User Register settings'),
            '#description' => t('These options adjust User Register form (provided by <em>BOTCHA</em> module)'),
            '#collapsible' => TRUE,
            '#collapsed' => TRUE,
          );
          $form['register'] += $myform;
          $form['register']['#weight'] = $form['registration']['#weight'] + 0.001;
          foreach ($myform['#submit'] as $func) {
            $form['#submit'][] = $func;
          }
          break;
    */
    case 'user_register':
      if (FALSE === strpos($form['#action'], 'user/register')) {
        if (!variable_get('botcha_allow_on_admin_pages', FALSE)) {
          $botcha = FALSE;
        }

        // Only change the registration form. There is also 'user_register' form at /admin/config/people/user/create path, but we leave it alone.
      }
      break;
  }
  if ($botcha) {
    $form['#process'] = array(
      'botcha_fprocess',
    );
    module_load_include('inc', 'botcha', 'botcha.botcha');
    botcha_form_alter_botcha($form, $form_state, $form_id, $botcha);
    $form_state['no_cache'] = TRUE;
  }

  // UNUSED
  //  // Add a warning about caching on the Perfomance settings page.
  //  if ($form_id == 'system_performance_settings') {
  //    $icon = theme('image', array('path' => 'misc/watchdog-warning.png', 'width' => 18, 'height' => 18, 'alt' => t('warning'), 'title' => t('warning')));
  //    $form['caching']['botcha'] = array(
  //      '#type' => 'item',
  //      '#title' => t('BOTCHA'),
  //      '#markup' => t('!icon The BOTCHA module will disable the caching of pages that contain forms processed by BOTCHA.', array(
  //        '!icon' => '<span class="icon">' . $icon . '</span>')
  //      ),
  //      '#attributes' => array('class' => array('warning')),
  //    );
  //  }
}

/**
 * Process form callback for BOTCHA form.
 * Using hooking mechanism that is a hack - we want to come in before #process,
 * but our _form_alter() is way too early (we want to let it use cache for all other
 * modules & data). Ideal would be to have #process callback
 * work on the form, but FAPI does not call it on forms, despite the
 * documentation stating that it does. In fact, D6 code needs '#input'=TRUE on
 * the element to get into calling #process callbacks. So we set '#input'=>TRUE
 * on the form when inserting our callback into form['#process'], and it forces
 * the FAPI to call that code path and we get our intercept. Works like a charm!
 */
function botcha_fprocess($element, &$form_state) {

  // UNUSED
  //  // Prevent caching of the page with BOTCHA elements.
  //  // This needs to be done even if the BOTCHA will be ommitted later:
  //  // other untrusted users should not get a cached page.
  //  global $conf;
  //  $conf['cache'] = 0;
  //  // This temporarily disables cache (for this page request)
  //  // Pressflow in CACHE_EXTERNAL caching mode additionally requires to mark
  //  // this request as non-cacheable to bypass external caches (e.g., Varnish).
  //  if (function_exists('drupal_page_is_cacheable')) {
  //    drupal_set_header('Cache-Control', 'no-cache, must-revalidate, post-check=0, pre-check=0', FALSE);
  //  }
  return $element;
}

/**
 * Implements hook_simpletest().
 */

// function botcha_simpletest() {
//   // Scan through botcha/tests directory for any .test files to tell SimpleTest module.
//   $tests = file_scan_directory('./tests', '/\.test/');
//   return array_keys($tests);
// }

/**
 * Implements hook_menu_alter().
 */
function botcha_menu_alter(&$items) {
}
function _botcha_variables($i18n = FALSE) {
  $ret = array();
  if (!$i18n) {
    $ret += array(
      'botcha_secret',
      'botcha_loglevel',
      'botcha_form_passed_counter',
      'botcha_form_blocked_counter',
    );
  }
  return $ret;
}
function _botcha_i18n() {
  $variables = _botcha_variables(TRUE);
  $i18n_variables = variable_get('i18n_variables', array());
  if (in_array($variables[0], $i18n_variables)) {
    return;
  }
  $i18n_variables = array_merge($i18n_variables, $variables);
  variable_set('i18n_variables', $i18n_variables);
}

/**
 * Custom form validation - jump to __botcha_form_validate().
 * FIXME: Is there a standard way to put #validate handlers in a separate file?
 */
function _botcha_form_validate($form, &$form_state) {
  module_load_include('inc', 'botcha', 'botcha.botcha');
  __botcha_form_validate($form, $form_state);
}

// END

Functions

Namesort descending Description
botcha_form_alter Implements hook_form_alter().
botcha_fprocess Process form callback for BOTCHA form. Using hooking mechanism that is a hack - we want to come in before #process, but our _form_alter() is way too early (we want to let it use cache for all other modules & data). Ideal would be to have #process…
botcha_help Implements hook_help().
botcha_menu Implements hook_menu().
botcha_menu_alter Implements hook_menu_alter().
botcha_permission Implements hook_permission().
botcha_theme Implements hook_theme().
_botcha_form_validate Custom form validation - jump to __botcha_form_validate(). FIXME: Is there a standard way to put #validate handlers in a separate file?
_botcha_i18n
_botcha_variables

Constants

Namesort descending Description
BOTCHA_LOG @file BOTCHA - Spam Prevention It modifies forms by adding various botcha's.
BOTCHA_LOGLEVEL