HEX
Server: Apache/2.4.65 (Debian)
System: Linux web6 5.10.0-36-amd64 #1 SMP Debian 5.10.244-1 (2025-09-29) x86_64
User: innocamp (1028)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: /home/capeglid/public_html/wp-content/plugins/gravityformsrecaptcha/class-gf-recaptcha.php
<?php

namespace Gravity_Forms\Gravity_Forms_RECAPTCHA;

defined( 'ABSPATH' ) || die();

use GFForms;
use GFAddOn;
use GF_Fields;
use GFAPI;
use GFCommon;
use GFFormDisplay;
use GFFormsModel;
use Gravity_Forms\Gravity_Forms_RECAPTCHA\Settings;

// Include the Gravity Forms Add-On Framework.
GFForms::include_addon_framework();

/**
 * Gravity Forms Gravity Forms Recaptcha Add-On.
 *
 * @since     1.0
 * @package   GravityForms
 * @author    Gravity Forms
 * @copyright Copyright (c) 2021, Gravity Forms
 */
class GF_RECAPTCHA extends GFAddOn {

	/**
	 * Option name for triggering quota hit notification.
	 *
	 * @since 1.7
	 */
	const RECAPTCHA_QUOTA_LIMIT_HIT = 'gf_recaptcha_quota_limit_hit';

	/**
	 * Contains an instance of this class, if available.
	 *
	 * @since  1.0
	 * @var    GF_RECAPTCHA $_instance If available, contains an instance of this class
	 */
	private static $_instance = null;

	/**
	 * Defines the version of the Gravity Forms Recaptcha Add-On.
	 *
	 * @since  1.0
	 * @var    string $_version Contains the version.
	 */
	protected $_version = GF_RECAPTCHA_VERSION;

	/**
	 * Defines the minimum Gravity Forms version required.
	 *
	 * @since  1.0
	 * @var    string $_min_gravityforms_version The minimum version required.
	 */
	protected $_min_gravityforms_version = GF_RECAPTCHA_MIN_GF_VERSION;

	/**
	 * Defines the plugin slug.
	 *
	 * @since  1.0
	 * @var    string $_slug The slug used for this plugin.
	 */
	protected $_slug = 'gravityformsrecaptcha';

	/**
	 * Defines the main plugin file.
	 *
	 * @since  1.0
	 * @var    string $_path The path to the main plugin file, relative to the plugins folder.
	 */
	protected $_path = 'gravityformsrecaptcha/recaptcha.php';

	/**
	 * Defines the full path to this class file.
	 *
	 * @since  1.0
	 * @var    string $_full_path The full path.
	 */
	protected $_full_path = __FILE__;

	/**
	 * Defines the URL where this add-on can be found.
	 *
	 * @since  1.0
	 * @var    string The URL of the Add-On.
	 */
	protected $_url = 'https://gravityforms.com';

	/**
	 * Defines the title of this add-on.
	 *
	 * @since  1.0
	 * @var    string $_title The title of the add-on.
	 */
	protected $_title = 'Gravity Forms reCAPTCHA Add-On';

	/**
	 * Defines the short title of the add-on.
	 *
	 * @since  1.0
	 * @var    string $_short_title The short title.
	 */
	protected $_short_title = 'reCAPTCHA';

	/**
	 * Defines if Add-On should use Gravity Forms servers for update data.
	 *
	 * @since  1.0
	 * @var    bool
	 */
	protected $_enable_rg_autoupgrade = true;

	/**
	 * Defines the capabilities needed for the Gravity Forms Recaptcha Add-On
	 *
	 * @since  1.0
	 * @var    array $_capabilities The capabilities needed for the Add-On
	 */
	protected $_capabilities = array( 'gravityforms_recaptcha', 'gravityforms_recaptcha_uninstall' );

	/**
	 * Defines the capability needed to access the Add-On settings page.
	 *
	 * @since  1.0
	 * @var    string $_capabilities_settings_page The capability needed to access the Add-On settings page.
	 */
	protected $_capabilities_settings_page = 'gravityforms_recaptcha';

	/**
	 * Defines the capability needed to access the Add-On form settings page.
	 *
	 * @since  1.0
	 * @var    string $_capabilities_form_settings The capability needed to access the Add-On form settings page.
	 */
	protected $_capabilities_form_settings = 'gravityforms_recaptcha';

	/**
	 * Defines the capability needed to uninstall the Add-On.
	 *
	 * @since  1.0
	 * @var    string $_capabilities_uninstall The capability needed to uninstall the Add-On.
	 */
	protected $_capabilities_uninstall = 'gravityforms_recaptcha_uninstall';

	/**
	 * Class instance.
	 *
	 * @var RECAPTCHA_API
	 */
	private $api;

	/**
	 * Object responsible for verifying tokens.
	 *
	 * @var Token_Verifier
	 */
	private $token_verifier;

	/**
	 * Prefix for add-on assets.
	 *
	 * @since 1.0
	 * @var string
	 */
	private $asset_prefix = 'gforms_recaptcha_';

	/**
	 * Wrapper class for plugin settings.
	 *
	 * @since 1.0
	 * @var Settings\Plugin_Settings
	 */
	private $plugin_settings;

	/**
	 * GF_Field_RECAPTCHA instance.
	 *
	 * @since 1.0
	 * @var GF_Field_RECAPTCHA
	 */
	private $field;

	/**
	 * Possible disabled states for v3.
	 *
	 * disabled: reCAPTCHA is disabled in feed settings.
	 * disconnected: No valid v3 site and secret keys are saved.
	 * disabled (quota limit): reCAPTCHA API quota limit hit.
	 * disabled (token refresh in progress): Another settings page view or form submission was refreshing the Enterprise auth token.
	 * disabled (token refresh failed): The request to refresh the Enterprise auth token failed.
	 *
	 * @var array
	 */
	private $v3_disabled_states = array(
		'disabled',
		'disconnected',
		'disabled (quota limit)',
		'disabled (token refresh in progress)',
		'disabled (token refresh failed)',
	);

	/**
	 * The value to be saved to the entry meta for the score when initializing the API fails.
	 *
	 * @since 2.0
	 *
	 * @var string
	 */
	private $init_error_status = 'disconnected';

	/**
	 * Returns an instance of this class, and stores it in the $_instance property.
	 *
	 * @since  1.0
	 *
	 * @return GF_RECAPTCHA $_instance An instance of the GF_RECAPTCHA class
	 */
	public static function get_instance() {
		if ( ! self::$_instance instanceof GF_RECAPTCHA ) {
			self::$_instance = new self();
		}

		return self::$_instance;
	}

	/**
	 * Run add-on pre-initialization processes.
	 *
	 * @since 1.0
	 */
	public function pre_init() {
		require_once plugin_dir_path( __FILE__ ) . '/includes/settings/class-plugin-settings.php';
		require_once plugin_dir_path( __FILE__ ) . '/includes/class-gf-field-recaptcha.php';
		require_once plugin_dir_path( __FILE__ ) . '/includes/class-recaptcha-api.php';
		require_once plugin_dir_path( __FILE__ ) . '/includes/class-token-verifier.php';

		$this->api             = new RECAPTCHA_API();
		$this->token_verifier  = new Token_Verifier( $this, $this->api );
		$this->plugin_settings = new Settings\Plugin_Settings( $this, $this->token_verifier );
		$this->field           = new GF_Field_RECAPTCHA();

		GF_Fields::register( $this->field );

		add_filter( 'gform_settings_menu', array( $this, 'replace_core_recaptcha_menu_item' ) );
		add_action( 'gform_update_status', array( $this, 'entry_status_change' ), 1, 3 );

		parent::pre_init();
	}

	/**
	 * Replaces the core recaptcha settings menu item with the addon settings menu item.
	 *
	 * @param array $settings_tabs Registered settings tabs.
	 *
	 * @since 1.0
	 *
	 * @return array
	 */
	public function replace_core_recaptcha_menu_item( $settings_tabs ) {
		// Get tab names with the same index as is in the settings tabs.
		$tabs = array_combine( array_keys( $settings_tabs ), array_column( $settings_tabs, 'name' ) );

		// Bail if for some reason this add-on is not registered as a settings tab.
		if ( ! in_array( $this->_slug, $tabs ) ) {
			return $settings_tabs;
		}

		$prepared_tabs = array_flip( $tabs );

		$settings_tabs[ rgar( $prepared_tabs, 'recaptcha' ) ]['name'] = $this->_slug;
		unset( $settings_tabs[ rgar( $prepared_tabs, $this->_slug ) ] );

		return $settings_tabs;
	}

