You are here

antibot.module in Antibot 8

Same filename and directory in other branches
  1. 7 antibot.module

Implements the antibot module.

File

antibot.module
View source
<?php

/**
 * @file
 * Implements the antibot module.
 */
use Drupal\antibot\AntibotFormAlter;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Site\Settings;

/**
 * Implements hook_help().
 */
function antibot_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {

    // Main module help for the antibot module.
    case 'help.page.antibot':
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('Prevent forms from being submitted without JavaScript enabled') . '</p>';
      return $output;
  }
}

/**
 * Implements hook_theme().
 */
function antibot_theme($existing, $type, $theme, $path) {
  $items = [];
  $items['antibot_no_js'] = [
    'template' => 'antibot-no-js',
    'variables' => [
      'message' => NULL,
    ],
    'path' => $path . '/templates',
  ];
  return $items;
}

/**
 * Implements hook_form_alter().
 */
function antibot_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $protection = FALSE;

  // Load module config.
  $config = \Drupal::config('antibot.settings');

  // Get the configured active form IDs for antibot.
  if ($form_ids = $config
    ->get('form_ids')) {

    // Check if this form is a match.
    if (\Drupal::service('path.matcher')
      ->matchPath($form_id, implode("\n", $form_ids))) {

      // Enable protection for this form.
      antibot_protect_form($form);

      // Track that protection was added.
      $protection = TRUE;
    }
  }

  // Determine if we should display the form ID.
  if ($config
    ->get('show_form_ids')) {

    // Check if the user has permission to view these messages.
    if (\Drupal::currentUser()
      ->hasPermission('administer antibot configuration')) {

      // Set a message with the form ID and status.
      \Drupal::messenger()
        ->addMessage(t('Antibot (:id): :status', [
        ':id' => $form_id,
        ':status' => $protection ? t('enabled') : t('disabled'),
      ]));
    }
  }

  // Tag this form with Antibot settings config so that if the list of forms is
  // changing the form cache is invalidated. Note that also the unprotected
  // forms are tagged as they might be protected with the new Antibot settings.
  $cache_metadata = CacheableMetadata::createFromRenderArray($form);
  $cache_metadata
    ->addCacheableDependency($config);
  $cache_metadata
    ->applyTo($form);
}

/**
 * Implements hook_page_attachments().
 */
function antibot_page_attachments(array &$page) {

  // Adds noscript style to HEAD.
  $noscript_style = [
    '#tag' => 'style',
    '#value' => 'form.antibot * :not(.antibot-message) { display: none !important; }',
    '#noscript' => TRUE,
  ];
  $page['#attached']['html_head'][] = [
    $noscript_style,
    'antibot_style',
  ];
}

/**
 * Helper function to enable Antibot protection for a given form.
 *
 * @param array &$form
 *   The form to enable Antibot protection on.
 */
function antibot_protect_form(array &$form) {

  // Stop if the form is already protected.
  if (!empty($form['#antibot_key'])) {
    return;
  }

  // Generate a key for this form.
  $key = Crypt::hmacBase64($form['#form_id'], Settings::getHashSalt());

  // Store the key in the form.
  $form['#antibot_key'] = $key;

  // Add a hidden value which will receive the key via JS.
  // The point of this is to add an additonal layer of protection for remotely
  // POSTed forms. Since the key will not be present in that scenario, the
  // remote post will fail.
  $form['antibot_key'] = [
    '#type' => 'hidden',
    '#value' => '',
  ];

  // Provide a message in the event that the user does not have JavaScript.
  $form['antibot_no_js'] = [
    '#theme' => 'antibot_no_js',
    '#message' => t('You must have JavaScript enabled to use this form.'),
    '#weight' => -500,
  ];

  // Add a pre-render function.
  $form['#pre_render'][] = [
    AntibotFormAlter::class,
    'preRender',
  ];

  // Add validation for the key.
  $form['#validate'][] = 'antibot_form_validation';
}

/**
 * Validation callback for Antibot-enabled forms.
 *
 * @see antibot_form_alter()
 */
function antibot_form_validation($form, FormStateInterface $form_state) {

  // Stop validation if the form was submitted programmatically.
  if ($form_state
    ->isProgrammed()) {
    return;
  }

  // Get the user input.
  $input = $form_state
    ->getUserInput();

  // Extract the submitted key.
  $submitted_key = isset($input['antibot_key']) ? $input['antibot_key'] : NULL;

  // Views exposed forms will initially load and submit without the key.
  if ($form['#form_id'] == 'views_exposed_form' && $submitted_key === NULL) {

    // We must allow this.
    return;
  }

  // Check if the key is missing or is not a match.
  if (!$submitted_key || $submitted_key != $form['#antibot_key']) {
    $form_state
      ->setErrorByName('', t('Submission failed. Please reload the page, ensure JavaScript is enabled and try again.'));
  }
}

Functions

Namesort descending Description
antibot_form_alter Implements hook_form_alter().
antibot_form_validation Validation callback for Antibot-enabled forms.
antibot_help Implements hook_help().
antibot_page_attachments Implements hook_page_attachments().
antibot_protect_form Helper function to enable Antibot protection for a given form.
antibot_theme Implements hook_theme().