update plugins

This commit is contained in:
Tony Volpe
2024-06-17 14:42:23 -04:00
parent a00f379f7f
commit 38e314323c
9467 changed files with 2032414 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
## 1.5.0 | 2024-04-30
- Fixed an issue where duplicate entries are created when using Conversational Forms with reCAPTCHA v3.
- Fixed an issue where form submission hangs after Stripe 3DS validation.
- Fixed an issue where all REST API submissions are marked as spam.
- Note: If used alongside the Stripe Add-On, this version of the reCAPTCHA Add-On requires version 5.5.0 or later of the Stripe Add-On.
## 1.4.0 | 2024-01-17
- Fixed an issue where reCaptcha v3 validation is not triggered when using the Stripe Payment Element.
- Fixed the PHP 8.2 creation of dynamic property deprecation notice that occurs on form submission.
## 1.3.0 | 2023-11-09
- Fixed an issue where a JavaScript error can occur on the front-end if the page also includes custom or third-party forms.
- Fixed an issue where the v3 settings aren't populated by the GF_RECAPTCHA_V3_SITE_KEY and GF_RECAPTCHA_V3_SECRET_KEY constants.
## 1.2.0 | 2023-08-31
- Updated the reCAPTCHA settings link for the Captcha field "To use the reCAPTCHA field" message in the form editor.
- Fixed an issue where reCAPTCHA fails validation when using the Stripe Payment Element.
- Fixed an issue that causes the scripts for the frontend to not be available in production mode when compiled by Webpack.
- Fixed an issue where scripts are sometimes missing dependencies, and sometimes getting loaded unnecessarily.
## 1.1 | 2021-07-21
- Fixed an issue where an undefined variable notice appears on the add-on settings page.
- Fixed an issue where forms can fail validation if they include dynamically added fields such as the honeypot.
- Fixed an issue where the reCAPTCHA response is saved and output by merge tags.
- Fixed an issue where submissions from the User Registration Add-On login form are blocked.
## 1.0 | 2021-06-23
- It's all new!

View File

