You are here

honeypot.module in Honeypot 2.0.x

Same filename and directory in other branches
  1. 8 honeypot.module
  2. 6 honeypot.module
  3. 7 honeypot.module

Honeypot module, for deterring spam bots from completing Drupal forms.

File

honeypot.module
View source
<?php

/**
 * @file
 * Honeypot module, for deterring spam bots from completing Drupal forms.
 */
use Drupal\Core\Form\FormStateInterface;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Routing\RouteMatchInterface;

/**
 * Implements hook_help().
 */
function honeypot_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.honeypot':
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('The Honeypot module uses both the honeypot and timestamp methods of deterring spam bots from completing forms on your Drupal site. These methods are effective against many spam bots, and are not as intrusive as CAPTCHAs or other methods which punish the user. For more information, see the <a href=":url">online documentation for the Honeypot module</a>.', [
        ':url' => 'https://www.drupal.org/docs/8/modules/honeypot',
      ]) . '</p>';
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Configuring Honeypot') . '</dt>';
      $output .= '<dd>' . t('All settings for this module are on the Honeypot configuration page, under the Configuration section, in the Content authoring settings. You can visit the configuration page directly from the Honeypot configuration link below. The configuration settings are described in the <a href=":url">online documentation for the Honeypot module</a>.', [
        ':url' => 'https://www.drupal.org/docs/8/modules/honeypot/using-honeypot',
      ]) . '</dd>';
      $output .= '<dt>' . t('Setting up Honeypot in your own forms') . '</dt>';
      $output .= '<dd>' . t("Honeypot protection can be bypassed for certain user roles. For instance, site administrators, who just might be able to fill out a form in less than 5 seconds.  And, Honeypot protection can be enabled only for certain forms. Or, it can protect all forms on the site. Finally, honeypot protection can be used in any of your own forms by simply including a little code snippet included on the module's project page.") . '</dd>';
      $output .= '</dl>';
      return $output;
  }
}

/**
 * Implements hook_cron().
 */
function honeypot_cron() {

  // Delete {honeypot_user} entries older than the value of honeypot_expire.
  $expire_limit = \Drupal::config('honeypot.settings')
    ->get('expire');
  \Drupal::database()
    ->delete('honeypot_user')
    ->condition('timestamp', \Drupal::time()
    ->getRequestTime() - $expire_limit, '<')
    ->execute();
}

/**
 * Implements hook_form_alter().
 *
 * Add Honeypot features to forms enabled in the Honeypot admin interface.
 */
function honeypot_form_alter(&$form, FormStateInterface $form_state, $form_id) {

  // Don't use for maintenance mode forms (install, update, etc.).
  if (defined('MAINTENANCE_MODE')) {
    return;
  }

  // Add a tag to all forms, so that if they are cached and honeypot
  // configuration is changed, the cached forms are invalidated and honeypot
  // protection can be re-evaluated.
  $form['#cache']['tags'][] = 'config:honeypot.settings';

  // Get list of unprotected forms and setting for whether to protect all forms.
  $unprotected_forms = \Drupal::config('honeypot.settings')
    ->get('unprotected_forms');
  $protect_all_forms = \Drupal::config('honeypot.settings')
    ->get('protect_all_forms');

  // If configured to protect all forms, add protection to every form.
  if ($protect_all_forms && !in_array($form_id, $unprotected_forms)) {

    // Don't protect system forms - only admins should have access, and system
    // forms may be programmatically submitted by drush and other modules.
    if (preg_match('/[^a-zA-Z]system_/', $form_id) === 0 && preg_match('/[^a-zA-Z]search_/', $form_id) === 0 && preg_match('/[^a-zA-Z]views_exposed_form_/', $form_id) === 0) {
      honeypot_add_form_protection($form, $form_state, [
        'honeypot',
        'time_restriction',
      ]);
    }
  }
  elseif ($forms_to_protect = honeypot_get_protected_forms()) {
    foreach ($forms_to_protect as $protect_form_id) {

      // For most forms, do a straight check on the form ID.
      if ($form_id == $protect_form_id) {
        honeypot_add_form_protection($form, $form_state, [
          'honeypot',
          'time_restriction',
        ]);
      }
    }
  }
}

/**
 * Build an array of all the protected forms on the site, by form_id.
 */
function honeypot_get_protected_forms() {
  $forms =& drupal_static(__FUNCTION__);

  // If the data isn't already in memory, get from cache or look it up fresh.
  if (!isset($forms)) {
    if ($cache = \Drupal::cache()
      ->get('honeypot_protected_forms')) {
      $forms = $cache->data;
    }
    else {
      $form_settings = \Drupal::config('honeypot.settings')
        ->get('form_settings');
      if (!empty($form_settings)) {

        // Add each form that's enabled to the $forms array.
        foreach ($form_settings as $form_id => $enabled) {
          if ($enabled) {
            $forms[] = $form_id;
          }
        }
      }
      else {
        $forms = [];
      }

      // Save the cached data.
      \Drupal::cache()
        ->set('honeypot_protected_forms', $forms);
    }
  }
  return $forms;
}

