cookie_name = 'itsec-recaptcha-opt-in-' . COOKIEHASH; // Run on init so that we can use is_user_logged_in() // Warning: BuddyPress has issues with using is_user_logged_in() on plugins_loaded add_action( 'init', array( $this, 'setup' ), - 100 ); add_filter( 'itsec_lockout_modules', array( $this, 'register_lockout_module' ) ); // Check for the opt-in and set the cookie. if ( isset( $_REQUEST['recaptcha-opt-in'] ) && 'true' === $_REQUEST['recaptcha-opt-in'] ) { ITSEC_Lib::set_cookie( $this->cookie_name, 'true', array( 'length' => MONTH_IN_SECONDS, ) ); } } public function setup() { $this->settings = ITSEC_Modules::get_settings( 'recaptcha' ); if ( empty( $this->settings['site_key'] ) || empty( $this->settings['secret_key'] ) ) { // Only run when the settings are fully filled out. return; } ITSEC_Recaptcha_API::init( $this ); if ( 'v3' === $this->settings['type'] && 'everywhere' === $this->settings['v3_include_location'] ) { add_action( 'wp_footer', array( $this, 'enqueue_everywhere' ), 19 ); } // Logged in users are people, we don't need to re-verify if ( is_user_logged_in() ) { return; } add_action( 'login_head', array( $this, 'print_login_styles' ) ); if ( $this->settings['comments'] ) { add_filter( 'comment_form_submit_button', array( $this, 'comment_form_submit_button' ) ); add_filter( 'preprocess_comment', array( $this, 'filter_preprocess_comment' ) ); } if ( $this->settings['login'] ) { add_action( 'login_form', array( $this, 'login_form' ) ); add_filter( 'login_form_middle', array( $this, 'wp_login_form' ), 100 ); add_filter( 'authenticate', array( $this, 'filter_authenticate' ), 30, 2 ); } if ( $this->settings['register'] ) { add_action( 'register_form', array( $this, 'register_form' ) ); add_filter( 'registration_errors', array( $this, 'registration_errors' ) ); } if ( $this->settings['reset_pass'] ) { add_action( 'lostpassword_form', [ $this, 'reset_password_form' ] ); add_action( 'lostpassword_post', [ $this, 'reset_password_errors' ] ); } } public function enqueue_everywhere() { foreach ( wp_scripts()->registered as $handle => $dependency ) { if ( ! $dependency instanceof _WP_Dependency || ! $dependency->src ) { continue; } // Quick check if ( false === strpos( $dependency->src, 'google.com/recaptcha/api.js' ) ) { continue; } if ( ! $parsed = parse_url( $dependency->src ) ) { continue; } if ( ! isset( $parsed['host'] ) || ( 'www.google.com' !== $parsed['host'] && 'google.com' !== $parsed['host'] ) ) { continue; } if ( ! isset( $parsed['path'] ) || ( '/recaptcha/api.js' !== $parsed['path'] && 'recaptcha/api.js' !== $parsed['path'] ) ) { continue; } if ( wp_script_is( $handle ) || wp_script_is( $handle, 'done' ) ) { return; } } wp_enqueue_script( 'itsec-recaptcha-api', $this->build_google_api_script( false ), array(), '', true ); } public function print_login_styles() { echo ''; } /** * Preferred method to add recaptcha form to comment form. Used in WP 4.2+ * * @since 1.17 * * @param string $submit_button The submit button in the comment form * * @return string The submit button with our recaptcha field prepended */ public function comment_form_submit_button( $submit_button ) { $submit_button = $this->get_recaptcha( array( 'action' => self::A_COMMENT ) ) . $submit_button; return $submit_button; } /** * Enqueue assets for the opt-in dialog. * * @param array $args */ private function enqueue_opt_in( $args ) { wp_enqueue_style( 'itsec-recaptcha-opt-in', plugin_dir_url( __FILE__ ) . 'css/itsec-recaptcha.css', array(), ITSEC_Core::get_plugin_build() ); if ( ! $this->settings['on_page_opt_in'] ) { return; } if ( wp_script_is( 'itsec-recaptcha-opt-in' ) ) { return; } $localize = array( 'googlejs' => $this->build_google_api_script(), ); switch ( $this->settings['type'] ) { case 'v3': $localize['onload'] = 'itsecRecaptchav3Load'; break; case 'v2': default: $localize['onload'] = 'itsecRecaptchav2Load'; break; } wp_enqueue_script( 'itsec-recaptcha-opt-in', plugin_dir_url( __FILE__ ) . 'js/optin.js', array( 'jquery', 'itsec-recaptcha-script' ), ITSEC_Core::get_plugin_build() ); wp_localize_script( 'itsec-recaptcha-opt-in', 'ITSECRecaptchaOptIn', $localize ); wp_enqueue_script( 'itsec-recaptcha-script', $this->build_itsec_script(), array( 'jquery' ), ITSEC_Core::get_plugin_build() ); } /** * Add the recaptcha field to the login form * * @since 1.13 * * @return void */ public function login_form() { $this->show_recaptcha( array( 'action' => self::A_LOGIN ) ); } /** * Add the Recaptcha to the `wp_login_form()` template function. * * @param string $html * * @return string */ public function wp_login_form( $html ) { $html .= $this->get_recaptcha( array( 'action' => self::A_LOGIN, 'margin' => array( 'top' => 10, 'bottom' => 10 ) ) ); return $html; } /** * Process recaptcha for comments * * @since 1.13 * * @param array $comment_data Comment data. * * @return array Comment data. */ public function filter_preprocess_comment( $comment_data ) { $result = $this->validate_captcha( array( 'action' => self::A_COMMENT ) ); if ( is_wp_error( $result ) ) { wp_die( $result->get_error_message() ); } return $comment_data; } /** * Add the recaptcha field to the registration form * * @since 1.13 * * @return void */ public function register_form() { $this->show_recaptcha( array( 'action' => self::A_REGISTER ) ); } /** * Set the registration error if captcha wasn't validated * * @since 1.13 * * @param WP_Error $errors A WP_Error object containing any errors encountered * during registration. * * @return WP_Error A WP_Error object containing any errors encountered * during registration. */ public function registration_errors( $errors ) { $result = $this->validate_captcha( array( 'action' => self::A_REGISTER ) ); if ( is_wp_error( $result ) ) { $errors->add( $result->get_error_code(), $result->get_error_message() ); } return $errors; } /** * Adds the recaptcha to the reset password form. */ public function reset_password_form() { $this->show_recaptcha( array( 'action' => self::A_RESET_PASS ) ); } /** * Validates that the user submitted the recaptcha when requesting a password reset link. * * @param WP_Error $errors */ public function reset_password_errors( $errors ) { $result = $this->validate_captcha( array( 'action' => self::A_RESET_PASS ) ); if ( is_wp_error( $result ) ) { $errors->add( $result->get_error_code(), $result->get_error_message() ); } } // Leave this in as iThemes Exchange relies upon it. public function show_field( $echo = true, $deprecated1 = true, $margin_top = 0, $margin_right = 0, $margin_bottom = 0, $margin_left = 0, $deprecated2 = null ) { $args = compact( 'margin_top', 'margin_right', 'margin_bottom', 'margin_left' ); if ( $echo ) { $this->show_recaptcha( $args ); } else { return $this->get_recaptcha( $args ); } } public function show_recaptcha( $args = array() ) { $args['margin'] = wp_parse_args( isset( $args['margin'] ) ? $args['margin'] : array(), array( 'top' => 10, 'bottom' => 10, ) ); echo $this->get_recaptcha( $args ); } private function has_visitor_opted_in() { if ( isset( $_REQUEST['recaptcha-opt-in'] ) && 'true' === $_REQUEST['recaptcha-opt-in'] ) { return true; } if ( isset( $_COOKIE[ $this->cookie_name ] ) && 'true' === $_COOKIE[ $this->cookie_name ] ) { return true; } return false; } private function show_opt_in( $args ) { if ( ! ITSEC_Modules::get_setting( 'recaptcha', 'gdpr' ) || ITSEC_Modules::get_setting( 'recaptcha', 'type' ) === 'v3' ) { return ''; } if ( $this->has_visitor_opted_in() ) { return ''; } $this->enqueue_opt_in( $args ); $url = ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; if ( false === strpos( $url, '?' ) ) { $url .= '?recaptcha-opt-in=true'; } else { $url .= '&recaptcha-opt-in=true'; } /* Translators: 1: Google's privacy policy URL, 2: Google's terms of use URL */ $p1 = sprintf( wp_kses( __( 'For security, use of Google\'s reCAPTCHA service is required which is subject to the Google Privacy Policy and Terms of Use.', 'it-l10n-ithemes-security-pro' ), array( 'a' => array( 'href' => array() ) ) ), 'https://policies.google.com/privacy', 'https://policies.google.com/terms' ); $p2 = sprintf( esc_html__( '%1$sI agree to these terms%2$s.', 'it-l10n-ithemes-security-pro' ), '', '' ); $html = '
' . $p1 . '
'; $html .= '' . $p2 . '
'; $html .= ''; $html .= '%1$s
.', 'it-l10n-ithemes-security-pro' ), $code ) );
} else {
ITSEC_Modules::set_setting( 'recaptcha', 'last_error', sprintf( esc_html__( 'The reCAPTCHA server reported the following errors: %1$s
.', 'it-l10n-ithemes-security-pro' ), implode( ', ', $status['error-codes'] ) ) );
}
}
$GLOBALS['__itsec_recaptcha_cached_result'] = true;
return $GLOBALS['__itsec_recaptcha_cached_result'];
}
$GLOBALS['__itsec_recaptcha_cached_result'] = $validation_error;
$this->log_failed_validation( $GLOBALS['__itsec_recaptcha_cached_result'], $args );
return $GLOBALS['__itsec_recaptcha_cached_result'];
}
/**
* Is the reCAPTCHA response from Google valid.
*
* @param array $response
*
* @return bool
*/
private function is_valid_response_format( $response ) {
if ( ! is_array( $response ) ) {
return false;
}
if ( ! isset( $response['success'] ) ) {
return false;
}
if ( 'v3' === $this->settings['type'] && ! isset( $response['score'], $response['action'] ) ) {
return false;
}
return true;
}
/**
* Validate the response.
*
* @param array $response The response from Google.
* @param array $args The args passed by the user.
*
* @return WP_Error|null
*/
private function validate_response( $response, $args ) {
ITSEC_Log::add_debug( 'recaptcha', 'validate-response', compact( 'response', 'args' ) );
$error = new WP_Error( 'itsec-recaptcha-incorrect', esc_html__( 'The captcha response you submitted does not appear to be valid. Please try again.', 'it-l10n-ithemes-security-pro' ) );
if ( ! $response['success'] ) {
$error->add_data( array( 'validate_error' => 'invalid-token', 'args' => $args ) );
return $error;
}
if ( ! $this->validate_host( $response ) ) {
$error->add_data( array( 'validate_error' => 'host-mismatch', 'args' => $args ) );
return $error;
}
if ( ! $this->validate_action( $response, $args ) ) {
$error->add_data( array( 'validate_error' => 'action-mismatch', 'args' => $args ) );
return $error;
}
if ( ! $this->validate_score( $response, $args ) ) {
$error->add_data( array( 'validate_error' => 'insufficient_score', 'args' => $args ) );
return $error;
}
return null;
}
/**
* Validate the hostname the Recaptcha was filled on.
*
* This allows the user to disable "Domain Name Validation" on large multisite installations because Google
* limits the number of sites a recaptcha key can be used on.
*
* @since 4.2.0
*
* @param array $status
*
* @return bool
*/
private function validate_host( $status ) {
if ( ! apply_filters( 'itsec_recaptcha_validate_host', false ) ) {
return true;
}
if ( ! isset( $status['hostname'] ) ) {
return true;
}
$site_parsed = parse_url( site_url() );
if ( ! is_array( $site_parsed ) || ! isset( $site_parsed['host'] ) ) {
return true;
}
return $site_parsed['host'] === $status['hostname'];
}
/**
* Validate that the action matches and the score is above the threshold..
*
* @param array $status Response from Google.
* @param array $args Validation args.
*
* @return bool
*/
private function validate_action( $status, $args ) {
if ( 'v3' !== $this->settings['type'] ) {
return true;
}
return empty( $args['action'] ) || $status['action'] === $args['action'];
}
/**
* Validate that the action matches and the score is above the threshold..
*
* @param array $status Response from Google.
* @param array $args Validation args.
*
* @return bool
*/
private function validate_score( $status, $args ) {
if ( 'v3' !== $this->settings['type'] ) {
return true;
}
$threshold = isset( $args['v3_threshold'] ) ? $args['v3_threshold'] : $this->settings['v3_threshold'];
return $status['score'] >= $threshold;
}
/**
* Log when Recaptcha fails to validate.
*
* @param WP_Error $error
* @param array $args
*/
private function log_failed_validation( $error, $args ) {
/** @var ITSEC_Lockout $itsec_lockout */
global $itsec_lockout;
/**
* Fires when a user fails the reCAPTCHA test.
*
* @param WP_Error $error
* @param array $args
*/
do_action( 'itsec_failed_recaptcha', $error, $args );
ITSEC_Log::add_notice( 'recaptcha', 'failed-validation', $error );
$data = $error->get_error_data();
$context = new Host_Context( 'recaptcha' );
if ( ! empty( $data['args']['user'] ) ) {
$context->set_login_user_id( $data['args']['user'] );
} elseif ( ! empty( $data['args']['username'] ) ) {
$context->set_login_username( $data['args']['username'] );
}
$itsec_lockout->do_lockout( $context );
if ( 'itsec-recaptcha-form-not-submitted' === $error->get_error_code() ) {
ITSEC_Dashboard_Util::record_event( 'recaptcha-empty' );
} else {
ITSEC_Dashboard_Util::record_event( 'recaptcha-invalid' );
}
}
/**
* Set the login error if captcha wasn't validated
*
* @since 1.13
*
* @param WP_User|WP_Error|null $user WP_User if the user is authenticated, WP_Error or null otherwise.
* @param string $username
*
* @return WP_User|WP_Error|null
*/
public function filter_authenticate( $user, $username ) {
if ( empty( $_POST ) || ITSEC_Core::is_api_request() ) {
return $user;
}
ITSEC_Lib::load( 'login' );
$args = array( 'action' => self::A_LOGIN );
if ( $user instanceof WP_User ) {
$args['user'] = $user->ID;
} elseif ( $found_user = ITSEC_Lib_Login::get_user( $username ) ) {
$args['user'] = $found_user->ID;
} else {
$args['username'] = $username;
}
$result = $this->validate_captcha( $args );
if ( is_wp_error( $result ) ) {
return $result;
}
return $user;
}
/**
* Register recaptcha for lockout
*
* @since 1.13
*
* @param array $lockout_modules array of lockout modules
*
* @return array array of lockout modules
*/
public function register_lockout_module( $lockout_modules ) {
$lockout_modules['recaptcha'] = array(
'type' => 'recaptcha',
'reason' => __( 'too many failed captcha submissions.', 'it-l10n-ithemes-security-pro' ),
'label' => __( 'reCAPTCHA', 'it-l10n-ithemes-security-pro' ),
'host' => isset( $this->settings['error_threshold'] ) ? absint( $this->settings['error_threshold'] ) : 7,
'period' => isset( $this->settings['check_period'] ) ? absint( $this->settings['check_period'] ) : 5,
);
return $lockout_modules;
}
}