@@ -0,0 +1,992 @@
<?php
namespace Gravity_Forms\Gravity_Forms_RECAPTCHA;
defined( 'ABSPATH' ) || die();
use GFForms;
use GFAddOn;
use GF_Fields;
use GFAPI;
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 {
/**
* 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.
*
* @var array
*/
private $v3_disabled_states = array(
'disabled',
'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' ) );
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, '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 );
}
/**
* Register admin initialization hooks.
*
* @since 1.0
*/
public function init_admin() {
parent::init_admin();
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_recaptcha_script' ) );
}
/**
* 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' ) );
}
/**
* Register scripts.
*
* @since 1.0
*
* @return array
*/
public function scripts() {
$scripts = array(
array(
'handle' => "{$this->asset_prefix}frontend",
'src' => $this->get_script_url( 'frontend' ),
'version' => $this->_version,
'deps' => array( 'jquery', "{$this->asset_prefix}recaptcha" ),
'in_footer' => true,
'enqueue' => array(
array( $this, 'frontend_script_callback' ),
),
),
);
// 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" );
} 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 );
}
/**
* 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() {
return $this->plugin_settings->get_settings( parent::get_plugin_settings() );
}
/**
* 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 ) {
$this->plugin_settings->update_settings( $settings );
parent::update_plugin_settings( $settings );
}
/**
* 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() ) {
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
*
* @return bool
*/
private function initialize_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;
}
/**
* 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();
}
/**
* 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 enqueue_recaptcha_script() {
if ( ! $this->requires_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( 'jquery' ),
$this->_version,
true
);
wp_localize_script(
"{$this->asset_prefix}recaptcha",
"{$this->asset_prefix}recaptcha_strings",
array(
'site_key' => $this->plugin_settings->get_recaptcha_key( 'site_key_v3' ),
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( "{$this->_slug}_verify_token_nonce" ),
)
);
if ( $this->get_plugin_setting( 'disable_badge_v3' ) !== '1' ) {
return;
}
// Add inline JS to disable the badge.
wp_add_inline_script(
"{$this->asset_prefix}recaptcha",
'(function($){grecaptcha.ready(function(){$(\'.grecaptcha-badge\').css(\'visibility\',\'hidden\');});})(jQuery);'
);
}
/**
* Callback to determine whether to render the frontend script.
*
* @since 1.0
*
* @param array $form The form array.
*
* @return bool
*/
public function frontend_script_callback( $form ) {
return $form && ! is_admin();
}
/**
* 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
*
* @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;
}
if ( $this->is_disabled_by_form_setting( $form ) ) {
$this->log_debug( __METHOD__ . '(): reCAPTCHA v3 disabled on form ' . rgar( $form, 'id' ) );
return 'disabled';
}
if ( ! $this->initialize_api() ) {
return 'disconnected';
}
return $this->token_verifier->get_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( $score ),
esc_html( 'https://docs.gravityforms.com/captcha/' ),
esc_html__( 'Click here to learn more about reCAPTCHA.', 'gravityformsrecaptcha' )
);
}
/**
* 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->log_debug( __METHOD__ . '(): Score check skipped.' );
return false;
}
$score = empty( $entry ) ? $this->token_verifier->get_score() : $this->get_score_from_entry( $entry );
$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();
}
/**
* 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' );
}
/**
* 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->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 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 );
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace Gravity_Forms\Gravity_Forms_RECAPTCHA;
use GF_Field;
use GFCommon;
use GFAPI;
/**
* Class GF_Field_RECAPTCHA
*
* @since 1.0
*
* @package Gravity_Forms\Gravity_Forms_RECAPTCHA
*/
class GF_Field_RECAPTCHA extends GF_Field {
/**
* Recaptcha field type.
*
* @since 1.0
* @var string
*/
public $type = 'recaptcha';
/**
* Prevent the field being saved to the entry.
*
* @since 1.1
* @var bool
*/
public $displayOnly = true;
/**
* Decoded field data.
*
* @since 1.0
* @var object
*/
private $data;
/**
* Return empty array to prevent the field from showing up in the form editor.
*
* @since 1.0
* @return array
*/
public function get_form_editor_button() {
return array();
}
/**
* The field markup.
*
* @since 1.0
*
* @param array $form The form array.
* @param string $value The field value.
* @param array|null $entry The entry array.
*
* @return string
*/
public function get_field_input( $form, $value = '', $entry = null ) {
$plugin_settings = gf_recaptcha()->get_plugin_settings_instance();
$site_key = $plugin_settings->get_recaptcha_key( 'site_key_v3' );
$secret_key = $plugin_settings->get_recaptcha_key( 'secret_key_v3' );
if ( empty( $site_key ) || empty( $secret_key ) ) {
GFCommon::log_error( __METHOD__ . sprintf( '(): reCAPTCHA secret keys not saved in the reCAPTCHA Settings (%s). The reCAPTCHA field will always fail validation during form submission.', admin_url( 'admin.php' ) . '?page=gf_settings&subview=recaptcha' ) );
}
$this->formId = absint( rgar( $form, 'id' ) );
$name = $this->get_input_name();
$tabindex = GFCommon::$tab_index > 0 ? GFCommon::$tab_index ++ : 0;
return "<div class='gf_invisible ginput_recaptchav3' data-sitekey='" . esc_attr( $site_key ) . "' data-tabindex='{$tabindex}'>"
. '<input id="' . esc_attr( $name ) . '" class="gfield_recaptcha_response" type="hidden" name="' . esc_attr( $name ) . '" value=""/>'
. '</div>';
}
/**
* Modify the validation result if the Recaptcha response has been altered.
*
* This is a callback to the gform_validation filter to allow us to validate the values in the hidden field.
*
* @since 1.0
*
* @see GF_RECAPTCHA::init()
*
* @param array $validation_data The validation data.
*
* @return array
*/
public function validation_check( $validation_data ) {
$this->formId = absint( rgars( $validation_data, 'form/id' ) );
if ( $this->is_valid_field_data() ) {
// Set is_spam value.
$validation_data['is_spam'] = gf_recaptcha()->is_spam_submission( rgar( $validation_data, 'form' ) );
return $validation_data;
}
// Set is_valid to false and return the validation data.
return $this->invalidate( $validation_data );
}
/**
* Validates that the data in the hidden input is a valid Recaptcha entry.
*
* @since 1.0
*
* @return bool
*/
private function is_valid_field_data() {
$data = rgpost( $this->get_input_name() );
if ( empty( $data ) ) {
gf_recaptcha()->log_debug( __METHOD__ . "(): Input {$this->get_input_name()} empty." );
return false;
}
return gf_recaptcha()->get_token_verifier()->verify_submission( $data );
}
/**
* Set is_valid to false on the validation data.
*
* @since 1.0
*
* @param array $validation_data The validation data.
*
* @return mixed
*/
private function invalidate( $validation_data ) {
$validation_data['is_valid'] = false;
return $validation_data;
}
/**
* Returns the value of the input name attribute.
*
* @since 1.1
* @since 1.2 Added optional form_id parameter.
*
* @return string
*/
public function get_input_name( $form_id = null ) {
if ( $form_id ) {
$this->formId = absint( $form_id );
}
return 'input_' . md5( 'recaptchav3' . gf_recaptcha()->get_version() . $this->formId );
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* API wrapper for the Recaptcha service.
*
* @since 1.0
* @package Gravity_Forms\Gravity_Forms_RECAPTCHA
*/
namespace Gravity_Forms\Gravity_Forms_RECAPTCHA;
/**
* Class RECAPTCHA_API
*
* @package Gravity_Forms\Gravity_Forms_RECAPTCHA
*/
class RECAPTCHA_API {
/**
* Google Recaptcha token verification URL.
*
* @since 1.0
* @var string
*/
private $verification_url = 'https://www.google.com/recaptcha/api/siteverify';
/**
* Get the result of token verification from the Recaptcha API.
*
* @param string $token The token to verify.
* @param string $secret The site's secret key.
*
* @return array|\WP_Error
*/
public function verify_token( $token, $secret ) {
return wp_remote_post(
$this->verification_url,
array(
'body' => array(
'secret' => $secret,
'response' => $token,
),
)
);
}
}

View File

@@ -0,0 +1,364 @@
<?php
/**
* Class responsible for verifying tokens returned by Recaptcha.
*
* @package Gravity_Forms\Gravity_Forms_RECAPTCHA
*/
namespace Gravity_Forms\Gravity_Forms_RECAPTCHA;
use GFCommon;
use stdClass;
/**
* Class Token_Verifier
*
* @since 1.0
*
* @package Gravity_Forms\Gravity_Forms_RECAPTCHA
*/
class Token_Verifier {
/**
* Error code returned if a token or secret is missing.
*
* @since 1.0
*/
const ERROR_CODE_MISSING_TOKEN_OR_SECRET = 'gravityformsrecaptcha-missing-token-or-secret';
/**
* Error code returned if the token cannot be verified.
*
* @since 1.0
*/
const ERROR_CODE_CANNOT_VERIFY_TOKEN = 'gravityforms-cannot-verify-token';
/**
* Instance of the add-on class.
*
* @since 1.0
* @var GF_RECAPTCHA
*/
private $addon;
/**
* Class instance.
*
* @since 1.0
* @var RECAPTCHA_API
*/
private $api;
/**
* Minimum score the Recaptcha API can return before a form submission is marked as spam.
*
* @since 1.0
* @var float
*/
private $score_threshold;
/**
* Token generated by the Recaptcha service that requires validation.
*
* @since 1.0
* @var string
*/
private $token;
/**
* Recaptcha application secret used to verify the token.
*
* @since 1.0
* @var string
*/
private $secret;
/**
* Result of the recaptcha request.
*
* @var stdClass
*/
private $recaptcha_result;
/**
* The reCAPTCHA action.
*
* @since 1.4 Previously a dynamic property.
*
* @var string
*/
private $action;
/**
* Token_Verifier constructor.
*
* @since 1.0
*
* @param GF_RECAPTCHA $addon Instance of the GF_RECAPTCHA add-on.
* @param RECAPTCHA_API $api Instance of the Recaptcha API.
*/
public function __construct( GF_RECAPTCHA $addon, RECAPTCHA_API $api ) {
$this->addon = $addon;
$this->api = $api;
}
/**
* Initializes this object for use.
*
* @param string $token The reCAPTCHA token.
* @param string $action The reCAPTCHA action.
*
* @since 1.0
*/
public function init( $token = '', $action = '' ) {
$this->token = $token;
$this->action = $action;
$this->secret = $this->addon->get_plugin_settings_instance()->get_recaptcha_key( 'secret_key_v3' );
$this->score_threshold = $this->addon->get_plugin_setting( 'score_threshold_v3', 0.5 );
}
/**
* Get the reCAPTCHA result.
*
* Returns a stdClass if it's already been processed.
*
* @since 1.0
*
* @return stdClass|null
*/
public function get_recaptcha_result() {
return $this->recaptcha_result;
}
/**
* Validate that the reCAPTCHA response data has the required properties and meets expectations.
*
* @since 1.0
*
* @param array $response_data The response data to validate.
*
* @return bool
*/
private function validate_response_data( $response_data ) {
if (
! empty( $response_data->{'error-codes'} )
|| ( property_exists( $response_data, 'success' ) && $response_data->success !== true )
) {
return false;
}
$validation_properties = array( 'hostname', 'action', 'success', 'score', 'challenge_ts' );
$response_properties = array_filter(
$validation_properties,
function( $property ) use ( $response_data ) {
return property_exists( $response_data, $property );
}
);
if ( count( $validation_properties ) !== count( $response_properties ) ) {
return false;
}
return (
$response_data->success
&& $this->verify_hostname( $response_data->hostname )
&& $this->verify_action( $response_data->action )
&& $this->verify_score( $response_data->score )
&& $this->verify_timestamp( $response_data->challenge_ts )
);
}
/**
* Verify the submission data.
*
* @since 1.0
*
* @param string $token The Recapatcha token.
*
* @return bool
*/
public function verify_submission( $token ) {
$data = \GFCache::get( 'recaptcha_' . $token, $found );
if ( $found ) {
$this->addon->log_debug( __METHOD__ . '(): Using cached reCAPTCHA result: ' . print_r( $data, true ) );
$this->recaptcha_result = $data;
return true;
}
$this->addon->log_debug( __METHOD__ . '(): Verifying reCAPTCHA submission.' );
if ( empty( $token ) ) {
$this->addon->log_debug( __METHOD__ . '(): Could not verify the submission because no token was found.' . PHP_EOL );
return false;
}
$this->init( $token, 'submit' );
$data = $this->get_response_data( $this->api->verify_token( $token, $this->addon->get_plugin_settings_instance()->get_recaptcha_key( 'secret_key_v3' ) ) );
if ( is_wp_error( $data ) ) {
$this->addon->log_debug( __METHOD__ . '(): Validating the reCAPTCHA response has failed due to the following: ' . $data->get_error_message() );
wp_send_json_error(
array(
'error' => $data->get_error_message(),
'code' => self::ERROR_CODE_CANNOT_VERIFY_TOKEN,
)
);
}
if ( ! $this->validate_response_data( $data ) ) {
$this->addon->log_debug(
__METHOD__ . '(): Could not validate the token request from the reCAPTCHA service. ' . PHP_EOL
. "token: {$token}" . PHP_EOL
. "response: " . print_r( $data, true ) . PHP_EOL // @codingStandardsIgnoreLine
);
return false;
}
// @codingStandardsIgnoreLine
$this->addon->log_debug( __METHOD__ . '(): Validated reCAPTCHA: ' . print_r( $data, true ) );
$this->recaptcha_result = $data;
// Caching result for 1 hour.
\GFCache::set( 'recaptcha_' . $token, $data, true, 60 * 60 );
return true;
}
/**
* Get the data from the response.
*
* @since 1.0
*
* @param WP_Error|string $response The response from the API request.
*
* @return mixed
*/
private function get_response_data( $response ) {
if ( is_wp_error( $response ) ) {
return $response;
}
return json_decode( wp_remote_retrieve_body( $response ) );
}
/**
* Verify the reCAPTCHA hostname.
*
* @since 1.0
*
* @param string $hostname Verify that the host name returned matches the site.
*
* @return bool
*/
private function verify_hostname( $hostname ) {
if ( ! has_filter( 'gform_recaptcha_valid_hostnames' ) ) {
$this->addon->log_debug( __METHOD__ . '(): gform_recaptcha_valid_hostnames filter not implemented. Skipping.' );
return true;
}
$this->addon->log_debug( __METHOD__ . '(): gform_recaptcha_valid_hostnames filter detected. Verifying hostname.' );
/**
* Filter for the set of hostnames considered valid by this site.
*
* Google returns a 'hostname' value in reCAPTCHA verification results. We validate against this value to ensure
* that the data is good. By default, we use only the WordPress installation's home URL, but have extended
* this via a filter so developers can define an array of hostnames to allow.
*
* @since 1.0
*
* @param array $valid_hostnames {
* An indexed array of valid hostname strings. Example:
* array( 'example.com', 'another-example.com' )
* }
*/
$valid_hostnames = apply_filters(
'gform_recaptcha_valid_hostnames',
array(
wp_parse_url( get_home_url(), PHP_URL_HOST ),
)
);
return is_array( $valid_hostnames ) ? in_array( $hostname, $valid_hostnames, true ) : false;
}
/**
* Verify the reCAPTCHA action.
*
* @since 1.0
*
* @param string $action The reCAPTCHA result action.
*
* @return bool
*/
private function verify_action( $action ) {
$this->addon->log_debug( __METHOD__ . '(): Verifying action from reCAPTCHA response.' );
return $this->action === $action;
}
/**
* Verify that the score is valid.
*
* @since 1.0
*
* @param float $score The reCAPTCHA v3 score.
*
* @return bool
*/
private function verify_score( $score ) {
$this->addon->log_debug( __METHOD__ . '(): Verifying score from reCAPTCHA response.' );
return is_float( $score ) && $score >= 0.0 && $score <= 1.0;
}
/**
* Verify that the timestamp of the submission is valid.
*
* Google allows a reCAPTCHA token to be valid for two minutes. On multi-page forms, we generate a new token with
* the advancement of each page, but the timestamp that's returned is always the same. Thus, we'll allow a longer
* time frame for form submissions before considering them to be invalid.
*
* @since 1.0
*
* @param string $challenge_ts The challenge timestamp from the reCAPTCHA service.
*
* @return bool
*/
private function verify_timestamp( $challenge_ts ) {
$this->addon->log_debug( __METHOD__ . '(): Verifying timestamp from reCAPTCHA response.' );
return ( gmdate( time() ) - strtotime( $challenge_ts ) ) <= 24 * HOUR_IN_SECONDS;
}
/**
* Get the score from the Recaptcha result.
*
* @since 1.0
*
* @return float
*/
public function get_score() {
if ( empty( $this->recaptcha_result ) || ! property_exists( $this->recaptcha_result, 'score' ) ) {
return $this->addon->is_preview() ? 0.9 : 0.0;
}
return (float) $this->recaptcha_result->score;
}
/**
* Get the decoded response data from the API.
*
* @param string $token The validation token.
* @param string $secret The stored secret key from the settings page.
*
* @since 1.0
*
* @return WP_Error|mixed|string
*/
public function verify( $token, $secret ) {
return $this->get_response_data( $this->api->verify_token( $token, $secret ) );
}
}

View File

@@ -0,0 +1,630 @@
<?php
/**
* Object responsible for organizing and constructing the plugin settings page.
*
* @package Gravity_Forms\Gravity_Forms_RECAPTCHA\Settings
*/
namespace Gravity_Forms\Gravity_Forms_RECAPTCHA\Settings;
use Gravity_Forms\Gravity_Forms_RECAPTCHA\GF_RECAPTCHA;
use Gravity_Forms\Gravity_Forms\Settings\Fields\Text;
use Gravity_Forms\Gravity_Forms_RECAPTCHA\Token_Verifier;
use GF_Field_CAPTCHA;
use GFCommon;
/**
* Class Plugin_Settings
*
* @since 1.0
* @package Gravity_Forms\Gravity_Forms_RECAPTCHA\Settings
*/
class Plugin_Settings {
/**
* Add-on instance.
*
* @var GF_RECAPTCHA
*/
private $addon;
/**
* Token_Verifier instance.
*
* @var Token_Verifier
*/
private $token_verifier;
/**
* Plugin_Settings constructor.
*
* @since 1.0
*
* @param GF_RECAPTCHA $addon GF_RECAPTCHA instance.
* @param Token_Verifier $token_verifier Instance of the Token_Verifier class.
*/
public function __construct( $addon, $token_verifier ) {
$this->addon = $addon;
$this->token_verifier = $token_verifier;
}
/**
* Get the plugin settings fields.
*
* @since 1.0
* @see GF_RECAPTCHA::plugin_settings_fields()
*
* @return array
*/
public function get_fields() {
return array(
$this->get_description_fields(),
$this->get_v3_fields(),
$this->get_v2_fields(),
);
}
/**
* Gets any custom plugin settings, ensuring they contain the latest values from the constants.
*
* @since 1.0
*
* @param array $settings Add-on's parent plugin settings.
*
* @return array
*/
public function get_settings( $settings ) {
if ( ! is_array( $settings ) ) {
$settings = array();
}
$site_key = $this->get_recaptcha_key( 'site_key_v3', true );
if ( $site_key ) {
$settings['site_key_v3'] = $site_key;
}
$secret_key = $this->get_recaptcha_key( 'secret_key_v3', true );
if ( $secret_key ) {
$settings['secret_key_v3'] = $secret_key;
}
return array_merge(
$settings,
array(
'site_key_v2' => get_option( 'rg_gforms_captcha_public_key' ),
'secret_key_v2' => get_option( 'rg_gforms_captcha_private_key' ),
'type_v2' => get_option( 'rg_gforms_captcha_type' ),
)
);
}
/**
* Handles updating of custom plugin settings.
*
* @since 1.0
*
* @param array $settings Update the v2 settings.
*/
public function update_settings( $settings ) {
update_option( 'rg_gforms_captcha_public_key', rgar( $settings, 'site_key_v2' ) );
update_option( 'rg_gforms_captcha_private_key', rgar( $settings, 'secret_key_v2' ) );
update_option( 'rg_gforms_captcha_type', rgar( $settings, 'type_v2' ) );
}
/**
* Get the description section for the plugin settings.
*
* @since 1.0
* @return array
*/
private function get_description_fields() {
return array(
'id' => 'gravityformsrecaptcha_description',
'title' => esc_html__( 'reCAPTCHA Settings', 'gravityformsrecaptcha' ),
'description' => $this->get_settings_intro_description(),
'fields' => array(
array(
'type' => 'html',
),
),
);
}
/**
* Get the plugin settings fields for reCAPTCHA v3.
*
* @since 1.0
* @return array
*/
private function get_v3_fields() {
$site_key = $this->get_recaptcha_key( 'site_key_v3', true );
$secret_key = $this->get_recaptcha_key( 'secret_key_v3', true );
return array(
'id' => 'gravityformsrecaptcha_v3',
'title' => esc_html__( 'reCAPTCHA v3', 'gravityformsrecaptcha' ),
'fields' => array(
array(
'name' => 'site_key_v3',
'label' => esc_html__( 'Site Key', 'gravityformsrecaptcha' ),
'type' => 'text',
'feedback_callback' => array( $this, 'v3_keys_status_feedback_callback' ),
'readonly' => empty( $site_key ) ? '' : 'readonly',
'after_input' => $this->get_constant_message( $site_key, 'GF_RECAPTCHA_V3_SITE_KEY' ),
),
array(
'name' => 'secret_key_v3',
'label' => esc_html__( 'Secret Key', 'gravityformsrecaptcha' ),
'type' => 'text',
'feedback_callback' => array( $this, 'v3_keys_status_feedback_callback' ),
'readonly' => empty( $secret_key ) ? '' : 'readonly',
'after_input' => $this->get_constant_message( $secret_key, 'GF_RECAPTCHA_V3_SECRET_KEY' ),
),
array(
'name' => 'score_threshold_v3',
'label' => esc_html__( 'Score Threshold', 'gravityformsrecaptcha' ),
'description' => $this->get_score_threshold_description(),
'default_value' => 0.5,
'type' => 'text',
'input_type' => 'number',
'step' => '0.01',
'min' => '0.0',
'max' => '1.0',
'validation_callback' => array( $this, 'validate_score_threshold_v3' ),
),
array(
'name' => 'disable_badge_v3',
'label' => esc_html__( 'Disable Google reCAPTCHA Badge', 'gravityformsrecaptcha' ),
'description' => esc_html__( 'By default reCAPTCHA v3 displays a badge on every page of your site with links to the Google terms of service and privacy policy. You are allowed to hide the badge as long as you include the reCAPTCHA branding and links visibly in the user flow.', 'gravityformsrecaptcha' ),
'type' => 'checkbox',
'choices' => array(
array(
'name' => 'disable_badge_v3',
'label' => esc_html__( 'I have added the reCAPTCHA branding, terms of service and privacy policy to my site. ', 'gravityformsrecaptcha' ),
),
),
),
array(
'name' => 'recaptcha_keys_status_v3',
'type' => 'checkbox',
'default_value' => $this->get_recaptcha_key( 'recaptcha_keys_status_v3' ),
'hidden' => true,
'choices' => array(
array(
'type' => 'checkbox',
'name' => 'recaptcha_keys_status_v3',
),
),
),
),
);
}
/**
* Returns the setting info message to be displayed when the value is defined using a constant.
*
* @since 1.3
*
* @param string $value The value.
* @param string $constant The constant name.
*
* @return string
*/
private function get_constant_message( $value, $constant ) {
if ( empty( $value ) ) {
return '';
}
return '<div class="alert gforms_note_info">' . sprintf( esc_html__( 'Value defined using the %s constant.', 'gravityformsrecaptcha' ), $constant ) . '</div>';
}
/**
* Get the plugin settings fields for reCAPTCHA v2.
*
* @since 1.0
* @return array
*/
private function get_v2_fields() {
return array(
'id' => 'gravityformsrecaptcha_v2',
'title' => esc_html__( 'reCAPTCHA v2', 'gravityformsrecaptcha' ),
'fields' => array(
array(
'name' => 'site_key_v2',
'label' => esc_html__( 'Site Key', 'gravityformsrecaptcha' ),
'tooltip' => gform_tooltip( 'settings_recaptcha_public', null, true ),
'type' => 'text',
'feedback_callback' => array( $this, 'validate_key_v2' ),
),
array(
'name' => 'secret_key_v2',
'label' => esc_html__( 'Secret Key', 'gravityformsrecaptcha' ),
'tooltip' => gform_tooltip( 'settings_recaptcha_private', null, true ),
'type' => 'text',
'feedback_callback' => array( $this, 'validate_key_v2' ),
),
array(
'name' => 'type_v2',
'label' => esc_html__( 'Type', 'gravityformsrecaptcha' ),
'tooltip' => gform_tooltip( 'settings_recaptcha_type', null, true ),
'type' => 'radio',
'horizontal' => true,
'default_value' => 'checkbox',
'choices' => array(
array(
'label' => esc_html__( 'Checkbox', 'gravityformsrecaptcha' ),
'value' => 'checkbox',
),
array(
'label' => esc_html__( 'Invisible', 'gravityformsrecaptcha' ),
'value' => 'invisible',
),
),
),
array(
'name' => 'reset_v2',
'label' => esc_html__( 'Validate Keys', 'gravityformsrecaptcha' ),
'type' => 'recaptcha_reset',
'callback' => array( $this, 'handle_recaptcha_v2_reset' ),
'hidden' => true,
'validation_callback' => function( $field, $value ) {
// If reCAPTCHA key is empty, exit.
if ( rgblank( $value ) ) {
return;
}
$values = $this->addon->get_settings_renderer()->get_posted_values();
// Get public, private keys, API response.
$public_key = rgar( $values, 'site_key_v2' );
$private_key = rgar( $values, 'secret_key_v2' );
$response = rgpost( 'g-recaptcha-response' );
// If keys and response are provided, verify and save.
if ( $public_key && $private_key && $response ) {
// Log public, private keys, API response.
// @codingStandardsIgnoreStart - print_r okay for logging.
GFCommon::log_debug( __METHOD__ . '(): reCAPTCHA Site Key:' . print_r( $public_key, true ) );
GFCommon::log_debug( __METHOD__ . '(): reCAPTCHA Secret Key:' . print_r( $private_key, true ) );
GFCommon::log_debug( __METHOD__ . '(): reCAPTCHA Response:' . print_r( $response, true ) );
// Verify response.
$recaptcha = new GF_Field_CAPTCHA();
$recaptcha_response = $recaptcha->verify_recaptcha_response( $response, $private_key );
// Log verification response.
GFCommon::log_debug( __METHOD__ . '(): reCAPTCHA verification response:' . print_r( $recaptcha_response, true ) );
// @codingStandardsIgnoreEnd
// If response is false, return validation error.
if ( $recaptcha_response === false ) {
$field->set_error( __( 'reCAPTCHA keys are invalid.', 'gravityformsrecaptcha' ) );
}
// Save status.
update_option( 'gform_recaptcha_keys_status', $recaptcha_response );
} else {
// Delete existing status.
delete_option( 'gform_recaptcha_keys_status' );
}
},
),
),
);
}
/**
* Convert an array containing arrays of translated strings into HTML paragraphs.
*
* @param array $paragraphs An array of arrays containing translated text.
*
* @since 1.0
* @return string
*/
private function get_description( array $paragraphs ) {
$description_text = array();
foreach ( $paragraphs as $paragraph ) {
$description_text[] = '<p>' . implode( ' ', $paragraph ) . '</p>';
}
return implode( '', $description_text );
}
/**
* Get the contents of the description field.
*
* @since 1.0
* @return array
*/
private function get_settings_intro_description() {
$description = array();
$description[] = array(
esc_html__( 'Google reCAPTCHA is a free anti-spam service that protects your website from fraud and abuse.', 'gravityformsrecaptcha' ),
esc_html__( 'By adding reCAPTCHA to your forms, you can deter automated software from submitting form entries, while still ensuring a user-friendly experience for real people.', 'gravityformsrecaptcha' ),
);
$description[] = array(
esc_html__( 'Gravity Forms integrates with three types of Google reCAPTCHA.', 'gravityformsrecaptcha' ),
'<ul><li>',
esc_html__( 'reCAPTCHA v3 - Adds a script to every page of your site and uploads form content for processing by Google.', 'gravityformsrecaptcha' ),
esc_html__( 'All submissions are accepted and suspicious submissions are marked as spam.', 'gravityformsrecaptcha' ),
esc_html__( 'When reCAPTCHA v3 is configured, it is enabled automatically on all forms by default. It can be disabled for specific forms in the form settings.', 'gravityformsrecaptcha' ),
'</li><li>',
esc_html__( 'reCAPTCHA v2 (Invisible) - Displays a badge on your form and will present a challenge to the user if the activity is suspicious e.g. select the traffic lights.', 'gravityformsrecaptcha' ),
esc_html__( 'Please note, only v2 keys are supported and checkbox keys are not compatible with invisible reCAPTCHA.', 'gravityformsrecaptcha' ),
esc_html__( 'To activate reCAPTCHA v2 on your form, simply add the CAPTCHA field in the form editor.', 'gravityformsrecaptcha' ),
sprintf(
'<a href="%s">%s</a>',
esc_url( 'https://docs.gravityforms.com/captcha/' ),
__( 'Read more about reCAPTCHA.', 'gravityformsrecaptcha' )
),
'</li><li>',
esc_html__( 'reCAPTCHA v2 (Checkbox) - Requires a user to click a checkbox to indicate that they are not a robot and displays a challenge if the activity is suspicious', 'gravityformsrecaptcha' ),
'</li></ul>',
);
$description[] = array(
esc_html__( 'For more information on reCAPTCHA, which version is right for you, and how to add it to your forms,', 'gravityformsrecaptcha' ),
sprintf(
'<a href="%s">%s</a>',
esc_url( 'https://docs.gravityforms.com/captcha/' ),
esc_html__( 'check out our documentation.', 'gravityformsrecaptcha' )
),
);
return $this->get_description( $description );
}
/**
* Get the description for the score threshold.
*
* @since 1.0
* @return string
*/
private function get_score_threshold_description() {
$description = array(
array(
esc_html__( 'reCAPTCHA v3 returns a score (1.0 is very likely a good interaction, 0.0 is very likely a bot).', 'gravityformsrecaptcha' ),
esc_html__( 'If the score is less than or equal to this threshold, the form submission will be sent to spam.', 'gravityformsrecaptcha' ),
esc_html__( 'The default threshold is 0.5.', 'gravityformsrecaptcha' ),
sprintf(
'<a href="%s">Learn about about reCAPTCHA.</a>',
esc_url( 'https://docs.gravityforms.com/captcha/' )
),
),
);
return $this->get_description( $description );
}
/**
* Renders a reCAPTCHA verification field.
*
* @since 1.0
*
* @param array $props Field properties.
* @param bool $echo Output the field markup directly.
*
* @return string
*/
public function handle_recaptcha_v2_reset( $props = array(), $echo = true ) {
// Add setup message.
$html = sprintf(
'<p id="gforms_checkbox_recaptcha_message" style="margin-bottom:10px;">%s</p>',
esc_html__( 'Please complete the reCAPTCHA widget to validate your reCAPTCHA keys:', 'gravityforms' )
);
// Add reCAPTCHA container, reset input.
$html .= '<div id="recaptcha"></div>';
$html .= sprintf( '<input type="hidden" name="%s_%s" />', esc_attr( $this->addon->get_settings_renderer()->get_input_name_prefix() ), esc_attr( $props['name'] ) );
return $html;
}
/**
* Validate that the score is a number between 0.0 and 1.0
*
* @since 1.0
*
* @param Base $field Settings field object.
* @param string $score The submitted score threshold.
*
* @return bool
*/
public function validate_score_threshold_v3( $field, $score ) {
if ( ! $field instanceof Text ) {
$field->set_error( esc_html__( 'Unexpected field type.', 'gravityformsrecaptcha' ) );
return false;
}
$field_value = (float) $score;
if ( ! is_numeric( $score ) || $field_value < $field->min || $field_value > $field->max ) {
$field->set_error( esc_html__( 'Score threshold must be between 0.0 and 1.0', 'gravityformsrecaptcha' ) );
return false;
}
return true;
}
/**
* Returns true, false, or null, depending on the state of validation.
*
* The add-on framework will use this value to determine which field icon to display.
*
* @since 1.0
*
* @param null|string $key_status The status of the key (a string of 1 or 0).
* @param string $value The posted value of the field to validate.
*
* @return bool|null
*/
public function check_validated_status( $key_status, $value ) {
if ( ! is_null( $key_status ) ) {
return (bool) $key_status;
}
return rgblank( $value ) ? null : false;
}
/**
* Return strue, false, or null, depending on the state of validation.
*
* The add-on framework will use this value to determine which field icon to display.
*
* @since 1.0
*
* @param string $value The posted value of the field.
*
* @return bool|null
*/
public function validate_key_v2( $value ) {
return $this->check_validated_status( get_option( 'gform_recaptcha_keys_status', null ), $value );
}
/**
* Feedback callback for v3 key validation.
*
* @param string $value The posted value.
*
* @return bool|null
*/
public function v3_keys_status_feedback_callback( $value ) {
return $this->check_validated_status( $this->addon->get_setting( 'recaptcha_keys_status_v3' ), $value );
}
/**
* Ajax callback to verify the secret key on the plugin settings screen.
*
* @since 1.0
*/
public function verify_v3_keys() {
$result = $this->token_verifier->verify(
sanitize_text_field( rgpost( 'token' ) ),
sanitize_text_field( rgpost( 'secret_key_v3' ) )
);
$this->apply_status_changes( $result );
if ( is_wp_error( $result ) ) {
$this->addon->log_debug( __METHOD__ . '(): Failed to verify reCAPTCHA token. ' . $result->get_error_message() );
wp_send_json_error();
}
$this->addon->log_debug( __METHOD__ . '(): reCAPTCHA token successfully verified.' );
$result->keys_status = $this->addon->get_plugin_setting( 'recaptcha_keys_status_v3' );
wp_send_json_success( $result );
}
/**
* Applies updates to the verified key status when the site and secret v3 keys are saved.
*
* @since 1.0
*
* @param object $response The response of the secret key verification process.
*/
private function apply_status_changes( $response ) {
$posted_keys = $this->get_posted_keys();
// Set the updated status of the keys.
$posted_keys['recaptcha_keys_status_v3'] = ( ! is_wp_error( $response ) && $response->success === true ) ? '1' : '0';
$this->addon->update_plugin_settings(
array_merge(
$this->addon->get_plugin_settings(),
$posted_keys
)
);
}
/**
* Get the posted of the v3 keys from the settings page.
*
* @since 1.0
*
* @return array
*/
private function get_posted_keys() {
$settings = $this->addon->get_plugin_settings();
$posted_site_key = $this->get_posted_key( 'site_key_v3' );
$posted_secret_key = $this->get_posted_key( 'secret_key_v3' );
if (
$posted_site_key === rgar( $settings, 'site_key_v3' )
&& $posted_secret_key === rgar( $settings, 'secret_key_v3' )
) {
return array();
}
return array(
'site_key_v3' => $posted_site_key,
'secret_key_v3' => $posted_secret_key,
);
}
/**
* Gets the value of the specified input from the $_POST.
*
* @since 1.3
*
* @param string $key_name The name of the key to retrieve.
*
* @return string
*/
private function get_posted_key( $key_name ) {
if ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) {
$key_name = "_gform_setting_{$key_name}";
}
return sanitize_text_field( rgpost( $key_name ) );
}
/**
* Get the value of one of the reCAPTCHA keys from the plugin settings.
*
* Checks first for a value defined as a constant, and secondarily, the add-on options.
*
* @since 1.0
* @since 1.3 Added the $only_from_constant param.
*
* @param string $key_name The name of the key to retrieve.
* @param bool $only_from_constant Indicates if value should only be retrieved from the constant.
*
* @return string
*/
public function get_recaptcha_key( $key_name, $only_from_constant = false ) {
if ( ! $only_from_constant && is_admin() ) {
$posted_key = $this->get_posted_key( $key_name );
if ( $posted_key ) {
return $posted_key;
}
}
$keys = array(
'site_key_v3' => defined( 'GF_RECAPTCHA_V3_SITE_KEY' ) ? GF_RECAPTCHA_V3_SITE_KEY : '',
'secret_key_v3' => defined( 'GF_RECAPTCHA_V3_SECRET_KEY' ) ? GF_RECAPTCHA_V3_SECRET_KEY : '',
'site_key_v2' => '',
'secret_key_v2' => '',
);
if ( ! in_array( $key_name, array_keys( $keys ), true ) ) {
return '';
}
$key = rgar( $keys, $key_name, '' );
if ( ! empty( $key ) || $only_from_constant ) {
return $key;
}
$key = $this->addon->get_plugin_setting( $key_name );
return ! empty( $key ) ? $key : '';
}
}

View File

@@ -0,0 +1,156 @@
/******/ (function() { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./js/src/frontend.js":
/*!****************************!*\
!*** ./js/src/frontend.js ***!
\****************************/
/***/ (function() {
var _this = this;
/* global jQuery, gform, gforms_recaptcha_recaptcha_strings, grecaptcha */
(function ($, gform, grecaptcha, strings) {
/**
* Make the API request to Google to get the reCAPTCHA token right before submission.
*
* @since 1.0
*
* @param {Object} e The event object.
* @return {void}
*/
var getToken = function getToken(e) {
var form = $(e.data.form);
var recaptchaField = form.find('.ginput_recaptchav3');
var dataInput = recaptchaField.find('.gfield_recaptcha_response');
if (!dataInput.length || dataInput.val().length) {
return;
}
e.preventDefault();
grecaptcha.ready(function () {
grecaptcha.execute(strings.site_key, {
action: 'submit'
}).then(function (token) {
if (token.length && typeof token === 'string') {
dataInput.val(token);
} // Sometimes the submit button is disabled to prevent the user from clicking it again,
// for example when 3DS is being processed for stripe elements.
// We need to enable it before submitting the form, otherwise it won't be submitted.
var $submitButton = $('#gform_submit_button_' + form[0].dataset.formid);
if ($submitButton.prop('disabled') === true) {
$submitButton.prop('disabled', false);
}
form.submit();
});
});
};
/**
* Add event listeners to the form.
*
* @since 1.0
*
* @param {string|number} formId The numeric ID of the form.
* @return {void}
*/
var addFormEventListeners = function addFormEventListeners(formId) {
var $form = $("#gform_".concat(formId, ":not(.recaptcha-v3-initialized)"));
$form.on('submit', {
form: $form
}, getToken);
$form.addClass('recaptcha-v3-initialized');
};
/**
* The reCAPTCHA handler.
*
* @since 1.0
*
* @return {void}
*/
var gfRecaptcha = function gfRecaptcha() {
var self = _this;
/**
* Initialize the Recaptcha handler.
*
* @since 1.0
*
* @return {void}
*/
self.init = function () {
self.elements = {
formIds: self.getFormIds()
};
self.addEventListeners();
};
/**
* Get an array of form IDs.
*
* @since 1.0
*
* @return {Array} Array of form IDs.
*/
self.getFormIds = function () {
var ids = [];
var forms = document.querySelectorAll('.gform_wrapper form');
forms.forEach(function (form) {
if ('formid' in form.dataset) {
ids.push(form.dataset.formid);
} else {
ids.push(form.getAttribute('id').split('gform_')[1]);
}
});
return ids;
};
/**
* Add event listeners to the page.
*
* @since 1.0
*
* @return {void}
*/
self.addEventListeners = function () {
self.elements.formIds.forEach(function (formId) {
addFormEventListeners(formId);
});
$(document).on('gform_post_render', function (event, formId) {
addFormEventListeners(formId);
});
};
self.init();
}; // Initialize and run the whole shebang.
$(document).ready(function () {
gfRecaptcha();
});
})(jQuery, gform, grecaptcha, gforms_recaptcha_recaptcha_strings);
/***/ })
/******/ });
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module is referenced by other modules so it can't be inlined
/******/ var __webpack_exports__ = {};
/******/ __webpack_modules__["./js/src/frontend.js"]();
/******/
/******/ })()
;
//# sourceMappingURL=frontend.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
({"./js/src/frontend.js":function(){var r,i,o,e,n=this;function a(t){(t=r("#gform_".concat(t,":not(.recaptcha-v3-initialized)"))).on("submit",{form:t},e),t.addClass("recaptcha-v3-initialized")}r=jQuery,gform,i=grecaptcha,o=gforms_recaptcha_recaptcha_strings,e=function(t){var e=r(t.data.form),n=e.find(".ginput_recaptchav3").find(".gfield_recaptcha_response");n.length&&!n.val().length&&(t.preventDefault(),i.ready(function(){i.execute(o.site_key,{action:"submit"}).then(function(t){t.length&&"string"==typeof t&&n.val(t);t=r("#gform_submit_button_"+e[0].dataset.formid);!0===t.prop("disabled")&&t.prop("disabled",!1),e.submit()})}))},r(document).ready(function(){var t;(t=n).init=function(){t.elements={formIds:t.getFormIds()},t.addEventListeners()},t.getFormIds=function(){var e=[];return document.querySelectorAll(".gform_wrapper form").forEach(function(t){"formid"in t.dataset?e.push(t.dataset.formid):e.push(t.getAttribute("id").split("gform_")[1])}),e},t.addEventListeners=function(){t.elements.formIds.forEach(function(t){a(t)}),r(document).on("gform_post_render",function(t,e){a(e)})},t.init()})}})["./js/src/frontend.js"]();

