Commit 1a3adbdd authored by Jakob P's avatar Jakob P
Browse files

Issue #2220275 by alunyov, DarrellDuane, thisisnotrealpeople, oldspot: Allow...

Issue #2220275 by alunyov, DarrellDuane, thisisnotrealpeople, oldspot: Allow for skipping captcha based on IP Address or IP Range
parent 01dbc953
Loading
Loading
Loading
Loading
+98 −13
Original line number Diff line number Diff line
@@ -47,6 +47,9 @@ define('CAPTCHA_STATUS_EXAMPLE', 2);
define('CAPTCHA_DEFAULT_VALIDATION_CASE_SENSITIVE', 0);
define('CAPTCHA_DEFAULT_VALIDATION_CASE_INSENSITIVE', 1);

define('CAPTCHA_WHITELIST_IP_ADDRESS', 'addresses');
define('CAPTCHA_WHITELIST_IP_RANGE', 'ranges');

// Default captcha field access.
define('CAPTCHA_FIELD_DEFAULT_ACCESS', 1);

@@ -94,12 +97,12 @@ function captcha_point_load($id) {
 * Implements hook_theme().
 */
function captcha_theme() {
  // @phpstan-ignore-next-line
  $path = Drupal::hasService('extension.list.module') ? Drupal::service('extension.list.module')->getPath('captcha') : drupal_get_path('module', 'captcha');
  return [
    'captcha' => [
      'render element' => 'element',
      'template' => 'captcha',
      // @phpstan-ignore-next-line
      'path' => $path . '/templates',
    ],
  ];
@@ -211,6 +214,15 @@ function captcha_form_alter(array &$form, FormStateInterface $form_state, $form_
    }

    if (!empty($captcha_point) && $captcha_point->status()) {
      // Checking if user's ip is whitelisted.
      if (captcha_whitelist_ip_whitelisted()) {
        // If form is setup to have captcha, but user's ip is whitelisted, then
        // we still have to disable form caching to prevent showing cached form
        // for users with not whitelisted ips.
        $form['#cache'] = ['max-age' => 0];
        \Drupal::service('page_cache_kill_switch')->trigger();
      }
      else {
        // Build CAPTCHA form element.
        $captcha_element = [
          '#type' => 'captcha',
@@ -228,6 +240,7 @@ function captcha_form_alter(array &$form, FormStateInterface $form_state, $form_
        }
      }
    }
  }
  elseif ($config->get('administration_mode') && $account->hasPermission('administer CAPTCHA settings')
    && (!\Drupal::service('router.admin_context')
      ->isAdminRoute() || $config->get('allow_on_admin_pages'))
@@ -695,3 +708,75 @@ function captcha_captcha($op, $captcha_type = '') {
      break;
  }
}

/**
 * Parse values of whitelist ip addresses and ranges.
 *
 * @param string $whitelist_ips_value
 *   Contains list of ip addresses and ranges set one per line.
 *
 * @return array
 *   Array of parsed ip addresses and ranges.
 */
function captcha_whitelist_ips_parse_values($whitelist_ips_value) {
  $whitelist_ips = [
    CAPTCHA_WHITELIST_IP_RANGE => [],
    CAPTCHA_WHITELIST_IP_ADDRESS => [],
  ];

  if (empty(trim($whitelist_ips_value))) {
    return $whitelist_ips;
  }

  $value_rows = explode("\n", $whitelist_ips_value);
  foreach ($value_rows as $value_row) {
    $value_row = trim($value_row);
    if (strpos($value_row, '-') !== FALSE) {
      $whitelist_ips[CAPTCHA_WHITELIST_IP_RANGE][] = $value_row;
    }
    else {
      $whitelist_ips[CAPTCHA_WHITELIST_IP_ADDRESS][] = $value_row;
    }
  }

  return $whitelist_ips;
}

/**
 * Check if ip address is whitelisted.
 *
 * @param string $ip_address
 *   Optional. IP address to be checked if it is in whitelist. If no ip value
 *   provided user's current ip will be used to be verified.
 *
 * @return bool
 *   TRUE if requested IP address is whitelisted, FALSE if it is not.
 */
function captcha_whitelist_ip_whitelisted($ip_address = '') {
  if (empty($ip_address)) {
   $ip_address = Drupal::request()->getClientIp();
  }

  $config = \Drupal::config('captcha.settings');
  $whitelist_ips_value = $config->get('whitelist_ips');
  $whitelist_ips = captcha_whitelist_ips_parse_values($whitelist_ips_value);

  if (in_array($ip_address, $whitelist_ips[CAPTCHA_WHITELIST_IP_ADDRESS])) {
    return TRUE;
  }
  elseif (empty($whitelist_ips[CAPTCHA_WHITELIST_IP_RANGE])) {
    return FALSE;
  }

  foreach ($whitelist_ips[CAPTCHA_WHITELIST_IP_RANGE] as $ip_range) {
    list($ip_lower, $ip_upper) = explode('-', $ip_range, 2);
    $ip_lower_dec = (float) sprintf("%u", ip2long($ip_lower));
    $ip_upper_dec = (float) sprintf("%u", ip2long($ip_upper));
    $ip_address_dec = (float) sprintf("%u", ip2long($ip_address));
    if (($ip_address_dec >= $ip_lower_dec) && ($ip_address_dec <= $ip_upper_dec)) {
       return TRUE;
    }
  }

  return FALSE;
}
+1 −0
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ default_challenge: 'captcha/Math'
description: 'This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.'
administration_mode: false
allow_on_admin_pages: false
whitelist_ips: ''
add_captcha_description: true
wrong_captcha_response_message: 'The answer you entered for the CAPTCHA was not correct.'
default_validation: 1
+3 −0
Original line number Diff line number Diff line
@@ -34,6 +34,9 @@ captcha.settings:
    allow_on_admin_pages:
      type: boolean
      label: 'Allow CAPTCHAs and CAPTCHA administration links on administrative pages'
    whitelist_ips:
      type: string
      label: 'IP addresses list'
    add_captcha_description:
      type: boolean
      label: 'Add a description to the CAPTCHA'
+77 −2
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@ use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Displays the captcha settings form.
@@ -37,6 +38,13 @@ class CaptchaSettingsForm extends ConfigFormBase {
   */
  protected $moduleHandler;

  /**
   * The request object.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected $requestStack;

  /**
   * Constructs a \Drupal\captcha\Form\CaptchaSettingsForm object.
   *
@@ -48,12 +56,15 @@ class CaptchaSettingsForm extends ConfigFormBase {
   *   Module handler.
   * @param \Drupal\captcha\Service\CaptchaService $captcha_service
   *   The captcha service.
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack object.
   */
  public function __construct(ConfigFactoryInterface $config_factory, CacheBackendInterface $cache_backend, ModuleHandlerInterface $moduleHandler, CaptchaService $captcha_service) {
  public function __construct(ConfigFactoryInterface $config_factory, CacheBackendInterface $cache_backend, ModuleHandlerInterface $moduleHandler, CaptchaService $captcha_service, RequestStack $request_stack) {
    parent::__construct($config_factory);
    $this->cacheBackend = $cache_backend;
    $this->moduleHandler = $moduleHandler;
    $this->captchaService = $captcha_service;
    $this->requestStack = $request_stack;
  }

  /**
@@ -64,7 +75,8 @@ class CaptchaSettingsForm extends ConfigFormBase {
      $container->get('config.factory'),
      $container->get('cache.default'),
      $container->get('module_handler'),
      $container->get('captcha.helper')
      $container->get('captcha.helper'),
      $container->get('request_stack')
    );
  }

@@ -127,6 +139,23 @@ class CaptchaSettingsForm extends ConfigFormBase {
      '#description' => $this->t("This option makes it possible to add CAPTCHAs to forms on administrative pages. CAPTCHAs are disabled by default on administrative pages (which shouldn't be accessible to untrusted users normally) to avoid the related overhead. In some situations, e.g. in the case of demo sites, it can be useful to allow CAPTCHAs on administrative pages."),
    ];

    // Adding configuration for ip protection.
    $form['form_protection']['whitelist_ips_settings'] = [
      '#type' => 'details',
      '#title' => $this->t('Whitelisted IP Addresses'),
      '#description' => $this->t('Enter the IP addresses or IP address ranges you want to skip all CAPTCHAs on this site.'),
      '#open' => !empty($config->get('whitelist_ips')),
    ];

    $ip_address = $this->requestStack->getCurrentRequest()->getClientIp();
    $form['form_protection']['whitelist_ips_settings']['whitelist_ips'] = [
      '#title' => $this->t('IP addresses list'),
      '#type' => 'textarea',
      '#required' => FALSE,
      '#default_value' => $config->get('whitelist_ips'),
      '#description' => $this->t('Enter one per single line IP-address in format XXX.XXX.XXX.XXX, or IP-address range in format XXX.XXX.XXX.YYY-XXX.XXX.XXX.ZZZ. No spaces allowed. Your current IP address is %ip_address.', ['%ip_address' => $ip_address]),
    ];

    // Button for clearing the CAPTCHA placement cache.
    // Based on Drupal core's "Clear all caches" (performance settings page).
    $form['form_protection']['placement_caching'] = [
@@ -235,6 +264,49 @@ class CaptchaSettingsForm extends ConfigFormBase {
    return parent::buildForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    // Validating whitelisted ip addresses.
    $whitelist_ips_value = trim($form_state->getValue('whitelist_ips', ''));
    if (!empty($whitelist_ips_value)) {
      $whitelist_ips = captcha_whitelist_ips_parse_values($whitelist_ips_value);

      // Checking single ip addresses.
      foreach ($whitelist_ips[CAPTCHA_WHITELIST_IP_ADDRESS] as $ip_address) {
        if (filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE) == FALSE) {
          $form_state->setErrorByName('whitelist_ips', $this->t('IP address %ip_address is not valid.', ['%ip_address' => $ip_address]));
        }
      }

      // Checking ip ranges.
      foreach ($whitelist_ips[CAPTCHA_WHITELIST_IP_RANGE] as $ip_range) {
        list($ip_lower, $ip_upper) = explode('-', $ip_range, 2);

        if (filter_var($ip_lower, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE) == FALSE) {
          $form_state->setErrorByName('whitelist_ips', $this->t('Lower IP address %ip_address in range %ip_range is not valid.', ['%ip_address' => $ip_lower, '%ip_range' => $ip_range]));
        }

        if (filter_var($ip_upper, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE) == FALSE) {
          $form_state->setErrorByName('whitelist_ips', $this->t('Upper IP address %ip_address in range %ip_range is not valid.', ['%ip_address' => $ip_upper, '%ip_range' => $ip_range]));
        }

        $ip_lower_dec = (float) sprintf("%u", ip2long($ip_lower));
        $ip_upper_dec = (float) sprintf("%u", ip2long($ip_upper));

        if ($ip_lower_dec == $ip_upper_dec) {
          $form_state->setErrorByName('whitelist_ips', $this->t('Lower and upper IP addresses should be different. Please correct range %ip_range.', ['%ip_range' => $ip_range]));
        }
        elseif ($ip_lower_dec > $ip_upper_dec) {
          $form_state->setErrorByName('whitelist_ips', $this->t("Lower IP can't be greater than upper IP addresses in range. Please correct range %ip_range.", ['%ip_range' => $ip_range]));
        }
      }
    }

    parent::validateForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
@@ -245,6 +317,9 @@ class CaptchaSettingsForm extends ConfigFormBase {
    $config->set('default_challenge', $form_state->getValue('default_challenge'));
    $config->set('enabled_default', $form_state->getValue('enabled_default'));

    // Whitelisted ip addresses and ranges.
    $config->set('whitelist_ips', $form_state->getValue('whitelist_ips'));

    // CAPTCHA description stuff.
    $config->set('add_captcha_description', $form_state->getValue('add_captcha_description'));
    // Save (or reset) the CAPTCHA descriptions.