namespace WPForms\Frontend;
private function hooks() {
add_filter( 'script_loader_tag', [ $this, 'set_defer_attribute' ], 10, 3 );
add_action( 'send_headers', [ $this, 'send_headers' ] );
add_action( 'wpforms_frontend_output', [ $this, 'recaptcha' ], 20, 5 );
add_action( 'wp_enqueue_scripts', [ $this, 'recaptcha_noconflict' ], 9999 );
add_action( 'wp_footer', [ $this, 'recaptcha_noconflict' ], 19 );
add_action( 'wpforms_wp_footer', [ $this, 'assets_recaptcha' ] );
* Send HTTP headers to prevent warning in the browser console.
public function send_headers(): void {
$urls = '"https://www.google.com" "https://www.gstatic.com" "https://recaptcha.net" "https://challenges.cloudflare.com" "https://hcaptcha.com"';
"private-state-token-redemption=(self $urls), " .
"private-state-token-issuance=(self $urls)",
* CAPTCHA output if configured.
* @param array $form_data Form data and settings.
* @param null $deprecated Deprecated in v1.3.7, previously was $form object.
* @param bool $title Whether to display form title.
* @param bool $description Whether to display form description.
* @param array $errors List of all errors filled in WPForms_Process::process().
* @noinspection HtmlUnknownAttribute
* @noinspection PhpUnusedParameterInspection
public function recaptcha( $form_data, $deprecated, $title, $description, $errors ) {
// Check that CAPTCHA is configured in the settings.
$captcha_settings = $this->get_form_captcha_settings( $form_data );
if ( ! $captcha_settings ) {
$frontend = wpforms()->obj( 'frontend' );
$container_classes = [ 'wpforms-recaptcha-container', 'wpforms-is-' . $captcha_settings['provider'] ];
if ( $captcha_settings['provider'] === 'recaptcha' ) {
$container_classes[] = 'wpforms-is-recaptcha-type-' . $captcha_settings['recaptcha_type'];
'<div class="%1$s" %2$s>',
wpforms_sanitize_classes( $container_classes, true ),
$frontend->pages ? 'style="display:none;"' : ''
$this->print_recaptcha_fields( $captcha_settings, $form_data );
if ( ! empty( $errors['recaptcha'] ) ) {
$frontend->form_error( 'recaptcha', $errors['recaptcha'] );
* Get a provider-specific captcha class.
* @param string $provider Captcha provider.
private function get_captcha_class( string $provider ): string {
'recaptcha' => 'g-recaptcha',
'hcaptcha' => 'h-captcha',
'turnstile' => 'wpforms-turnstile',
return $classes[ $provider ] ?? 'g-recaptcha';
* @param array $captcha_settings Captcha settings.
* @param array $form_data Form data and settings.
private function get_recaptcha_data( array $captcha_settings, array $form_data ): array {
* Filters captcha sitekey.
* @param array $sitekey Sitekey.
* @param array $form_data Form data and settings.
$data = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
'wpforms_frontend_recaptcha',
[ 'sitekey' => $captcha_settings['site_key'] ],
$is_recaptcha = $captcha_settings['provider'] === 'recaptcha';
$is_turnstile = $captcha_settings['provider'] === 'turnstile';
if ( $is_recaptcha && $captcha_settings['recaptcha_type'] === 'invisible' ) {
$data['size'] = 'invisible';
* Filter Turnstile action value.
* @param string $action Action value. Can only contain up to 32 alphanumeric characters including _ and -.
* @param array $form_data Form data and settings.
$data['action'] = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
'wpforms_frontend_recaptcha_turnstile_action',
* Print recaptcha fields.
* @param array $captcha_settings Captcha settings.
* @param array $form_data Form data and settings.
private function print_recaptcha_fields( array $captcha_settings, array $form_data ) {
$data = $this->get_recaptcha_data( $captcha_settings, $form_data );
$is_recaptcha = $captcha_settings['provider'] === 'recaptcha';
$is_recaptcha_v3 = $is_recaptcha && $captcha_settings['recaptcha_type'] === 'v3';
if ( $is_recaptcha_v3 ) {
// The value adds via JS code.
echo '<input type="hidden" name="wpforms[recaptcha]" value="">';
$captcha_class = $this->get_captcha_class( $captcha_settings['provider'] );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div ' . wpforms_html_attributes( '', [ $captcha_class ], $data ) . '></div>';
if ( $is_recaptcha && $captcha_settings['recaptcha_type'] === 'invisible' ) {
'<input type="text" name="g-recaptcha-hidden" class="wpforms-recaptcha-hidden" style="position:absolute!important;clip:rect(0,0,0,0)!important;height:1px!important;width:1px!important;border:0!important;overflow:hidden!important;padding:0!important;margin:0!important;" data-rule-%1$s="1">',
esc_attr( $captcha_settings['provider'] )
* Get captcha settings for form output.
* Return null if captcha is disabled.
* @param array $form_data Form data and settings.
* @noinspection NullPointerExceptionInspection
private function get_form_captcha_settings( $form_data ) {
$captcha_settings = wpforms_get_captcha_settings();
empty( $captcha_settings['provider'] ) ||
$captcha_settings['provider'] === 'none' ||
empty( $captcha_settings['site_key'] ) ||
empty( $captcha_settings['secret_key'] )
// Check that the CAPTCHA is configured for the specific form.
! isset( $form_data['settings']['recaptcha'] ) ||
$form_data['settings']['recaptcha'] !== '1'
$is_recaptcha_v3 = $captcha_settings['provider'] === 'recaptcha' && $captcha_settings['recaptcha_type'] === 'v3';
if ( wpforms()->obj( 'amp' )->output_captcha( $is_recaptcha_v3, $captcha_settings, $form_data ) ) {
return $captcha_settings;
* Google reCAPTCHA no-conflict mode.
* When enabled in the WPForms settings, forcefully remove all other
* reCAPTCHA enqueues to prevent conflicts. Filter can be used to target
* @since 1.6.4 Added hCaptcha support.
public function recaptcha_noconflict() {
$captcha_settings = wpforms_get_captcha_settings();
empty( $captcha_settings['provider'] ) ||
$captcha_settings['provider'] === 'none' ||
empty( wpforms_setting( 'recaptcha-noconflict' ) ) ||
* Filters recaptcha no conflict flag.
* @param bool $recaptcha_no_conflict No conflict flag.
! apply_filters( 'wpforms_frontend_recaptcha_noconflict', true ) // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
$urls = [ 'google.com/recaptcha', 'gstatic.com/recaptcha', 'hcaptcha.com/1' ];
foreach ( $scripts->queue as $handle ) {
// Skip the WPForms javascript-assets.
! isset( $scripts->registered[ $handle ] ) ||
false !== strpos( $scripts->registered[ $handle ]->handle, 'wpforms' )
foreach ( $urls as $url ) {
if ( false !== strpos( $scripts->registered[ $handle ]->src, $url ) ) {
wp_dequeue_script( $handle );
wp_deregister_script( $handle );
* Load the assets needed for the CAPTCHA.
* @since 1.6.4 Added hCaptcha support.
* @param array $forms Forms being displayed.
public function assets_recaptcha( $forms ) {
$captcha_settings = $this->get_assets_captcha_settings( $forms );
if ( ! $captcha_settings ) {
$is_recaptcha_v3 = $captcha_settings['provider'] === 'recaptcha' && $captcha_settings['recaptcha_type'] === 'v3';
$recaptcha_url = $is_recaptcha_v3 ?
'https://www.google.com/recaptcha/api.js?render=' . $captcha_settings['site_key'] :
* For backward compatibility reason we have to filter only the v2 reCAPTCHA.
* @param string $url The reCaptcha v2 URL.
apply_filters( 'wpforms_frontend_recaptcha_url', 'https://www.google.com/recaptcha/api.js?onload=wpformsRecaptchaLoad&render=explicit' ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
'hcaptcha' => 'https://hcaptcha.com/1/api.js?onload=wpformsRecaptchaLoad&render=explicit&recaptchacompat=off',
'recaptcha' => $recaptcha_url,
'turnstile' => 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=wpformsRecaptchaLoad&render=explicit',
* Filter the CAPTCHA API URL.
* @param string $captcha_api The CAPTCHA API URL.
$captcha_api = apply_filters( 'wpforms_frontend_captcha_api', $captcha_api_array[ $captcha_settings['provider'] ] );
$in_footer = ! wpforms_is_frontend_js_header_force_load();
$is_recaptcha_v3 ? [] : [ 'jquery' ],
null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
* Filter the string containing the CAPTCHA JavaScript to be added.
* @param string $captcha_inline The CAPTCHA JavaScript.
$captcha_inline = apply_filters(
'wpforms_frontend_captcha_inline_script',
$this->get_captcha_inline_script( $captcha_settings )
wp_add_inline_script( 'wpforms-recaptcha', $captcha_inline );
* Get captcha settings for assets output.
* Return null if captcha is disabled.
* @param array $forms Forms being displayed.
* @noinspection NullPointerExceptionInspection
private function get_assets_captcha_settings( $forms ) {
* Filters disable captcha switch.
* @param bool $is_captcha_disabled Whether captcha is disabled.
if ( apply_filters( 'wpforms_frontend_recaptcha_disable', false ) ) { // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
// Load CAPTCHA support if form supports it.
$captcha_settings = wpforms_get_captcha_settings();
empty( $captcha_settings['provider'] ) ||
$captcha_settings['provider'] === 'none' ||
empty( $captcha_settings['site_key'] ) ||
empty( $captcha_settings['secret_key'] )
// Whether at least 1 form on a page has CAPTCHA enabled.
foreach ( $forms as $form ) {
if ( ! empty( $form['settings']['recaptcha'] ) ) {
if ( ! $captcha && ! wpforms()->obj( 'frontend' )->assets_global() ) {
return $captcha_settings;
* Retrieve the string containing the CAPTCHA inline javascript.
* @param array $captcha_settings The CAPTCHA settings.
* @noinspection JSUnusedLocalSymbols
* @noinspection UnnecessaryLocalVariableJS
* @noinspection JSUnresolvedVariable
* @noinspection JSDeprecatedSymbols
* @noinspection JSUnresolvedFunction
protected function get_captcha_inline_script( $captcha_settings ) {
// IE11 polyfills for native `matches()` and `closest()` methods.
$polyfills = /** @lang JavaScript */
'if (!Element.prototype.matches) {
Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
if (!Element.prototype.closest) {
Element.prototype.closest = function (s) {
if (Element.prototype.matches.call(el, s)) { return el; }
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === 1);
// Native equivalent for jQuery's `trigger()` method.
$dispatch = /** @lang JavaScript */
'var wpformsDispatchEvent = function (el, ev, custom) {
var e = document.createEvent(custom ? "CustomEvent" : "HTMLEvents");
custom ? e.initCustomEvent(ev, true, true, false) : e.initEvent(ev, true, true);
// Update container class after changing Turnstile type.
$turnstile_update_class = /** @lang JavaScript */
'var turnstileUpdateContainer = function (el) {
let form = el.closest( "form" ),
iframeWrapperHeight = el.offsetHeight;
parseInt(iframeWrapperHeight) === 0 ?
form.querySelector(".wpforms-is-turnstile").classList.add( "wpforms-is-turnstile-invisible" ) :
form.querySelector(".wpforms-is-turnstile").classList.remove( "wpforms-is-turnstile-invisible" );
// Captcha callback, used by hCaptcha and checkbox reCaptcha v2.
$callback = /** @lang JavaScript */
'var wpformsRecaptchaCallback = function (el) {
var hdn = el.parentNode.querySelector(".wpforms-recaptcha-hidden");
var err = el.parentNode.querySelector("#g-recaptcha-hidden-error");
wpformsDispatchEvent(hdn, "change", false);
hdn.classList.remove("wpforms-error");
err && hdn.parentNode.removeChild(err);