diff --git a/includes/class-recaptcha.php b/includes/class-recaptcha.php index 51edbd6ac5..a66f1832f5 100644 --- a/includes/class-recaptcha.php +++ b/includes/class-recaptcha.php @@ -501,21 +501,9 @@ function refreshToken() { */ public static function verify_recaptcha_on_checkout() { $url = \home_url( \add_query_arg( null, null ) ); - $should_verify_captcha = apply_filters( 'newspack_recaptcha_verify_captcha', self::can_use_captcha(), $url ); + $should_verify_captcha = apply_filters( 'newspack_recaptcha_verify_captcha', self::can_use_captcha(), $url, 'checkout' ); $version = self::get_setting( 'version' ); - // Only use v2 if we are in modal checkout context. - // TODO: Remove this check once we have a way to enable v2 for non-modal checkouts. - if ( - ( 'v2' === $version || 'v2_invisible' === $version ) && - ( - ! method_exists( 'Newspack_Blocks\Modal_Checkout', 'is_modal_checkout' ) || - ! \Newspack_Blocks\Modal_Checkout::is_modal_checkout() - ) - ) { - $should_verify_captcha = false; - } - if ( ! $should_verify_captcha ) { return; } @@ -534,7 +522,7 @@ public static function verify_recaptcha_on_checkout() { */ public static function verify_recaptcha_on_add_payment_method( $is_valid ) { $url = \home_url( \add_query_arg( null, null ) ); - $should_verify_captcha = apply_filters( 'newspack_recaptcha_verify_captcha', self::can_use_captcha(), $url ); + $should_verify_captcha = apply_filters( 'newspack_recaptcha_verify_captcha', self::can_use_captcha(), $url, 'add_payment_method' ); if ( ! $should_verify_captcha ) { return $is_valid; } diff --git a/includes/reader-activation/class-reader-activation.php b/includes/reader-activation/class-reader-activation.php index 0af8036f5f..3c0298b808 100644 --- a/includes/reader-activation/class-reader-activation.php +++ b/includes/reader-activation/class-reader-activation.php @@ -1830,7 +1830,8 @@ public static function process_auth_form() { } // reCAPTCHA test on account registration only. - if ( 'register' === $action && Recaptcha::can_use_captcha( 'v3' ) ) { + $should_verify_captcha = apply_filters( 'newspack_recaptcha_verify_captcha', Recaptcha::can_use_captcha(), $current_page_url, 'auth_modal' ); + if ( 'register' === $action && $should_verify_captcha ) { $captcha_result = Recaptcha::verify_captcha(); if ( \is_wp_error( $captcha_result ) ) { return self::send_auth_form_response( $captcha_result ); diff --git a/src/blocks/reader-registration/index.php b/src/blocks/reader-registration/index.php index 9b4b906927..ff4edc7030 100644 --- a/src/blocks/reader-registration/index.php +++ b/src/blocks/reader-registration/index.php @@ -398,7 +398,11 @@ function process_form() { } // reCAPTCHA test. - if ( Recaptcha::can_use_captcha( 'v3' ) ) { + $current_page_url = \wp_parse_url( \wp_get_raw_referer() ); + if ( ! empty( $current_page_url['path'] ) ) { + $current_page_url = \esc_url( \home_url( $current_page_url['path'] ) ); + } + if ( apply_filters( 'newspack_recaptcha_verify_captcha', Recaptcha::can_use_captcha(), $current_page_url, 'registration_block' ) ) { $captcha_result = Recaptcha::verify_captcha(); if ( \is_wp_error( $captcha_result ) ) { return send_form_response( $captcha_result ); diff --git a/src/other-scripts/recaptcha/index.js b/src/other-scripts/recaptcha/index.js index 83c1f678e4..9fcf4882ce 100644 --- a/src/other-scripts/recaptcha/index.js +++ b/src/other-scripts/recaptcha/index.js @@ -30,6 +30,7 @@ const isInvisible = 'v2_invisible' === newspack_recaptcha_data.version; * @param {Function|null} onError Callback to handle errors. Optional. */ function renderV2Widget( form, onSuccess = null, onError = null ) { + form.removeAttribute( 'data-recaptcha-validated' ); // Common render options for reCAPTCHA v2 widget. See https://developers.google.com/recaptcha/docs/invisible#render_param for supported params. const options = { sitekey: siteKey, @@ -37,88 +38,85 @@ function renderV2Widget( form, onSuccess = null, onError = null ) { isolated: true, }; - const submitButtons = [ - ...form.querySelectorAll( 'input[type="submit"], button[type="submit"]' ) - ]; - submitButtons.forEach( button => { - // Don't render widget if the button has a data-skip-recaptcha attribute. - if ( button.hasAttribute( 'data-skip-recaptcha' ) ) { - return; + // Callback when reCAPTCHA passes validation or skip flag is present. + const successCallback = token => { + onSuccess?.(); + // Ensure the token gets submitted with the form submission. + let hiddenField = form.querySelector( '[name="g-recaptcha-response"]' ); + if ( ! hiddenField ) { + hiddenField = document.createElement( 'input' ); + hiddenField.type = 'hidden'; + hiddenField.name = 'g-recaptcha-response'; + form.appendChild( hiddenField ); } - // Callback when reCAPTCHA passes validation or skip flag is present. - const successCallback = token => { - onSuccess?.(); - // Ensure the token gets submitted with the form submission. - let hiddenField = form.querySelector( '[name="g-recaptcha-response"]' ); - if ( ! hiddenField ) { - hiddenField = document.createElement( 'input' ); - hiddenField.type = 'hidden'; - hiddenField.name = 'g-recaptcha-response'; - form.appendChild( hiddenField ); - } - hiddenField.value = token; - form.requestSubmit( button ); - refreshV2Widget( button ); - }; - // Callback when reCAPTCHA rendering fails or expires. - const errorCallback = () => { - const retryCount = parseInt( button.getAttribute( 'data-recaptcha-retry-count' ) ) || 0; - if ( retryCount < 3 ) { - refreshV2Widget( button ); - grecaptcha.execute( button.getAttribute( 'data-recaptcha-widget-id' ) ); - button.setAttribute( 'data-recaptcha-retry-count', retryCount + 1 ); + hiddenField.value = token; + const buttons = [ + ...form.querySelectorAll( 'input[type="submit"], button[type="submit"]' ) + ]; + form.setAttribute( 'data-recaptcha-validated', '1' ); + form.requestSubmit( buttons[ buttons.length - 1 ] ); + refreshV2Widget( form ); + }; + // Callback when reCAPTCHA rendering fails or expires. + const errorCallback = () => { + form.removeAttribute( 'data-recaptcha-validated' ); + const retryCount = parseInt( form.getAttribute( 'data-recaptcha-retry-count' ) ) || 0; + if ( retryCount < 3 ) { + refreshV2Widget( form ); + grecaptcha.execute( form.getAttribute( 'data-recaptcha-widget-id' ) ); + form.setAttribute( 'data-recaptcha-retry-count', retryCount + 1 ); + } else { + const message = wp.i18n.__( 'There was an error connecting with reCAPTCHA. Please reload the page and try again.', 'newspack-plugin' ); + if ( onError ) { + onError( message ); } else { - const message = wp.i18n.__( 'There was an error connecting with reCAPTCHA. Please reload the page and try again.', 'newspack-plugin' ); - if ( onError ) { - onError( message ); - } else { - addErrorMessage( form, message ); - } + addErrorMessage( form, message ); } } - // Attach widget to form events. - const attachListeners = () => { - getIntersectionObserver( () => renderV2Widget( form, onSuccess, onError ) ).observe( form, { attributes: true } ); - button.addEventListener( 'click', e => { + }; + + // Attach widget to form events. + const attachListeners = () => { + getIntersectionObserver( () => renderV2Widget( form, onSuccess, onError ) ).observe( form, { attributes: true } ); + form.addEventListener( 'submit', e => { + if ( ! form.hasAttribute( 'data-recaptcha-validated' ) && ! form.hasAttribute( 'data-skip-recaptcha' ) ) { e.preventDefault(); e.stopImmediatePropagation(); // Empty error messages if present. removeErrorMessages( form ); - // Skip reCAPTCHA verification if the button has a data-skip-recaptcha attribute. - if ( button.hasAttribute( 'data-skip-recaptcha' ) ) { - successCallback(); - } else { - grecaptcha.execute( widgetId ).then( () => { - // If we are in an iframe scroll to top. - if ( window?.location !== window?.parent?.location ) { - document.body.scrollIntoView( { behavior: 'smooth' } ); - } - } ); - } - } ); - } - // Refresh reCAPTCHA widgets on Woo checkout update and error. - if ( jQuery ) { - jQuery( document ).on( 'updated_checkout', () => attachListeners ); - jQuery( document.body ).on( 'checkout_error', () => attachListeners ); - } - // Refresh widget if it already exists. - if ( button.hasAttribute( 'data-recaptcha-widget-id' ) ) { - refreshV2Widget( button ); - return; - } - const container = document.createElement( 'div' ); - container.classList.add( 'grecaptcha-container' ); - document.body.append( container ); - const widgetId = grecaptcha.render( container, { - ...options, - callback: successCallback, - 'error-callback': errorCallback, - 'expired-callback': errorCallback, - } ); - button.setAttribute( 'data-recaptcha-widget-id', widgetId ); - attachListeners(); + + grecaptcha.execute( widgetId ).then( () => { + // If we are in an iframe scroll to top. + if ( window?.location !== window?.parent?.location ) { + document.body.scrollIntoView( { behavior: 'smooth' } ); + } + } ); + } else { + form.removeAttribute( 'data-recaptcha-validated' ); + } + }, true ); + } + // Refresh reCAPTCHA widgets on Woo checkout update and error. + if ( jQuery ) { + jQuery( document ).on( 'updated_checkout', () => renderV2Widget( form, onSuccess, onError ) ); + jQuery( document.body ).on( 'checkout_error', () => renderV2Widget( form, onSuccess, onError ) ); + } + // Refresh widget if it already exists. + if ( form.hasAttribute( 'data-recaptcha-widget-id' ) ) { + refreshV2Widget( form ); + return; + } + const container = document.createElement( 'div' ); + container.classList.add( 'grecaptcha-container' ); + document.body.append( container ); + const widgetId = grecaptcha.render( container, { + ...options, + callback: successCallback, + 'error-callback': errorCallback, + 'expired-callback': errorCallback, } ); + form.setAttribute( 'data-recaptcha-widget-id', widgetId ); + attachListeners(); } /** @@ -137,7 +135,7 @@ function render( forms = [], onSuccess = null, onError = null ) { const formsToHandle = forms.length ? forms : [ ...document.querySelectorAll( - 'form[data-newspack-recaptcha],form#add_payment_method', + 'form[data-newspack-recaptcha],form#add_payment_method,form.checkout', ) ]; formsToHandle.forEach( form => { diff --git a/src/other-scripts/recaptcha/utils.js b/src/other-scripts/recaptcha/utils.js index 8c9d581625..c36b8fa37b 100644 --- a/src/other-scripts/recaptcha/utils.js +++ b/src/other-scripts/recaptcha/utils.js @@ -4,7 +4,7 @@ const MINIMUM_VISIBLE_TIME = 250; // The minimum percentage of an element that must be in the viewport before being considered visible. -const MINIMUM_VISIBLE_PERCENTAGE = 0.5; +const MINIMUM_VISIBLE_PERCENTAGE = 0.1; /** * Specify a function to execute when the DOM is fully loaded. diff --git a/src/reader-activation-auth/auth-form.js b/src/reader-activation-auth/auth-form.js index c627363da7..b00d8d75d6 100644 --- a/src/reader-activation-auth/auth-form.js +++ b/src/reader-activation-auth/auth-form.js @@ -112,10 +112,10 @@ window.newspackRAS.push( function ( readerActivation ) { const newspack_grecaptcha = window.newspack_grecaptcha || {}; if ( 'v2_invisible' === newspack_grecaptcha?.version ) { if ( 'register' === action ) { - submitButtons.forEach( button => button.removeAttribute( 'data-skip-recaptcha' ) ); + form.removeAttribute( 'data-skip-recaptcha' ); newspack_grecaptcha.render( [ form ], ( error ) => form.setMessageContent( error, true ) ); } else { - submitButtons.forEach( button => button.setAttribute( 'data-skip-recaptcha', '' ) ); + form.setAttribute( 'data-skip-recaptcha', '1' ); } } if ( 'otp' === action ) {