	/**
	 * Register initialization hooks.
	 *
	 * @since  1.0
	 */
	public function init() {
		parent::init();

		if ( ! $this->is_gravityforms_supported( $this->_min_gravityforms_version ) ) {
			return;
		}

		// Enqueue shared scripts that need to run everywhere, instead of just on forms pages.
		add_action( 'wp_enqueue_scripts', array( $this, 'maybe_enqueue_recaptcha_script' ) );
		add_action( 'gform_preview_init', array( $this, 'maybe_enqueue_recaptcha_script' ) );

		// Add Recaptcha field to the form output.
		add_filter( 'gform_form_tag', array( $this, 'add_recaptcha_input' ), 50, 2  );

		// Register a custom metabox for the entry details page.
		add_filter( 'gform_entry_detail_meta_boxes', array( $this, 'register_meta_box' ), 10, 3 );

		add_filter( 'gform_entry_is_spam', array( $this, 'check_for_spam_entry' ), 10, 3 );
		add_filter( 'gform_validation', array( $this, 'validate_submission' ) );

		add_filter( 'gform_field_content', array( $this, 'update_captcha_field_settings_link' ), 10, 2 );
		add_filter( 'gform_incomplete_submission_pre_save', array( $this, 'add_recaptcha_v3_input_to_draft' ), 10, 3 );

		// Catch the ajax call to remove the reCAPTCHA quota notice.
		add_action( 'wp_ajax_gf_recaptcha_quota_notice', array( $this, 'gf_recaptcha_quota_notice_dismiss' ), 10, 0 );

	}

	/**
	 * Register admin initialization hooks.
	 *
	 * @since 1.0
	 */
	public function init_admin() {
		$this->plugin_settings->maybe_update_auth_tokens();
		parent::init_admin();

		add_action( 'admin_enqueue_scripts', array( $this, 'maybe_enqueue_recaptcha_script' ) );
		add_action( 'admin_notices', array( $this, 'recaptcha_quota_notice' ), 10, 0 );
		add_filter( 'gform_entries_field_value', array( $this, 'entries_field_value' ), 10, 3 );
	}

	/**
	 * Override plugin_settings_init to maybe display the saved settings message.
	 *
	 * @since 1.7.0
	 *
	 * @return void
	 */
	public function plugin_settings_init() {
		parent::plugin_settings_init();
		$this->maybe_display_settings_saved_message();
	}

	/**
	 * Validate the secret key on the plugin settings screen.
	 *
	 * @since 1.0
	 */
	public function init_ajax() {
		parent::init_ajax();

		add_action( 'wp_ajax_verify_secret_key', array( $this->plugin_settings, 'verify_v3_keys' ) );
		add_action( 'wp_ajax_update_reload_settings', array( $this, 'update_reload_settings' ) );
		add_action( 'wp_ajax_perform_enterprise_oauth', array( $this->plugin_settings, 'ajax_perform_enterprise_oauth' ) );
		add_action( 'wp_ajax_disconnect_recaptcha', array( $this, 'ajax_disconnect_recaptcha' ) );
		add_action( 'wp_ajax_get_enterprise_site_keys', array( $this, 'ajax_get_enterprise_site_keys' ) );
		add_action( 'wp_ajax_save_recaptcha_enterprise_data', array( $this, 'ajax_save_recaptcha_enterprise_data' ) );
	}

	/**
	 * Register scripts.
	 *
	 * @since  1.0
	 *
	 * @return array
	 */
	public function scripts() {
		$scripts = array();

		// Prevent plugin settings from loading on the frontend. Remove this condition to see it in action.
		if ( is_admin() ) {
			if ( $this->requires_recaptcha_script() ) {
				$admin_deps = array( 'jquery', "{$this->asset_prefix}recaptcha", 'gform_gravityforms' );
			} else {
				$admin_deps = array( 'jquery' );
			}

			$scripts[] = array(
				'handle'  => "{$this->asset_prefix}plugin_settings",
				'src'     => $this->get_script_url( 'plugin_settings' ),
				'version' => $this->_version,
				'deps'    => $admin_deps,
				'enqueue' => array(
					array(
						'admin_page' => array( 'plugin_settings' ),
						'tab'        => $this->_slug,
					),
				),
			);
		}

		return array_merge( parent::scripts(), $scripts );
	}

	/**
	 * Registers the reCAPTCHA front-end scripts with no-conflict mode, so the badge will display or hide on the settings page.
	 *
	 * @since 2.0
	 *
	 * @param array $scripts The script handles registered with no-conflict mode.
	 *
	 * @return array
	 */
	public function register_noconflict_scripts( $scripts ) {
		$scripts[] = $this->asset_prefix . 'recaptcha';
		$scripts[] = $this->asset_prefix . ( version_compare( GFForms::$version, '2.9.0-dev-1', '<' ) ? 'frontend-legacy' : 'frontend' );

		return parent::register_noconflict_scripts( $scripts );
	}

	/**
	 * Get the URL for a JavaScript file.
	 *
	 * @since 1.0
	 *
	 * @param string $filename The name of the script to return.
	 *
	 * @return string
	 */
	private function get_script_url( $filename ) {
		$base_path = $this->get_base_path() . '/js';
		$base_url  = $this->get_base_url() . '/js';

		// Production scripts.
		if ( is_readable( "{$base_path}/{$filename}.min.js" ) && ! ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ) {
			return "{$base_url}/{$filename}.min.js";
		}

		// Uncompiled scripts.
		if ( is_readable( "{$base_path}/src/{$filename}.js" ) ) {
			return "{$base_url}/src/{$filename}.js";
		}

		// Compiled dev scripts.
		return "{$base_url}/{$filename}.js";
	}

	// # PLUGIN SETTINGS -----------------------------------------------------------------------------------------------

	/**
	 * Define plugin settings fields.
	 *
	 * @since  1.0
	 *
	 * @return array
	 */
	public function plugin_settings_fields() {
		return $this->plugin_settings->get_fields();
	}

	/**
	 * Initialize the plugin settings.
	 *
	 * This method overrides the add-on framework because we need to retrieve the values for reCAPTCHA v2 from core
	 * and populate them if they exist. Since the Plugin_Settings class houses all of the logic related to the plugin
	 * settings screen, we need to pass the return value of this method's parent to delegate that responsibility.
	 *
	 * In a future release, once reCAPTCHA logic is migrated into this add-on, we
	 * should be able to safely remove this override.
	 *
	 * @since 1.0
	 *
	 * @return array
	 */
	public function get_plugin_settings() {
		$current_settings = parent::get_plugin_settings();
		// If the mode is enterprise, we don't need the v2 core settings.
		if ( rgar( $current_settings, 'connection_type' ) === 'enterprise' ) {
			if ( empty( $current_settings['access_token'] ) && empty( $current_settings['action'] ) ) {
				unset( $current_settings['action'] );
			}

			return $current_settings;
		}

		// Merge in the v2 core settings in other modes.
		return $this->plugin_settings->get_settings( parent::get_plugin_settings() );
	}

	/**
	 * Sets a nonce for the reCAPTCHA settings page
	 *
	 * @since  1.0.0
	 *
	 * @return void
	 */
	public function settings_nonce_connect() {
		echo sprintf( '<input type="hidden" name="recaptcha_nonce" value="%s" />', esc_attr( wp_create_nonce( 'connect_recaptcha' ) ) );
	}

	/**
	 * Setting for the disconnect button.
	 *
	 * @since 1.7.0
	 *
	 * @return void
	 */
	public function settings_disconnect_recaptcha() {
		$disconnect_uri  = esc_url_raw(
			add_query_arg(
				array(
					'page'    => 'gf_settings',
					'subview' => 'gravityformsrecaptcha',
					'action'  => 'gfrecaptcha-disconnect',
					'nonce'   => wp_create_nonce( 'gforms_google_recaptcha_disconnect' ),
				),
				admin_url( 'admin.php' )
			)
		);
		$disconnect_link = sprintf( '<a href="%s" class="button gfrecaptcha-disconnect">%s</a> ', esc_url_raw( $disconnect_uri ), esc_html__( 'Disconnect from reCAPTCHA', 'gravityformsrecaptcha' ) );
		echo wp_kses_post( $disconnect_link );
	}

