You are here

recaptcha_v3.module in reCAPTCHA v3 7

Same filename and directory in other branches
  1. 8 recaptcha_v3.module

Verifies if user is a human without necessity to solve a CAPTCHA.

File

recaptcha_v3.module
View source
<?php

/**
 * @file
 * Verifies if user is a human without necessity to solve a CAPTCHA.
 */

/**
 * Implements hook_help().
 */
function recaptcha_v3_help($path, $arg) {
  switch ($path) {

    // Main module help for the block module
    case 'admin/help#recaptcha_v3':
    case 'admin/config/people/captcha/recaptcha-v3':
      $output = '<p>' . t('reCAPTCHA v3 returns a score for each request without user friction. The score is based on interactions with your site and enables you to take an appropriate action for your site. Register reCAPTCHA v3 keys <a href="@keys_link" target="_blank">here</a>. This page explains how to enable and customize reCAPTCHA v3 on your webpage.', array(
        '@keys_link' => 'https://g.co/recaptcha/v3',
      )) . '</p>';
      $output .= '<p>' . t('At first you need to create at least one action: populate action name<sup>1</sup>, choose score threshold<sup>2</sup> and select action on user verification fail<sup>3</sup>.') . '</p>';
      $items = array(
        t('reCAPTCHA v3 introduces a new concept: actions. Actions name will be displayed in detailed break-down of data for your top ten actions in the <a href="@admin_console_link" target="_blank">admin console</a>', array(
          '@admin_console_link' => 'https://g.co/recaptcha/admin',
        )),
        t('reCAPTCHA v3 returns a score (1.0 is very likely a good interaction, 0.0 is very likely a bot). Based on the score, you can take variable action in the context of your site.'),
        t('You could specify additional validation challenge, for failed recaptcha v3 validations. If you leave empty "Default challenge type" and "Challenge" for concrete action, user could not submit form if his validation failed.'),
      );
      $output .= theme('item_list', array(
        'items' => $items,
        'type' => 'ol',
      ));
      return $output;
  }
}

/**
 * Implements hook_menu().
 */
function recaptcha_v3_menu() {
  $items['admin/config/people/captcha/recaptcha-v3'] = array(
    'title' => 'reCAPTCHA v3',
    'description' => 'Administer the Google reCAPTCHA v3 web service.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'recaptcha_v3_admin_settings',
    ),
    'access arguments' => array(
      'administer CAPTCHA settings',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'recaptcha_v3.admin.inc',
    'weight' => 1,
  );
  $items['admin/config/people/captcha/recaptcha-v3/%/delete'] = array(
    'title' => 'Delete reCAPTCHA v3 action',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'recaptcha_v3_admin_settings_delete_action',
      5,
    ),
    'access arguments' => array(
      'administer CAPTCHA settings',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'recaptcha_v3.admin.inc',
    'weight' => 1,
  );
  return $items;
}

/**
 * Place recaptcha v3 preprocess function at the beginning, so in this way
 * it is possible to verify captcha and in case of fail, replace it to
 * fallback captcha challenge.
 *
 * Implements hook_element_info_alter().
 */
function recaptcha_v3_element_info_alter(&$type) {
  array_unshift($type['captcha']['#process'], 'recaptcha_v3_element_captcha_process');
  $type['captcha']['#process'][] = 'recaptcha_v3_element_captcha_process_validate';
}

/**
 * Implements of hook_theme().
 */
function recaptcha_v3_theme() {
  return array(
    'recaptcha_v3_admin_settings_actions_overview' => array(
      'render element' => 'form',
      'file' => 'recaptcha_v3.admin.inc',
    ),
  );
}

/**
 * Implements hook_captcha().
 */
function recaptcha_v3_captcha($op, $captcha_type = '', $captcha_sid = NULL) {
  switch ($op) {
    case 'list':
      $actions = _recaptcha_v3_get_all_actions();
      return array_keys($actions);
    case 'generate':
      $captcha['form']['recaptcha_v3_token'] = recaptcha_v3_token_element($captcha_type);
      $captcha['form']['captcha_response'] = array(
        '#type' => 'hidden',
        '#value' => 'Google recaptcha v3',
      );
      $captcha['solution'] = TRUE;
      $captcha['captcha_validate'] = 'recaptcha_v3_captcha_validate';
      $captcha['cacheable'] = TRUE;
      return $captcha;
  }
}

/**
 * Need to validate recaptcha v3 token value here,
 * because otherwise in case of failed verification it is no way how to replace
 * captcha element type.
 *
 * @param $element
 * @param $form_state
 * @param $complete_form
 *
 * @return mixed
 */
