PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
← All changes | src/component/class-captcha.php +487 -129 5.7.0 → 6.3.0 View file →
@@ -6,11 +6,15 @@
6 6 */
7 7
8 8 namespace WP_Defender\Component;
9 9
10 +use WP_Error;
10 11 use Calotes\Base\Component;
11 12 use WP_Defender\Integrations\Buddypress;
12 13 use WP_Defender\Integrations\Woocommerce;
14 +use WP_Defender\Component\Captcha\Turnstile;
15 +use WP_Defender\Component\Captcha\Recaptcha;
16 +use WP_Defender\Component\Captcha\Interface_Provider;
13 17 use WP_Defender\Model\Setting\Captcha as Captcha_Model;
14 18
15 19 /**
16 20 * Provides methods to handle CAPTCHA integration, including rendering, validation, and script management.
@@ -19,12 +23,9 @@
19 23
20 24 /**
21 25 * Default form identifiers for CAPTCHA integration.
22 26 */
23 - public const DEFAULT_LOGIN_FORM = 'login',
24 - DEFAULT_REGISTER_FORM = 'register',
25 - DEFAULT_LOST_PASSWORD_FORM = 'lost_password',
26 - DEFAULT_COMMENT_FORM = 'comments';
27 + public const DEFAULT_LOGIN_FORM = 'login', DEFAULT_REGISTER_FORM = 'register', DEFAULT_LOST_PASSWORD_FORM = 'lost_password', DEFAULT_COMMENT_FORM = 'comments';
27 28
28 29 /**
29 30 * The CAPTCHA settings model.
30 31 *
@@ -43,101 +44,167 @@
43 44 *
44 45 * @var Buddypress|null
45 46 */
46 47 private ?Buddypress $buddypress;
48 + /**
49 + * Active CAPTCHA provider instance.
50 + *
51 + * @var Interface_Provider|null
52 + */
53 + private ?Interface_Provider $provider = null;
47 54
48 55 /**
49 56 * Captcha constructor.
50 57 *
51 - * @param Captcha_Model $model The CAPTCHA settings model.
58 + * @param Captcha_Model $model The CAPTCHA settings model.
52 59 */
53 60 public function __construct( Captcha_Model $model ) {
54 61 $this->model = $model;
55 62 $this->woo = wd_di()->get( Woocommerce::class );
56 63 $this->buddypress = wd_di()->get( Buddypress::class );
64 + $this->provider = $this->get_provider();
57 65 }
58 66
59 67 /**
68 + * Get the active captcha provider.
69 + *
70 + * @return Interface_Provider
71 + */
72 + public function get_provider(): Interface_Provider {
73 + if ( 'recaptcha' === $this->model->provider && ! $this->provider instanceof Recaptcha ) {
74 + $this->provider = new Recaptcha( $this->model );
75 + }
76 + if ( Captcha_Model::TURNSTILE === $this->model->provider && ! $this->provider instanceof Turnstile ) {
77 + $this->provider = new Turnstile( $this->model );
78 + }
79 + // Fallback to Recaptcha if provider is invalid.
80 + if ( null === $this->provider ) {
81 + $this->provider = new Recaptcha( $this->model );
82 + }
83 + return $this->provider;
84 + }
85 +
86 + /**
87 + * Retrieves the list of default forms where CAPTCHA can be integrated.
88 + *
89 + * @return array An associative array of form identifiers and their display names.
90 + */
91 + public static function get_forms(): array {
92 + return array(
93 + self::DEFAULT_LOGIN_FORM => esc_html__( 'Login', 'defender-security' ),
94 + self::DEFAULT_REGISTER_FORM => esc_html__( 'Register', 'defender-security' ),
95 + self::DEFAULT_LOST_PASSWORD_FORM => esc_html__( 'Lost Password', 'defender-security' ),
96 + self::DEFAULT_COMMENT_FORM => esc_html__( 'Comments', 'defender-security' ),
97 + );
98 + }
99 +
100 + /**
60 101 * Determines if any CAPTCHA location is enabled.
61 102 *
62 - * @param bool $exist_woo Whether WooCommerce is active.
63 - * @param bool $exist_bp Whether BuddyPress is active.
103 + * @param bool $exist_woo Whether WooCommerce is active.
104 + * @param bool $exist_bp Whether BuddyPress is active.
64 105 *
65 106 * @return bool True if any location is enabled, false otherwise.
66 107 */
67 - public function enable_any_location( $exist_woo, $exist_bp ): bool {
68 - return $this->model->enable_default_location()
69 - || $this->model->check_woo_locations( $exist_woo )
70 - || $this->model->check_buddypress_locations( $exist_bp );
108 + public function enable_any_location( bool $exist_woo, bool $exist_bp ): bool {
109 + return $this->model->enable_default_location() || $this->model->check_woo_locations( $exist_woo ) || $this->model->check_buddypress_locations( $exist_bp );
71 110 }
72 111
73 112 /**
74 - * Excludes CAPTCHA for specific requests.
113 + * Modifies the script loader tag for the 'wpdef_captcha_api' handle.
75 114 *
76 - * @return bool
115 + * @param string $tag The original script loader tag.
116 + * @param string $handle The handle being loaded.
117 + *
118 + * @return string The modified script loader tag.
77 119 */
78 - public function exclude_captcha_for_requests(): bool {
79 - if ( $this->is_recaptcha_active_provider() ) {
80 - return $this->exclude_recaptcha_for_requests();
81 - } else {
82 - return $this->exclude_cloudflare_turnstile_for_requests();
120 + public function script_loader_tag( string $tag, string $handle ): string {
121 + if ( 'wpdef_captcha_api' === $handle ) {
122 + $tag = str_replace( ' src', ' data-cfasync="false" async="async" defer="defer" src', $tag );
83 123 }
124 +
125 + return $tag;
84 126 }
85 127
86 128 /**
87 - * Excludes reCAPTCHA for specific requests.
129 + * Add scripts when comments are lazy loaded.
88 130 *
89 - * @return bool
131 + * @return void
132 + * @since 2.6.1
90 133 */
91 - private function exclude_recaptcha_for_requests(): bool {
92 - $current_request = defender_get_data_from_request( 'REQUEST_URI', 's' ) ?? '/';
93 - $excluded_requests = (array) apply_filters( 'wd_recaptcha_excluded_requests', array() );
134 + public function add_scripts_for_lazy_load(): void {
135 + if ( in_array(
136 + $this->provider->type,
137 + array(
138 + 'v2_checkbox',
139 + 'v2_invisible',
140 + ),
141 + true
142 + ) && ( is_single() || is_page() ) && comments_open() ) {
143 + if ( ! wp_script_is( 'wpdef_captcha_api', 'registered' ) ) {
144 + $api_url = $this->provider->get_api_url();
145 + $deps = array( 'jquery' );
146 + wp_register_script( 'wpdef_captcha_api', $api_url, $deps, DEFENDER_VERSION, true );
147 + }
94 148
95 - return in_array( $current_request, $excluded_requests, true );
149 + $this->add_captcha_scripts();
150 + }
96 151 }
97 152
98 153 /**
99 - * Excludes Cloudflare Turnstile for specific requests.
154 + * Enqueues the necessary scripts for the reCAPTCHA frontend.
100 155 *
101 - * @return bool
102 - * @since v5.7.0.
156 + * @return void
103 157 */
104 - private function exclude_cloudflare_turnstile_for_requests(): bool {
105 - $current_request = defender_get_data_from_request( 'REQUEST_URI', 's' ) ?? '/';
106 - $excluded_requests = (array) apply_filters( 'wd_cloudflare_turnstile_excluded_requests', array() );
158 + public function add_captcha_scripts(): void {
159 + if ( isset( $this->provider->type ) ) {
160 + $this->remove_duplicate_captcha_scripts();
161 + }
162 + $handle = $this->provider->get_script_handle();
163 + $src_url = $this->provider->get_script_url();
164 + if ( ! wp_script_is( $handle ) ) {
165 + wp_enqueue_script(
166 + $handle,
167 + plugins_url( $src_url, WP_DEFENDER_FILE ),
168 + array(
169 + 'jquery',
170 + 'wpdef_captcha_api',
171 + ),
172 + DEFENDER_VERSION,
173 + true
174 + );
175 + }
107 176
108 - return in_array( $current_request, $excluded_requests, true );
177 + // @since 2.5.6
178 + do_action( 'wd_recaptcha_extra_assets' );
179 + wp_localize_script(
180 + $handle,
181 + 'WPDEF',
182 + array(
183 + 'options' => $this->provider->get_options(),
184 + 'vars' => array(
185 + 'visibility' => ( 'login_footer' === current_filter() ),
186 + ),
187 + )
188 + );
109 189 }
110 190
111 191 /**
112 192 * Removes duplicate CAPTCHA scripts.
113 193 *
114 - * @return bool|void False if no scripts are registered, or void otherwise.
194 + * @return void
115 195 */
116 - public function remove_duplicate_captcha_scripts() {
196 + public function remove_duplicate_captcha_scripts(): void {
117 197 global $wp_scripts;
118 198
119 199 if ( ! is_object( $wp_scripts ) ) {
120 - return false;
200 + return;
121 201 }
122 - /**
123 - * Exclude scripts from Defender and Forminator to display reCAPTCHA.
124 - *
125 - * @since 5.1.0
126 - */
127 - $excluded_handles = (array) apply_filters(
128 - $this->is_recaptcha_active_provider() ? 'wd_recaptcha_excluded_handles' : 'wd_turnstile_excluded_handles',
129 - array(
130 - 'wpdef_captcha_api',
131 - 'forminator-google-recaptcha',
132 - 'forminator-turnstile',
133 - )
134 - );
202 +
203 + $excluded_handles = $this->provider->get_duplicate_script_handles();
204 + $search_pattern = $this->provider->get_duplicate_script_pattern();
135 205 foreach ( $wp_scripts->registered as $script_name => $args ) {
136 - $search_pattern = $this->is_recaptcha_active_provider() ? '|google\.com/recaptcha/api\.js|' : '|challenges\.cloudflare\.com/turnstile/v0/api\.js|';
137 - if ( is_string( $args->src ) && preg_match( $search_pattern, $args->src )
138 - && ! in_array( $script_name, $excluded_handles, true )
139 - ) {
206 + if ( is_string( $args->src ) && preg_match( $search_pattern, $args->src ) && ! in_array( $script_name, $excluded_handles, true ) ) {
140 207 wp_dequeue_script( $script_name );
141 208 }
142 209 }
143 210 }
@@ -142,128 +209,419 @@
142 209 }
143 210 }
144 211
145 212 /**
146 - * Returns a custom error message for reCAPTCHA validation failure.
213 + * Display the CAPTCHA field.
147 214 *
148 - * @return string The formatted error message.
215 + * @return void
149 216 */
150 - public function error_message(): string {
151 - $default_values = $this->model->get_default_values();
152 - if ( $this->is_recaptcha_active_provider() ) {
153 - $message = '' === $this->model->message ? $default_values['message'] : $this->model->message;
154 - } else {
155 - $message = array() !== $this->model->data_turnstile && '' !== $this->model->data_turnstile['message'] ? $this->model->data_turnstile['message'] : $default_values['turnstile_message'];
217 + public function display_login_captcha(): void {
218 + if ( in_array( $this->provider->type, array( 'v2_checkbox', Captcha_Model::TURNSTILE ), true ) ) {
219 + $from_width = 302; ?>
220 + <style media="screen">
221 + .login-action-login #loginform,
222 + .login-action-lostpassword #lostpasswordform,
223 + .login-action-register #registerform {
224 + width: <?php echo esc_attr( $from_width ); ?>px !important;
225 + }
226 +
227 + #login_error,
228 + .message {
229 + width: <?php echo esc_attr( $from_width + 20 ); ?>px !important;
230 + }
231 +
232 + .login-action-login #loginform .captcha_wrap,
233 + .login-action-lostpassword #lostpasswordform .captcha_wrap,
234 + .login-action-register #registerform .captcha_wrap {
235 + margin-bottom: 10px;
236 + }
237 +
238 + #group-create-body .captcha_wrap {
239 + margin-top: 15px;
240 + }
241 + </style>
242 + <?php
243 + } elseif ( 'v2_invisible' === $this->provider->type ) {
244 + ?>
245 + <style>
246 + .login-action-lostpassword #lostpasswordform .captcha_wrap,
247 + .login-action-login #loginform .captcha_wrap,
248 + .login-action-register #registerform .captcha_wrap {
249 + margin-bottom: 10px;
250 + }
251 +
252 + #signup-content .captcha_wrap,
253 + #group-create-body .captcha_wrap {
254 + margin-top: 10px;
255 + }
256 + </style>
257 + <?php
156 258 }
157 - return sprintf( '<strong>%s:</strong> %s', esc_html__( 'Error', 'defender-security' ), $message );
259 + echo wp_kses( $this->display_recaptcha(), $this->get_allowed_html() );
158 260 }
159 261
160 262 /**
161 - * Sends an HTTP POST request to the Google reCAPTCHA API and returns the validation result.
263 + * Display the output of the recaptcha.
162 264 *
163 - * @param array $post_body The POST request body.
265 + * @return string
266 + */
267 + protected function display_recaptcha(): string {
268 + $content = $this->provider->render_widget();
269 + // Register reCAPTCHA script.
270 + $locations = $this->model->locations;
271 + if ( ! wp_script_is( 'wpdef_captcha_api', 'registered' ) ) {
272 + $script = $this->provider->get_api_script();
273 + wp_register_script( 'wpdef_captcha_api', $script['url'], $script['deps'], $script['version'], false );
274 + add_action( 'wp_footer', array( $this, 'add_captcha_scripts' ) );
275 + if ( in_array( self::DEFAULT_LOGIN_FORM, $locations, true ) || in_array( self::DEFAULT_REGISTER_FORM, $locations, true ) || in_array( self::DEFAULT_LOST_PASSWORD_FORM, $locations, true ) ) {
276 + add_action( 'login_footer', array( $this, 'add_captcha_scripts' ) );
277 + }
278 + }
279 +
280 + return $content;
281 + }
282 +
283 + /**
284 + * Get allowed HTML tags and attributes for reCaptcha output sanitization.
164 285 *
165 - * @return bool True if the reCAPTCHA validation is successful, false otherwise.
286 + * @return array
166 287 */
167 - public function captcha_post_request( array $post_body ): bool {
168 - $args = array(
169 - 'body' => $post_body,
170 - 'sslverify' => false,
288 + private function get_allowed_html(): array {
289 + return array(
290 + 'div' => array(
291 + 'class' => array(),
292 + 'id' => array(),
293 + 'style' => array(),
294 + ),
295 + 'iframe' => array(
296 + 'src' => array(),
297 + 'frameborder' => array(),
298 + 'scrolling' => array(),
299 + 'style' => array(),
300 + ),
301 + 'noscript' => array(),
302 + 'textarea' => array(
303 + 'name' => array( 'g-recaptcha-response' ),
304 + 'class' => array( 'g-recaptcha-response' ),
305 + 'style' => array(),
306 + ),
307 + 'input' => array(
308 + 'type' => array( 'hidden' ),
309 + 'class' => array( 'g-recaptcha-response' ),
310 + 'name' => array( 'g-recaptcha-response' ),
311 + ),
171 312 );
172 - $url = $this->is_recaptcha_active_provider() ? 'https://www.google.com/recaptcha/api/siteverify' : 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
173 - $request = wp_remote_post( $url, $args );
313 + }
174 314
175 - if ( is_wp_error( $request ) ) {
176 - return false;
315 + /**
316 + * Validates the CAPTCHA response for the login form.
317 + *
318 + * @param null|WP_Error $error WP_Error object if validation fails, else null.
319 + *
320 + * @return null|WP_Error WP_Error object if validation fails else null.
321 + */
322 + public function validate_login_captcha( $error ) {
323 + $post_data = defender_get_data_from_request( null, 'p' );
324 + if ( array() === $post_data ) {
325 + return $error;
177 326 }
327 + // Skip turnstile verification for current sprint plan.
328 + if ( $this->provider->should_skip_check( $this->woo, $this->buddypress ) ) {
329 + return $error;
330 + }
331 + // Check if the $_POST array is not empty and if 'g-recaptcha-response' key is also empty.
332 + $captcha_response = $this->provider->get_response();
333 + if ( '' === $captcha_response ) {
334 + $code = 'captcha_error';
335 + $message = __( 'Please verify that you are not a robot.', 'defender-security' );
178 336
179 - $response_body = wp_remote_retrieve_body( $request );
180 - $response_keys = json_decode( $response_body, true );
181 - if ( 'v3_recaptcha' === $this->model->active_type ) {
182 - if (
183 - $response_keys['success']
184 - && isset( $this->model->data_v3_recaptcha['threshold'], $response_keys['score'] )
185 - && is_numeric( $this->model->data_v3_recaptcha['threshold'] )
186 - && is_numeric( $response_keys['score'] )
187 - ) {
188 - $is_success = $response_keys['score'] >= (float) $this->model->data_v3_recaptcha['threshold'];
337 + if ( is_wp_error( $error ) ) {
338 + $error->add( $code, $message );
189 339 } else {
190 - $is_success = false;
340 + // Replace $user with a new WP_Error object with an error message.
341 + $error = new WP_Error( $code, $message );
191 342 }
192 - } else {
193 - $is_success = (bool) $response_keys['success'];
194 343 }
195 344
196 - return $is_success;
345 + // Return the $error variable.
346 + return $error;
197 347 }
198 348
199 349 /**
200 - * Retrieves the list of default forms where CAPTCHA can be integrated.
350 + * Verify the captcha code on the Login page.
201 351 *
202 - * @return array An associative array of form identifiers and their display names.
352 + * @param WP_User|WP_Error $user WP_User or WP_Error object if a previous callback failed authentication.
353 + *
354 + * @return WP_Error|WP_User
203 355 */
204 - public static function get_forms(): array {
205 - return array(
206 - self::DEFAULT_LOGIN_FORM => esc_html__( 'Login', 'defender-security' ),
207 - self::DEFAULT_REGISTER_FORM => esc_html__( 'Register', 'defender-security' ),
208 - self::DEFAULT_LOST_PASSWORD_FORM => esc_html__( 'Lost Password', 'defender-security' ),
209 - self::DEFAULT_COMMENT_FORM => esc_html__( 'Comments', 'defender-security' ),
210 - );
356 + public function validate_captcha_field_on_login( $user ) {
357 + if ( $this->woo && $this->woo->is_woocommerce_page() ) {
358 + return $user;
359 + }
360 + // Skip check if connecting to XMLRPC.
361 + if ( defined( 'XMLRPC_REQUEST' ) ) {
362 + return $user;
363 + }
364 + // Skip turnstile verification for current sprint plan.
365 + if ( $this->provider->should_skip_check( $this->woo, $this->buddypress ) ) {
366 + return $user;
367 + }
368 + if ( ! $this->provider->verify_response( 'default_login' ) ) {
369 + if ( is_wp_error( $user ) ) {
370 + $user->add( 'invalid_captcha', $this->provider->error_message() );
371 +
372 + return $user;
373 + }
374 +
375 + return new WP_Error( 'invalid_captcha', $this->provider->error_message() );
376 + }
377 +
378 + return $user;
211 379 }
380 +
212 381 /**
213 - * Validate the token
382 + * Verify the recaptcha code on the Registration page.
214 383 *
215 - * @param string $token The token from the admin-side widget.
216 - * @return bool Indicating if validation was successful.
384 + * @param WP_Error $errors A WP_Error object containing any errors encountered during registration.
385 + *
386 + * @return WP_Error
217 387 */
218 - public function verify_turnstile_key( string $token ): bool {
219 - $key = $this->model->data_turnstile['key'] ?? '';
220 - $secret = $this->model->data_turnstile['secret'] ?? '';
221 - // Skip validation only if both secret and site key are empty.
222 - if ( '' === $secret && '' === $key ) {
223 - return true;
388 + public function validate_captcha_field_on_registration( WP_Error $errors ) {
389 + // Skip check if connecting to XMLRPC.
390 + if ( defined( 'XMLRPC_REQUEST' ) ) {
391 + return $errors;
224 392 }
225 - // If secret key is provided but site key is not (or vice versa), require both.
226 - if ( '' === $secret || '' === $key ) {
227 - return false;
393 +
394 + if ( ! $this->provider->verify_response( 'default_registration' ) ) {
395 + $errors->add( 'invalid_captcha', $this->provider->error_message() );
228 396 }
229 - // If no token provided, validation cannot proceed.
230 - if ( '' === $token ) {
231 - return false;
397 + $_POST['g-recaptcha-response-check'] = true;
398 +
399 + return $errors;
400 + }
401 +
402 + /**
403 + * Add captcha to the multisite signup form.
404 + *
405 + * @param WP_Error $errors A WP_Error object possibly containing 'blogname' or 'blog_title' errors.
406 + *
407 + * @return void
408 + */
409 + public function display_signup_captcha( WP_Error $errors ): void {
410 + $error_message = $errors->get_error_message( 'invalid_captcha' );
411 + if ( is_string( $error_message ) && '' !== $error_message ) {
412 + printf( '<p class="error">%s</p>', wp_kses_post( $error_message ) );
232 413 }
233 - return $this->captcha_post_request(
234 - array(
235 - 'secret' => $this->model->data_turnstile['secret'],
236 - 'response' => $token,
237 - )
238 - );
414 + echo wp_kses( $this->display_recaptcha(), $this->get_allowed_html() );
239 415 }
240 416
241 417 /**
242 - * Check if the Google reCAPTCHA is active provider.
418 + * Verify the recaptcha code on the multisite signup page.
243 419 *
420 + * @param array $result An array of errors.
421 + *
422 + * @return array
423 + */
424 + public function validate_captcha_field_on_wpmu_registration( array $result ): array {
425 + global $current_user;
426 + if ( is_admin() && ! defined( 'DOING_AJAX' ) && isset( $current_user->data->ID ) && 0 < (int) $current_user->data->ID ) {
427 + return $result;
428 + }
429 +
430 + // Skip if BuddyPress is handling the registration.
431 + if ( $this->buddypress && $this->buddypress->is_activated() && in_array( Buddypress::REGISTER_FORM, $this->model->buddypress_checked_locations, true ) ) {
432 + return $result;
433 + }
434 +
435 + if ( ! $this->provider->verify_response( 'wpmu_registration' ) ) {
436 + if ( isset( $result['errors'] ) && $result['errors'] instanceof WP_Error ) {
437 + $errors = $result['errors'];
438 + } else {
439 + $errors = new WP_Error();
440 + }
441 + $errors->add( 'invalid_captcha', $this->provider->error_message() );
442 + $result['errors'] = $errors;
443 +
444 + return $result;
445 + }
446 +
447 + return $result;
448 + }
449 +
450 + /**
451 + * Verify the recaptcha code on Woo login page.
452 + *
453 + * @param WP_Error $errors A WP_Error object containing any errors encountered during login.
454 + *
455 + * @return WP_Error
456 + */
457 + public function validate_captcha_field_on_woo_login( WP_Error $errors ) {
458 + // Skip check if connecting to XMLRPC.
459 + if ( defined( 'XMLRPC_REQUEST' ) ) {
460 + return $errors;
461 + }
462 +
463 + if ( ! $this->provider->verify_response( 'woo_login' ) ) {
464 + // Remove 'Error: ' because Woo has it by default.
465 + $message = str_replace( sprintf( '<strong>%s:</strong> ', esc_html__( 'Error', 'defender-security' ) ), '', $this->provider->error_message() );
466 + $errors->add( 'invalid_captcha', $message );
467 + }
468 +
469 + return $errors;
470 + }
471 +
472 + /**
473 + * Check recaptcha on Woo registration form.
474 + *
475 + * @param WP_Error $errors A WP_Error object containing any errors encountered during registration.
476 + *
477 + * @return WP_Error
478 + */
479 + public function validate_captcha_field_on_woo_registration( WP_Error $errors ) {
480 + if ( defined( 'WOOCOMMERCE_CHECKOUT' ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
481 + return $errors;
482 + }
483 + if ( ! $this->provider->verify_response( 'woo_registration' ) ) {
484 + // Remove 'Error: ' because Woo has it by default.
485 + $message = str_replace( sprintf( '<strong>%s:</strong> ', esc_html__( 'Error', 'defender-security' ) ), '', $this->provider->error_message() );
486 + $errors->add( 'invalid_captcha', $message );
487 + }
488 +
489 + return $errors;
490 + }
491 +
492 + /**
493 + * Fires before errors are returned from a password reset request.
494 + * Without 2nd `$user_data` parameter because it's since WP 5.4.0.
495 + *
496 + * @param WP_Error $errors A WP_Error object containing any errors encountered during password reset.
497 + *
498 + * @return void
499 + */
500 + public function validate_captcha_field_on_lostpassword( WP_Error $errors ): void {
501 + if ( ! $this->provider->verify_response( 'default_lost_password' ) ) {
502 + $errors->add( 'invalid_captcha', $this->provider->error_message() );
503 + }
504 + }
505 +
506 + /**
507 + * Validates the reCAPTCHA field on the WooCommerce checkout form.
508 + *
509 + * @param array $fields The fields of the checkout form.
510 + * @param WP_Error $errors The errors encountered during the checkout process.
511 + *
512 + * @return void
513 + */
514 + public function validate_captcha_field_on_woo_checkout( $fields, $errors ): void {
515 + if ( ! $this->provider->verify_response( 'woo_checkout' ) ) {
516 + // Remove 'Error: ' because Woo has it by default.
517 + $message = str_replace( sprintf( '<strong>%s:</strong> ', esc_html__( 'Error', 'defender-security' ) ), '', $this->provider->error_message() );
518 + $errors->add( 'invalid_captcha', $message );
519 + }
520 + }
521 +
522 + /**
523 + * Display google recaptcha on comments form.
524 + *
525 + * @param array $defaults The default comment form arguments.
526 + *
527 + * @return array
528 + */
529 + public function comment_form_defaults( array $defaults ) {
530 + $defaults['comment_notes_after'] .= '<p>' . $this->display_recaptcha() . '</p>';
531 +
532 + return $defaults;
533 + }
534 +
535 + /**
536 + * Check JS enabled for comment form.
537 + *
538 + * @param int $comment_post_id Post ID.
539 + *
540 + * @return void
541 + */
542 + public function validate_captcha_field_on_comment( $comment_post_id ) {
543 + if ( $this->exclude_captcha_for_requests() ) {
544 + return;
545 + }
546 + // Skip if it's from WooCommerce review form.
547 + if ( 'product' === get_post_type( $comment_post_id ) ) {
548 + return;
549 + }
550 +
551 + if ( ! $this->provider->verify_response( 'default_comments' ) ) {
552 + // @since v2.5.6
553 + wp_die( wp_kses_post( apply_filters( 'wd_recaptcha_require_valid_comment', $this->provider->error_message() ) ) );
554 + }
555 + }
556 +
557 + /**
558 + * Excludes CAPTCHA for specific requests.
559 + *
244 560 * @return bool
245 561 */
246 - public function is_recaptcha_active_provider(): bool {
247 - return 'recaptcha' === $this->model->provider;
562 + public function exclude_captcha_for_requests(): bool {
563 + $uri = defender_get_data_from_request( 'REQUEST_URI', 's' ) ?? '/';
564 + $excluded_requests = $this->provider->get_excluded_requests();
565 +
566 + return in_array( $uri, $excluded_requests, true );
248 567 }
249 568
250 569 /**
251 - * Skip turnstile check as per current sprint plan.
570 + * Display the BuddyPress reCAPTCHA.
252 571 *
253 - * @return bool true if validation should skip, false otherwise.
572 + * @return void
254 573 */
255 - public function should_skip_turnstile_check(): bool {
256 - // Only apply for Turnstile provider.
257 - if ( $this->is_recaptcha_active_provider() ) {
258 - return false;
574 + public function display_buddypress_recaptcha(): void {
575 + $error = '';
576 +
577 + if ( function_exists( 'buddypress' ) ) {
578 + $obj = buddypress();
579 + if ( isset( $obj->signup->errors['failed_captcha_verification'] ) && '' !== $obj->signup->errors['failed_captcha_verification'] ) {
580 + $error = $obj->signup->errors['failed_captcha_verification'];
581 + }
259 582 }
260 583
261 - // Skip if neither WooCommerce nor BuddyPress is active.
262 - if ( ! $this->woo->is_activated() && ! $this->buddypress->is_activated() ) {
263 - return false;
584 + if ( '' !== $error ) {
585 + $output = '<div class="error">';
586 + $output .= $error;
587 + $output .= '</div>';
588 +
589 + echo wp_kses_post( $output );
264 590 }
265 591
266 - // Skip turnstile check if in WooCommerce or BuddyPress login context.
267 - return $this->woo->is_wc_login_context() || $this->buddypress->is_login_context();
592 + echo wp_kses( $this->display_recaptcha(), $this->get_allowed_html() );
593 + }
594 +
595 + /**
596 + * Validates the reCAPTCHA field on the BuddyPress registration form.
597 + *
598 + * @return void
599 + */
600 + public function validate_captcha_field_on_buddypress_registration(): void {
601 + if ( ! $this->provider->verify_response( 'buddypress_registration' ) && function_exists( 'buddypress' ) ) {
602 + buddypress()->signup->errors['failed_captcha_verification'] = $this->provider->error_message();
603 + }
604 + }
605 +
606 + /**
607 + * Verify BuddyPress group form captcha.
608 + *
609 + * @return void
610 + */
611 + public function validate_captcha_field_on_buddypress_group(): void {
612 + if ( function_exists( 'bp_is_group_creation_step' ) && ! bp_is_group_creation_step( 'group-details' ) ) {
613 + return;
614 + }
615 +
616 + if (
617 + ! $this->provider->verify_response( 'buddypress_create_group' )
618 + && function_exists( 'bp_core_add_message' )
619 + && function_exists( 'bp_core_redirect' )
620 + && function_exists( 'bp_get_root_domain' )
621 + && function_exists( 'bp_get_groups_root_slug' )
622 + ) {
623 + bp_core_add_message( $this->provider->error_message(), 'error' );
624 + bp_core_redirect( bp_get_root_domain() . '/' . bp_get_groups_root_slug() . '/create/step/group-details/' );
625 + }
268 626 }
269 627 }