	/**
	 * Setting for the change connection type button.
	 *
	 * @since 1.7.0
	 *
	 * @return void
	 */
	public function settings_change_connection_type() {
		$disconnect_uri  = esc_url_raw(
			add_query_arg(
				array(
					'page'    => 'gf_settings',
					'subview' => 'gravityformsrecaptcha',
					'action'  => 'gfrecaptcha-disconnect',
					'nonce'   => wp_create_nonce( 'gforms_google_recaptcha_disconnect' ),
				),
				admin_url( 'admin.php' )
			)
		);
		$disconnect_link = sprintf( '<a href="%s" class="button gfrecaptcha-disconnect gfrecaptcha-changetype">%s</a> ', esc_url_raw( $disconnect_uri ), esc_html__( 'Change Connection Type', 'gravityformsrecaptcha' ) );
		echo wp_kses_post( $disconnect_link );
	}

	/**
	 * Returns the message to display when there is an issue communicating with Google.
	 *
	 * @since 2.0
	 *
	 * @return string
	 */
	private function comms_error_message() {
		if ( method_exists( 'GFCommon', 'get_support_url' ) ) {
			$support_url = GFCommon::get_support_url();
		} else {
			$support_url = 'https://www.gravityforms.com/open-support-ticket/';
		}

		/* translators: 1: Open link tag 2: Screen reader text opening span tag 3: Screen reader text closing span tag, external link span tags, and closing link tag */

		return sprintf( esc_html__( 'There is a problem communicating with Google right now. Please check back later. If this issue persists for more than a day, please %1$sopen a support ticket%2$s(opens in a new tab)%3$s.', 'gravityformsrecaptcha' ), "<a href='" . esc_url( $support_url ) . "' target='_blank'>", '<span class="screen-reader-text">', '</span>&nbsp;<span class="gform-icon gform-icon--external-link"></span></a>' );
	}

	/**
	 * Echos an error message.
	 *
	 * @since 2.0
	 *
	 * @return void
	 */
	private function echo_error_message( $message ) {
		echo '<div class="error-alert-container alert-container">
					<div class="gform-alert gform-alert--error" data-js="gform-alert">
						<span class="gform-alert__icon gform-icon gform-icon--circle-close" aria-hidden="true"></span>
						<div class="gform-alert__message-wrap">
							<p class="gform-alert__message">' . $message . '</p>
						</div>
					</div>
				</div>';
	}

	/**
	 * Setting to display the reCAPTCHA Enterprise fields.
	 *
	 * @since 1.7.0
	 *
	 * @return false|void
	 */
	public function settings_recaptcha_enterprise_fields() {
		$plugin_settings        = $this->get_plugin_settings();
		$current_project_number = $this->get_plugin_settings_instance()->get_recaptcha_key( 'project_number' );
		$current_site_key       = rgar( $plugin_settings, 'site_key_v3_enterprise' ) ? rgar( $plugin_settings, 'site_key_v3_enterprise' ) : '';
		if ( ! $this->initialize_api() ) {
			$this->log_debug( __METHOD__ . '(): Unable to initialize reCAPTCHA API.' );
			$this->echo_error_message( $this->comms_error_message() );

			return false;
		}

		if ( ! $this->current_user_can_any( $this->_capabilities_form_settings ) ) {
			$this->log_debug( __METHOD__ . '(): User does not have Form Settings capability.' );

			return false;
		}

		$response = $this->api->get_recaptcha_projects();

		if ( is_wp_error( $response ) ) {
			$this->log_debug( __METHOD__ . '(): Could not retrieve Google projects.' );
			$this->echo_error_message( esc_html__( 'You have no available projects for reCAPTCHA or have insufficient permissions', 'gravityformsrecaptcha' ) );

			return false;
		}

		if ( defined( 'GF_RECAPTCHA_PROJECT_NUMBER' ) ) {
			echo '<div class="gform-settings-field">';
			echo '<span class="gform-settings-input__container"><input type="text" readonly value="' . esc_attr( GF_RECAPTCHA_PROJECT_NUMBER ) . '"> </span>';
			echo wp_kses_post( $this->plugin_settings->get_constant_message( GF_RECAPTCHA_PROJECT_NUMBER, 'GF_RECAPTCHA_PROJECT_NUMBER' ) );
			echo '</div>';
		} else {
			echo '<div class="gform-settings-field">';
			echo '<select name="recaptcha_project" id="recaptcha_project">';
			echo '<option value="">' . esc_html__( 'Select a Project', 'gravityformsrecaptcha' ) . '</option>';
			foreach ( $response['projects'] as $project ) {
				if ( $project['lifecycleState'] !== 'ACTIVE' ) {
					continue;
				}

				$is_selected = $current_project_number === $project['projectNumber'] ? 'selected' : '';

				printf(
					'<option value="%1$s" data-project-id="%2$s" data-project-name="%3$s" %4$s>%3$s</option>',
					esc_attr( $project['projectNumber'] ),
					esc_attr( $project['projectId'] ),
					esc_html( $project['name'] ),
					esc_html( $is_selected )
				);
			}
			echo '</select></div>';
		}

		if ( defined( 'GF_RECAPTCHA_V3_SITE_KEY_ENTERPRISE' ) ) {
			echo '<div class="gform-settings-field__header"><label for="recaptcha-site-keys" class="gform-settings-label">' . esc_html__( 'Enterprise Site Key', 'gravityformsrecaptcha' ) . '</label></div>';
			echo '<span class="gform-settings-input__container"><input type="text" readonly value="' . esc_attr( GF_RECAPTCHA_V3_SITE_KEY_ENTERPRISE ) . '"> </span>';
			echo wp_kses_post( $this->plugin_settings->get_constant_message( GF_RECAPTCHA_V3_SITE_KEY_ENTERPRISE, 'GF_RECAPTCHA_V3_SITE_KEY_ENTERPRISE' ) );
		} else {
			if ( ! empty( $current_project_number ) ) {
				$site_keys = $this->api->get_enterprise_site_keys( $current_project_number );

				if ( is_wp_error( $site_keys ) ) {
					$this->log_debug( __METHOD__ . '(): Error retrieving site keys associated with the selected project.' );
					$this->echo_error_message( esc_html__( 'There was an error retrieving the reCAPTCHA site keys.', 'gravityformsrecaptcha' ) );

					return false;
				}

				// Create select field markup.
				$html  = '<div class="gform-settings-field__header"><label for="recaptcha-site-keys" class="gform-settings-label">' . esc_html__( 'Enterprise Site Key', 'gravityformsrecaptcha' ) . '</label></div>';
				$html .= '<select name="recaptcha-site-keys">';
				$html .= '<option value="">' . esc_html__( 'Select a site key', 'gravityformsrecaptcha' ) . '</option>';
				if ( rgar( $site_keys, 'keys' ) ) {
					foreach ( rgar( $site_keys, 'keys' ) as $site_key ) {
						$current_site_key_name = basename( $site_key['name'] );
						if ( $current_site_key === $current_site_key_name ) {
							$is_site_key_selected = 'selected';
						} else {
							$is_site_key_selected = '';
						}
						$html .= sprintf(
							'<option value="%1$s" data-site-key-display-name="%2$s" %3$s>%2$s</option>',
							esc_attr( basename( $site_key['name'] ) ),
							esc_attr( $site_key['displayName'] ),
							$is_site_key_selected
						);
					}
				}
				$html .= '</select>';
				echo '<div id="recaptcha-site-keys">' . $html . '</div>';
			}
			?>
			<div id="recaptcha-site-keys"></div>
			<?php
		}
	}