/**
 * Form builder function to add different types of protection to forms.
 *
 * @param array $options
 *   Array of options to be added to form. Currently accepts 'honeypot' and
 *   'time_restriction'.
 */
function honeypot_add_form_protection(&$form, FormStateInterface $form_state, array $options = []) {
  $account = \Drupal::currentUser();

  // Allow other modules to alter the protections applied to this form.
  \Drupal::moduleHandler()
    ->alter('honeypot_form_protections', $options, $form);

  // Don't add any protections if the user can bypass the Honeypot.
  if ($account
    ->hasPermission('bypass honeypot protection')) {
    return;
  }

  // Build the honeypot element.
  if (in_array('honeypot', $options)) {

    // Get the element name (default is generic 'url').
    $honeypot_element = \Drupal::config('honeypot.settings')
      ->get('element_name');

    // Build the honeypot element.
    $honeypot_class = $honeypot_element . '-textfield';
    $form[$honeypot_element] = [
      '#theme_wrappers' => [
        0 => 'form_element',
        'container' => [
          '#id' => NULL,
          '#attributes' => [
            'class' => [
              $honeypot_class,
            ],
            'style' => [
              'display: none !important;',
            ],
          ],
        ],
      ],
      '#type' => 'textfield',
      '#title' => t('Leave this field blank'),
      '#size' => 20,
      '#weight' => 100,
      '#attributes' => [
        'autocomplete' => 'off',
      ],
      '#element_validate' => [
        '_honeypot_honeypot_validate',
      ],
    ];
  }

  // Set the time restriction for this form (if it's not disabled).
  if (in_array('time_restriction', $options) && \Drupal::config('honeypot.settings')
    ->get('time_limit') != 0) {

    // Set the current time in a hidden value to be checked later.
    $input = $form_state
      ->getUserInput();
    if (empty($input['honeypot_time'])) {
      $identifier = Crypt::randomBytesBase64();
      \Drupal::service('keyvalue.expirable')
        ->get('honeypot_time_restriction')
        ->setWithExpire($identifier, time(), 3600 * 24);
    }
    else {
      $identifier = $input['honeypot_time'];
    }
    $form['honeypot_time'] = [
      '#type' => 'hidden',
      '#title' => t('Timestamp'),
      '#default_value' => $identifier,
      '#element_validate' => [
        '_honeypot_time_restriction_validate',
      ],
      '#cache' => [
        'max-age' => 0,
      ],
    ];

    // Disable page caching to make sure timestamp isn't cached.
    $account = \Drupal::currentUser();
    if ($account
      ->id() == 0) {

      // TODO D8 - Use DIC? See: http://drupal.org/node/1539454
      // Should this now set 'omit_vary_cookie' instead?
      Drupal::service('page_cache_kill_switch')
        ->trigger();
    }
  }

  // Allow other modules to react to addition of form protection.
  if (!empty($options)) {
    \Drupal::moduleHandler()
      ->invokeAll('honeypot_add_form_protection', [
      $options,
      $form,
    ]);
  }
}

/**
 * Validate honeypot field.
 */
function _honeypot_honeypot_validate($element, FormStateInterface $form_state) {

  // Get the honeypot field value.
  $honeypot_value = $element['#value'];

  // Make sure it's empty.
  if (!empty($honeypot_value) || $honeypot_value == '0') {
    _honeypot_log($form_state
      ->getValue('form_id'), 'honeypot');
    $form_state
      ->setErrorByName('', t('There was a problem with your form submission. Please refresh the page and try again.'));
  }
}

/**
 * Validate honeypot's time restriction field.
 */
function _honeypot_time_restriction_validate($element, FormStateInterface $form_state) {
  if ($form_state
    ->isProgrammed()) {

    // Don't do anything if the form was submitted programmatically.
    return;
  }
  $triggering_element = $form_state
    ->getTriggeringElement();

  // Don't do anything if the triggering element is a preview button.
  if ($triggering_element['#value'] == t('Preview')) {
    return;
  }

  // Get the time value.
  $identifier = $form_state
    ->getValue('honeypot_time', FALSE);
  $honeypot_time = \Drupal::service('keyvalue.expirable')
    ->get('honeypot_time_restriction')
    ->get($identifier, 0);

  // Get the honeypot_time_limit.
  $time_limit = honeypot_get_time_limit($form_state
    ->getValues());

  // Make sure current time - (time_limit + form time value) is greater than 0.
  // If not, throw an error.
  if (!$honeypot_time || \Drupal::time()
    ->getRequestTime() < $honeypot_time + $time_limit) {
    _honeypot_log($form_state
      ->getValue('form_id'), 'honeypot_time');
    $time_limit = honeypot_get_time_limit();
    \Drupal::service('keyvalue.expirable')
      ->get('honeypot_time_restriction')
      ->setWithExpire($identifier, \Drupal::time()
      ->getRequestTime(), 3600 * 24);
    $form_state
      ->setErrorByName('', t('There was a problem with your form submission. Please wait @limit seconds and try again.', [
      '@limit' => $time_limit,
    ]));
  }
}