View File

@@ -0,0 +1,444 @@
/******/ (function() { // webpackBootstrap
var __webpack_exports__ = {};
/*!***********************************!*\
!*** ./js/src/plugin_settings.js ***!
\***********************************/
/* global jQuery, gform, grecaptcha, gforms_recaptcha_recaptcha_strings */
(function ($) {
/**
* Handles reCAPTCHA v2 plugin settings validation.
*
* @since 1.0
*
* @return {void}
*/
var recaptchaV2Settings = function recaptchaV2Settings() {
var v2Settings = {};
/**
* Initialize reCAPTCHA v2 settings.
*
* @since 1.0
*
* @return {void}
*/
v2Settings.init = function () {
v2Settings.cacheElements();
v2Settings.addEventListeners();
};
/**
* Cache the fields used by this handler.
*
* @since 1.0
*
* @return {void}
*/
v2Settings.cacheElements = function () {
v2Settings.container = $('div[id="gform_setting_reset_v2"]');
v2Settings.fields = {
siteKey: $('input[name="_gform_setting_site_key_v2"]'),
secretKey: $('input[name="_gform_setting_secret_key_v2"]'),
reset: $('input[name="_gform_setting_reset_v2"]'),
type: $('input[name="_gform_setting_type_v2"]')
};
};
/**
* Add event listeners for this handler.
*
* @since 1.0
*
* @return {void}
*/
v2Settings.addEventListeners = function () {
v2Settings.fields.siteKey.on('change', window.loadRecaptcha);
v2Settings.fields.secretKey.on('change', window.loadRecaptcha);
v2Settings.fields.type.on('change', function () {
return window.loadRecaptcha();
});
};
/**
* Handles showing and hiding the reCAPTCHA itself.
*
* @since 1.0
*
* @return {void}
*/
window.loadRecaptcha = function () {
var self = {};
/**
* Initialize the reCAPTCHA rendering process.
*
* @since 1.0
*
* @return {void}
*/
self.init = function () {
v2Settings.recaptcha = $('#recaptcha');
v2Settings.save = $('#gform-settings-save');
self.flushExistingState(); // Reset key status.
// Note: recaptcha is misspelled here for legacy reasons.
$('#recpatcha .gform-settings-field__feedback').remove(); // If no public or private key is provided, exit.
if (!self.canBeDisplayed()) {
self.hideRecaptcha();
return;
}
v2Settings.save.prop('disabled', true);
self.showSelectedRecaptcha();
};
/**
* Renders the v2 reCAPTCHA.
*
* @since 1.0
*
* @param {string} typeValue The selected type to render.
*
* @return {void}
*/
self.render = function (typeValue) {
// Render reCAPTCHA.
grecaptcha.render('recaptcha', {
sitekey: v2Settings.fields.siteKey.val().trim(),
size: typeValue === 'invisible' ? typeValue : '',
badge: 'inline',
'error-callback': function errorCallback() {},
callback: function callback() {
return v2Settings.save.prop('disabled', false);
}
});
};
/**
* Flush the existing state of the reCAPTCHA handler.
*
* @since 1.0
*
* @return {void}
*/
self.flushExistingState = function () {
window.___grecaptcha_cfg.clients = {};
window.___grecaptcha_cfg.count = 0;
v2Settings.recaptcha.html('');
v2Settings.fields.reset.val('1');
};
/**
* Determines whether the reCAPTCHA can be shown.
*
* @since 1.0
*
* @return {boolean} Whether the reCAPTCHA can be shown.
*/
self.canBeDisplayed = function () {
return v2Settings.fields.siteKey.val() && v2Settings.fields.secretKey.val();
};
/**
* Hides the reCAPTCHA element.
*
* @since 1.0
*
* @return {void}
*/
self.hideRecaptcha = function () {
v2Settings.save.prop('disabled', false);
v2Settings.container.hide();
};
/**
* Show the selected reCAPTCHA type.
*
* @since 1.0
*
* @return {void}
*/
self.showSelectedRecaptcha = function () {
var typeValue = $('input[name="_gform_setting_type_v2"]:checked').val();
self.render(typeValue);
switch (typeValue) {
case 'checkbox':
$('#gforms_checkbox_recaptcha_message, label[for="reset"]').show();
break;
case 'invisible':
$('#gforms_checkbox_recaptcha_message, label[for="reset"]').hide();
break;
default:
throw new Error('Unexpected type selected.');
}
v2Settings.container.show();
if (typeValue === 'invisible') {
grecaptcha.execute();
}
};
self.init();
};
v2Settings.init();
};
/**
* Handles reCAPTCHA v3 plugin settings validation.
*
* @since 1.0
*
* @return {void}
*/
var recaptchaV3Settings = function recaptchaV3Settings() {
var v3Settings = {};
/**
* Initializes the reCAPTCHA v3 settings handler.
*
* @since 1.0
*
* @return {void}
*/
v3Settings.init = function () {
v3Settings.token = '';
v3Settings.strings = gforms_recaptcha_recaptcha_strings;
v3Settings.cacheElements();
v3Settings.validateKeysV3();
v3Settings.addEventListeners();
};
/**
* Cache HTML elements for the v3 reCAPTCHA settings.
*
* @since 1.0
*
* @return {void}
*/
v3Settings.cacheElements = function () {
v3Settings.fields = {
siteKey: '#site_key_v3',
secretKey: '#secret_key_v3',
threshold: '#score_threshold_v3',
disableBadge: '#disable_badge_v3',
keysStatus: '#gform_setting_recaptcha_keys_status_v3'
};
v3Settings.cache = {
siteKey: $(v3Settings.fields.siteKey),
secretKey: $(v3Settings.fields.secretKey),
keysStatus: $(v3Settings.fields.keysStatus),
save: $('#gform-settings-save')
};
};
/**
* Setup event listeners for field validation.
*
* @since 1.0
*
* @return {void}
*/
v3Settings.addEventListeners = function () {
if (!v3Settings.strings.site_key.length) {
return;
}
$(v3Settings.fields.siteKey).on('keyup', function () {
return v3Settings.clearValidationFeedback();
});
$(v3Settings.fields.secretKey).on('keyup', function () {
return v3Settings.clearValidationFeedback();
});
};
/**
* Empty out the validation feedback if the fields are modified, as we can't yet know the status.
*
* @since 1.0
*
* @return {void}
*/
v3Settings.clearValidationFeedback = function () {
v3Settings.unsetValid(v3Settings.cache.siteKey.closest('.gform-settings-input__container'));
v3Settings.unsetValid(v3Settings.cache.secretKey.closest('.gform-settings-input__container'));
};
/**
* Handles validation of the v3 site key.
*
* @since 1.0
*
* @return {Promise<unknown>} Returns a promise so this can be verified synchronously if checking the secret key.
*/
v3Settings.getRecaptchaToken = function () {
return new Promise(function (resolve, reject) {
var siteKeyContainer = v3Settings.cache.siteKey.closest('.gform-settings-input__container');
try {
var siteKey = v3Settings.cache.siteKey;
var siteKeyValue = siteKey.val().trim();
if (0 === siteKeyValue.length) {
v3Settings.unsetValid(siteKeyContainer);
v3Settings.unsetValid(v3Settings.cache.keysStatus.closest('.gform-settings-input__container'));
$(v3Settings.fields.keysStatus).find('input').val('0');
return;
}
grecaptcha.ready(function () {
try {
grecaptcha.execute(siteKeyValue, {
action: 'submit'
}).then(function (token) {
resolve(token);
});
} catch (error) {
reject(error);
}
});
} catch (error) {
reject(error);
}
});
};
/**
* Handles validation of the v3 site and secret keys.
*
* On page load, attempt to generate a reCAPTCHA token and immediately validate it on the server. If it's good,
* we'll update the presentation of the keys to indicate success or failure.
*
* @since 1.0
*
* @return {void}
*/
v3Settings.validateKeysV3 = function () {
var siteKeyContainer = v3Settings.cache.siteKey.closest('.gform-settings-input__container');
var secretKeyContainer = v3Settings.cache.secretKey.closest('.gform-settings-input__container');
var keysStatusInput = $(v3Settings.fields.keysStatus).find('input');
if (!$(v3Settings.fields.siteKey).val().trim().length) {
v3Settings.unsetValid(siteKeyContainer);
v3Settings.unsetValid(secretKeyContainer);
keysStatusInput.val('0');
return;
}
v3Settings.getRecaptchaToken().then(function (token) {
v3Settings.token = token;
}).catch(function () {
v3Settings.setInvalid(siteKeyContainer);
v3Settings.setInvalid(secretKeyContainer);
keysStatusInput.val('0');
}).finally(function () {
$.ajax({
method: 'POST',
dataType: 'JSON',
url: v3Settings.strings.ajaxurl,
data: {
action: 'verify_secret_key',
nonce: v3Settings.strings.nonce,
token: v3Settings.token,
site_key_v3: $(v3Settings.fields.siteKey).val(),
secret_key_v3: $(v3Settings.fields.secretKey).val()
}
}).then(function (response) {
switch (response.data.keys_status) {
case '1':
v3Settings.setValid(siteKeyContainer);
v3Settings.setValid(secretKeyContainer);
keysStatusInput.val('1');
break;
case '0':
v3Settings.setInvalid(siteKeyContainer);
v3Settings.setInvalid(secretKeyContainer);
keysStatusInput.val('0');
break;
default:
v3Settings.unsetValid(siteKeyContainer);
v3Settings.unsetValid(secretKeyContainer);
keysStatusInput.val('0');
}
});
});
};
/**
* Updates the text field to display no feedback.
*
* @since 1.0
*
* @param {Object} el The jQuery element.
*
* @return {void}
*/
v3Settings.unsetValid = function (el) {
el.removeClass('gform-settings-input__container--feedback-success');
el.removeClass('gform-settings-input__container--feedback-error');
};
/**
* Updates the text field to display the successful feedback.
*
* @since 1.0
*
* @param {Object} el The jQuery element.
*
* @return {void}
*/
v3Settings.setValid = function (el) {
el.addClass('gform-settings-input__container--feedback-success');
el.removeClass('gform-settings-input__container--feedback-error');
};
/**
* Updates the text field to display the error feedback.
*
* @since 1.0
*
* @param {Object} el The jQuery element.
*
* @return {void}
*/
v3Settings.setInvalid = function (el) {
el.removeClass('gform-settings-input__container--feedback-success');
el.addClass('gform-settings-input__container--feedback-error');
};
v3Settings.init();
};
$(document).ready(function () {
recaptchaV3Settings();
recaptchaV2Settings();
gform.adminUtils.handleUnsavedChanges('#gform-settings');
});
})(jQuery);
/******/ })()
;
//# sourceMappingURL=plugin_settings.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
!function(){var i;(i=jQuery)(document).ready(function(){var a,n;(a={init:function(){a.token="",a.strings=gforms_recaptcha_recaptcha_strings,a.cacheElements(),a.validateKeysV3(),a.addEventListeners()},cacheElements:function(){a.fields={siteKey:"#site_key_v3",secretKey:"#secret_key_v3",threshold:"#score_threshold_v3",disableBadge:"#disable_badge_v3",keysStatus:"#gform_setting_recaptcha_keys_status_v3"},a.cache={siteKey:i(a.fields.siteKey),secretKey:i(a.fields.secretKey),keysStatus:i(a.fields.keysStatus),save:i("#gform-settings-save")}},addEventListeners:function(){a.strings.site_key.length&&(i(a.fields.siteKey).on("keyup",function(){return a.clearValidationFeedback()}),i(a.fields.secretKey).on("keyup",function(){return a.clearValidationFeedback()}))},clearValidationFeedback:function(){a.unsetValid(a.cache.siteKey.closest(".gform-settings-input__container")),a.unsetValid(a.cache.secretKey.closest(".gform-settings-input__container"))},getRecaptchaToken:function(){return new Promise(function(t,n){var e=a.cache.siteKey.closest(".gform-settings-input__container");try{var s=a.cache.siteKey.val().trim();0===s.length?(a.unsetValid(e),a.unsetValid(a.cache.keysStatus.closest(".gform-settings-input__container")),i(a.fields.keysStatus).find("input").val("0")):grecaptcha.ready(function(){try{grecaptcha.execute(s,{action:"submit"}).then(function(e){t(e)})}catch(e){n(e)}})}catch(e){n(e)}})},validateKeysV3:function(){var t=a.cache.siteKey.closest(".gform-settings-input__container"),n=a.cache.secretKey.closest(".gform-settings-input__container"),s=i(a.fields.keysStatus).find("input");i(a.fields.siteKey).val().trim().length?a.getRecaptchaToken().then(function(e){a.token=e}).catch(function(){a.setInvalid(t),a.setInvalid(n),s.val("0")}).finally(function(){i.ajax({method:"POST",dataType:"JSON",url:a.strings.ajaxurl,data:{action:"verify_secret_key",nonce:a.strings.nonce,token:a.token,site_key_v3:i(a.fields.siteKey).val(),secret_key_v3:i(a.fields.secretKey).val()}}).then(function(e){switch(e.data.keys_status){case"1":a.setValid(t),a.setValid(n),s.val("1");break;case"0":a.setInvalid(t),a.setInvalid(n),s.val("0");break;default:a.unsetValid(t),a.unsetValid(n),s.val("0")}})}):(a.unsetValid(t),a.unsetValid(n),s.val("0"))},unsetValid:function(e){e.removeClass("gform-settings-input__container--feedback-success"),e.removeClass("gform-settings-input__container--feedback-error")},setValid:function(e){e.addClass("gform-settings-input__container--feedback-success"),e.removeClass("gform-settings-input__container--feedback-error")},setInvalid:function(e){e.removeClass("gform-settings-input__container--feedback-success"),e.addClass("gform-settings-input__container--feedback-error")}}).init(),n={init:function(){n.cacheElements(),n.addEventListeners()},cacheElements:function(){n.container=i('div[id="gform_setting_reset_v2"]'),n.fields={siteKey:i('input[name="_gform_setting_site_key_v2"]'),secretKey:i('input[name="_gform_setting_secret_key_v2"]'),reset:i('input[name="_gform_setting_reset_v2"]'),type:i('input[name="_gform_setting_type_v2"]')}},addEventListeners:function(){n.fields.siteKey.on("change",window.loadRecaptcha),n.fields.secretKey.on("change",window.loadRecaptcha),n.fields.type.on("change",function(){return window.loadRecaptcha()})}},window.loadRecaptcha=function(){var t={init:function(){n.recaptcha=i("#recaptcha"),n.save=i("#gform-settings-save"),t.flushExistingState(),i("#recpatcha .gform-settings-field__feedback").remove(),t.canBeDisplayed()?(n.save.prop("disabled",!0),t.showSelectedRecaptcha()):t.hideRecaptcha()},render:function(e){grecaptcha.render("recaptcha",{sitekey:n.fields.siteKey.val().trim(),size:"invisible"===e?e:"",badge:"inline","error-callback":function(){},callback:function(){return n.save.prop("disabled",!1)}})},flushExistingState:function(){window.___grecaptcha_cfg.clients={},window.___grecaptcha_cfg.count=0,n.recaptcha.html(""),n.fields.reset.val("1")},canBeDisplayed:function(){return n.fields.siteKey.val()&&n.fields.secretKey.val()},hideRecaptcha:function(){n.save.prop("disabled",!1),n.container.hide()},showSelectedRecaptcha:function(){var e=i('input[name="_gform_setting_type_v2"]:checked').val();switch(t.render(e),e){case"checkbox":i('#gforms_checkbox_recaptcha_message, label[for="reset"]').show();break;case"invisible":i('#gforms_checkbox_recaptcha_message, label[for="reset"]').hide();break;default:throw new Error("Unexpected type selected.")}n.container.show(),"invisible"===e&&grecaptcha.execute()}};t.init()},n.init(),gform.adminUtils.handleUnsavedChanges("#gform-settings")})}();