function recaptcha_v3_element_captcha_process($element, &$form_state, $complete_form) {
  $parts = explode('/', $element['#captcha_type']);
  if (count($parts) === 2 && $parts[0] === 'recaptcha_v3') {
    $element['#recaptcha_v3_valid'] = FALSE;
    $element['#recaptcha_v3_token'] = FALSE;
    $element['#recaptcha_v3_action'] = _recaptcha_v3_get_action_by_id($parts[1]);
    if ($form_state['process_input']) {
      if (isset($form_state['input']['recaptcha_v3_token'])) {
        $element['#recaptcha_v3_token'] = $form_state['input']['recaptcha_v3_token'];
      }
      if ($element['#recaptcha_v3_token']) {
        $element['#recaptcha_v3_valid'] = recaptcha_v3_token_validate($element);
      }

      // Recaptcha v3 verification failed - try to find fallback challenge
      if (!$element['#recaptcha_v3_valid']) {
        $challenge = $element['#recaptcha_v3_action']['challenge'];
        $challenge = empty($challenge) ? variable_get('recaptcha_v3_captcha_default_challenge', FALSE) : $challenge;

        // If fallback challenge exist - change captcha type to this value,
        // so instead of recaptcha v3 selected fallback widget will be processed
        if (!empty($challenge)) {
          $element['#captcha_type'] = $challenge;
        }
      }
    }
  }
  return $element;
}

/**
 * In case of failed recaptcha v3 token validation need to replace captcha
 * validate callback in $element['#captcha_validate'] for displaying
 * correct error message (otherwise will be executed replaced challenge validation
 * with possible passed validation or wrong error message).
 *
 * @param $element
 * @param $form_state
 * @param $complete_form
 *
 * @return mixed
 */
function recaptcha_v3_element_captcha_process_validate($element, &$form_state, $complete_form) {

  // Replacing validation callback only if correct next statements:
  // 1. user submit form
  // 2. in form exist recaptcha v3 token element
  if ($form_state['process_input'] && !empty($element['#recaptcha_v3_token'])) {
    $element['#captcha_validate'] = 'recaptcha_v3_captcha_validate';
  }
  return $element;
}

/**
 * Validate recaptcha token.
 *
 * @param $element
 *
 * @return bool
 */
function recaptcha_v3_token_validate($element) {
  if (empty($element['#recaptcha_v3_token'])) {
    watchdog('reCAPTCHA v3', 'Missing recaptcha v3 token value', array(), WATCHDOG_ERROR);
    return FALSE;
  }
  if (empty($element['#recaptcha_v3_action']['action'])) {
    watchdog('reCAPTCHA v3', 'Missing recaptcha v3 action value', array(), WATCHDOG_ERROR);
    return FALSE;
  }

  // Use drupal_http_request() to circumvent all issues with the Google library.
  $recaptcha = new \ReCaptcha\ReCaptcha(variable_get('recaptcha_v3_secret_key', ''), new \ReCaptcha\RequestMethod\Drupal7Post());

  // Ensures the hostname matches. Required if "Domain Name Validation" is
  // disabled for credentials.
  if (variable_get('recaptcha_v3_verify_hostname', FALSE)) {
    $recaptcha
      ->setExpectedHostname($_SERVER['SERVER_NAME']);
  }
  $resp = $recaptcha
    ->verify($element['#recaptcha_v3_token'], ip_address());
  if ($resp
    ->isSuccess()) {

    // wrong action id
    if ($resp
      ->getAction() !== $element['#recaptcha_v3_action']['action']) {
      watchdog('reCAPTCHA v3', 'Mismatched site and response actions.', array(), WATCHDOG_ERROR);
      return FALSE;
    }

    // To low user trust score level
    if ($resp
      ->getScore() < $element['#recaptcha_v3_action']['score'] / 10) {
      watchdog('reCAPTCHA v3', 'Response score lower then action threshold score.', array(), WATCHDOG_ERROR);
      return FALSE;
    }

    // Verified!
    return TRUE;
  }

  // Error code reference, https://developers.google.com/recaptcha/docs/verify
  $error_codes = array(
    'action-mismatch' => t('Expected action did not match.'),
    'apk_package_name-mismatch' => t('Expected APK package name did not match.'),
    'bad-response' => t('Did not receive a 200 from the service.'),
    'bad-request' => t('The request is invalid or malformed.'),
    'challenge-timeout' => t('Challenge timeout.'),
    'connection-failed' => t('Could not connect to service.'),
    'invalid-input-response' => t('The response parameter is invalid or malformed.'),
    'invalid-input-secret' => t('The secret parameter is invalid or malformed.'),
    'missing-input-response' => t('The response parameter is missing.'),
    'missing-input-secret' => t('The secret parameter is missing.'),
    'hostname-mismatch' => t('Expected hostname did not match.'),
    'invalid-json' => t('The json response is invalid or malformed.'),
    'score-threshold-not-met' => t('Score threshold not met.'),
    'unknown-error' => t('Not a success, but no error codes received!'),
    'timeout-or-duplicate' => t('Timeout or duplicate!'),
  );
  foreach ($resp
    ->getErrorCodes() as $code) {
    if (!isset($error_codes[$code])) {
      $code = 'unknown-error';
    }
    watchdog('reCAPTCHA v3 web service', '@error', array(
      '@error' => $error_codes[$code],
    ), WATCHDOG_ERROR);
  }
  return FALSE;
}

