diff --git a/captcha.module b/captcha.module index fb940e9..eede0bf 100755 --- a/captcha.module +++ b/captcha.module @@ -43,6 +43,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'); + /** * Implements hook_help(). */ @@ -146,28 +149,36 @@ function captcha_form_alter(array &$form, FormStateInterface $form_state, $form_ // Visitor does not have permission to skip CAPTCHAs. module_load_include('inc', 'captcha'); if (!$account->hasPermission('skip CAPTCHA')) { - /* @var CaptchaPoint $captcha_point */ $captcha_point = \Drupal::entityTypeManager() ->getStorage('captcha_point') ->load($form_id); if ($captcha_point && $captcha_point->status()) { - // Build CAPTCHA form element. - $captcha_element = [ - '#type' => 'captcha', - '#captcha_type' => $captcha_point->getCaptchaType(), - ]; - - // Add a CAPTCHA description if required. - if ($config->get('add_captcha_description')) { - $captcha_element['#description'] = _captcha_get_description(); + // Checking is 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', + '#captcha_type' => $captcha_point->getCaptchaType(), + ]; - // Get placement in form and insert in form. - $captcha_placement = _captcha_get_captcha_placement($form_id, $form); - _captcha_insert_captcha_element($form, $captcha_placement, $captcha_element); + // Add a CAPTCHA description if required. + if ($config->get('add_captcha_description')) { + $captcha_element['#description'] = _captcha_get_description(); + } + // Get placement in form and insert in form. + $captcha_placement = _captcha_get_captcha_placement($form_id, $form); + _captcha_insert_captcha_element($form, $captcha_placement, $captcha_element); + } } } elseif ($config->get('administration_mode') && $account->hasPermission('administer CAPTCHA settings') @@ -574,3 +585,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; +} diff --git a/config/install/captcha.settings.yml b/config/install/captcha.settings.yml index ac797b0..88222d2 100755 --- a/config/install/captcha.settings.yml +++ b/config/install/captcha.settings.yml @@ -2,6 +2,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 default_validation: 1 persistence: 1 diff --git a/config/schema/captcha.settings.yml b/config/schema/captcha.settings.yml index 3cdcddb..154e52c 100755 --- a/config/schema/captcha.settings.yml +++ b/config/schema/captcha.settings.yml @@ -17,6 +17,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' diff --git a/src/Form/CaptchaSettingsForm.php b/src/Form/CaptchaSettingsForm.php index a9ecbd4..c8d1325 100755 --- a/src/Form/CaptchaSettingsForm.php +++ b/src/Form/CaptchaSettingsForm.php @@ -8,6 +8,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. @@ -22,16 +23,26 @@ class CaptchaSettingsForm extends ConfigFormBase { protected $cacheBackend; /** + * The request object. + * + * @var \Symfony\Component\HttpFoundation\RequestStack + */ + protected $requestStack; + + /** * Constructs a \Drupal\captcha\Form\CaptchaSettingsForm object. * * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The factory for configuration objects. * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend * Cache backend instance to use. + * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack + * The request stack object. */ - public function __construct(ConfigFactoryInterface $config_factory, CacheBackendInterface $cache_backend) { + public function __construct(ConfigFactoryInterface $config_factory, CacheBackendInterface $cache_backend, RequestStack $request_stack) { parent::__construct($config_factory); $this->cacheBackend = $cache_backend; + $this->requestStack = $request_stack; } /** @@ -40,7 +51,8 @@ class CaptchaSettingsForm extends ConfigFormBase { public static function create(ContainerInterface $container) { return new static( $container->get('config.factory'), - $container->get('cache.default') + $container->get('cache.default'), + $container->get('request_stack') ); } @@ -97,6 +109,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'] = [ @@ -198,12 +227,58 @@ class CaptchaSettingsForm extends ConfigFormBase { /** * {@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} + */ public function submitForm(array &$form, FormStateInterface $form_state) { $config = $this->config('captcha.settings'); $config->set('administration_mode', $form_state->getValue('administration_mode')); $config->set('allow_on_admin_pages', $form_state->getValue('allow_on_admin_pages')); $config->set('default_challenge', $form_state->getValue('default_challenge')); + // 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.