	/**
	 * Setting to display the hidden reCAPTCHA action.
	 *
	 * @since 1.7.0
	 *
	 * @return void
	 */
	public function settings_recaptcha_action() {
		$connection_type = $this->get_connection_type();

		$actions = array(
			'enterprise' => 'gf_recaptcha_enterprise',
			'classic'    => 'gf_recaptcha_v3_classic',
			'v2'         => 'gf_recaptcha_v2',
		);

		if ( isset( $actions[ $connection_type ] ) ) {
			echo '<input type="hidden" name="gf_recaptcha_action" value="' . esc_attr( $actions[ $connection_type ] ) . '" />';
		} else {
			echo '<input type="hidden" name="gf_recaptcha_action" value="gf_recaptcha_v3_classic" />';
			echo '<input type="hidden" name="gf_recaptcha_action" value="gf_recaptcha_v2" />';
		}
	}

	/**
	 * Callback to update plugin settings on save.
	 *
	 * We override this method in order to save values for reCAPTCHA v2 with their original keys in the options table.
	 * In a future release, we'll eventually migrate all previous reCAPTCHA logic into this add-on, at which time we
	 * should be able to remove this method altogether.
	 *
	 * @since 1.0
	 *
	 * @param array $settings The settings to update.
	 */
	public function update_plugin_settings( $settings ) {

		if ( $this->get_connection_type() !== 'enterprise' ) {
			$this->plugin_settings->update_settings( $settings );
			parent::update_plugin_settings( $settings );
		} else {
			// In Enterprise we need to merge the settings so we don't lost the access token and refresh token.
			$current_settings = $this->get_plugin_settings();
			if ( is_array( $current_settings ) ) {
				$settings = array_merge( $current_settings, $settings );
			}

			parent::update_plugin_settings( $settings );
		}
	}

	/**
	 * Maybe display the settings saved message on the enterprise settings screen.
	 *
	 * @since 1.7.0
	 *
	 * @return void
	 */
	private function maybe_display_settings_saved_message() {
		if ( $this->get_connection_type() === 'enterprise' && rgget( 'subview' ) === 'gravityformsrecaptcha' ) {
			$renderer = $this->get_settings_renderer();
			if ( ! $renderer ) {
				return;
			}
			$renderer->set_postback_message_callback(
				function() {
					if ( rgget( 'saved' ) === '1' ) {
						return 'Settings Saved';
					}
				}
			);
			$this->set_settings_renderer( $renderer );
		}
	}

	/**
	 * The settings page icon.
	 *
	 * @since 1.0
	 * @return string
	 */
	public function get_menu_icon() {
		return 'gform-icon--recaptcha';
	}

	/**
	 * Add the recaptcha field to the end of the form.
	 *
	 * @since 1.0
	 *
	 * @depecated 1.1
	 *
	 * @param array $form The form array.
	 *
	 * @return array
	 */
	public function add_recaptcha_field( $form ) {
		return $form;
	}

	/**
	 * Add the recaptcha input to the form.
	 *
	 * @since 1.1
	 *
	 * @param string $form_tag The form tag.
	 * @param array  $form     The form array.
	 *
	 * @return string
	 */
	public function add_recaptcha_input( $form_tag, $form ) {
		if ( empty( $form_tag ) || $this->is_disabled_by_form_setting( $form ) || ! $this->initialize_api( false ) ) {
			return $form_tag;
		}

		return $form_tag . $this->field->get_field_input( $form );
	}

	// # FORM SETTINGS

	/**
	 * Register a form settings tab for reCAPTCHA v3.
	 *
	 * @since 1.0
	 *
	 * @param array $form The form data.
	 *
	 * @return array
	 */
	public function form_settings_fields( $form ) {
		return array(
			array(
				'title'  => 'reCAPTCHA Settings',
				'fields' => array(
					array(
						'type'    => 'checkbox',
						'name'    => 'disable-recaptchav3',
						'choices' => array(
							array(
								'name'          => 'disable-recaptchav3',
								'label'         => __( 'Disable reCAPTCHA v3 for this form.', 'gravityformsrecaptcha' ),
								'default_value' => 0,
							),
						),
					),
				),
			),
		);
	}

	/**
	 * Updates the query string for the settings link displayed in the form editor preview of the Captcha field.
	 *
	 * @since 1.2
	 *
	 * @param string    $field_content The field markup.
	 * @param \GF_Field $field         The field being processed.
	 *
	 * @return string
	 */
	public function update_captcha_field_settings_link( $field_content, $field ) {
		if ( $field->type !== 'captcha' || ! $field->is_form_editor() ) {
			return $field_content;
		}

		return str_replace(
			array( '&subview=recaptcha', '?page=gf_settings' ),
			array( '', '?page=gf_settings&subview=gravityformsrecaptcha' ),
			$field_content
		);
	}

	// # HELPER METHODS ------------------------------------------------------------------------------------------------

	/**
	 * Get the instance of the Token_Verifier class.
	 *
	 * @since 1.0
	 *
	 * @return Token_Verifier
	 */
	public function get_token_verifier() {
		return $this->token_verifier;
	}

	/**
	 * Get the instance of the Plugin_Settings class.
	 *
	 * @return Settings\Plugin_Settings
	 */
	public function get_plugin_settings_instance() {
		return $this->plugin_settings;
	}

	/**
	 * Initialize the connection to the reCAPTCHA API.
	 *
	 * @since 1.0
	 * @since 1.7.0 Separate methods for initialize enterprise and classic APIs.
	 * @since 1.8.0 Added the optional $refresh_token param.
	 *
	 * @param bool $refresh_token Indicates if the auth token should be refreshed.
	 *
	 * @return bool
	 */
	private function initialize_api( $refresh_token = true ) {
		static $result = null;

		if ( is_bool( $result ) ) {
			return $result;
		}

		$plugin_settings = $this->get_plugin_settings();
		$connection_type = rgar( $plugin_settings, 'connection_type' );

		switch ( $connection_type ) {
			case 'enterprise':
				$result = $this->initialize_enterprise_api( $plugin_settings, $refresh_token );
				break;
			case 'v2':
				$this->log_debug( __METHOD__ . '(): Aborting; v2 connection type selected.' );
				$result = false;
				break;
			default:
				$result = $this->initialize_classic_api();
		}

		return $result;
	}

	/**
	 * Initialize the Enterprise API.
	 *
	 * @since 1.7.0
	 * @since 1.8.0 Added the optional $refresh_token param and refresh locking.
	 *
	 * @param array $plugin_settings The plugin settings.
	 * @param bool  $refresh_token   Indicates if the auth token should be refreshed.
	 *
	 * @return bool
	 */
	private function initialize_enterprise_api( $plugin_settings, $refresh_token ) {
		if ( ! rgar( $plugin_settings, 'access_token' ) ) {
			$this->log_debug( __METHOD__ . '(): Access token does not exist, unable to initialize API.' );

			return false;
		}

		$date_created = (int) rgar( $plugin_settings, 'date_token', 0 );
		if ( empty( $date_created ) ) {
			$date_created = (int) rgar( $plugin_settings, 'date_created', 0 );
		}

		if ( ! $refresh_token || ! ( time() > ( $date_created + 3600 ) ) ) {
			$this->log_debug( __METHOD__ . '(): Enterprise API Initialized.' );
			$this->get_api_instance();

			return true;
		}

		if ( ! rgar( $plugin_settings, 'refresh_token' ) ) {
			$this->log_error( __METHOD__ . '(): API tokens expired; refresh token does not exist, unable to refresh access token.' );

			return false;
		}

		$this->log_debug( __METHOD__ . '(): API tokens expired, start refreshing.' );

		if ( ! class_exists( 'Gravity_Forms\Gravity_Forms_RECAPTCHA\Refresh_Lock_Handler' ) ) {
			require_once 'includes/class-refresh-lock-handler.php';
		}

		$refresh_lock_handler = new Refresh_Lock_Handler( $this );

		if ( $refresh_lock_handler->can_refresh_token() === false ) {
			$this->log_debug( __METHOD__ . '():  Aborting; ' . $refresh_lock_handler->refresh_lock_reason );
			$this->init_error_status = 'disabled (token refresh in progress)';

			return false;
		}

		$refresh_lock_handler->lock();

		// Refresh token.
		$auth_response = $this->api->refresh_token( $plugin_settings['refresh_token'] );

		if ( is_wp_error( $auth_response ) ) {
			$this->log_error( __METHOD__ . '(): API access token failed to be refreshed; ' . $auth_response->get_error_message() );
			$refresh_lock_handler->release_lock();
			$refresh_lock_handler->increment_rate_limit();
			$this->init_error_status = 'disabled (token refresh failed)';

			return false;
		}

		$decoded_response = json_decode( rgar( $auth_response, 'auth_payload' ), true );

		$plugin_settings['access_token']  = rgar( $decoded_response, 'access_token' );
		$plugin_settings['refresh_token'] = rgar( $decoded_response, 'refresh_token' );
		$plugin_settings['date_token']    = rgar( $decoded_response, 'created' );

		// Save plugin settings.
		$this->update_plugin_settings( $plugin_settings );
		$this->log_debug( __METHOD__ . '(): API access token has been refreshed; Enterprise API Initialized.' );
		$this->get_api_instance();
		$refresh_lock_handler->release_lock();
		$refresh_lock_handler->reset_rate_limit();

		return true;
	}

