diff --git a/adminpages/securitysettings.php b/adminpages/securitysettings.php index bba05bb5d5..7444afce59 100644 --- a/adminpages/securitysettings.php +++ b/adminpages/securitysettings.php @@ -28,6 +28,11 @@ $nuclear_HTTPS = 0; } pmpro_setOption( "nuclear_HTTPS", $nuclear_HTTPS ); + + // If there is a captcha option, save it. + if ( isset( $_POST['captcha'] ) ) { + update_option( 'pmpro_captcha', sanitize_text_field( $_POST['captcha'] ) ); + } /** * Fires after security settings are saved. @@ -46,6 +51,7 @@ $spamprotection = get_option( 'pmpro_spamprotection' ); $use_ssl = get_option( 'pmpro_use_ssl' ); $nuclear_HTTPS = get_option( 'pmpro_nuclear_HTTPS' ); + $captcha = pmpro_captcha(); // Create an array of plugin files to check. $plugin_files['pmpro-akismet'] = 'pmpro-akismet/pmpro-akismet.php'; @@ -159,13 +165,26 @@ function pmpro_is_plugin_installed_or_active( $plugin_file ) {

+ + + + + + +

+ + -
+
+
+
+ + Error: Captcha verification failed. Please try again.', 'paid-memberships-pro' ), array( 'strong' => array() ) ) ); + } + } + + return $user; +} +add_filter( 'pmpro_authenticate_after_default_login_checks', 'pmpro_cloudflare_default_login_validation', 10, 2 ); + +/** + * Reset password validation check for CloudFlare Turnstile. + * + * @since TBD + * + * @param WP_User|WP_Error $user + * @param string $captcha + * @return void + */ +function pmpro_cloudflare_password( $user, $captcha ) { + //Check if Turnstile has been filled in + if ( $captcha == 'turnstile' ) { + // Validate the reCAPTCHA response here. If it's empty, assume it failed. + $validated = pmpro_cloudflare_turnstile_validation(); + if ( ! $validated ) { + $user = new WP_Error( 'captcha-failed', wp_kses( __( 'Error: Captcha verification failed. Please try again.', 'paid-memberships-pro' ), array( 'strong' => array() ) ) ); + } + } + + return $user; +} +add_filter( 'pmpro_password_reset_captcha_check', 'pmpro_cloudflare_password', 10, 2 ); + +/** + * Lost password validation check for CloudFlare Turnstile. + * + * @since TBD + * + * @param WP_Error $errors + * @param string $captcha + * @return WP_Error|void + */ +function pmpro_cloudflare_lostpassword_validation( $errors, $captcha ) { + //Check if Turnstile has been filled in + if ( $captcha == 'turnstile' ) { + $validated = pmpro_cloudflare_turnstile_validation(); + if ( ! $validated ) { + $errors = new WP_Error( 'captcha-failed', wp_kses( __( 'Error: Captcha verification failed. Please try again.', 'paid-memberships-pro' ), array( 'strong' => array() ) ) ); + } + + // Remove session data to reset turnstile validation. + pmpro_unset_session_var( 'pmpro_turnstile_validated' ); + + } + + return $errors; +} +add_filter( 'pmpro_lostpassword_submission_check', 'pmpro_cloudflare_lostpassword_validation', 10, 2 ); + /** * CloudFlare Turnstile Security Settings * @@ -92,31 +254,20 @@ function pmpro_cloudflare_turnstile_validation( $okay ) { */ function pmpro_cloudflare_turnstile_settings() { // Get the options - $cloudflare_turnstile = get_option( 'pmpro_cloudflare_turnstile', '0' ); + $cloudflare_turnstile = pmpro_captcha(); $cloudflare_site_key = get_option( 'pmpro_cloudflare_turnstile_site_key', '' ); $cloudflare_secret_key = get_option( 'pmpro_cloudflare_turnstile_secret_key', '' ); // If CloudFlare Turnstile is not enabled, hide some settings by default. - $tr_style = empty( $cloudflare_turnstile ) ? 'display: none;' : ''; + $tr_style = ( $cloudflare_turnstile !== 'turnstile' ) ? 'display: none;' : ''; // Output settings - ?> - - - - - - -

.

- - - + ?> + +

.

@@ -127,8 +278,8 @@ function pmpro_cloudflare_turnstile_settings() { -
+
@@ -121,37 +136,228 @@ function pmpro_recaptcha_get_html() { $already_shown = true; } + +/** + * Load the reCAPTCHA HTML and logic on the checkout and billing pages. + * + * @param object $level The membership level object. + * @return string $recaptcha_html The HTML for the ReCAPTCHA. + */ +function pmpro_checkout_form_recaptcha( $level ) { + // If ReCAPTCHA is not enabled, don't do anything. + if ( pmpro_captcha() !== 'recaptcha' ) { + return; + } + + // If ReCAPTCHA has already been validated, return. + if ( true === pmpro_recaptcha_is_validated() ) { + return; + } + + // Output the ReCAPTCHA HTML. + pmpro_recaptcha_get_html(); +} add_action( 'pmpro_checkout_before_submit_button', 'pmpro_recaptcha_get_html' ); add_action( 'pmpro_billing_before_submit_button', 'pmpro_recaptcha_get_html' ); +/** + * Check default wp-login.php login for reCAPTCHA validation. + * + * @param [type] $user + * @param [type] $captcha + * @return void + */ +function pmpro_default_login_captcha_validation( $user, $captcha ) { + //Check if reCAPTCHA has been filled in, assume they hit submit if we have a $password value. + if ( $captcha == 'recaptcha' && pmpro_login_has_failed_attempt() ) { + $recaptcha_response = pmpro_getParam( 'g-recaptcha-response' ); + + // Validate the reCAPTCHA response here. If it's empty, assume it failed. + $validated = pmpro_validate_recaptcha( $recaptcha_response ); + if ( ! $validated ) { + $user = new WP_Error( 'captcha-failed', wp_kses( __( 'Error: Captcha verification failed. Please try again.', 'paid-memberships-pro' ), array( 'strong' => array() ) ) ); + } + } + + return $user; +} +add_filter( 'pmpro_authenticate_after_default_login_checks', 'pmpro_default_login_captcha_validation', 10, 2 ); + +/** + * Check that the reCAPTCHA was completed on the password reset form. + * + * @since TBD + */ +function pmpro_password_reset_form_recaptcha( $user, $captcha ) { + + if ( $captcha == 'recaptcha' ) { + $recaptcha_response = pmpro_getParam( 'g-recaptcha-response' ); + + // Validate the reCAPTCHA response here. If it's empty, assume it failed. + $validated = pmpro_validate_recaptcha( $recaptcha_response ); + if ( ! $validated ) { + $user = new WP_Error( 'captcha-failed', wp_kses( __( 'Error: Captcha verification failed. Please try again.', 'paid-memberships-pro' ), array( 'strong' => array() ) ) ); + } + } + + return $user; +} +add_action( 'pmpro_password_reset_captcha_check', 'pmpro_password_reset_form_recaptcha', 10, 2 ); + +/** + * Adds reCAPTCHA to the PMPro login form + * + * @since TBD + */ +function pmpro_login_form_recaptcha( $login_form, $args ) { + + // Let's bail if we're not loading our version of the login form. + if ( ! isset( $args['pmpro_login_form_used'] ) ) { + return $login_form; + } + + // If this isn't reCAPTCHA, don't load it. + if ( pmpro_captcha() !== 'recaptcha' ) { + return $login_form; + } + + // Only show the reCAPTCHA if we have a tracked failed login attempt. + if ( ! pmpro_login_has_failed_attempt() ) { + return $login_form; + } + + ob_start(); + pmpro_recaptcha_get_html( 'wp-submit' ); + $pmpro_recaptcha = ob_get_contents(); + ob_end_clean(); + return $login_form . $pmpro_recaptcha; + +} +add_filter( 'login_form_middle', 'pmpro_login_form_recaptcha', 10, 2 ); + +/** + * Adds reCAPTCHA to the WP login form + * + * @since TBD + */ +function pmpro_wp_login_form_recaptcha() { + + // Enable reCAPTCHA only after a failed login attempt has been tracked. + if ( pmpro_captcha() === 'recaptcha' && pmpro_login_has_failed_attempt() ) { + pmpro_recaptcha_get_html( 'wp-submit' ); + } +} +add_action( 'login_form', 'pmpro_wp_login_form_recaptcha', 10 ); +add_action( 'lostpassword_form', 'pmpro_wp_login_form_recaptcha', 10 ); + +// This is for PMPro Lost Password form, since the button is different. +function pmpro_lost_password_form_recaptcha() { + + // Enable reCAPTCHA + if ( pmpro_captcha() === 'recaptcha' ) { + pmpro_recaptcha_get_html( 'pmpro_btn-submit' ); + } +} +add_action( 'pmpro_lost_password_before_submit_button', 'pmpro_lost_password_form_recaptcha', 10 ); + +/** + * Apply custom CSS to the ReCAPTCHA element on the WP login page. For V2 reCAPTCHA only. + * This is used to fix the alignment of the ReCAPTCHA element on the default login and password reset page. + * + * @since TBD + * + */ +function pmpro_wp_login_style_v2_recaptcha() { + // If ReCAPTCHA is not enabled, don't do anything. + if ( pmpro_captcha() !== 'recaptcha' ) { + return; + } + + // No need to load this for V3 reCAPTCHA. + $recaptcha_version = get_option( 'pmpro_recaptcha_version' ); + if ( $recaptcha_version === '3_invisible' ) { + return; + } + + // Style the RECAPTCHA. + ?> + + verifyResponse( pmpro_get_ip(), sanitize_text_field( $_REQUEST['g-recaptcha-response'] ) ); + $resp = $reCaptcha->verifyResponse( pmpro_get_ip(), sanitize_text_field( $response ) ); + if ( $resp->success ) { - pmpro_set_session_var( 'pmpro_recaptcha_validated', true ); - echo "1"; + pmpro_set_session_var( 'pmpro_recaptcha_validated', true ); + return true; } else { - echo "0"; + pmpro_unset_session_var( 'pmpro_recaptcha_validated' ); + return false; } - - exit; -} -add_action( 'wp_ajax_nopriv_pmpro_validate_recaptcha', 'pmpro_wp_ajax_validate_recaptcha' ); -add_action( 'wp_ajax_pmpro_validate_recaptcha', 'pmpro_wp_ajax_validate_recaptcha' ); +} + +/** + * Clear the reCAPTCHA validation session variable after checkout or billing update or login. + */ function pmpro_after_checkout_reset_recaptcha() { - pmpro_unset_session_var( 'pmpro_recaptcha_validated' ); + pmpro_unset_session_var( 'pmpro_recaptcha_validated' ); + + // This is to clear the transient that tracks failed login attemtpts. + pmpro_login_clear_failed_attempt(); } -add_action( 'pmpro_after_checkout', 'pmpro_after_checkout_reset_recaptcha' ); -add_action( 'pmpro_after_update_billing', 'pmpro_after_checkout_reset_recaptcha' ); +add_action( 'pmpro_after_checkout', 'pmpro_after_checkout_reset_recaptcha' ); // Clear on checkout +add_action( 'pmpro_after_update_billing', 'pmpro_after_checkout_reset_recaptcha' ); // Clear on update billing info +add_action( 'wp_login', 'pmpro_after_checkout_reset_recaptcha' ); // Clear on successful login. /** * Check if ReCAPTCHA is validated. @@ -159,7 +365,8 @@ function pmpro_after_checkout_reset_recaptcha() { * @return true|string True if validated, error message if not. */ function pmpro_recaptcha_is_validated() { - // Check if the user has already been validated. + + // Check if the user has already been validated. Let's return true and clear the session variable. $recaptcha_validated = pmpro_get_session_var( 'pmpro_recaptcha_validated' ); if ( ! empty( $recaptcha_validated ) ) { return true; @@ -188,7 +395,7 @@ function pmpro_recaptcha_is_validated() { // earlier. We should remove/refactor this code. require_once(PMPRO_DIR . '/includes/lib/recaptchalib.php' ); $reCaptcha = new pmpro_ReCaptcha( $recaptcha_privatekey ); - $resp = $reCaptcha->verifyResponse( pmpro_get_ip(), $_POST["g-recaptcha-response"] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $resp = $reCaptcha->verifyResponse( pmpro_get_ip(), $_POST["g-recaptcha-response"] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $recaptcha_valid = $resp->success; $recaptcha_errors = $resp->errorCodes; @@ -217,8 +424,10 @@ function pmpro_recaptcha_validation_check( $continue = true ) { return false; } + $captcha = pmpro_captcha(); + // If ReCAPTCHA is not enabled, return. - if ( empty( get_option( 'pmpro_recaptcha' ) ) ) { + if ( $captcha !== 'recaptcha' ) { return true; } @@ -242,29 +451,17 @@ function pmpro_recaptcha_validation_check( $continue = true ) { */ function pmpro_recaptcha_settings() { // Get the current options. - $recaptcha = get_option( 'pmpro_recaptcha' ); + $recaptcha = pmpro_captcha(); $recaptcha_version = get_option( 'pmpro_recaptcha_version' ); $recaptcha_publickey = get_option( 'pmpro_recaptcha_publickey' ); $recaptcha_privatekey = get_option( 'pmpro_recaptcha_privatekey' ); // If reCAPTCHA is not enabled, hide some settings by default. - $tr_style = empty( $recaptcha ) ? 'display: none;' : ''; + $tr_style = ( $recaptcha !== 'recaptcha' ) ? 'display: none;' : ''; // Output settings fields. ?> - - - - - - -

.

- - + @@ -272,7 +469,7 @@ function pmpro_recaptcha_settings() { -

+

.

@@ -289,8 +486,8 @@ function pmpro_recaptcha_settings() {