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() {