	/**
	 * Initialize the v2 and v3 Classic settings.
	 *
	 * @since 1.7.0
	 *
	 * @return bool
	 */
	private function initialize_classic_api() {
		static $result;

		if ( is_bool( $result ) ) {
			return $result;
		}

		$result     = false;
		$site_key   = $this->plugin_settings->get_recaptcha_key( 'site_key_v3' );
		$secret_key = $this->plugin_settings->get_recaptcha_key( 'secret_key_v3' );

		if ( ! ( $site_key && $secret_key ) ) {
			$this->log_debug( __METHOD__ . '(): Missing v3 key configuration. Please check the add-on settings.' );

			return false;
		}

		if ( '1' !== $this->get_plugin_setting( 'recaptcha_keys_status_v3' ) ) {
			$this->log_debug( __METHOD__ . '(): Could not initialize reCAPTCHA v3 because site and/or secret key is invalid.' );

			return false;
		}

		$result = true;
		$this->log_debug( __METHOD__ . '(): API Initialized.' );

		return true;
	}

	/**
	 * Get the Enterprise API instance.
	 *
	 * @since 1.7.0
	 *
	 * @return RECAPTCHA_API
	 */
	public function get_api_instance() {
		$plugin_settings = $this->get_plugin_settings();
		$auth_data       = array(
			'access_token'  => rgar( $plugin_settings, 'access_token' ),
			'refresh_token' => rgar( $plugin_settings, 'refresh_token' ),
			'project_id'    => rgar( $plugin_settings, 'project_id' ),
		);

		$this->api = new RECAPTCHA_API( $auth_data, $this );

		return $this->api;
	}

	/**
	 * Check to determine whether the reCAPTCHA script is needed on a page.
	 *
	 * The script is needed on every page of the front-end if we're able to initialize the API because we've already
	 * verified that the v3 site and secret keys are valid.
	 *
	 * On the back-end, we only want to load this on the settings page, and it should be available regardless of the
	 * status of the keys.
	 *
	 * @since 1.0
	 *
	 * @return bool
	 */
	private function requires_recaptcha_script() {
		return is_admin() ? $this->is_plugin_settings( $this->_slug ) : $this->initialize_api( false );
	}

	/**
	 * Custom enqueuing of the external reCAPTCHA script.
	 *
	 * This script is enqueued via the normal WordPress process because, on the front-end, it's needed on every
	 * single page of the site in order for reCAPTCHA to properly score the interactions leading up to the form
	 * submission.
	 *
	 * @since 1.0
	 * @see GF_RECAPTCHA::init()
	 */
	public function maybe_enqueue_recaptcha_script() {
		if ( ! $this->requires_recaptcha_script() ) {
			return;
		}

		if ( $this->get_connection_type() === 'enterprise' ) {
			$this->enqueue_enterprise_recaptcha_script();

			return;
		}

		$script_url = add_query_arg(
			'render',
			$this->plugin_settings->get_recaptcha_key( 'site_key_v3' ),
			'https://www.google.com/recaptcha/api.js'
		);

		wp_enqueue_script(
			"{$this->asset_prefix}recaptcha",
			$script_url,
			array(),
			$this->_version,
			$this->get_enqueue_script_args()
		);

		$strings                    = $this->localize_script_common_strings();
		$strings['site_key']        = $this->plugin_settings->get_recaptcha_key( 'site_key_v3' );
		$strings['connection_type'] = 'classic';

		wp_localize_script(
			"{$this->asset_prefix}recaptcha",
			"{$this->asset_prefix}recaptcha_strings",
			$strings
		);

		$this->enqueue_frontend_script();
	}

	/**
	 * Enqueues our frontend script that handles executing the external script and hiding the badge.
	 *
	 * @since 1.8.0
	 *
	 * @return void
	 */
	private function enqueue_frontend_script() {
		$frontend_script_name = version_compare( GFForms::$version, '2.9.0-dev-1', '<' ) ? 'frontend-legacy' : 'frontend';
		$deps                 = array( "{$this->asset_prefix}recaptcha" );

		if ( $frontend_script_name === 'frontend-legacy' ) {
			$deps[] = 'jquery';
		}

		wp_enqueue_script(
			$this->asset_prefix . $frontend_script_name,
			$this->get_script_url( $frontend_script_name ),
			$deps,
			$this->_version,
			$this->get_enqueue_script_args()
		);
	}

	/**
	 * Returns the array used for the args param of wp_enqueue_script().
	 *
	 * @since 1.8.0
	 *
	 * @return array
	 */
	private function get_enqueue_script_args() {
		return array(
			'strategy'  => 'defer',
			'in_footer' => true,
		);
	}

	/**
	 * Custom enqueuing of the external reCAPTCHA Enterprise script.
	 *
	 * This script is enqueued via the normal WordPress process because, on the front-end, it's needed on every
	 * single page of the site in order for reCAPTCHA to properly score the interactions leading up to the form
	 * submission.
	 *
	 * @since 1.8.0
	 */
	private function enqueue_enterprise_recaptcha_script() {
		$script_url = add_query_arg(
			'render',
			$this->plugin_settings->get_recaptcha_key( 'site_key_v3_enterprise' ),
			'https://www.google.com/recaptcha/enterprise.js'
		);

		wp_enqueue_script(
			"{$this->asset_prefix}recaptcha",
			$script_url,
			array(),
			$this->_version,
			$this->get_enqueue_script_args()
		);

		$strings                    = $this->localize_script_common_strings();
		$strings['site_key']        = $this->plugin_settings->get_recaptcha_key( 'site_key_v3_enterprise' );
		$strings['connection_type'] = 'enterprise';
		$strings['ajaxurl']         = admin_url( 'admin-ajax.php' );

		wp_localize_script(
			"{$this->asset_prefix}recaptcha",
			"{$this->asset_prefix}recaptcha_strings",
			$strings
		);

		$this->enqueue_frontend_script();
	}

	/**
	 * Get the strings used to localize classic and enterprise reCAPTCHA scripts.
	 *
	 * @since 1.7.0
	 *
	 * @return array
	 */
	private function localize_script_common_strings() {
		$disable_badge = ( $this->is_plugin_settings( $this->_slug ) && rgpost( '_gform_setting_disable_badge_v3' ) === '1' ) || $this->get_plugin_setting( 'disable_badge_v3' ) === '1';

		return array(
			'nonce'                          => wp_create_nonce( "{$this->_slug}_verify_token_nonce" ),
			'disconnect'                     => wp_strip_all_tags( __( 'Disconnecting', 'gravityformsrecaptcha' ) ),
			'change_connection_type'         => wp_strip_all_tags( __( 'Resetting', 'gravityformsrecaptcha' ) ),
			'spinner'                        => GFCommon::get_base_url() . '/images/spinner.svg',
			'connection_type'                => $this->get_connection_type(),
			'disable_badge'                  => $disable_badge,
			'change_connection_type_title'   => __( 'Change Connection Type', 'gravityformsrecaptcha' ),
			'change_connection_type_message' => __( 'Changing the connection type will delete your current settings.  Do you want to proceed?', 'gravityformsrecaptcha' ),
			'disconnect_title'               => __( 'Disconnect', 'gravityformsrecaptcha' ),
			'disconnect_message'             => __( 'Disconnecting from reCAPTCHA will delete your current settings.  Do you want to proceed?', 'gravityformsrecaptcha' ),
		);
	}