/**
 * CAPTCHA Callback; Validates the reCAPTCHA v3 code.
 */
function recaptcha_v3_captcha_validate($solution, $response, $element, &$form_state) {

  // Check validated previously result.
  if ($element['#recaptcha_v3_valid'] === TRUE) {
    return TRUE;
  }
  $message = variable_get('recaptcha_v3_error_message', t('Recaptcha verification failed.'));
  form_set_error('captcha_response', $message);
  return FALSE;
}

/**
 * Form element for recaptcha v3.
 * @param $action_id
 *
 * @return array
 */
function recaptcha_v3_token_element($action_id) {
  $action = _recaptcha_v3_get_action_by_id($action_id);
  return array(
    '#type' => 'hidden',
    '#default_value' => '',
    '#attributes' => array(
      'data-recaptcha-v3-action' => $action['action'],
      'data-recaptcha-v3-sitekey' => variable_get('recaptcha_v3_site_key', ''),
    ),
    '#attached' => array(
      'js' => array(
        array(
          'type' => 'file',
          'data' => drupal_get_path('module', 'recaptcha_v3') . '/js/recaptcha_v3.js',
          'scope' => 'footer',
        ),
        array(
          'type' => 'external',
          'data' => url('https://www.google.com/recaptcha/api.js', array(
            'query' => array(
              'render' => variable_get('recaptcha_v3_site_key', ''),
            ),
            'absolute' => TRUE,
          )),
          'scope' => 'header',
          'group' => JS_LIBRARY,
        ),
      ),
    ),
  );
}

/**
 * Returns whether a recaptcha v3 id already exists.
 *
 * @see form_validate_machine_name()
 */
function _recaptcha_v3_id_exists($value) {
  return db_query_range('SELECT 1 FROM {recaptcha_v3_actions} WHERE id = :value', 0, 1, array(
    ':value' => $value,
  ))
    ->fetchField();
}

/**
 * Return recaptcha v3 action by given id.
 *
 * @see form_validate_machine_name()
 */
function _recaptcha_v3_get_action_by_id($id) {
  return db_select('recaptcha_v3_actions', 'a')
    ->fields('a')
    ->condition('id', $id)
    ->execute()
    ->fetchAssoc();
}

/**
 * Returns array of all existing actions.
 *
 * @see form_validate_machine_name()
 */
function _recaptcha_v3_get_all_actions() {
  return db_select('recaptcha_v3_actions', 'a')
    ->fields('a')
    ->execute()
    ->fetchAllAssoc('id', PDO::FETCH_ASSOC);
}

/**
 * Implements hook_variable_group_info().
 */
function recaptcha_v3_variable_group_info() {
  $groups['recaptcha_v3'] = array(
    'title' => t('reCAPTCHA v3 settings'),
    'description' => t('reCAPTCHA v3 settings'),
    'access' => 'administer site configuration',
    'path' => array(
      'admin/config/people/captcha/recaptcha-v3',
    ),
  );
  return $groups;
}

/**
 * Implements hook_variable_info().
 */
function recaptcha_v3_variable_info($options) {
  $variables['recaptcha_v3_error_message'] = array(
    'type' => 'string',
    'title' => t('Error message', array(), $options),
    'default' => 'Recaptcha verification failed.',
    'description' => t('This message will be displayed to user in case of failed recaptcha v3 verification.', array(), $options),
    'required' => TRUE,
    'group' => 'recaptcha_v3',
  );
  return $variables;
}

Functions

Namesort descending Description
recaptcha_v3_captcha Implements hook_captcha().
recaptcha_v3_captcha_validate CAPTCHA Callback; Validates the reCAPTCHA v3 code.
recaptcha_v3_element_captcha_process Need to validate recaptcha v3 token value here, because otherwise in case of failed verification it is no way how to replace captcha element type.
recaptcha_v3_element_captcha_process_validate In case of failed recaptcha v3 token validation need to replace captcha validate callback in $element['#captcha_validate'] for displaying correct error message (otherwise will be executed replaced challenge validation with possible passed…
recaptcha_v3_element_info_alter Place recaptcha v3 preprocess function at the beginning, so in this way it is possible to verify captcha and in case of fail, replace it to fallback captcha challenge.
recaptcha_v3_help Implements hook_help().
recaptcha_v3_menu Implements hook_menu().
recaptcha_v3_theme Implements of hook_theme().
recaptcha_v3_token_element Form element for recaptcha v3.
recaptcha_v3_token_validate Validate recaptcha token.
recaptcha_v3_variable_group_info Implements hook_variable_group_info().
recaptcha_v3_variable_info Implements hook_variable_info().
_recaptcha_v3_get_action_by_id Return recaptcha v3 action by given id.
_recaptcha_v3_get_all_actions Returns array of all existing actions.
_recaptcha_v3_id_exists Returns whether a recaptcha v3 id already exists.