Commit a1eae61f authored by Tim Rohaly's avatar Tim Rohaly Committed by Tim Rohaly
Browse files

Issue #2949447 by TR, markdorison, AaronBauman: Expose honeypot protection via service

parent e0dcca52
Loading
Loading
Loading
Loading
+1 −1
Changes for README.md: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -28,7 +28,7 @@ If you want to add honeypot to your own forms, or to any form through your own
module's hook_form_alter's, you can simply place the following function call
inside your form builder function (or inside a hook_form_alter):

    honeypot_add_form_protection(
    \Drupal::service('honeypot')->addFormProtection(
      $form,
      $form_state,
      ['honeypot', 'time_restriction']
+43 −227
Changes for honeypot.module: 43 added lines, 227 removed lines.
Original line number Diff line number Diff line
@@ -5,7 +5,6 @@
 * Honeypot module, for deterring spam bots from completing Drupal forms.
 */

use Drupal\Component\Utility\Crypt;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
@@ -91,14 +90,14 @@ function honeypot_form_alter(&$form, FormStateInterface $form_state, $form_id) {
    // 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']);
      \Drupal::service('honeypot')->addFormProtection($form, $form_state, ['honeypot', 'time_restriction']);
    }
  }
  // Otherwise add form protection only to the admin-configured forms.
  elseif (in_array($form_id, honeypot_get_protected_forms())) {
  elseif (in_array($form_id, \Drupal::service('honeypot')->getProtectedForms())) {
    // The $form_id of the form we're currently altering is found
    // in the list of protected forms.
    honeypot_add_form_protection($form, $form_state, ['honeypot', 'time_restriction']);
    \Drupal::service('honeypot')->addFormProtection($form, $form_state, ['honeypot', 'time_restriction']);
  }
}

@@ -108,234 +107,58 @@ function honeypot_form_alter(&$form, FormStateInterface $form_state, $form_id) {
 * @return array
 *   An array whose values are the form_ids of all the protected forms
 *   on the site.
 *
 * @deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the
 *   'honeypot' service instead. For example, \Drupal::service('honeypot')
 *   ->getProtectedForms().
 *
 * @see https://www.drupal.org/node/2949447
 */
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 {
      $forms = [];
      $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;
          }
        }
      }

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

  return $forms;
  @trigger_error("honeypot_get_protected_forms() is deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the 'honeypot' service instead. For example, \Drupal::service('honeypot')->getProtectedForms(). See https://www.drupal.org/node/2949447", E_USER_DEPRECATED);
  return \Drupal::service('honeypot')->getProtectedForms();
}

/**
 * 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, \Drupal::time()->getCurrentTime(), 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 https://www.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.
 *   (optional) Array of options to be added to form. Currently accepts
 *   'honeypot' and 'time_restriction'.
 *
 * @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.
 * @deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the
 *   'honeypot' service instead. For example, \Drupal::service('honeypot')
 *   ->addFormProtection($form, $form_state, $options).
 *
 * @see https://www.drupal.org/node/2949447
 */
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('Blocked submission of %form due to @cause.', $variables);
  }
function honeypot_add_form_protection(&$form, FormStateInterface $form_state, array $options = []) {
  @trigger_error("honeypot_add_form_protection() is deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the 'honeypot' service instead. For example, \Drupal::service('honeypot')->addFormProtection(\$form, \$form_state, \$options). See https://www.drupal.org/node/2949447", E_USER_DEPRECATED);
  \Drupal::service('honeypot')->addFormProtection($form, $form_state, $options);
}

/**
 * Look up the time limit for the current user.
 * Looks up the time limit for the current user.
 *
 * @param array $form_values
 *   Array of form values (optional).
 *   (optional) Array of form values.
 *
 * @return int
 *   The time limit in seconds.
 *
 * @deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the
 *   'honeypot' service instead. For example, \Drupal::service('honeypot')
 *   ->getTimeLimit($form_values).
 *
 * @see https://www.drupal.org/node/2949447
 */
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 time than the expiration window.
    $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;
  @trigger_error("honeypot_get_time_limit() is deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the 'honeypot' service instead. For example, \Drupal::service('honeypot')->getTimeLimit(\$form_values). See https://www.drupal.org/node/2949447", E_USER_DEPRECATED);
  return \Drupal::service('honeypot')->getTimeLimit($form_values);
}

/**
 * Log the failed submission with timestamp and hostname.
 * Logs the failed submission with timestamp and hostname.
 *
 * @param string $form_id
 *   Form ID for the rejected form submission.
@@ -343,23 +166,16 @@ function honeypot_get_time_limit(array $form_values = []) {
 *   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.
 *
 * @deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the
 *   'honeypot' service instead. For example, \Drupal::service('honeypot')
 *   ->logFailure($form_id, $type).
 *
 * @see https://www.drupal.org/node/2949447
 */
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]);
  @trigger_error("honeypot_log_failure() is deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the 'honeypot' service instead. For example, \Drupal::service('honeypot')->logFailure(\$form_id, \$type). See https://www.drupal.org/node/2949447", E_USER_DEPRECATED);
  \Drupal::service('honeypot')->logFailure($form_id, $type);
}

/**

honeypot.services.yml

0 → 100644
+4 −0
Changes for honeypot.services.yml: 4 added lines, 0 removed lines.
Original line number Diff line number Diff line
services:
  honeypot:
    class: Drupal\honeypot\HoneypotService
    arguments: ['@current_user', '@module_handler', '@config.factory', '@keyvalue.expirable', '@page_cache_kill_switch', '@database', '@logger.factory', '@datetime.time', '@string_translation', '@cache.default', '@request_stack']
+388 −0

File added.

Preview size limit exceeded, changes collapsed.

+57 −0
Changes for src/HoneypotServiceInterface.php: 57 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

namespace Drupal\honeypot;

use Drupal\Core\Form\FormStateInterface;

/**
 * Provides a service to append Honeypot protection to forms.
 */
interface HoneypotServiceInterface {

  /**
   * Builds an array of all the protected forms on the site.
   *
   * @return array
   *   An array whose values are the form_ids of all the protected forms
   *   on the site.
   */
  public function getProtectedForms();

  /**
   * Looks up the time limit for the current user.
   *
   * @param array $form_values
   *   (optional) Array of form values.
   *
   * @return int
   *   The time limit in seconds.
   */
  public function getTimeLimit(array $form_values = []);

  /**
   * Adds honeypot protection to provided form.
   *
   * @param array $form
   *   Drupal form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Drupal form state object.
   * @param array $options
   *   (optional) Array of options to be added to form. Currently accepts
   *   'honeypot' and 'time_restriction'.
   */
  public function addFormProtection(array &$form, FormStateInterface $form_state, array $options = []);

  /**
   * Logs 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.
   */
  public function logFailure($form_id, $type);

}
Loading