View File

@@ -0,0 +1,186 @@
# Copyright (C) 2024 Gravity Forms
# This file is distributed under the GPL-3.0+.
msgid ""
msgstr ""
"Project-Id-Version: Gravity Forms reCAPTCHA Add-On 1.5.0\n"
"Report-Msgid-Bugs-To: https://gravityforms.com/support\n"
"Last-Translator: Gravity Forms <support@gravityforms.com>\n"
"Language-Team: Gravity Forms <support@gravityforms.com>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-04-30T18:32:31+00:00\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"X-Generator: WP-CLI 2.8.1\n"
"X-Domain: gravityformsrecaptcha\n"
#. Plugin Name of the plugin
msgid "Gravity Forms reCAPTCHA Add-On"
msgstr ""
#. Plugin URI of the plugin
#. Author URI of the plugin
msgid "https://gravityforms.com"
msgstr ""
#. Description of the plugin
msgid "Enhance Gravity Forms with support for Google reCAPTCHA."
msgstr ""
#. Author of the plugin
msgid "Gravity Forms"
msgstr ""
#: class-gf-recaptcha.php:491
msgid "Disable reCAPTCHA v3 for this form."
msgstr ""
#: class-gf-recaptcha.php:673
msgid "reCAPTCHA Score"
msgstr ""
#: class-gf-recaptcha.php:734
msgid "reCAPTCHA"
msgstr ""
#: class-gf-recaptcha.php:755
msgid "Score"
msgstr ""
#: class-gf-recaptcha.php:758
msgid "Click here to learn more about reCAPTCHA."
msgstr ""
#: includes/settings/class-plugin-settings.php:122
msgid "reCAPTCHA Settings"
msgstr ""
#: includes/settings/class-plugin-settings.php:144
msgid "reCAPTCHA v3"
msgstr ""
#: includes/settings/class-plugin-settings.php:148
#: includes/settings/class-plugin-settings.php:233
msgid "Site Key"
msgstr ""
#: includes/settings/class-plugin-settings.php:156
#: includes/settings/class-plugin-settings.php:240
msgid "Secret Key"
msgstr ""
#: includes/settings/class-plugin-settings.php:164
msgid "Score Threshold"
msgstr ""
#: includes/settings/class-plugin-settings.php:176
msgid "Disable Google reCAPTCHA Badge"
msgstr ""
#: includes/settings/class-plugin-settings.php:177
msgid "By default reCAPTCHA v3 displays a badge on every page of your site with links to the Google terms of service and privacy policy. You are allowed to hide the badge as long as you include the reCAPTCHA branding and links visibly in the user flow."
msgstr ""
#: includes/settings/class-plugin-settings.php:182
msgid "I have added the reCAPTCHA branding, terms of service and privacy policy to my site. "
msgstr ""
#: includes/settings/class-plugin-settings.php:217
msgid "Value defined using the %s constant."
msgstr ""
#: includes/settings/class-plugin-settings.php:229
msgid "reCAPTCHA v2"
msgstr ""
#: includes/settings/class-plugin-settings.php:247
msgid "Type"
msgstr ""
#: includes/settings/class-plugin-settings.php:254
msgid "Checkbox"
msgstr ""
#: includes/settings/class-plugin-settings.php:258
msgid "Invisible"
msgstr ""
#: includes/settings/class-plugin-settings.php:265
msgid "Validate Keys"
msgstr ""
#: includes/settings/class-plugin-settings.php:301
msgid "reCAPTCHA keys are invalid."
msgstr ""
#: includes/settings/class-plugin-settings.php:344
msgid "Google reCAPTCHA is a free anti-spam service that protects your website from fraud and abuse."
msgstr ""
#: includes/settings/class-plugin-settings.php:345
msgid "By adding reCAPTCHA to your forms, you can deter automated software from submitting form entries, while still ensuring a user-friendly experience for real people."
msgstr ""
#: includes/settings/class-plugin-settings.php:349
msgid "Gravity Forms integrates with three types of Google reCAPTCHA."
msgstr ""
#: includes/settings/class-plugin-settings.php:351
msgid "reCAPTCHA v3 - Adds a script to every page of your site and uploads form content for processing by Google."
msgstr ""
#: includes/settings/class-plugin-settings.php:352
msgid "All submissions are accepted and suspicious submissions are marked as spam."
msgstr ""
#: includes/settings/class-plugin-settings.php:353
msgid "When reCAPTCHA v3 is configured, it is enabled automatically on all forms by default. It can be disabled for specific forms in the form settings."
msgstr ""
#: includes/settings/class-plugin-settings.php:355
msgid "reCAPTCHA v2 (Invisible) - Displays a badge on your form and will present a challenge to the user if the activity is suspicious e.g. select the traffic lights."
msgstr ""
#: includes/settings/class-plugin-settings.php:356
msgid "Please note, only v2 keys are supported and checkbox keys are not compatible with invisible reCAPTCHA."
msgstr ""
#: includes/settings/class-plugin-settings.php:357
msgid "To activate reCAPTCHA v2 on your form, simply add the CAPTCHA field in the form editor."
msgstr ""
#: includes/settings/class-plugin-settings.php:361
msgid "Read more about reCAPTCHA."
msgstr ""
#: includes/settings/class-plugin-settings.php:364
msgid "reCAPTCHA v2 (Checkbox) - Requires a user to click a checkbox to indicate that they are not a robot and displays a challenge if the activity is suspicious"
msgstr ""
#: includes/settings/class-plugin-settings.php:369
msgid "For more information on reCAPTCHA, which version is right for you, and how to add it to your forms,"
msgstr ""
#: includes/settings/class-plugin-settings.php:373
msgid "check out our documentation."
msgstr ""
#: includes/settings/class-plugin-settings.php:389
msgid "reCAPTCHA v3 returns a score (1.0 is very likely a good interaction, 0.0 is very likely a bot)."
msgstr ""
#: includes/settings/class-plugin-settings.php:390
msgid "If the score is less than or equal to this threshold, the form submission will be sent to spam."
msgstr ""
#: includes/settings/class-plugin-settings.php:391
msgid "The default threshold is 0.5."
msgstr ""
#: includes/settings/class-plugin-settings.php:438
msgid "Unexpected field type."
msgstr ""
#: includes/settings/class-plugin-settings.php:445
msgid "Score threshold must be between 0.0 and 1.0"
msgstr ""

