model = $this->get_model(); // Use default msg to avoid empty message error. $default_values = $this->model->get_default_values(); $this->default_msg = $default_values['message']; $this->register_routes(); $this->is_woo_activated = wd_di()->get( \WP_Defender\Integrations\Woocommerce::class )->is_activated(); add_filter( 'wp_defender_advanced_tools_data', array( $this, 'script_data' ) ); if ( $this->model->is_active() // No need the check by is_woo_activated because we use this below. && ( $this->model->enable_default_locations() || $this->model->enable_woo_locations() ) && ! $this->exclude_recaptcha_for_requests() ) { $this->declare_variables(); $this->add_actions(); add_filter( 'script_loader_tag', array( $this, 'script_loader_tag' ), 10, 2 ); } } protected function declare_variables() { $this->recaptcha_type = $this->model->active_type; $this->recaptcha_theme = 'light'; $this->recaptcha_size = 'invisible'; $this->language = ! empty( $this->model->language ) && 'automatic' !== $this->model->language ? $this->model->language : get_locale(); // Add the reCAPTCHA keys depending on the reCAPTCHA type. if ( 'v2_checkbox' === $this->recaptcha_type ) { $this->public_key = $this->model->data_v2_checkbox['key']; $this->private_key = $this->model->data_v2_checkbox['secret']; $this->recaptcha_theme = $this->model->data_v2_checkbox['style']; $this->recaptcha_size = $this->model->data_v2_checkbox['size']; } elseif ( 'v2_invisible' === $this->recaptcha_type ) { $this->public_key = $this->model->data_v2_invisible['key']; $this->private_key = $this->model->data_v2_invisible['secret']; } elseif ( 'v3_recaptcha' === $this->recaptcha_type ) { $this->public_key = $this->model->data_v3_recaptcha['key']; $this->private_key = $this->model->data_v3_recaptcha['secret']; } } protected function add_actions() { $extra_conditions = is_admin() && ! defined( 'DOING_AJAX' ); // @since 2.5.6 do_action( 'wd_recaptcha_before_actions', $extra_conditions ); if ( $extra_conditions ) { return; } $is_user_logged_in = is_user_logged_in(); $locations = $this->model->locations; // Default login form. if ( in_array( self::DEFAULT_LOGIN_FORM, $locations, true ) ) { add_action( 'login_form', array( $this, 'display_login_recaptcha' ) ); add_filter( 'wp_authenticate_user', array( $this, 'validate_captcha_field_on_login' ), 8 ); } // Default register form. if ( in_array( self::DEFAULT_REGISTER_FORM, $locations, true ) ) { if ( ! is_multisite() ) { add_action( 'register_form', array( $this, 'display_login_recaptcha' ) ); add_filter( 'registration_errors', array( $this, 'validate_captcha_field_on_registration' ), 10 ); } else { add_action( 'signup_extra_fields', array( $this, 'display_signup_recaptcha' ) ); add_action( 'signup_blogform', array( $this, 'display_signup_recaptcha' ) ); add_filter( 'wpmu_validate_user_signup', array( $this, 'validate_captcha_field_on_wpmu_registration' ), 10 ); } } // Default lost password form. if ( in_array( self::DEFAULT_LOST_PASSWORD_FORM, $locations, true ) ) { add_action( 'lostpassword_form', array( $this, 'display_login_recaptcha' ) ); if ( ! $this->is_woocommerce_page() && ! isset( $_POST['wc_reset_password'], $_POST['user_login'] ) ) { add_action( 'lostpassword_post', array( $this, 'validate_captcha_field_on_lostpassword' ) ); } } // For Woo forms. Mandatory check for the activated Woo before. if ( $this->is_woo_activated && $this->model->enable_woo_locations() ) { $woo_locations = $this->model->woo_checked_locations; // Woo login form. if ( in_array( self::WOO_LOGIN_FORM, $woo_locations, true ) ) { add_action( 'woocommerce_login_form', array( $this, 'display_login_recaptcha' ) ); add_filter( 'woocommerce_process_login_errors', array( $this, 'validate_captcha_field_on_woo_login' ), 10 ); } // Woo register form. if ( in_array( self::WOO_REGISTER_FORM, $woo_locations, true ) ) { add_action( 'woocommerce_register_form', array( $this, 'display_login_recaptcha' ) ); add_filter( 'woocommerce_registration_errors', array( $this, 'validate_captcha_field_on_woo_registration' ), 10 ); } // Woo lost password form. if ( in_array( self::WOO_LOST_PASSWORD_FORM, $woo_locations, true ) ) { add_action( 'woocommerce_lostpassword_form', array( $this, 'display_login_recaptcha' ) ); // Use default WP hook because Woo doesn't have own hook, so there's the extra check for Woo form. if ( isset( $_POST['wc_reset_password'], $_POST['user_login'] ) ) { add_action( 'lostpassword_post', array( $this, 'validate_captcha_field_on_lostpassword' ) ); } } } // Default comment form. if ( ! $is_user_logged_in && in_array( self::DEFAULT_COMMENT_FORM, $locations, true ) ) { add_action( 'comment_form_after_fields', array( $this, 'display_comment_recaptcha' ) ); add_action( 'pre_comment_on_post', array( $this, 'validate_captcha_field_on_comment' ) ); // When comments are loaded via Hummingbird's lazy load feature. if ( $this->is_lazy_load_comments_enabled() ) { add_action( 'wp_footer', array( $this, 'add_scripts_for_lazy_load' ) ); } } // @since 2.5.6 do_action( 'wd_recaptcha_after_actions', $is_user_logged_in ); } /** * Add the async and defer tag. * @param string $tag * @param string $handle * * @return string */ public function script_loader_tag( $tag, $handle ) { if ( 'wpdef_recaptcha_api' === $handle ) { $tag = str_replace( ' src', ' data-cfasync="false" async="async" defer="defer" src', $tag ); } return $tag; } /** * @return string */ protected function get_api_url() { $api_url = null; if ( isset( $this->recaptcha_type ) && in_array( $this->recaptcha_type, array( 'v2_checkbox', 'v2_invisible' ), true ) ) { $api_url = sprintf( 'https://www.google.com/recaptcha/api.js?hl=%s&render=explicit', $this->language ); } if ( isset( $this->recaptcha_type ) && 'v3_recaptcha' === $this->recaptcha_type ) { $api_url = sprintf( 'https://www.google.com/recaptcha/api.js?hl=%s&render=%s', $this->language, $this->public_key ); } return $api_url; } protected function remove_dublicate_scripts() { global $wp_scripts; if ( ! is_object( $wp_scripts ) || empty( $wp_scripts ) ) { return false; } foreach ( $wp_scripts->registered as $script_name => $args ) { if ( preg_match( '|google\.com/recaptcha/api\.js|', $args->src ) && 'wpdef_recaptcha_api' !== $script_name ) { // Remove a previously enqueued script. wp_dequeue_script( $script_name ); } } } public function add_scripts() { if ( isset( $this->recaptcha_type ) ) { $this->remove_dublicate_scripts(); } wp_enqueue_script( 'wpdef_recaptcha_script', plugins_url( 'assets/js/recaptcha_frontend.js', WP_DEFENDER_FILE ), array( 'jquery', 'wpdef_recaptcha_api' ), DEFENDER_VERSION, true ); // @since 2.5.6 do_action( 'wd_recaptcha_extra_assets' ); $error_text = __( 'More than one reCAPTCHA has been found in the current form. Please remove all unnecessary reCAPTCHA fields to make it work properly.', 'wpdef' ); $options = array( 'hl' => $this->language, 'size' => $this->recaptcha_size, 'version' => $this->recaptcha_type, 'sitekey' => $this->public_key, 'error' => sprintf( '%s: %s', __( 'Warning', 'wpdef' ), $error_text ), // For default comment form. 'disable' => '', ); if ( 'v2_checkbox' === $this->recaptcha_type ) { $options['theme'] = $this->recaptcha_theme; } wp_localize_script( 'wpdef_recaptcha_script', 'WPDEF', array( 'options' => $options, 'vars' => array( 'visibility' => ( 'login_footer' === current_filter() ), ), ) ); } /** * Add scripts when comments are lazy loaded. * * @since 2.6.1 */ public function add_scripts_for_lazy_load() { if ( in_array( $this->recaptcha_type, array( 'v2_checkbox', 'v2_invisible' ), true ) && ( is_single() || is_page() ) && comments_open() ) { if ( ! wp_script_is( 'wpdef_recaptcha_api', 'registered' ) ) { $api_url = $this->get_api_url(); $deps = array( 'jquery' ); wp_register_script( 'wpdef_recaptcha_api', $api_url, $deps, DEFENDER_VERSION, true ); } $this->add_scripts(); } } /** * Display the reCAPTCHA field. */ public function display_login_recaptcha() { if ( 'v2_checkbox' === $this->recaptcha_type ) { $from_width = 302; ?> recaptcha_type ) { ?> display_recaptcha(); } /** * @since 2.5.6 * @return bool */ protected function exclude_recaptcha_for_requests() { $current_request = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '/'; $excluded_requests = (array) apply_filters( 'wd_recaptcha_excluded_requests', array() ); return in_array( $current_request, $excluded_requests, true ); } /** * Display the output of the recaptcha. * * @return string */ protected function display_recaptcha() { $deps = null; $content = '
%s
', $error_message ); } echo $this->display_recaptcha(); } /** * Verify the recaptcha code on the multisite signup page. * * @param array $result * * @return array|WP_user */ public function validate_captcha_field_on_wpmu_registration( $result ) { global $current_user; if ( is_admin() && ! defined( 'DOING_AJAX' ) && ! empty( $current_user->data->ID ) ) { return $result; } if ( ! $this->recaptcha_response( 'wpmu_registration' ) ) { if ( isset( $result['errors'] ) && ! empty( $result['errors'] ) ) { $errors = $result['errors']; } else { $errors = new WP_Error(); } $errors->add( 'invalid_captcha', $this->error_message() ); $result['errors'] = $errors; return $result; } return $result; } /** * Verify the recaptcha code on Woo login page. * * @param WP_Error $errors * * @return WP_Error */ public function validate_captcha_field_on_woo_login( $errors ) { // Skip check if connecting to XMLRPC. if ( defined( 'XMLRPC_REQUEST' ) ) { return $errors; } if ( ! $this->recaptcha_response( 'woo_login' ) ) { // Remove 'Error: ' because Woo has it by default. $message = str_replace( __( 'Error: ', 'wpdef' ), '', $this->error_message() ); $errors->add( 'invalid_captcha', $message ); } return $errors; } /** * Check recaptcha on Woo registration form. * * @param WP_Error $errors * * @return WP_Error */ public function validate_captcha_field_on_woo_registration( $errors ) { if ( defined( 'WOOCOMMERCE_CHECKOUT' ) ) { return $errors; } if ( ! $this->recaptcha_response( 'woo_registration' ) ) { // Remove 'Error: ' because Woo has it by default. $message = str_replace( __( 'Error: ', 'wpdef' ), '', $this->error_message() ); $errors->add( 'invalid_captcha', $message ); } return $errors; } /** * Fires before errors are returned from a password reset request. * Without 2nd `$user_data` parameter because it's since WP 5.4.0. * * @param WP_Error $errors */ public function validate_captcha_field_on_lostpassword( $errors ) { if ( ! $this->recaptcha_response( 'default_lost_password' ) ) { $errors->add( 'invalid_captcha', $this->error_message() ); } } /** * Add google recaptcha to the comment form. */ public function display_comment_recaptcha() { echo ''; echo $this->display_recaptcha(); return true; } /** * Check JS enabled for comment form. * * @return void */ public function validate_captcha_field_on_comment() { if ( $this->exclude_recaptcha_for_requests() ) { return; } if ( ! $this->recaptcha_response( 'default_comments' ) ) { // @since v2.5.6 wp_die( (string) apply_filters( 'wd_recaptcha_require_valid_comment', $this->error_message() ) ); } } /** * @return \WP_Defender\Model\Setting\Recaptcha */ private function get_model() { if ( is_object( $this->model ) ) { return $this->model; } return new \WP_Defender\Model\Setting\Recaptcha(); } /** * Get the reCAPTCHA API response. * * @param string $form * * @return bool */ protected function recaptcha_response( $form ) { if ( empty( $this->private_key ) || empty( $_POST['g-recaptcha-response'] ) ) { return false; } // reCAPTCHA response post data. $response = stripslashes( sanitize_text_field( $_POST['g-recaptcha-response'] ) ); $remote_ip = filter_var( $_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP ); // @since v2.5.6 $remote_ip = (string) apply_filters( 'wd_recaptcha_remote_ip', $remote_ip ); $post_body = array( 'secret' => $this->private_key, 'response' => $response, 'remoteip' => $remote_ip, ); $result = $this->recaptcha_post_request( $post_body ); // @since 2.5.6 return apply_filters( 'wd_recaptcha_check_result', $result, $form ); } /** * Display custom error message. * * @return string */ private function error_message() { return sprintf( /* translators: ... */ __( 'Error: %s', 'wpdef' ), empty( $this->model->message ) ? $this->default_msg : $this->model->message ); } /** * Send HTTP POST request and return the response. * Also initialize the error text if an error response is received. * * @param array $post_body - HTTP POST body * * @return bool */ protected function recaptcha_post_request( $post_body ) { $args = array( 'body' => $post_body, 'sslverify' => false, ); $url = 'https://www.google.com/recaptcha/api/siteverify'; $request = wp_remote_post( $url, $args ); // Get the request response body if ( is_wp_error( $request ) ) { return false; } $response_body = wp_remote_retrieve_body( $request ); $response_keys = json_decode( $response_body, true ); if ( 'v3_recaptcha' === $this->recaptcha_type ) { if ( $response_keys['success'] && isset( $this->model->data_v3_recaptcha['threshold'], $response_keys['score'] ) && is_numeric( $this->model->data_v3_recaptcha['threshold'] ) && is_numeric( $response_keys['score'] ) ) { $is_success = $response_keys['score'] >= (float) $this->model->data_v3_recaptcha['threshold']; } else { $is_success = false; } } else { $is_success = (bool) $response_keys['success']; } return $is_success; } /** * @param array $data * * @return mixed * @throws \ReflectionException */ public function script_data( $data ) { $data['recaptcha'] = $this->data_frontend(); return $data; } /** * Save settings. * @param Request $request * * @return Response * @defender_route */ public function save_settings( Request $request ) { $data = $request->get_data_by_model( $this->model ); $this->model->import( $data ); if ( $this->model->validate() ) { $this->model->save(); Config_Hub_Helper::set_clear_active_flag(); return new Response( true, array_merge( array( 'message' => __( 'Google reCAPTCHA settings saved successfully.', 'wpdef' ), ), $this->data_frontend() ) ); } return new Response( false, // Merge stored data to avoid errors. array_merge( array( 'message' => $this->model->get_formatted_errors() ), $this->data_frontend() ) ); } /** * @param Request $request * * @return Response * @defender_route */ public function load_recaptcha_preview( Request $request ) { $onload = null; $js = null; $data = $request->get_data( array( 'captcha_type' => array( 'type' => 'string', ), ) ); $this->recaptcha_type = $data['captcha_type']; $model = $this->get_model(); $language = ! empty( $model->language ) && 'automatic' !== $model->language ? $model->language : get_locale(); $notice = '