	/**
	 * Sets up additional data points for sorting on the entry.
	 *
	 * @since 1.0
	 *
	 * @param array $entry_meta The entry metadata.
	 * @param int   $form_id The ID of the form.
	 *
	 * @return array
	 */
	public function get_entry_meta( $entry_meta, $form_id ) {
		$entry_meta[ "{$this->_slug}_score" ] = array(
			'label'                      => __( 'reCAPTCHA Score', 'gravityformsrecaptcha' ),
			'is_numeric'                 => true,
			'update_entry_meta_callback' => array( $this, 'update_entry_meta' ),
			'is_default_column'          => true,
			'filter'                     => array(
				'operators' => array( 'is', '>', '<' ),
			),
		);

		return $entry_meta;
	}

	/**
	 * Save the Recaptcha metadata values to the entry.
	 *
	 * @since 1.0
	 * @since 2.0 Updated to save the Enterprise assessment ID, if available.
	 *
	 * @see   GF_RECAPTCHA::get_entry_meta()
	 *
	 * @param string $key   The entry meta key.
	 * @param array  $entry The entry data.
	 * @param array  $form  The form data.
	 *
	 * @return float|void
	 */
	public function update_entry_meta( $key, $entry, $form ) {
		if ( $key !== "{$this->_slug}_score" ) {
			return;
		}

		$existing_value = rgar( $entry, $key );
		if ( $this->is_entry_edit() || ! rgblank( $existing_value ) ) {
			return $existing_value;
		}

		$entry_id = rgar( $entry, 'id' );
		$form_id  = rgar( $form, 'id' );

		if ( $this->is_disabled_by_form_setting( $form ) ) {
			$this->log_debug( __METHOD__ . sprintf( '(): Not saving score for entry #%d for form #%d; disabled via setting.', $entry_id, $form_id ) );

			return 'disabled';
		}

		if ( $this->is_disabled_by_quota_limit() ) {
			$this->log_debug( __METHOD__ . sprintf( '(): Not saving score for entry #%d for form #%d; disabled due to API quota limit.', $entry_id, $form_id ) );

			return 'disabled (quota limit)';
		}

		if ( ! $this->initialize_api() ) {
			$this->log_debug( __METHOD__ . sprintf( '(): Not saving score for entry #%d for form #%d; API not initialized.', $entry_id, $form_id ) );

			return $this->init_error_status;
		}

		if ( $this->get_connection_type() === 'enterprise' && ! $this->enterprise_keys_configured() ) {
			$this->log_debug( __METHOD__ . sprintf( '(): Not saving score for entry #%d for form #%d; the Enterprise project and/or key settings are not configured.', $entry_id, $form_id ) );

			return 'disconnected';
		}

		$assessment_id = $this->token_verifier->get_assessment_id();
		if ( $assessment_id ) {
			$this->log_debug( __METHOD__ . sprintf( '(): Saving assessment ID (%s) for entry #%d for form #%d.', $assessment_id, $entry_id, $form_id ) );
			gform_update_meta( $entry_id, $this->get_slug() . '_assessment_id', $assessment_id, $form_id );
		}

		$score = $this->token_verifier->get_score();
		$this->log_debug( __METHOD__ . sprintf( '(): Saving score (%s) for entry #%d for form #%d.', $score, $entry_id, $form_id ) );

		return $score;
	}

	/**
	 * Registers a metabox on the entry details screen.
	 *
	 * @since 1.0
	 *
	 * @param array $metaboxes Gravity Forms registered metaboxes.
	 * @param array $entry     The entry array.
	 * @param array $form      The form array.
	 *
	 * @return array
	 */
	public function register_meta_box( $metaboxes, $entry, $form ) {
		$score = $this->get_score_from_entry( $entry );

		if ( ! $score ) {
			return $metaboxes;
		}

		$metaboxes[ $this->_slug ] = array(
			'title'    => esc_html__( 'reCAPTCHA', 'gravityformsrecaptcha' ),
			'callback' => array( $this, 'add_recaptcha_meta_box' ),
			'context'  => 'side',
		);

		return $metaboxes;
	}

	/**
	 * Callback to output the entry details metabox.
	 *
	 * @since 1.0
	 * @see   GF_RECAPTCHA::register_meta_box()
	 *
	 * @param array $data An array containing the form and entry data.
	 */
	public function add_recaptcha_meta_box( $data ) {
		$score = $this->get_score_from_entry( rgar( $data, 'entry' ) );

		printf(
			'<div><p>%s: %s</p><p><a href="%s">%s</a></p></div>',
			esc_html__( 'Score', 'gravityformsrecaptcha' ),
			esc_html( $this->get_score_display_value( $score ) ),
			esc_html( 'https://docs.gravityforms.com/captcha/' ),
			esc_html__( 'Click here to learn more about reCAPTCHA.', 'gravityformsrecaptcha' )
		);
	}

	/**
	 * Returns the value to be displayed on the entries list page.
	 *
	 * @since 2.0
	 *
	 * @param mixed $value    The value to be displayed.
	 * @param int   $form_id  The ID of the form the entries are being listed for.
	 * @param int   $field_id The field ID or entry meta key for the value being displayed.
	 *
	 * @return mixed
	 */
	public function entries_field_value( $value, $form_id, $field_id ) {
		if ( empty( $value ) || $field_id !== "{$this->_slug}_score" ) {
			return $value;
		}

		return esc_html( $this->get_score_display_value( $value ) );
	}

	/**
	 * Callback to gform_entry_is_spam that determines whether to categorize this entry as such.
	 *
	 * @since 1.0
	 *
	 * @see   GF_RECAPTCHA::init();
	 *
	 * @param bool  $is_spam Whether the entry is spam.
	 * @param array $form    The form data.
	 * @param array $entry   The entry data.
	 *
	 * @return bool
	 */
	public function check_for_spam_entry( $is_spam, $form, $entry ) {

		if ( $is_spam ) {
			$this->log_debug( __METHOD__ . '(): Skipping, entry has already been identified as spam by another anti-spam solution.' );
			return $is_spam;
		}

		$is_spam = $this->is_spam_submission( $form, $entry );
		$this->log_debug( __METHOD__ . '(): Is submission considered spam? ' . ( $is_spam ? 'Yes.' : 'No.' ) );

		return $is_spam;
	}

	/**
	 * Determines if the submission is spam by comparing its score with the threshold.
	 *
	 * @since 1.4
	 * @since 1.5 Added the optional $entry param.
	 *
	 * @param array $form  The form being processed.
	 * @param array $entry The entry being processed.
	 *
	 * @return bool
	 */
	public function is_spam_submission( $form, $entry = array() ) {
		if ( $this->should_skip_validation( $form ) || $this->is_disabled_by_quota_limit() ) {
			$this->log_debug( __METHOD__ . '(): Score check skipped.' );

			return false;
		}

		$score = empty( $entry ) ? $this->token_verifier->get_score() : $this->get_score_from_entry( $entry );
		if ( ! is_numeric( $score ) ) {
			return false;
		}

		$threshold = $this->get_spam_score_threshold();

		return (float) $score <= (float) $threshold;
	}

	/**
	 * Get the Recaptcha score from the entry details.
	 *
	 * @since 1.0
	 *
	 * @param array $entry The entry array.
	 *
	 * @return float|string
	 */
	private function get_score_from_entry( $entry ) {
		$score = rgar( $entry, "{$this->_slug}_score" );

		if ( in_array( $score, $this->v3_disabled_states, true ) ) {
			return $score;
		}

		return $score ? (float) $score : $this->token_verifier->get_score();
	}

	/**
	 * Returns the score to be displayed or the state display label.
	 *
	 * @since 2.0
	 *
	 * @param float|string $meta_value The entry meta value.
	 *
	 * @return float|string
	 */
	private function get_score_display_value( $meta_value ) {
		if ( is_numeric( $meta_value ) ) {
			return $meta_value;
		}

		$states = array(
			'disabled'                             => __( 'Disabled', 'gravityformsrecaptcha' ),
			'disconnected'                         => __( 'Disconnected', 'gravityformsrecaptcha' ),
			'disabled (quota limit)'               => __( 'Disabled (quota limit)', 'gravityformsrecaptcha' ),
			'disabled (token refresh in progress)' => __( 'Disabled (token refresh in progress)', 'gravityformsrecaptcha' ),
			'disabled (token refresh failed)'      => __( 'Disabled (token refresh failed)', 'gravityformsrecaptcha' ),
		);

		return rgar( $states, $meta_value, $meta_value );
	}

