This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -0,0 +1,80 @@
### 2.2.2 | 2026-05-07
- Updated the entry spam note to include the reCAPTCHA score and the threshold at the time entry was marked as spam.
- Fixed a fatal error that can occur when v3 classic is the selected connection type and the key settings are being validated.
### 2.2.1 | 2026-04-28
- Added security enhancements.
### 2.2.0 | 2026-04-27
- Added support for checkbox type to Enterprise keys via the new reCAPTCHA Checkbox field type.
- Updated the Enterprise key setting to include the key types.
- Updated the minimum required Gravity Forms version to 2.9.5.
- Fixed a conflict with Apple Pay in the upcoming Stripe v.7.0.
- Fixed an issue where the core Captcha field sidebar message links to the wrong settings page.
### 2.1.0 | 2025-11-13
- Added a new [`gform_recaptcha_spam_score_threshold`](https://docs.gravityforms.com/gform_recaptcha_spam_score_threshold/) filter to control score threshold used to identify spam.
- Updated the connection error message on the settings page (shown when auth token refresh fails) to indicate when the connected Google Workspace account has a policy requiring periodic reauthentication.
- Updated to skip validation if the form or honeypot is already invalid, or it's not the first or last page, reducing the Google credit usage by some forms.
- Updated Enterprise API auth token refresh to log the response for an edge case where a 200 status is returned but a new auth token is not issued.
- Fixed an issue where the entry detail page metabox is not added when the score is 0.
- Fixed an issue where an entry note is not added when the entry is marked as spam.
### 2.0.0 | 2025-08-20
- Added support for notifying Google (annotating the assessment) when an entry that has a reCAPTCHA score is marked as spam or not spam via the entries list or detail pages. Note: only applies to the Enterprise connection type and entries created after this version is installed.
- Fixed a JavaScript error on the Forms > Settings > reCAPTCHA page when no-conflict mode is enabled.
- Fixed an issue where the Forms > Settings > reCAPTCHA page reloads with the connection type settings displayed when attempting to connect using the Enterprise connection type.
- Fixed a PHP fatal error that can occur in some hosting environments when determining if the badge is disabled on script enqueue if $_POST is null.
- Fixed an issue where the status displayed instead of the score on the entries list and entry detail pages is not translatable.
- Fixed an issue where new submissions not processed because of an Enterprise API auth token refresh issue save a status of disconnected to the entry meta.
- Fixed an issue where the score changes to 0 or a status (e.g. disconnected) when an existing entry is updated.
- Fixed handling of an unexpected response during the Enterprise API auth token refresh.
- Fixed an issue where an incorrect logging statement can be written to the Gravity Forms core log when using the Enterprise connection type.
- Fixed an unhandled WP_Error during the Enterprise API auth token refresh that caused the add-on to become disconnected.
### 1.9.0 | 2025-07-21
- Fixed an issue where reCAPTCHA Enterprise can fail if reCAPTCHA returns a response of 1 instead of 1.0.
### 1.8.0 | 2025-07-08
- Added a new [`gform_recaptcha_enterprise_keys_page_size`](https://docs.gravityforms.com/gform_recaptcha_enterprise_keys_page_size/) filter to control the number of keys returned per API call.
- Fixed an issue where when connected with the Enterprise connection method, the settings page only retrieves 10 reCAPTCHA keys.
- Fixed an issue where the reCAPTCHA badge is visible on pages that don't include a form when the "Disable Google reCAPTCHA Badge" setting is enabled.
- Fixed an issue where multiple Enterprise API auth token refresh requests can occur when the site has multiple concurrent visitors, causing the add-on to become disconnected.
- Fixed an issue where the Enterprise API can be initialised multiple times during the same request.
### 1.7.0 | 2025-06-04
- Added support for using reCAPTCHA Enterprise keys and the reCAPTCHA Enterprise API.
- Updated the connection screen to improve clarity and differentiate connection options.
- Fixed an issue where form submission fails if the reCAPTCHA quota limit has been reached.
### 1.6.0 | 2024-07-30
- Added support for the upcoming gform/submission/pre_submission filter that will be released with Gravity Forms 2.9.
### 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!
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,412 @@
<?php
namespace Gravity_Forms\Gravity_Forms_RECAPTCHA;
use GFCache;
use GFCommon;
/**
* Class GF_Field_RECAPTCHA_Checkbox
*
* @since 2.2.0
*
* @package Gravity_Forms\Gravity_Forms_RECAPTCHA
*/
class GF_Field_RECAPTCHA_Checkbox extends \GF_Field {
/**
* Recaptcha field type.
*
* @since 2.2.0
*
* @var string
*/
public $type = 'recaptcha_checkbox';
/**
* Whether there can be more than one of this field type per form with GF 3.0+.
*
* @since 2.2.0
*
* @var bool
*/
public $duplicatable = false;
/**
* Whether the field can be used in a repeater with GF 3.0+.
*
* @since 2.2.0
*
* @var bool
*/
public $repeatable = false;
/**
* Whether the field is for front-end display only use.
*
* @since 2.2.0
*
* @var bool
*/
public $displayOnly = true;
/**
* Instantiates the field and adds the necessary hooks.
*
* @since 2.2.0
*
* @param array $data The field properties and their values.
*/
public function __construct( $data = array() ) {
parent::__construct( $data );
if ( $this->id && $this->formId ) {
add_action( 'gform_entry_created', array( $this, 'action_entry_created' ) );
add_filter( 'gform_ajax_submission_result' , array( $this, 'filter_ajax_submission_result' ) );
}
}
/**
* Get field button title.
*
* @since 2.2.0
*
* @return string
*/
public function get_form_editor_field_title() {
return esc_attr__( 'reCAPTCHA Checkbox', 'gravityformsrecaptcha' );
}
/**
* Returns the field's form editor icon.
*
* @since 2.2.0
*
* @return string
*/
public function get_form_editor_field_icon() {
return 'gform-icon--recaptcha';
}
/**
* Return empty array to prevent the field from showing up in the form editor.
*
* @since 2.2.0
*
* @return array
*/
public function get_form_editor_button() {
if ( ! $this->has_enterprise_checkbox_key() ) {
return array();
}
return array(
'group' => 'advanced_fields',
'text' => $this->get_form_editor_field_title(),
);
}
/**
* Removes the duplicate field link from the admin field buttons.
*
* @since 2.2.0
*
* @return string
*/
public function get_admin_buttons() {
add_filter( 'gform_duplicate_field_link', '__return_empty_string' );
$admin_buttons = parent::get_admin_buttons();
remove_filter( 'gform_duplicate_field_link', '__return_empty_string' );
return $admin_buttons;
}
/**
* Returns the field's form editor description.
*
* @since 2.2.0
*
* @return string
*/
public function get_form_editor_field_description() {
return esc_attr__( 'Adds a reCAPTCHA Enterprise checkbox field to your form to help protect your website from spam and bot abuse.', 'gravityformsrecaptcha' );
}
/**
* Get field settings in the form editor.
*
* @since 2.2.0
*
* @return array
*/
public function get_form_editor_field_settings() {
return array(
'label_setting',
'captcha_theme_setting',
'label_placement_setting',
'error_message_setting',
);
}
/**
* Returns the scripts to be included for this field type in the form editor.
*
* @since 2.2.0
*
* @return string
*/
public function get_form_editor_inline_script_on_page_render() {
$type = esc_js( $this->type );
$cant_add_field_title = json_encode( $this->get_form_editor_field_title() );
$cant_add_field_message = json_encode( esc_html__( 'Only one reCAPTCHA checkbox field can be added to the form.', 'gravityformsrecaptcha' ) );
$default_label = json_encode( __( 'reCAPTCHA', 'gravityformsrecaptcha' ) );
$a11y_string1 = json_encode( esc_html__( 'This field has accessibility issues. We recommend using a score-based key instead of this field.', 'gravityformsrecaptcha' ) );
$a11y_string2 = json_encode( esc_html__( '(opens in a new tab)', 'gravityformsrecaptcha' ) );
return <<<EOD
gform.addFilter( 'gform_form_editor_can_field_be_added', ( canFieldBeAdded, type ) => {
if ( type === '{$type}' && GetFieldsByType( [ '{$type}' ] ).length ) {
canFieldBeAdded = false;
gform.instances.dialogAlert( {$cant_add_field_title}, {$cant_add_field_message} );
}
return canFieldBeAdded;
} );
window.SetDefaultValues_{$type} = ( field ) => {
field.label = {$default_label};
field.captchaTheme = 'light';
}
gform.addAction( 'gform_post_load_field_settings', ( [ field, form ] ) => {
if ( field.type !== '{$type}' ) {
return;
}
const message = '<p class="gform-alert__message"><a href="https://docs.gravityforms.com/field-accessibility-warning" target="_blank">' + {$a11y_string1} + '<span class="screen-reader-text">' + {$a11y_string2} + '</span>&nbsp;<span class="gform-icon gform-icon--external-link" aria-hidden="true"></span></a></p>';
SetFieldAccessibilityWarning( 'label_setting', 'above', message );
const themeSetting = document.getElementById( 'field_captcha_theme' );
if ( themeSetting.value !== field.captchaTheme ) {
themeSetting.value = field.captchaTheme;
}
} );
EOD;
}
/**
* Returns the warning message to be displayed in the form editor sidebar.
*
* @since 2.2.0
*
* @return string|array
*/
public function get_field_sidebar_messages() {
if ( $this->has_enterprise_checkbox_key() ) {
return '';
}
return array(
'type' => 'notice',
'content' => sprintf(
'%s<div class="gform-spacing gform-spacing--top-1">%s</div>',
esc_html__( 'Configuration Required', 'gravityformsrecaptcha' ),
// Translators: 1. Opening <a> tag with link to the Forms > Settings > reCAPTCHA page. 2. closing <a> tag.
sprintf(
esc_html__( 'To use this field, configure the %1$sreCAPTCHA Add-On Settings%2$s using the Enterprise connection type, and select a checkbox type key.', 'gravityformsrecaptcha' ),
'<a href="?page=gf_settings&subview=gravityformsrecaptcha" target="_blank">',
'<span class="screen-reader-text">' . esc_html__( '(opens in a new tab)', 'gravityformsrecaptcha' ) . '</span>&nbsp;<span class="gform-icon gform-icon--external-link" aria-hidden="true"></span></a>'
)
),
'icon_helper_text' => esc_html__( 'This field requires additional configuration', 'gravityformsrecaptcha' ),
);
}
/**
* Determine if the enterprise checkbox key is configured.
*
* @since 2.2.0
*
* @return bool
*/
private function has_enterprise_checkbox_key() {
return gf_recaptcha()->has_enterprise_checkbox_key();
}
/**
* The field markup.
*
* @since 2.2.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 ) {
if ( $this->is_form_editor() ) {
return $this->get_field_input_form_editor();
}
if ( ! $this->has_enterprise_checkbox_key() ) {
gf_recaptcha()->log_debug( __METHOD__ . '(): A reCAPTCHA v3 Enterprise checkbox type key is not configured.' );
return '';
}
if ( empty( $value ) || ! is_string( $value ) ) {
$value = '';
}
return sprintf(
"<div class='ginput_container ginput_container_recaptcha_checkbox'><div class='g-recaptcha' data-sitekey='%s' data-theme='%s' data-action='submit' data-tabindex='%d'></div>"
. "<input class='gfield_recaptcha_response' type='hidden' name='input_%d' value='%s'/>"
. "</div>",
esc_attr( gf_recaptcha()->get_plugin_settings_instance()->get_recaptcha_key( 'site_key_v3_enterprise' ) ),
GFCommon::whitelist( $this->captchaTheme, array( 'light', 'dark' ) ),
esc_attr( GFCommon::$tab_index > 0 ? GFCommon::$tab_index ++ : 0 ),
$this->id,
esc_attr( $this->get_context_property( 'assessment_id_hash' ) ?: $value )
);
}
/**
* Returns the field input markup for the form editor.
*
* @since 2.2.0
*
* @return string
*/
private function get_field_input_form_editor() {
if ( $this->has_enterprise_checkbox_key() ) {
$theme = GFCommon::whitelist( $this->captchaTheme, array( 'light', 'dark' ) );
$img_url = GFCommon::get_base_url() . sprintf( '/images/captcha_%s.svg', $theme );
return sprintf( '<div class="ginput_container"><img class="gfield_captcha" src="%s" alt="%s" width="304" height="78"></div>', esc_attr( $img_url ), esc_attr__( 'An example reCAPTCHA checkbox', 'gravityformsrecaptcha' ) );
}
return '<div class="ginput_container ginput_container_addon_message ginput_container_addon_message_recaptcha_checkbox">
<div class="gform-alert gform-alert--info gform-alert--theme-cosmos gform-spacing gform-spacing--bottom-0 gform-theme__disable">
<span
class="gform-icon gform-icon--information-simple gform-icon--preset-active gform-icon-preset--status-info gform-alert__icon"
aria-hidden="true"
></span>
<div class="gform-alert__message-wrap">
<div class="gform-alert__message">
' . esc_html__( 'Configuration Required', 'gravityformsrecaptcha' ) . '
<div class="gform-spacing gform-spacing--top-1">' . sprintf(
esc_html__( 'To use this field, configure the %1$sreCAPTCHA Add-On Settings%2$s using the Enterprise connection type, and select a checkbox type key.', 'gravityformsrecaptcha' ),
'<a href="?page=gf_settings&subview=gravityformsrecaptcha" target="_blank">',
'<span class="screen-reader-text">' . esc_html__( '(opens in a new tab)', 'gravityformsrecaptcha' ) . '</span>&nbsp;<span class="gform-icon gform-icon--external-link" aria-hidden="true"></span></a>'
) . '</div>
</div>
</div>
</div>
</div>';
}
/**
* Validates the submitted reCAPTCHA token or hash of an existing assessment ID.
*
* @since 2.2.0
*
* @param string $value The submitted value.
* @param array $form The current form.
*
* @return void
*/
public function validate( $value, $form ) {
if ( ( wp_doing_ajax() && rgpost( 'action' ) === 'gfcf_validate_field' ) || ! $this->has_enterprise_checkbox_key() ) {
return;
}
$cache_key_prefix = sprintf( 'gf_recaptcha_assessment_%s_%d_', \GFFormsModel::get_form_unique_id( $this->formId ), $this->id );
$token_verifier = gf_recaptcha()->get_token_verifier();
$token = rgpost( 'g-recaptcha-response' );
if ( empty( $token ) ) {
$hash = ! empty( $value ) && is_string( $value ) ? sanitize_key( $value ) : null;
if ( $hash ) {
$assessment = GFCache::get( $cache_key_prefix . $hash, $found );
if ( $found && ! empty( $assessment['name'] ) && $hash === wp_hash( $assessment['name'] ) ) {
// Setting to the cached assessment so it is available when the entry spam check runs.
$token_verifier->set_recaptcha_result( $assessment );
$this->set_context_property( 'assessment_id_hash', $hash );
gf_recaptcha()->log_debug( __METHOD__ . sprintf( '(): Using existing cached and validated assessment (ID: %s) for submitted hash (%s) for field #%d on form #%d.', $assessment['name'], $hash, $this->id, $this->formId ) );
return;
}
gf_recaptcha()->log_debug( __METHOD__ . sprintf( '(): Hash (%s) failed validation for field #%d on form #%d.', $hash, $this->id, $this->formId ) );
}
gf_recaptcha()->log_debug( __METHOD__ . sprintf( '(): No token submitted for field #%d on form #%d.', $this->id, $this->formId ) );
$this->set_required_error( '' );
return;
}
if ( ! gf_recaptcha()->initialize_api() ) {
gf_recaptcha()->log_debug( __METHOD__ . sprintf( '(): Skipping validation for field #%d on form #%d.', $this->id, $this->formId ) );
return;
}
gf_recaptcha()->log_debug( __METHOD__ . sprintf( '(): Validating token for field #%d on form #%d.', $this->id, $this->formId ) );
if ( ! $token_verifier->verify_submission( $token ) ) {
$this->failed_validation = true;
$this->validation_message = $this->errorMessage ?: __( 'The reCAPTCHA was invalid. Go back and try it again.', 'gravityformsrecaptcha' );
return;
}
$assessment = $token_verifier->get_recaptcha_result();
$assessment_id_hash = wp_hash( rgar( $assessment, 'name' ) );
GFCache::set( $cache_key_prefix . $assessment_id_hash, $assessment, true, DAY_IN_SECONDS );
$this->set_context_property( 'assessment_id_hash', $assessment_id_hash );
gf_recaptcha()->log_debug( __METHOD__ . sprintf( '(): Caching validated assessment (ID: %s) using hash (%s).', $assessment['name'], $assessment_id_hash ) );
}
/**
* Adds the assessment ID hash to the AJAX submission result when the response includes a page markup change.
*
* @since 2.2.0
*
* @param array $result The AJAX submission result.
*
* @return array
*/
public function filter_ajax_submission_result( $result ) {
$hash = $this->get_context_property( 'assessment_id_hash' );
if ( ! $hash || ! isset( $result['page_markup'] ) ) {
return $result;
}
$result['recaptcha_checkbox_response'] = $hash;
return $result;
}
/**
* Deletes the cached assessment data after the entry has been saved.
*
* @since 2.2.0
*
* @return void
*/
public function action_entry_created() {
$hash = $this->get_context_property( 'assessment_id_hash' );
if ( ! $hash ) {
return;
}
GFCache::delete( sprintf( 'gf_recaptcha_assessment_%s_%d_', \GFFormsModel::get_form_unique_id( $this->formId ), $this->id ) . $hash );
gf_recaptcha()->log_debug( __METHOD__ . sprintf( '(): Deleted assessment cache for hash (%s).', $hash ) );
}
}
\GF_Fields::register( new GF_Field_RECAPTCHA_Checkbox() );
@@ -0,0 +1,172 @@
<?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
* @since 2.2.0 Updated not to output the input when a checkbox key is used.
*
* @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 ) {
if ( gf_recaptcha()->get_connection_type() === 'enterprise' ) {
if ( ! gf_recaptcha()->enterprise_keys_configured() ) {
gf_recaptcha()->log_error( __METHOD__ . '(): Enterprise project and/or key not saved in the reCAPTCHA Settings.' );
return '';
}
$key_type = gf_recaptcha()->get_plugin_settings_instance()->get_recaptcha_key( 'site_key_type_v3_enterprise' );
$is_score_key = empty( $key_type ) || $key_type === 'SCORE';
if ( ! $is_score_key ) {
gf_recaptcha()->log_debug( __METHOD__ . '(): reCAPTCHA v3 Enterprise is not configured to use a score type key.' );
return '';
}
$site_key_attr = '';
} else {
$plugin_settings = gf_recaptcha()->get_plugin_settings_instance();
$site_key_attr = sprintf( "data-sitekey='%s'", esc_attr( $plugin_settings->get_recaptcha_key( 'site_key_v3' ) ) );
}
$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' {$site_key_attr} 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 );
}
}
@@ -0,0 +1,414 @@
<?php
/**
* API wrapper for the Recaptcha service.
*
* @since 1.0
* @package Gravity_Forms\Gravity_Forms_RECAPTCHA
*/
namespace Gravity_Forms\Gravity_Forms_RECAPTCHA;
use WP_Error;
/**
* 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';
/**
* OAuth Access Token.
*
* @since 1.7.0
*
* @var string
*/
protected $access_token;
/**
* OAuth Refresh Token.
*
* @since 1.7.0
*
* @var string
*/
protected $refresh_token;
/**
* The GF_RECAPTCHA instance.
*
* @since 1.7.0
*
* @var GF_RECAPTCHA|null The GF_RECAPTCHA instance.
*/
protected $addon;
/**
* The Google Cloud Project ID.
*
* @since 1.7.0
*
* @var string
*/
protected $project_id;
/**
* Constructor for RECAPTCHA_API
*
* @param string $auth_data The array of auth data.
* @param GF_RECAPTCHA $addon The GF_RECAPTCHA instance.
*/
public function __construct( $auth_data = null, $addon = null ) {
$this->addon = $addon;
$this->refresh_token = rgar( $auth_data, 'refresh_token' );
$this->access_token = rgar( $auth_data, 'access_token' );
$this->project_id = rgar( $auth_data, 'project_id' );
}
/**
* Get Gravity API URL for path.
*
* @since 1.7.0
*
* @param string $path Endpoint path.
*
* @return string URL for Gravity API endpoint.
*/
public static function get_gravity_api_url( $path = '' ) {
if ( '/' !== substr( $path, 0, 1 ) ) {
$path = '/' . $path;
}
return defined( 'GRAVITY_API_URL' ) ? GRAVITY_API_URL . '/auth/googlerecaptcha' . $path : self::$gravity_api_url . '/auth/googlerecaptcha' . $path;
}
/**
* Get the base URL for the Recaptcha API.
*
* @since 1.7.0
*
* @param string $base_path The base path.
*
* @return string
*/
private function get_base_url( $base_path = null ) {
if ( $base_path ) {
return $base_path;
}
return 'https://recaptchaenterprise.googleapis.com/v1/';
}
/**
* Make a request to the reCAPTCHA API.
*
* @param string $path The relative request path.
* @param array $body The request body.
* @param array $headers The request headers.
* @param string $method The request method.
* @param string $base_path The base API path.
*
* @since 1.7.0
*
* @return array|string|WP_Error
*/
private function make_request( $path = '', $body = array(), $headers = array(), $method = 'GET', $base_path = null ) {
gf_recaptcha()->log_debug( __METHOD__ . '(): Making request to: ' . $path );
// Build request URL.
$request_url = $this->get_base_url( $base_path ) . $path;
$args = array(
'method' => $method,
/**
* Filters if SSL verification should occur.
*
* @param bool $ssl_verify If the SSL certificate should be verified. Defaults to false.
* @param string $request_url The request URL.
*
* @return bool
*/
'sslverify' => apply_filters( 'https_local_ssl_verify', false, $request_url ),
/**
* Sets the HTTP timeout, in seconds, for the request.
*
* @param int $timeout_value The timeout limit, in seconds. Defaults to 30.
* @param string $request_url The request URL.
*
* @return int
*/
'timeout' => apply_filters( 'http_request_timeout', 30, $request_url ),
);
$args['headers'] = $headers;
if ( 'GET' === $method || 'POST' === $method ) {
$args['body'] = empty( $body ) ? '' : $body;
}
if ( 'POST' === $method ) {
$args['body'] = wp_json_encode( $body );
}
// Execute request.
$response = wp_remote_request(
$request_url,
$args
);
if ( is_wp_error( $response ) ) {
return $response;
}
$response_body = wp_remote_retrieve_body( $response );
$retrieved_response_code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $retrieved_response_code ) {
$error_message = rgars( $response_body, 'error/message', "Expected response code: 200. Returned response code: {$retrieved_response_code}." );
$error_code = rgars( $response_body, 'error/errors/reason', 'google_recaptcha_api_error' );
gf_recaptcha()->log_error( __METHOD__ . '(): Unable to validate with the Google Cloud API: ' . $error_message );
return new WP_Error( $error_code, $error_message, $retrieved_response_code );
}
return $response_body;
}
/**
* 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,
),
)
);
}
/**
* Create the reCAPTCHA enterprise assessment.
*
* @param string $access_token The OAuth access token.
* @param string $project_id The Google Cloud Project ID.
* @param string $token The reCAPTCHA token from the submission.
* @param string $site_key The reCAPTCHA site key.
* @param string $action Teh reCATPCHA action.
*
* @since 1.7.0
*
* @return array|mixed|WP_Error
*/
public function create_recaptcha_assessment( $access_token, $project_id, $token, $site_key, $action ) {
$url = 'https://recaptchaenterprise.googleapis.com/v1/projects/' . $project_id . '/assessments';
$payload = wp_json_encode(
array(
'event' => array(
'token' => $token,
'siteKey' => $site_key,
'expectedAction' => $action,
),
)
);
$response = wp_remote_post(
$url,
array(
'headers' => array(
'Authorization' => 'Bearer ' . $access_token,
'Content-Type' => 'application/json',
),
'body' => $payload,
)
);
if ( is_wp_error( $response ) ) {
return $response;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
return $body;
}
/**
* Annotates the assessment; informing Google that the submission was spam or ham.
*
* @since 2.0
*
* @param string $assessment_id The assessment ID
* @param string $annotation The annotation to added to the assessment. Possible values: LEGITIMATE or FRAUDULENT.
*
* @return array|mixed|WP_Error
*/
public function annotate_assessment( $assessment_id, $annotation ) {
$body = array(
'annotation' => $annotation,
);
$response = wp_remote_post(
sprintf( 'https://recaptchaenterprise.googleapis.com/v1/%s:annotate', $assessment_id ),
array(
'headers' => array(
'Authorization' => 'Bearer ' . $this->access_token,
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( $body ),
)
);
if ( is_wp_error( $response ) ) {
return $response;
}
$body = wp_remote_retrieve_body( $response );
return $body ? json_decode( $body, true ) : $body;
}
/**
* Refresh the reCAPTCHA access token.
*
* @param string $refresh_token The refresh token.
*
* @since 1.7.0
*
* @return array|WP_Error
*/
public function refresh_token( $refresh_token ) {
// Connect to Gravity Form's API.
$response = wp_remote_post(
$this->get_gravity_api_url( 'refresh' ),
array(
'body' => array(
'refresh_token' => rawurlencode( $refresh_token ),
),
)
);
if ( is_wp_error( $response ) ) {
return $response;
}
$retrieved_response_code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $retrieved_response_code ) {
$error_message = "Expected response code: 200. Returned response code: {$retrieved_response_code}.";
return new WP_Error( 'google_recaptcha_api_error', $error_message, $retrieved_response_code );
}
$response_body = gf_recaptcha()->maybe_decode_json( wp_remote_retrieve_body( $response ) );
if ( array_key_exists( 'auth_error', $response_body ) ) {
if ( empty( $response_body['auth_error'] ) ) {
$error_message = 'Google returned an empty response.';
} else {
$error_message = 'Google response: ' . print_r( $response_body['auth_error'], true );
}
return new WP_Error( 'google_recaptcha_api_error', $error_message );
}
return $response_body;
}
/**
* Get a list of Google Cloud projects.
*
* @since 1.7.0
*
* @return array|mixed
*/
public function get_recaptcha_projects() {
$request_path = 'projects';
$headers = array(
'Authorization' => 'Bearer ' . $this->access_token,
'Accept' => 'application/json',
);
$response = $this->make_request( $request_path, array(), $headers, 'GET', 'https://cloudresourcemanager.googleapis.com/v1/' );
if ( is_wp_error( $response ) ) {
return array();
}
$data = json_decode( $response, true );
return $data;
}
/**
* Get a list of enterprise site keys associates with the chosen project.
*
* @param string $project The Google Cloud Project ID.
*
* @since 1.7.0
* @since 1.8.0 Increased the default page size to 100 and added pagination to retrieve all keys.
*
* @return array|mixed
*/
public function get_enterprise_site_keys( $project ) {
$request_path_base = 'projects/' . $project . '/keys';
$headers = array(
'Authorization' => 'Bearer ' . $this->access_token,
'Accept' => 'application/json',
);
$all_keys = array(
'keys' => array(),
);
$page_token = null;
$page_size = apply_filters( 'gform_recaptcha_enterprise_keys_page_size', 100 );
$query_params = array(
'pageSize' => $page_size,
);
do {
if ( $page_token ) {
$query_params['pageToken'] = $page_token;
}
$request_path = $request_path_base . '?' . http_build_query( $query_params );
$response = $this->make_request( $request_path, array(), $headers );
if ( is_wp_error( $response ) ) {
$this->addon->log_error( __METHOD__ . '(): Unable to retrieve site keys: ' . $response->get_error_message() );
break;
}
$data = json_decode( $response, true );
if ( isset( $data['keys'] ) && is_array( $data['keys'] ) ) {
$all_keys['keys'] = array_merge( $all_keys['keys'], $data['keys'] );
}
$page_token = rgar( $data, 'nextPageToken' ) ? $data['nextPageToken'] : null;
} while ( $page_token );
return $all_keys;
}
}
@@ -0,0 +1,200 @@
<?php
namespace Gravity_Forms\Gravity_Forms_RECAPTCHA;
use GFCache, GFAddOn;
/**
* Refresh token lock.
*
* Hosts the logic required to prevent repeated refresh token requests.
*
* @since 1.8.0
*/
class Refresh_Lock_Handler {
/**
* How many seconds until the rate limiting cache lock is cleared.
*
* @since 1.8.0
*
* @var integer
*/
const RATE_LIMIT_CACHE_EXPIRATION_SECONDS = MINUTE_IN_SECONDS;
/**
* How many failed refresh requests until the refresh is locked becayse of rate limiting.
*
* @since 1.8.0
*
* @var integer
*/
const RATE_LIMIT_FAILED_REQUEST_THRESHOLD = 3;
/**
* How many seconds until the refresh in progress cache lock is cleared.
*
* @since 1.8.0
*
* @var integer
*/
const REFRESH_IN_PROGRESS_EXPIRATION_SECONDS = MINUTE_IN_SECONDS;
/**
* Failed request count option key.
*
* @since 1.8.0
*
* @var string
*/
protected $failed_requests_count_key = '';
/**
* The refresh in progress cache key.
*
* @since 1.8.0
*
* @var string
*/
protected $refresh_in_progress_lock_key = '';
/**
* The rate limiting cache key.
*
* @since 1.8.0
*
* @var string
*/
protected $rate_limit_lock_key = '';
/**
* The reason why the refreshing could be locked.
*
* @since 1.8.0
*
* @var string
*/
public $refresh_lock_reason = '';
/**
* An instance of the add-on.
*
* @since 1.8.0
*
* @var GFAddOn
*/
protected $addon;
/**
* Handler constructor.
*
* @since 1.8.0
*
* @param GFAddOn $addon The add-on the handler is being initialized for.
*/
public function __construct( $addon ) {
$this->addon = $addon;
$slug = $addon->get_slug();
$this->failed_requests_count_key = $slug . '_failed_refresh_token_requests_count';
$this->refresh_in_progress_lock_key = $slug . '_refresh_lock';
$this->rate_limit_lock_key = $slug . '_rate_limit';
}
/**
* Checks if the rate limit cache key is set, and sets the lock reason if locked.
*
* After consecutive failed requests, a cache key is set to prevent more failed requests.
*
* @sicne 1.8.0
*
* @return bool
*/
protected function is_rate_limited() {
$rate_limited = GFCache::get( $this->rate_limit_lock_key, $found );
if ( $found && $rate_limited ) {
$this->refresh_lock_reason = 'Refresh token request rate limit reached.';
return true;
}
return false;
}
/**
* Checks if the threshold for failed refresh requests has been reached, sets the cache key if so.
*
* @since 1.8.0
*/
public function increment_rate_limit() {
$failed_requests_count = intval( get_option( $this->failed_requests_count_key ) );
if ( $failed_requests_count >= self::RATE_LIMIT_FAILED_REQUEST_THRESHOLD ) {
$this->addon->log_debug( __METHOD__ . '(): Rate limit threshold reached, setting rate limit lock.' );
GFCache::set( $this->rate_limit_lock_key, true, true, self::RATE_LIMIT_CACHE_EXPIRATION_SECONDS );
update_option( $this->failed_requests_count_key, 0 );
} else {
$this->addon->log_debug( __METHOD__ . '(): Increasing failed requests count, current count: ' . $failed_requests_count );
update_option( $this->failed_requests_count_key, $failed_requests_count + 1 );
}
}
/**
* Checks if there is a request already being made for refreshing the token.
*
* @since 1.8.0
*
* @return bool
*/
public function is_locked() {
$locked = GFCache::get( $this->refresh_in_progress_lock_key, $found );
if ( $found && $locked ) {
$this->refresh_lock_reason = 'Token Refresh is already in progress.';
return true;
}
return false;
}
/**
* Sets the refresh in progress cache lock.
*
* @since 1.8.0
*/
public function lock() {
GFCache::set( $this->refresh_in_progress_lock_key, true, true, self::REFRESH_IN_PROGRESS_EXPIRATION_SECONDS );
$this->addon->log_debug( __METHOD__ . '(): Refresh in progress lock has been set.' );
}
/**
* Clears the rate limit lock.
*
* @since 1.8.0
*/
public function reset_rate_limit() {
GFCache::delete( $this->rate_limit_lock_key );
update_option( $this->failed_requests_count_key, 0 );
$this->addon->log_debug( __METHOD__ . '(): Rate limit lock cleared.' );
}
/**
* Clears the refresh in progress lock.
*
* @since 1.8.0
*/
public function release_lock() {
GFCache::delete( $this->refresh_in_progress_lock_key );
$this->addon->log_debug( __METHOD__ . '(): Refresh in progress lock cleared.' );
}
/**
* Checks the token can be refreshed after making sure no locks are in place.
*
* @since 1.8.0
*
* @return bool
*/
public function can_refresh_token() {
return $this->is_rate_limited() === false && $this->is_locked() === false;
}
}
@@ -0,0 +1,501 @@
<?php
/**
* Class responsible for verifying tokens returned by Recaptcha.
*
* @package Gravity_Forms\Gravity_Forms_RECAPTCHA
*/
namespace Gravity_Forms\Gravity_Forms_RECAPTCHA;
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|array
*/
private $recaptcha_result;
/**
* The reCAPTCHA action.
*
* @since 1.4 Previously a dynamic property.
*
* @var string
*/
private $action;
/**
* The connection type.
*
* @since 1.7.0
*
* @var string
*/
private $connection_type = '';
/**
* 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.
* @param string $connection_type The connection type.
*
* @since 1.0
*/
public function init( $token = '', $action = '', $connection_type = null ) {
$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 );
$this->connection_type = $connection_type;
}
/**
* Get the reCAPTCHA result.
*
* Returns a stdClass if it's already been processed.
*
* @since 1.0
*
* @return stdClass|array|null
*/
public function get_recaptcha_result() {
return $this->recaptcha_result;
}
/**
* Populates the reCAPTCHA result with the given data.
*
* @since 2.2.0
*
* @param array $result The reCAPTCHA result.
*
* @return void
*/
public function set_recaptcha_result( $result ) {
$this->recaptcha_result = $result;
}
/**
* Validate that the reCAPTCHA response data has the required properties and meets expectations.
*
* @since 1.0
* @since 1.7.0 Added support for enterprise reCAPTCHA.
*
* @param array $response_data The response data to validate.
* @param string $connection_type The connection type.
*
* @return bool
*/
private function validate_response_data( $response_data, $connection_type = null ) {
if ( $connection_type === 'enterprise' ) {
return $this->validate_enterprise_assessment_response( $response_data );
} else {
return $this->validate_classic_response( $response_data );
}
}
/**
* Validate the enterprise assessment response.
*
* @param array $response_data The response data.
*
* @since 1.7.0
*
* @return bool
*/
private function validate_enterprise_assessment_response( $response_data ) {
if ( rgar( $response_data, 'error' )
|| rgars( $response_data, 'tokenProperties/valid' ) !== true
|| ! rgars( $response_data, 'riskAnalysis/score' )
|| ! rgars( $response_data, 'tokenProperties/action' )
|| ! rgars( $response_data, 'tokenProperties/hostname' )
) {
return false;
}
return (
rgars( $response_data, 'tokenProperties/valid' ) === true
&& $this->verify_hostname( rgars( $response_data, 'tokenProperties/hostname' ) )
&& $this->verify_action( rgars( $response_data, 'tokenProperties/action' ) )
&& $this->verify_score( rgars( $response_data, 'riskAnalysis/score' ) )
);
}
/**
* Validate the classic reCAPTCHA response.
*
* @param array $response_data The response data.
*
* @since 1.0.0
* @since 1.7.0 Moved from the validate_response_data method.
*
* @return bool
*/
private function validate_classic_response( $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 ) ); // @codingStandardsIgnoreLine
$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;
}
$plugin_settings = $this->addon->get_plugin_settings();
$connection_type = rgar( $plugin_settings, 'connection_type' );
$this->init( $token, 'submit', $connection_type );
if ( $connection_type !== 'enterprise' ) {
$response = $this->get_response_data( $this->api->verify_token( $token, $this->addon->get_plugin_settings_instance()->get_recaptcha_key( 'secret_key_v3' ) ) );
} else {
$access_token = rgar( $plugin_settings, 'access_token' );
$project_id = $this->addon->get_plugin_settings_instance()->get_recaptcha_key( 'project_number' );
$response = $this->api->create_recaptcha_assessment( $access_token, $project_id, $token, $this->addon->get_plugin_settings_instance()->get_recaptcha_key( 'site_key_v3_enterprise' ), $action = 'submit' );
}
if ( is_wp_error( $response ) ) {
$this->addon->log_debug( __METHOD__ . '(): Validating the reCAPTCHA response has failed due to the following: ' . $response->get_error_message() );
wp_send_json_error(
array(
'error' => $data->get_error_message(),
'code' => self::ERROR_CODE_CANNOT_VERIFY_TOKEN,
)
);
}
if ( isset( $response->score ) && $response->score === 'disabled (quota limit)' ) {
$this->addon->log_debug( __METHOD__ . '(): Validation bypassed due to reCAPTCHA quota limit.' );
$this->recaptcha_result = $response;
return true;
}
if ( ! $this->validate_response_data( $response, $connection_type ) ) {
$this->addon->log_debug(
__METHOD__ . '(): Could not validate the token request from the reCAPTCHA service. ' . PHP_EOL
. "token: {$token}" . PHP_EOL
. "response: " . print_r( $response, true ) . PHP_EOL // @codingStandardsIgnoreLine
);
return false;
}
// @codingStandardsIgnoreLine
$this->addon->log_debug( __METHOD__ . '(): Validated reCAPTCHA: ' . print_r( $response, true ) );
$this->recaptcha_result = $response;
// Caching result for 1 hour.
\GFCache::set( 'recaptcha_' . $token, $response, 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;
}
$response_code = wp_remote_retrieve_response_code( $response );
/**
* If the reCAPTCHA API quota has been exceeded, a 429 status code
* is returned. This will fake a successful response to prevent
* the form from being blocked by a reCAPTCHA quota limit.
*/
if ( $response_code === 429 ) {
$this->addon->log_debug( __METHOD__ . '(): reCAPTCHA API quota limit exceeded.' );
update_option( GF_RECAPTCHA::RECAPTCHA_QUOTA_LIMIT_HIT, true );
$data = new stdClass;
$data->success = true;
$data->challenge_ts = date( 'c' );
$data->hostname = wp_parse_url( get_home_url(), PHP_URL_HOST );
$data->score = 'disabled (quota limit)';
$data->action = $this->action;
return $data;
}
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.' );
if ( $score === 'disabled (quota limit)' ) {
$this->addon->log_debug( __METHOD__ . '(): Score verfication bypassed due to exceeding the reCAPTCHA API quota limit.' );
return true;
}
return ( is_numeric( $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 ) ||
( ! rgars( $this->recaptcha_result, 'riskAnalysis/score' ) &&
! property_exists( $this->recaptcha_result, 'score' ) )
) {
return $this->addon->is_preview() ? 0.9 : 0.0;
}
if ( rgars( $this->recaptcha_result, 'riskAnalysis/score' ) ) {
$score = rgars( $this->recaptcha_result, 'riskAnalysis/score' );
} else {
$score = $this->recaptcha_result->score;
}
return (float) $score;
}
/**
* Gets the assessment ID (name) from the reCAPTCHA assessment response.
*
* @since 1.10
*
* @return string
*/
public function get_assessment_id() {
return $this->addon->is_preview() ? '' : rgar( $this->recaptcha_result, 'name', '' );
}
/**
* 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 ) );
}
}
@@ -0,0 +1,316 @@
/******/ (function() { // webpackBootstrap
/*!****************************!*\
!*** ./js/src/frontend.js ***!
\****************************/
function _regenerator() { /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */ var e, t, r = "function" == typeof Symbol ? Symbol : {}, n = r.iterator || "@@iterator", o = r.toStringTag || "@@toStringTag"; function i(r, n, o, i) { var c = n && n.prototype instanceof Generator ? n : Generator, u = Object.create(c.prototype); return _regeneratorDefine2(u, "_invoke", function (r, n, o) { var i, c, u, f = 0, p = o || [], y = !1, G = { p: 0, n: 0, v: e, a: d, f: d.bind(e, 4), d: function d(t, r) { return i = t, c = 0, u = e, G.n = r, a; } }; function d(r, n) { for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) { var o, i = p[t], d = G.p, l = i[2]; r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0)); } if (o || r > 1) return a; throw y = !0, n; } return function (o, p, l) { if (f > 1) throw TypeError("Generator is already running"); for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) { i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u); try { if (f = 2, i) { if (c || (o = "next"), t = i[o]) { if (!(t = t.call(i, u))) throw TypeError("iterator result is not an object"); if (!t.done) return t; u = t.value, c < 2 && (c = 0); } else 1 === c && (t = i.return) && t.call(i), c < 2 && (u = TypeError("The iterator does not provide a '" + o + "' method"), c = 1); i = e; } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break; } catch (t) { i = e, c = 1, u = t; } finally { f = 1; } } return { value: t, done: y }; }; }(r, o, i), !0), u; } var a = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} t = Object.getPrototypeOf; var c = [][n] ? t(t([][n]())) : (_regeneratorDefine2(t = {}, n, function () { return this; }), t), u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c); function f(e) { return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, _regeneratorDefine2(e, o, "GeneratorFunction")), e.prototype = Object.create(u), e; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, _regeneratorDefine2(u, "constructor", GeneratorFunctionPrototype), _regeneratorDefine2(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = "GeneratorFunction", _regeneratorDefine2(GeneratorFunctionPrototype, o, "GeneratorFunction"), _regeneratorDefine2(u), _regeneratorDefine2(u, o, "Generator"), _regeneratorDefine2(u, n, function () { return this; }), _regeneratorDefine2(u, "toString", function () { return "[object Generator]"; }), (_regenerator = function _regenerator() { return { w: i, m: f }; })(); }
function _regeneratorDefine2(e, r, n, t) { var i = Object.defineProperty; try { i({}, "", {}); } catch (e) { i = 0; } _regeneratorDefine2 = function _regeneratorDefine(e, r, n, t) { function o(r, n) { _regeneratorDefine2(e, r, function (e) { return this._invoke(r, n, e); }); } r ? i ? i(e, r, { value: n, enumerable: !t, configurable: !t, writable: !t }) : e[r] = n : (o("next", 0), o("throw", 1), o("return", 2)); }, _regeneratorDefine2(e, r, n, t); }
function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
/* global gform, gforms_recaptcha_recaptcha_strings, grecaptcha */
(function (gform, grecaptcha, strings) {
var initScore = function initScore() {
var isInitialized = false;
document.addEventListener('gform/post_render', function (event) {
// Abort if already initialized.
if (isInitialized) {
return;
}
isInitialized = true;
// Executing on AJAX validation.
gform.utils.addAsyncFilter('gform/ajax/pre_ajax_validation', /*#__PURE__*/function () {
var _ref = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(data) {
return _regenerator().w(function (_context) {
while (1) switch (_context.n) {
case 0:
_context.n = 1;
return execute(data.form);
case 1:
return _context.a(2, data);
}
}, _callee);
}));
return function (_x) {
return _ref.apply(this, arguments);
};
}());
// Executing recaptcha on form submission or Next button click.
gform.utils.addAsyncFilter('gform/submission/pre_submission', /*#__PURE__*/function () {
var _ref2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(data) {
var recaptchaRequired;
return _regenerator().w(function (_context2) {
while (1) switch (_context2.n) {
case 0:
recaptchaRequired = data.submissionType === gform.submission.SUBMISSION_TYPE_SUBMIT || data.submissionType === gform.submission.SUBMISSION_TYPE_NEXT;
if (!(recaptchaRequired && !data.abort)) {
_context2.n = 1;
break;
}
_context2.n = 1;
return execute(data.form);
case 1:
return _context2.a(2, data);
}
}, _callee2);
}));
return function (_x2) {
return _ref2.apply(this, arguments);
};
}());
});
maybeDisableBadge();
};
var initCheckbox = function initCheckbox() {
document.addEventListener('gform/post_render', function (event) {
renderCheckboxes(event.detail.formId);
});
document.addEventListener('gfcf/conversational/navigate/next', function (event) {
if (!event.detail.target.classList.contains('gfield--type-recaptcha_checkbox')) {
return;
}
var container = event.detail.target.querySelector('.g-recaptcha');
renderCheckbox(container);
});
gform.addAction('gform_post_conditional_logic_field_action', function (formId, action, targetId, defaultValues, isInit) {
if (action !== 'show') {
return;
}
var field = gform.utils.getNode(targetId, document, true);
if (!field || !field.classList.contains('gfield--type-recaptcha_checkbox')) {
return;
}
var container = field.querySelector('.g-recaptcha');
renderCheckbox(container, isInit);
});
gform.utils.addAsyncFilter('gform/ajax/post_ajax_submission', function (data) {
var _data$submissionResul;
if (!((_data$submissionResul = data.submissionResult) !== null && _data$submissionResul !== void 0 && (_data$submissionResul = _data$submissionResul.data) !== null && _data$submissionResul !== void 0 && _data$submissionResul.recaptcha_checkbox_response)) {
return data;
}
var input = gform.utils.getNode('input.gfield_recaptcha_response', data.form, true);
if (input) {
input.value = data.submissionResult.data.recaptcha_checkbox_response;
}
return data;
});
};
/**
* @function execute
* @description Execute standard or enterprise reCAPTCHA depending on the connection type.
*
* @since 2.2.0
*
* @param {HTMLFormElement} form The form element.
*
* @returns {Promise<void>}
*/
var execute = /*#__PURE__*/function () {
var _ref3 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3(form) {
return _regenerator().w(function (_context3) {
while (1) switch (_context3.n) {
case 0:
if (form) {
_context3.n = 1;
break;
}
return _context3.a(2);
case 1:
if (!((strings === null || strings === void 0 ? void 0 : strings.key_type) === 'CHECKBOX')) {
_context3.n = 2;
break;
}
return _context3.a(2);
case 2:
if (!(strings.connection_type === 'enterprise')) {
_context3.n = 4;
break;
}
_context3.n = 3;
return executeEnterpriseRecaptcha(form);
case 3:
_context3.n = 5;
break;
case 4:
_context3.n = 5;
return executeRecaptcha(form);
case 5:
return _context3.a(2);
}
}, _callee3);
}));
return function execute(_x3) {
return _ref3.apply(this, arguments);
};
}();
/**
* @function executeRecaptcha
* @description Execute standard reCAPTCHA, setting the token to the hidden input field.
*
* @since 2.2.0
*
* @param {HTMLFormElement} form The form element.
*
* @returns {Promise<void>}
*/
var executeRecaptcha = /*#__PURE__*/function () {
var _ref4 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4(form) {
var dataInput, token;
return _regenerator().w(function (_context4) {
while (1) switch (_context4.n) {
case 0:
dataInput = form.querySelector('.ginput_recaptchav3 .gfield_recaptcha_response'); // If reCAPTCHA fields can't be found, or recaptcha response is already set, don't execute reCAPTCHA.
if (!(!dataInput || dataInput.value.length)) {
_context4.n = 1;
break;
}
return _context4.a(2);
case 1:
_context4.n = 2;
return grecaptcha.execute(strings.site_key, {
action: 'submit'
});
case 2:
token = _context4.v;
if (token.length && typeof token === 'string') {
dataInput.value = token;
}
case 3:
return _context4.a(2);
}
}, _callee4);
}));
return function executeRecaptcha(_x4) {
return _ref4.apply(this, arguments);
};
}();
/**
* @function executeEnterpriseRecaptcha
* @description Execute enterprise reCAPTCHA, setting the token to the hidden input field.
*
* @since 2.2.0
*
* @param {HTMLFormElement} form The form element.
*
* @returns {Promise<void>}
*/
var executeEnterpriseRecaptcha = /*#__PURE__*/function () {
var _ref5 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee5(form) {
var dataInput, token, _t;
return _regenerator().w(function (_context5) {
while (1) switch (_context5.p = _context5.n) {
case 0:
dataInput = form.querySelector('.ginput_recaptchav3 .gfield_recaptcha_response'); // If reCAPTCHA fields can't be found, or recaptcha response is already set, don't execute reCAPTCHA.
if (!(!dataInput || dataInput.value.length)) {
_context5.n = 1;
break;
}
return _context5.a(2);
case 1:
_context5.p = 1;
_context5.n = 2;
return grecaptcha.enterprise.execute(strings.site_key, {
action: 'submit'
});
case 2:
token = _context5.v;
if (token.length && typeof token === 'string') {
dataInput.value = token;
}
_context5.n = 4;
break;
case 3:
_context5.p = 3;
_t = _context5.v;
return _context5.a(2);
case 4:
return _context5.a(2);
}
}, _callee5, null, [[1, 3]]);
}));
return function executeEnterpriseRecaptcha(_x5) {
return _ref5.apply(this, arguments);
};
}();
maybeDisableBadge = function maybeDisableBadge() {
if (strings.connection_type === 'enterprise') {
grecaptcha.enterprise.ready(function () {
hideBadge();
});
} else {
grecaptcha.ready(function () {
hideBadge();
});
}
};
hideBadge = function hideBadge() {
if (strings.disable_badge) {
var badge = document.querySelector('.grecaptcha-badge');
if (badge) {
badge.style.visibility = 'hidden';
}
}
};
var renderCheckboxes = function renderCheckboxes() {
var formId = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
var prefix = formId ? "#gform_".concat(formId) : '.gform_wrapper';
var containers = document.querySelectorAll("".concat(prefix, " .ginput_container_recaptcha_checkbox .g-recaptcha"));
if (!containers) {
return;
}
containers.forEach(renderCheckbox);
};
var renderCheckbox = function renderCheckbox(container) {
var _grecaptcha$enterpris;
var isLogicInit = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (!(grecaptcha !== null && grecaptcha !== void 0 && (_grecaptcha$enterpris = grecaptcha.enterprise) !== null && _grecaptcha$enterpris !== void 0 && _grecaptcha$enterpris.render) || !container || container.dataset.initialized === 'true' || container.dataset.widgetid || !isLogicInit && isContainerHiddenByParent(container)) {
return;
}
// If the iframe is present on the container, something else has already rendered it.
if (container.querySelector('iframe')) {
container.dataset.initialized = 'true';
return;
}
var widgetId = grecaptcha.enterprise.render(container);
if (!Number.isInteger(widgetId)) {
return;
}
container.dataset.initialized = 'true';
container.dataset.widgetid = widgetId;
};
var isContainerHiddenByParent = function isContainerHiddenByParent(container) {
// Walk up the DOM from the container to the document root.
var node = container.parentElement;
while (node) {
try {
var style = getComputedStyle(node);
if ((style === null || style === void 0 ? void 0 : style.display) === 'none') {
return true;
}
} catch (e) {
// Some environments may throw for detached nodes; ignore and continue.
}
node = node.parentElement;
}
return false;
};
window.gform = gform;
window.gform.recaptchaV3 = {
execute: execute
};
// Google requires a simple global function name for the onload callback. It looks like they call using array syntax: i.e. window['gravityformsrecaptchaRenderCheckboxes']();
window.gravityformsrecaptchaRenderCheckboxes = renderCheckboxes;
if ((strings === null || strings === void 0 ? void 0 : strings.key_type) === 'CHECKBOX') {
initCheckbox();
} else {
initScore();
}
})(window.gform || {}, window.grecaptcha || {}, gforms_recaptcha_recaptcha_strings);
/******/ })()
;
//# sourceMappingURL=frontend.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */
@@ -0,0 +1,588 @@
/******/ (function() { // webpackBootstrap
/*!***********************************!*\
!*** ./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;
}
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();
if (!typeValue) {
return;
}
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: 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 () {
var $form = $('#gform-settings');
var $action = $form.find('input[name="gf_recaptcha_action"]');
if ('gf_recaptcha_v3_classic' === $action.val()) {
recaptchaV3Settings();
} else if ('gf_recaptcha_v2' === $action.val()) {
recaptchaV2Settings();
}
if (gforms_recaptcha_recaptcha_strings.disable_badge) {
var badge = document.querySelector('.grecaptcha-badge');
if (badge) {
badge.style.visibility = 'hidden';
}
}
// Handle disconnects from the admin.
var $disconnect = $('.gfrecaptcha-disconnect');
if ($disconnect.length > 0) {
$disconnect.on('click', function (e) {
e.preventDefault();
// check if we have any saved settings that are going to be deleted
var $site_key_v3 = $('#site_key_v3');
var $site_key_v2 = $('#site_key_v2');
var hasSavedSettings = false;
if ($site_key_v3.length > 0 && $site_key_v3.val() !== '') {
hasSavedSettings = true;
}
if ($site_key_v2.length > 0 && $site_key_v2.val() !== '') {
hasSavedSettings = true;
}
// Confirm deleting settings if you change connection type or disconnect
if (hasSavedSettings || $disconnect.hasClass('gfrecaptcha-disconnect')) {
if ($disconnect.hasClass('gfrecaptcha-disconnect')) {
var title = gforms_recaptcha_recaptcha_strings.disconnect_title;
var message = gforms_recaptcha_recaptcha_strings.disconnect_message;
} else {
var title = gforms_recaptcha_recaptcha_strings.change_connection_type_title;
var message = gforms_recaptcha_recaptcha_strings.change_connection_type_message;
}
if (typeof gform.instances.dialogConfirmAsync !== 'function') {
if (!confirm(message)) {
return;
}
} else {
gform.instances.dialogConfirmAsync(title, message).then(function (userConfirmed) {
if (!userConfirmed) {
return;
}
});
}
}
if ($disconnect.hasClass('gfrecaptcha-changetype')) {
$disconnect.html(gforms_recaptcha_recaptcha_strings.change_connection_type);
} else {
$disconnect.html(gforms_recaptcha_recaptcha_strings.disconnect);
}
var url_params = wpAjax.unserialize(e.target.href);
var nonce_value = url_params.nonce;
// Perform Ajax request.
$.post(ajaxurl, {
action: 'disconnect_recaptcha',
nonce: nonce_value
}, function (response) {
window.location.href = window.location.href;
});
});
}
// Handle form submission on connect screen.
$form.on('submit', function (e) {
// Determining if we're just connecting for the first time.
if ($form.find('input[value="recaptcha_setup"]').length === 1) {
// is_postback
e.preventDefault();
// Set l18n.
var $save_button = $form.find('#gform-settings-save');
var $connection_type = $form.find('[name="_gform_setting_connection_type"]:checked').val();
// Get nonce.
var nonce_value = $form.find('input[name="recaptcha_nonce"]').val();
if ($connection_type === 'enterprise') {
// Perform Ajax request.
$.post(ajaxurl, {
action: 'perform_enterprise_oauth',
nonce: nonce_value,
mode: $connection_type
}, function (response) {
if (!response.data.errors) {
window.location.href = response.data.redirect;
}
}, 'json');
} else {
// Perform Ajax request.
$.post(ajaxurl, {
action: 'update_reload_settings',
nonce: nonce_value,
connection_type: $connection_type
}, function (response) {
if (!response.data.errors) {
var url = new URL(response.data.redirect);
url.searchParams.set('connection_type', $connection_type);
window.location.href = url;
}
}, 'json');
}
return;
}
if ('gf_recaptcha_enterprise' === $action.val()) {
e.preventDefault();
// Getting selected items from the account/property and data stream drop downs.
var $selected_project = $form.find('select[name="recaptcha_project"]').find(':selected');
var $selected_site_key = $form.find('#recaptcha-site-keys :selected');
$.post(ajaxurl, {
action: 'save_recaptcha_enterprise_data',
project_number: $selected_project.val(),
project_id: $selected_project.data('project-id'),
project_name: $selected_project.data('project-name'),
site_key_v3_enterprise: $selected_site_key.val(),
site_key_type_v3_enterprise: $selected_site_key.data('site-key-type'),
site_key_display_name: $selected_site_key.data('site-key-display-name'),
score_threshold_v3: $form.find('#score_threshold_v3').val(),
disable_badge_v3: $form.find('input[name="_gform_setting_disable_badge_v3"]').val(),
nonce: $form.find('input[name="recaptcha_nonce"]').val()
}, function (response) {
if (response.success) {
window.location.href = response['data'];
}
});
}
});
// Get keys available to the selected project.
$('#recaptcha_project').on('change', function (e) {
var $option = $(this).find(':selected');
var nonce = $('body').find('input[name="recaptcha_nonce"]').val();
$('#recaptcha-site-keys').html('<br /><img src="' + gforms_recaptcha_recaptcha_strings.spinner + '" />');
$.post(ajaxurl, {
action: 'get_enterprise_site_keys',
project: $option.val(),
nonce: nonce
}, function (response) {
if (response.success) {
var siteKeys = response['data'];
var select = document.createElement('select');
select.name = 'recaptcha-site-keys';
var label = document.createElement('label');
label.textContent = 'Enterprise Site Key';
label.setAttribute('for', 'recaptcha-site-keys');
label.classList.add('gform-settings-label');
var header = document.createElement('div');
header.classList.add('gform-settings-field__header');
header.appendChild(label);
var defaultOption = document.createElement('option');
defaultOption.value = '';
defaultOption.textContent = 'Select a site key';
select.appendChild(defaultOption);
siteKeys.forEach(function (key) {
var option = document.createElement('option');
option.value = key.value;
option.textContent = key.displayName;
option.setAttribute('data-site-key-display-name', key.displayName);
option.setAttribute('data-site-key-type', key.type);
select.appendChild(option);
});
var container = document.querySelector('#recaptcha-site-keys');
container.innerHTML = '';
container.appendChild(header);
container.appendChild(select);
} else {
$('#recaptcha-site-keys').html('');
}
});
});
gform.adminUtils.handleUnsavedChanges('#gform-settings');
});
})(jQuery);
/******/ })()
;
//# sourceMappingURL=plugin_settings.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,454 @@
# Copyright (C) 2026 Gravity Forms
# This file is distributed under the GPL-3.0+.
msgid ""
msgstr ""
"Project-Id-Version: Gravity Forms reCAPTCHA Add-On 2.2.2\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: 2026-05-07T17:29:34+00:00\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"X-Generator: WP-CLI 2.12.0\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:541
msgid "Disconnect from reCAPTCHA"
msgstr ""
#: class-gf-recaptcha.php:564
#: class-gf-recaptcha.php:1272
#: includes/settings/class-plugin-settings.php:641
msgid "Change Connection Type"
msgstr ""
#: class-gf-recaptcha.php:583
msgid "Your Google Workspace session requires periodic reauthentication. Please reconnect the add-on, and contact your Google Workspace administrator if you need help with this policy."
msgstr ""
#: class-gf-recaptcha.php:588
msgid "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."
msgstr ""
#: class-gf-recaptcha.php:638
msgid "You have no available projects for reCAPTCHA or have insufficient permissions"
msgstr ""
#: class-gf-recaptcha.php:651
msgid "Select a Project"
msgstr ""
#: class-gf-recaptcha.php:671
#: class-gf-recaptcha.php:686
msgid "Enterprise Site Key"
msgstr ""
#: class-gf-recaptcha.php:680
msgid "There was an error retrieving the reCAPTCHA site keys."
msgstr ""
#: class-gf-recaptcha.php:688
msgid "Select a site key"
msgstr ""
#: class-gf-recaptcha.php:866
msgid "Disable reCAPTCHA v3 for this form."
msgstr ""
#: class-gf-recaptcha.php:1267
msgid "Disconnecting"
msgstr ""
#: class-gf-recaptcha.php:1268
msgid "Resetting"
msgstr ""
#: class-gf-recaptcha.php:1273
msgid "Changing the connection type will delete your current settings. Do you want to proceed?"
msgstr ""
#: class-gf-recaptcha.php:1274
#: includes/settings/class-plugin-settings.php:620
msgid "Disconnect"
msgstr ""
#: class-gf-recaptcha.php:1275
msgid "Disconnecting from reCAPTCHA will delete your current settings. Do you want to proceed?"
msgstr ""
#: class-gf-recaptcha.php:1291
msgid "reCAPTCHA Score"
msgstr ""
#: class-gf-recaptcha.php:1385
#: includes/class-gf-field-recaptcha-checkbox.php:163
msgid "reCAPTCHA"
msgstr ""
#: class-gf-recaptcha.php:1406
#: class-gf-recaptcha.php:2091
msgid "Score"
msgstr ""
#: class-gf-recaptcha.php:1409
msgid "Click here to learn more about reCAPTCHA."
msgstr ""
#. translators: %1$s: The reCAPTCHA score for the entry. %2$s: The threshold at the time the entry was marked as spam.
#: class-gf-recaptcha.php:1458
msgid "Score of %1$s is less than or equal to the threshold of %2$s."
msgstr ""
#: class-gf-recaptcha.php:1530
msgid "Disabled"
msgstr ""
#: class-gf-recaptcha.php:1531
msgid "Disconnected"
msgstr ""
#: class-gf-recaptcha.php:1532
msgid "Disabled (quota limit)"
msgstr ""
#: class-gf-recaptcha.php:1533
msgid "Disabled (token refresh in progress)"
msgstr ""
#: class-gf-recaptcha.php:1534
msgid "Disabled (token refresh failed)"
msgstr ""
#: class-gf-recaptcha.php:1535
msgid "Disconnected (reauthentication required)"
msgstr ""
#. translators: %s is the link markup.
#: class-gf-recaptcha.php:1815
msgid "You have reached the quota limit for reCAPTCHA set by Google. Please check the quota on your %sreCAPTCHA Account%s."
msgstr ""
#: class-gf-recaptcha.php:1913
#: class-gf-recaptcha.php:2007
msgid "User does not have required permissions to setup reCAPTCHA."
msgstr ""
#: class-gf-recaptcha.php:1971
msgid "There was an error retrieving reCAPTHCA site keys."
msgstr ""
#: class-gf-recaptcha.php:2068
msgid "Google notified that the entry was marked as spam."
msgstr ""
#: class-gf-recaptcha.php:2072
msgid "Google notified that the entry was marked as not spam."
msgstr ""
#: class-gf-recaptcha.php:2092
#: includes/settings/class-plugin-settings.php:515
msgid "Checkbox"
msgstr ""
#: includes/class-gf-field-recaptcha-checkbox.php:76
msgid "reCAPTCHA Checkbox"
msgstr ""
#: includes/class-gf-field-recaptcha-checkbox.php:131
msgid "Adds a reCAPTCHA Enterprise checkbox field to your form to help protect your website from spam and bot abuse."
msgstr ""
#: includes/class-gf-field-recaptcha-checkbox.php:161
msgid "Only one reCAPTCHA checkbox field can be added to the form."
msgstr ""
#: includes/class-gf-field-recaptcha-checkbox.php:165
msgid "This field has accessibility issues. We recommend using a score-based key instead of this field."
msgstr ""
#: includes/class-gf-field-recaptcha-checkbox.php:166
#: includes/class-gf-field-recaptcha-checkbox.php:219
#: includes/class-gf-field-recaptcha-checkbox.php:302
msgid "(opens in a new tab)"
msgstr ""
#: includes/class-gf-field-recaptcha-checkbox.php:214
#: includes/class-gf-field-recaptcha-checkbox.php:298
msgid "Configuration Required"
msgstr ""
#. Translators: 1. Opening <a> tag with link to the Forms > Settings > reCAPTCHA page. 2. closing <a> tag.
#: includes/class-gf-field-recaptcha-checkbox.php:217
#: includes/class-gf-field-recaptcha-checkbox.php:300
msgid "To use this field, configure the %1$sreCAPTCHA Add-On Settings%2$s using the Enterprise connection type, and select a checkbox type key."
msgstr ""
#: includes/class-gf-field-recaptcha-checkbox.php:222
msgid "This field requires additional configuration"
msgstr ""
#: includes/class-gf-field-recaptcha-checkbox.php:287
msgid "An example reCAPTCHA checkbox"
msgstr ""
#: includes/class-gf-field-recaptcha-checkbox.php:361
msgid "The reCAPTCHA was invalid. Go back and try it again."
msgstr ""
#: includes/settings/class-plugin-settings.php:121
msgid "reCAPTCHA v3 Enterprise keys require Gravity Forms 2.9 or later. Upgrade your Gravity Forms installation to get access to this connection method"
msgstr ""
#: includes/settings/class-plugin-settings.php:122
msgid "Modern reCAPTCHA keys using the enterprise API. All newly created v3 keys are enterprise keys. Unlike classic keys, enterprise keys require only a site key to function."
msgstr ""
#: includes/settings/class-plugin-settings.php:126
msgid "v3 Enterprise"
msgstr ""
#: includes/settings/class-plugin-settings.php:130
msgid "Recommended"
msgstr ""
#: includes/settings/class-plugin-settings.php:132
#: includes/settings/class-plugin-settings.php:398
msgid "reCAPTCHA v3 Enterprise"
msgstr ""
#: includes/settings/class-plugin-settings.php:136
msgid "v2"
msgstr ""
#: includes/settings/class-plugin-settings.php:139
msgid "Challenge based"
msgstr ""
#: includes/settings/class-plugin-settings.php:141
#: includes/settings/class-plugin-settings.php:490
msgid "reCAPTCHA v2"
msgstr ""
#: includes/settings/class-plugin-settings.php:142
msgid "Challenge based keys using the v2 API. This method allows a reCAPTCHA challenge field to be inserted into your forms."
msgstr ""
#: includes/settings/class-plugin-settings.php:145
msgid "v3 Classic"
msgstr ""
#: includes/settings/class-plugin-settings.php:148
msgid "Legacy"
msgstr ""
#: includes/settings/class-plugin-settings.php:150
#: includes/settings/class-plugin-settings.php:316
msgid "reCAPTCHA v3 Classic"
msgstr ""
#: includes/settings/class-plugin-settings.php:151
msgid "Legacy v3 keys. New classic keys can no longer be created, but you can use an existing site key and secret."
msgstr ""
#: includes/settings/class-plugin-settings.php:157
msgid "Select reCAPTCHA Connection Type"
msgstr ""
#: includes/settings/class-plugin-settings.php:158
msgid "Select which type of keys to use for reCAPTCHA."
msgstr ""
#: includes/settings/class-plugin-settings.php:174
msgid "Continue &rarr;"
msgstr ""
#: includes/settings/class-plugin-settings.php:293
msgid "reCAPTCHA Settings"
msgstr ""
#: includes/settings/class-plugin-settings.php:320
#: includes/settings/class-plugin-settings.php:494
msgid "Site Key"
msgstr ""
#: includes/settings/class-plugin-settings.php:328
#: includes/settings/class-plugin-settings.php:501
msgid "Secret Key"
msgstr ""
#: includes/settings/class-plugin-settings.php:336
#: includes/settings/class-plugin-settings.php:407
msgid "Score Threshold"
msgstr ""
#: includes/settings/class-plugin-settings.php:355
#: includes/settings/class-plugin-settings.php:426
msgid "Disable Google reCAPTCHA Badge"
msgstr ""
#: includes/settings/class-plugin-settings.php:356
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:361
#: includes/settings/class-plugin-settings.php:432
msgid "I have added the reCAPTCHA branding, terms of service and privacy policy to my site. "
msgstr ""
#: includes/settings/class-plugin-settings.php:402
msgid "reCAPTCHA Project ID"
msgstr ""
#: includes/settings/class-plugin-settings.php:427
msgid "By default reCAPTCHA v3 (score type keys) 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 ""
#. translators: %s is the constant name.
#: includes/settings/class-plugin-settings.php:476
msgid "Value defined using the %s constant."
msgstr ""
#: includes/settings/class-plugin-settings.php:508
msgid "Type"
msgstr ""
#: includes/settings/class-plugin-settings.php:519
msgid "Invisible"
msgstr ""
#: includes/settings/class-plugin-settings.php:533
msgid "Validate Keys"
msgstr ""
#: includes/settings/class-plugin-settings.php:569
msgid "reCAPTCHA keys are invalid."
msgstr ""
#: includes/settings/class-plugin-settings.php:616
msgid "Disconnect reCAPTCHA"
msgstr ""
#: includes/settings/class-plugin-settings.php:637
msgid "Change reCAPTCHA Connection Type"
msgstr ""
#: includes/settings/class-plugin-settings.php:662
msgid "Google reCAPTCHA is a free anti-spam service that protects your website from fraud and abuse."
msgstr ""
#: includes/settings/class-plugin-settings.php:663
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:668
msgid "Gravity Forms integrates with three types of Google reCAPTCHA."
msgstr ""
#: includes/settings/class-plugin-settings.php:670
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:671
#: includes/settings/class-plugin-settings.php:691
#: includes/settings/class-plugin-settings.php:703
msgid "All submissions are accepted and suspicious submissions are marked as spam."
msgstr ""
#: includes/settings/class-plugin-settings.php:672
#: includes/settings/class-plugin-settings.php:692
#: includes/settings/class-plugin-settings.php:704
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:674
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:675
#: includes/settings/class-plugin-settings.php:697
msgid "Please note, only v2 keys are supported and checkbox keys are not compatible with invisible reCAPTCHA."
msgstr ""
#: includes/settings/class-plugin-settings.php:676
#: includes/settings/class-plugin-settings.php:698
msgid "To activate reCAPTCHA v2 on your form, simply add the CAPTCHA field in the form editor."
msgstr ""
#: includes/settings/class-plugin-settings.php:680
msgid "Read more about reCAPTCHA."
msgstr ""
#: includes/settings/class-plugin-settings.php:683
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:688
msgid "v3 enterprise reCAPTCHA adds a script to every page of your site and uploads form content for processing by Google."
msgstr ""
#: includes/settings/class-plugin-settings.php:689
msgid "A site secret is not required since you are authenticated by OAuth."
msgstr ""
#: includes/settings/class-plugin-settings.php:690
msgid "Enterprise reCAPTCHA gives you much greater control over how submissions are handled via the Cloud Console in your Google Cloud Project."
msgstr ""
#: includes/settings/class-plugin-settings.php:696
msgid "reCAPTCHA v2 (challenge-based) can run in invisible mode where a user will receive a challenge only if Google sees suspicious behavior, or with a checkbox, where the user will click a checkbox to indicate they are not a robot."
msgstr ""
#: includes/settings/class-plugin-settings.php:702
msgid "v3 Classic reCAPTCHA Adds a script to every page of your site and uploads form content for processing by Google."
msgstr ""
#: includes/settings/class-plugin-settings.php:709
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:713
msgid "check out our documentation."
msgstr ""
#: includes/settings/class-plugin-settings.php:729
msgid "reCAPTCHA v3 (score and checkbox type keys) 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:730
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:731
msgid "The default threshold is 0.5."
msgstr ""
#: includes/settings/class-plugin-settings.php:778
msgid "Unexpected field type."
msgstr ""
#: includes/settings/class-plugin-settings.php:785
msgid "Score threshold must be between 0.0 and 1.0"
msgstr ""
#: includes/settings/class-plugin-settings.php:1044
msgid "An error occured while connecting to the API."
msgstr ""
@@ -0,0 +1,80 @@
<?php
/*
Plugin Name: Gravity Forms reCAPTCHA Add-On
Plugin URI: https://gravityforms.com
Description: Enhance Gravity Forms with support for Google reCAPTCHA.
Version: 2.2.2
Requires at least: 6.5
Requires PHP: 7.4
Author: Gravity Forms
Author URI: https://gravityforms.com
License: GPL-3.0+
Text Domain: gravityformsrecaptcha
Domain Path: /languages
------------------------------------------------------------------------
Copyright 2023-2026 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', '2.2.2' );
// Defines the minimum version of Gravity Forms required to run Gravity Forms Recaptcha Add-On.
define( 'GF_RECAPTCHA_MIN_GF_VERSION', '2.9.5' );
// 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;
}