/**
 * Log blocked form submissions.
 *
 * @param string $form_id
 *   Form ID for the form on which submission was blocked.
 * @param string $type
 *   String indicating the reason the submission was blocked. Allowed values:
 *     - honeypot: If honeypot field was filled in.
 *     - honeypot_time: If form was completed before the configured time limit.
 */
function _honeypot_log($form_id, $type) {
  honeypot_log_failure($form_id, $type);
  if (\Drupal::config('honeypot.settings')
    ->get('log')) {
    $variables = [
      '%form' => $form_id,
      '@cause' => $type == 'honeypot' ? t('submission of a value in the honeypot field') : t('submission of the form in less than minimum required time'),
    ];
    \Drupal::logger('honeypot')
      ->notice(t('Blocked submission of %form due to @cause.', $variables));
  }
}

/**
 * Look up the time limit for the current user.
 *
 * @param array $form_values
 *   Array of form values (optional).
 */
function honeypot_get_time_limit(array $form_values = []) {
  $account = \Drupal::currentUser();
  $honeypot_time_limit = \Drupal::config('honeypot.settings')
    ->get('time_limit');

  // Only calculate time limit if honeypot_time_limit has a value > 0.
  if ($honeypot_time_limit) {
    $expire_time = \Drupal::config('honeypot.settings')
      ->get('expire');

    // Query the {honeypot_user} table to determine the number of failed
    // submissions for the current user.
    $uid = $account
      ->id();
    $query = \Drupal::database()
      ->select('honeypot_user', 'hu')
      ->condition('uid', $uid)
      ->condition('timestamp', \Drupal::time()
      ->getRequestTime() - $expire_time, '>');

    // For anonymous users, take the hostname into account.
    if ($uid === 0) {
      $hostname = \Drupal::request()
        ->getClientIp();
      $query
        ->condition('hostname', $hostname);
    }
    $number = $query
      ->countQuery()
      ->execute()
      ->fetchField();

    // Don't add more than 30 days' worth of extra time.
    $honeypot_time_limit = (int) min($honeypot_time_limit + exp($number) - 1, $expire_time);

    // TODO - Only accepts two args.
    $additions = \Drupal::moduleHandler()
      ->invokeAll('honeypot_time_limit', [
      $honeypot_time_limit,
      $form_values,
      $number,
    ]);
    if (count($additions)) {
      $honeypot_time_limit += array_sum($additions);
    }
  }
  return $honeypot_time_limit;
}

/**
 * Log the failed submission with timestamp and hostname.
 *
 * @param string $form_id
 *   Form ID for the rejected form submission.
 * @param string $type
 *   String indicating the reason the submission was blocked. Allowed values:
 *     - honeypot: If honeypot field was filled in.
 *     - honeypot_time: If form was completed before the configured time limit.
 */
function honeypot_log_failure($form_id, $type) {
  $account = \Drupal::currentUser();
  $uid = $account
    ->id();

  // Log failed submissions.
  \Drupal::database()
    ->insert('honeypot_user')
    ->fields([
    'uid' => $uid,
    'hostname' => Drupal::request()
      ->getClientIp(),
    'timestamp' => \Drupal::time()
      ->getRequestTime(),
  ])
    ->execute();

  // Allow other modules to react to honeypot rejections.
  // TODO - Only accepts two args.
  \Drupal::moduleHandler()
    ->invokeAll('honeypot_reject', [
    $form_id,
    $uid,
    $type,
  ]);
}

Functions

Namesort descending Description
honeypot_add_form_protection Form builder function to add different types of protection to forms.
honeypot_cron Implements hook_cron().
honeypot_form_alter Implements hook_form_alter().
honeypot_get_protected_forms Build an array of all the protected forms on the site, by form_id.
honeypot_get_time_limit Look up the time limit for the current user.
honeypot_help Implements hook_help().
honeypot_log_failure Log the failed submission with timestamp and hostname.
_honeypot_honeypot_validate Validate honeypot field.
_honeypot_log Log blocked form submissions.
_honeypot_time_restriction_validate Validate honeypot's time restriction field.