View File

@@ -0,0 +1,78 @@
<?php
/*
Plugin Name: Gravity Forms reCAPTCHA Add-On
Plugin URI: https://gravityforms.com
Description: Enhance Gravity Forms with support for Google reCAPTCHA.
Version: 1.5.0
Author: Gravity Forms
Author URI: https://gravityforms.com
License: GPL-3.0+
Text Domain: gravityformsrecaptcha
Domain Path: /languages
------------------------------------------------------------------------
Copyright 2023-2024 Rocketgenius Inc.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see http://www.gnu.org/licenses.
*/
defined( 'ABSPATH' ) || die();
use Gravity_Forms\Gravity_Forms_RECAPTCHA\GF_RECAPTCHA;
// Defines the current version of the Gravity Forms Recaptcha Add-On.
define( 'GF_RECAPTCHA_VERSION', '1.5.0' );
// Defines the minimum version of Gravity Forms required to run Gravity Forms Recaptcha Add-On.
define( 'GF_RECAPTCHA_MIN_GF_VERSION', '2.5-rc-1' );
// After Gravity Forms is loaded, load the Add-On.
add_action( 'gform_loaded', array( 'GF_RECAPTCHA_Bootstrap', 'load_addon' ), 5 );
/**
* Loads the Gravity Forms reCAPTCHA Add-On.
*
* Includes the main class and registers it with GFAddOn.
*
* @since 1.0
*/
class GF_RECAPTCHA_Bootstrap {
/**
* Loads the required files.
*
* @since 1.0
*/
public static function load_addon() {
// Requires the class file.
require_once plugin_dir_path( __FILE__ ) . '/class-gf-recaptcha.php';
// Registers the class name with GFAddOn.
GFAddOn::register( 'Gravity_Forms\Gravity_Forms_RECAPTCHA\GF_RECAPTCHA' );
}
}
/**
* Returns an instance of the GF_RECAPTCHA class
*
* @since 1.0
*
* @return GF_RECAPTCHA|bool An instance of the GF_RECAPTCHA class
*/
function gf_recaptcha() {
return class_exists( 'Gravity_Forms\Gravity_Forms_RECAPTCHA\GF_RECAPTCHA' ) ? GF_RECAPTCHA::get_instance() : false;
}