	/**
	 * The score that determines whether the entry is spam.
	 *
	 * Hard-coded for now, but this will eventually be an option within the add-on.
	 *
	 * @since 1.0
	 *
	 * @return float
	 */
	private function get_spam_score_threshold() {
		static $value;

		if ( ! empty( $value ) ) {
			return $value;
		}

		$value = (float) $this->get_plugin_setting( 'score_threshold_v3' );
		if ( empty( $value ) ) {
			$value = 0.5;
		}
		$this->log_debug( __METHOD__ . '(): ' . $value );

		return $value;
	}

	/**
	 * Determine whether a given form has disabled reCAPTCHA within its settings.
	 *
	 * @since 1.0
	 *
	 * @param array $form The form data.
	 *
	 * @return bool
	 */
	private function is_disabled_by_form_setting( $form ) {
		return empty( $form['id'] ) || '1' === rgar( $this->get_form_settings( $form ), 'disable-recaptchav3' );
	}

	/**
	 * Determine whether a given form has disabled reCAPTCHA within its settings.
	 *
	 * @since 1.7
	 *
	 * @return bool
	 */
	private function is_disabled_by_quota_limit() {
		$recaptcha_result = $this->token_verifier->get_recaptcha_result();

		if ( is_a( $recaptcha_result, "stdClass" ) && property_exists( $recaptcha_result, 'score' ) && $recaptcha_result->score === 'disabled (quota limit)' ) {
			return true;
		}

		return false;
	}

	/**
	 * Validate the form submission.
	 *
	 * @since 1.0
	 *
	 * @param array $submission_data The submitted form data.
	 *
	 * @return array
	 */
	public function validate_submission( $submission_data ) {
		$this->log_debug( __METHOD__ . '(): Validating form (#' . rgars( $submission_data, 'form/id' ) . ') submission.' );

		if ( $this->should_skip_validation( rgar( $submission_data, 'form' ) ) ) {
			$this->log_debug( __METHOD__ . '(): Validation skipped.' );

			return $submission_data;
		}

		$this->log_debug( __METHOD__ . '(): Validating reCAPTCHA v3.' );

		return $this->field->validation_check( $submission_data );
	}

	/**
	 * Check If reCaptcha validation should be skipped.
	 *
	 * In some situations where the form validation could be triggered twice, for example while making a stripe payment element transaction
	 * we want to skip the reCaptcha validation so it isn't triggered twice, as this will make it always fail.
	 *
	 * @since 1.4
	 * @since 1.5 Changed param to $form array.
	 *
	 * @param array $form The form being processed.
	 *
	 * @return bool
	 */
	public function should_skip_validation( $form ) {
		static $result = array();

		$form_id = rgar( $form, 'id' );
		if ( isset( $result[ $form_id ] ) ) {
			return $result[ $form_id ];
		}

		$result[ $form_id ] = true;

		if ( $this->is_preview() ) {
			$this->log_debug( __METHOD__ . '(): Yes! Form preview page.' );

			return true;
		}

		if ( ! $this->initialize_api() ) {
			$this->log_debug( __METHOD__ . '(): Yes! API not initialized.' );

			return true;
		}

		if ( $this->get_connection_type() === 'enterprise' && ! $this->enterprise_keys_configured() ) {
			$this->log_debug( __METHOD__ . '(): Yes! Enterprise has not been fully configured.' );

			return true;
		}

		if ( $this->is_disabled_by_form_setting( $form ) ) {
			$this->log_debug( __METHOD__ . '(): Yes! Disabled by form setting.' );

			return true;
		}

		if ( defined( 'REST_REQUEST' ) && REST_REQUEST && ! isset( $_POST[ $this->field->get_input_name( $form_id ) ] ) ) {
			$this->log_debug( __METHOD__ . '(): Yes! REST request without input.' );

			return true;
		}

		// For older versions of Stripe, skip the first validation attempt and only validate on the second attempt. Newer versions of Stripe will validate twice without a problem.
		if ( $this->is_stripe_validation() && version_compare( gf_stripe()->get_version(), '5.4.3', '<' ) ) {
			$this->log_debug( __METHOD__ . '(): Yes! Older Stripe validation.' );

			return true;
		}

		$result[ $form_id ] = false;

		return false;
	}

