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> <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 ) : '' ) ) );
}
}