	/**
	 * Check if the Enterprise keys are configured.
	 *
	 * @since 1.7.0
	 */
	public function enterprise_keys_configured() {
		$site_key = $this->plugin_settings->get_recaptcha_key( 'site_key_v3_enterprise' );
		$project  = $this->plugin_settings->get_recaptcha_key( 'project_number' );

		if ( ! ( $site_key && $project ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Check if this is a stripe validation request.
	 *
	 * @since 1.4
	 *
	 * @return bool Returns true if this is a stripe validation request. Returns false otherwise.
	 */
	public function is_stripe_validation() {
		return function_exists( 'gf_stripe' ) && rgpost( 'action' ) === 'gfstripe_validate_form';
	}

	/**
	 * Check if this is a preview request, taking into account Stripe's validation request.
	 *
	 * @since 1.4
	 *
	 * @return bool Returns true if this is a preview request. Returns false otherwise.
	 */
	public function is_preview() {

		return parent::is_preview() || ( $this->is_stripe_validation() && rgget( 'preview' ) === '1' );
	}

	/**
	 * Add the recaptcha v3 input and value to the draft.
	 *
	 * @since 1.2
	 *
	 * @param array  $submission_json The json containing the submitted values and the partial entry created from the values.
	 * @param string $resume_token    The resume token.
	 * @param array  $form            The form data.
	 *
	 * @return string The json string for the submission with the recaptcha v3 input and value added.
	 */
	public function add_recaptcha_v3_input_to_draft( $submission_json, $resume_token, $form ) {
		$submission                                   = json_decode( $submission_json, true );
		$input_name                                   = $this->field->get_input_name( rgar( $form , 'id' ) );
		$submission[ 'partial_entry' ][ $input_name ] = rgpost( $input_name );

		return wp_json_encode( $submission );
	}

	/**
	 * Shows admin notice if the quota limit has been reached. Once the notice
	 * is dismissed, the admin notice will go away until the next time the
	 * quota limit is reached.
	 *
	 * @since 1.7
	 *
	 * @return void
	 */
	public function recaptcha_quota_notice ( ) {
		if ( ! current_user_can( 'gform_full_access' ) ) {
			return;
		}

		if ( false === get_option( self::RECAPTCHA_QUOTA_LIMIT_HIT ) ) {
			return;
		}

		?>
		<div class="notice notice-warning is-dismissible gf-notice"
				data-gf_recaptcha_quota_nonce="<?php echo wp_create_nonce( 'gf_recaptcha_quota_notice' ) ?>" >
			<h2><?php echo $this->_title; ?></h2>
			<p>
				<?php
					// translators: %s is the link markup.
					echo sprintf(
						esc_html__( 'You have reached the quota limit for reCAPTCHA set by Google. Please check the quota on your %sreCAPTCHA Account%s.', 'gravityformsrecaptcha' ),
						'<a href="https://cloud.google.com/security/products/recaptcha" target="_blank">',
						'</a>'
					);
				?>
			</p>
		</div>
		<script>
			jQuery( document ).ready( function( $ ) {
				$( document ).on( 'click', '.notice-dismiss', function() {
					var $div = $( this ).closest( 'div.notice' );
					if ( $div.length > 0 ) {
						var nonce = $div.data( 'gf_recaptcha_quota_nonce' );
						jQuery.ajax( {
							url: ajaxurl,
							data: {
								action: 'gf_recaptcha_quota_notice',
								nonce: nonce,
							},
						} );
					}
				} );
			} );
		</script>
		<?php
	}

	/**
	 * Removes setting checked by the reCAPTCHA quota limit notice.
	 *
	 * @since 1.7
	 *
	 * @return void
	 */
	public function gf_recaptcha_quota_notice_dismiss() {
		check_admin_referer( 'gf_recaptcha_quota_notice', 'nonce' );
		delete_option( self::RECAPTCHA_QUOTA_LIMIT_HIT );
		wp_send_json_success();
	}

	/**
	 * Update and reload the settings.
	 *
	 * @since 1.7.0
	 *
	 * @return void
	 */
	public function update_reload_settings() {
		if ( ! wp_verify_nonce( rgpost( 'nonce' ), 'connect_recaptcha' ) || ! $this->current_user_can_any( $this->_capabilities_form_settings ) ) {
			wp_send_json_error(
				array(
					'errors'   => true,
					'redirect' => '',
				)
			);
		}

		$redirect_url                = admin_url( 'admin.php?page=gf_settings&subview=' . $this->_slug );
		$settings                    = $this->get_plugin_settings();
		$settings['connection_type'] = sanitize_text_field( rgpost( 'connection_type' ) );

		// Updating options.
		$this->update_plugin_settings( $settings );

		wp_send_json_success(
			array(
				'errors'   => false,
				'redirect' => esc_url_raw( $redirect_url ),
			)
		);
	}

	/**
	 * Get the connection type.
	 *
	 * @since 1.7.0
	 *
	 * @return string The connection type
	 */
	public function get_connection_type() {
		$settings = $this->get_plugin_settings();
		return rgar( $settings, 'connection_type' );
	}

	/**
	 * Disconnects user from reCAPTCHA and deletes relevant settings.
	 *
	 * @since 1.7.0
	 *
	 * @return void
	 */
	public function ajax_disconnect_recaptcha() {

		// Verify nonce and capability.
		$this->verify_ajax_nonce( 'gforms_google_recaptcha_disconnect' );

		if ( ! $this->current_user_can_any( $this->_capabilities_form_settings ) ) {
			$this->log_debug( __METHOD__ . '(): Permissions for form settings not met.' );
			wp_send_json_error( new WP_Error( 'google_recaptcha_error', wp_strip_all_tags( __( 'User does not have required permissions to setup reCAPTCHA.', 'gravityformsrecaptcha' ) ) ) );
		}

		delete_option( 'gravityformsaddon_gravityformsrecaptcha_settings' );
		delete_option( 'rg_gforms_captcha_public_key' );
		delete_option( 'rg_gforms_captcha_private_key' );
		delete_option( 'rg_gforms_captcha_type' );
		delete_option( 'gform_recaptcha_keys_status' );
		wp_send_json_success( array() );
	}

	/**
	 * Verify the ajax nonce.
	 *
	 * @param string $nonce_action The name of the nonce action. Defaults to 'connect_recaptcha'.
	 *
	 * @since 1.7.0
	 *
	 * @return void
	 */
	public function verify_ajax_nonce( $nonce_action = 'connect_recaptcha' ) {
		if ( ! wp_verify_nonce( rgpost( 'nonce' ), $nonce_action ) ) {
			$this->log_debug( __METHOD__ . '(): Nonce validation failed.' );
			wp_send_json_error( new WP_Error( 'google_recaptcha_error', wp_strip_all_tags( __( 'Nonce validation has failed.', 'gravityformsrecatpcha' ) ) ) );
		}
	}

	/**
	 * Get the Enterprise site keys with Ajax.
	 *
	 * @param string|null $project The Google Project ID.
	 *
	 * @since 1.7.0
	 *
	 * @return false|void
	 */
	public function ajax_get_enterprise_site_keys( $project = null ) {

		$this->verify_ajax_nonce();

		if ( ! $this->initialize_api() ) {
			$this->log_debug( __METHOD__ . '(): Unable to initialize reCAPTCHA API.' );

			return false;
		}

		if ( ! $this->current_user_can_any( $this->_capabilities_form_settings ) ) {
			$this->log_debug( __METHOD__ . '(): User does not have Form Settings capability.' );

			return false;
		}

		// Retrieving data streams.
		$project   = $project ? $project : sanitize_text_field( rgpost( 'project' ) );
		$site_keys = $this->api->get_enterprise_site_keys( $project );
		if ( is_wp_error( $site_keys ) ) {
			$this->log_debug( __METHOD__ . '(): Error retrieving sitekeys associated with the selected project.' );
			wp_send_json_error( new WP_Error( 'google_recaptcha_error', wp_strip_all_tags( __( 'There was an error retrieving reCAPTHCA site keys.', 'gravityformsrecaptcha' ) ) ) );
		}

		$data = array();

		foreach ( $site_keys['keys'] as $site_key ) {
			$data[] = array(
				'value'       => basename( $site_key['name'] ),
				'displayName' => $site_key['displayName'],
			);
		}

		wp_send_json_success( $data );
	}

	/**
	 * Update the plugin settings with the selected enterprise data.
	 *
	 * @since 1.7.0
	 *
	 * @return void
	 */
	public function ajax_save_recaptcha_enterprise_data() {

		$this->verify_ajax_nonce();

		$this->log_debug( __METHOD__ . '(): Saving reCAPTCHA Enterprise settings.' );
		$updated_settings = array();

		$updated_settings['project_number']         = sanitize_text_field( rgpost( 'project_number' ) );
		$updated_settings['project_id']             = sanitize_text_field( rgpost( 'project_id' ) );
		$updated_settings['site_key_v3_enterprise'] = sanitize_text_field( rgpost( 'site_key_v3_enterprise' ) );
		$updated_settings['site_key_display_name']  = sanitize_text_field( rgpost( 'site_key_display_name' ) );
		$updated_settings['score_threshold_v3']     = sanitize_text_field( rgpost( 'score_threshold_v3' ) );
		$updated_settings['disable_badge_v3']       = rgpost( 'disable_badge_v3' ) === '1' ? '1' : '0';

		$this->update_plugin_settings( $updated_settings );

		// Build redirect url and return it.
		$redirect_url = add_query_arg(
			array(
				'page'    => 'gf_settings',
				'subview' => 'gravityformsrecaptcha',
				'saved'   => '1',
			),
			admin_url( 'admin.php' )
		);
		wp_send_json_success( esc_url_raw( $redirect_url ) );
	}

	/**
	 * Callback for gform_update_status; notifies Google that the entry has been manually marked as spam or ham.
	 *
	 * @since 2.0
	 *
	 * @param int    $entry_id       The ID of the entry the status changed for.
	 * @param string $new_value      The value value of the status property.
	 * @param string $previous_value The previous value of the status property.
	 *
	 * @return void
	 */
	public function entry_status_change( $entry_id, $new_value, $previous_value ) {
		$mark_as_spam = ( $new_value === 'spam' && $previous_value === 'active' );
		$mark_as_ham  = ( $new_value === 'active' && $previous_value === 'spam' );

		if ( ! $mark_as_spam && ! $mark_as_ham ) {
			return;
		}

		$assessment_id = gform_get_meta( $entry_id, $this->get_slug() . '_assessment_id' );
		if ( empty( $assessment_id ) ) {
			$this->log_debug( __METHOD__ . sprintf( '(): Not processing entry #%d; No assessment ID.', $entry_id ) );

			return;
		}

		if ( $this->get_connection_type() !== 'enterprise' || ! $this->initialize_api() ) {
			$this->log_debug( __METHOD__ . sprintf( '(): Not processing entry #%d; Enterprise API not initialized.', $entry_id ) );

			return;
		}

		if ( $mark_as_spam ) {
			$note       = esc_html__( 'Google notified that the entry was marked as spam.', 'gravityformsrecaptcha' );
			$action     = 'spam';
			$annotation = 'FRAUDULENT';
		} else {
			$note       = esc_html__( 'Google notified that the entry was marked as not spam.', 'gravityformsrecaptcha' );
			$action     = 'ham';
			$annotation = 'LEGITIMATE';
		}

		$response = $this->api->annotate_assessment( $assessment_id, $annotation );
		$this->add_note( $entry_id, $note );
		$this->log_debug( __METHOD__ . sprintf( '(): Google notified that entry #%d (assessment ID: %s) was marked as %s.%s', $entry_id, $assessment_id, $action, ( $response ? ' Response: ' . print_r( $response, true ) : '' ) ) );
	}

}