PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.6.1
Defender Security – Malware Scanner, Login Security & Firewall v5.6.1
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
defender-security / src / controller / class-recaptcha.php

class-recaptcha.php in Defender Security – Malware Scanner, Login Security & Firewall 5.6.1, at src/controller/class-recaptcha.php

1,310 lines 39.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles reCAPTCHA related actions.
4 *
5 * @package WP_Defender\Controller
6 */
7
8 namespace WP_Defender\Controller;
9
10 use WP_User;
11 use WP_Error;
12 use WP_Defender\Event;
13 use Calotes\Component\Request;
14 use Calotes\Component\Response;
15 use WP_Defender\Component\Crypt;
16 use WP_Defender\Behavior\WPMUDEV;
17 use WP_Defender\Traits\Hummingbird;
18 use WP_Defender\Integrations\Buddypress;
19 use WP_Defender\Integrations\Woocommerce;
20 use WP_Defender\Component\Config\Config_Hub_Helper;
21 use WP_Defender\Component\Recaptcha as Recaptcha_Component;
22 use WP_Defender\Model\Setting\Recaptcha as Recaptcha_Model;
23
24 /**
25 * Handles reCAPTCHA related actions.
26 *
27 * @since 2.5.4
28 */
29 class Recaptcha extends Event {
30
31 use Hummingbird;
32
33 /**
34 * Accepted values: v2_checkbox, v2_invisible, v3_recaptcha.
35 *
36 * @var string
37 */
38 private $recaptcha_type;
39
40 /**
41 * Accepted values: light and dark.
42 *
43 * @var string
44 */
45 private $recaptcha_theme;
46
47 /**
48 * Accepted values: normal and compact.
49 *
50 * @var string
51 */
52 private $recaptcha_size;
53
54 /**
55 * Recaptcha API public key.
56 *
57 * @var string
58 */
59 private $public_key;
60
61 /**
62 * Recaptcha API private key.
63 *
64 * @var string
65 */
66 private $private_key;
67
68 /**
69 * Language for the reCAPTCHA.
70 *
71 * @var string
72 */
73 private $language;
74
75 /**
76 * Default message for the reCAPTCHA.
77 *
78 * @var string
79 */
80 private $default_msg;
81
82 /**
83 * The model for handling the data.
84 *
85 * @var Recaptcha_Model
86 */
87 public $model;
88
89 /**
90 * Service for handling logic.
91 *
92 * @var Recaptcha_Component
93 */
94 protected $service;
95
96 /**
97 * Is Woo activated.
98 *
99 * @var bool
100 */
101 private $is_woo_activated;
102
103 /**
104 * Is BuddyPress activated.
105 *
106 * @var bool
107 */
108 private $is_buddypress_activated;
109
110 /**
111 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
112 */
113 public function __construct() {
114 $this->model = wd_di()->get( Recaptcha_Model::class );
115 $this->service = new Recaptcha_Component( $this->model );
116 // Use default msg to avoid empty message error.
117 $default_values = $this->model->get_default_values();
118 $this->default_msg = $default_values['message'];
119 $this->register_routes();
120 $this->is_woo_activated = wd_di()->get( Woocommerce::class )->is_activated();
121 $this->is_buddypress_activated = wd_di()->get( Buddypress::class )->is_activated();
122 add_filter( 'wp_defender_advanced_tools_data', array( $this, 'script_data' ) );
123
124 if (
125 $this->model->is_active()
126 // No need the check by Woo and Buddypress are activated because we use this below.
127 && $this->service->enable_any_location( $this->is_woo_activated, $this->is_buddypress_activated )
128 && ! $this->service->exclude_recaptcha_for_requests()
129 ) {
130 $this->declare_variables();
131 $this->add_actions();
132
133 add_filter( 'script_loader_tag', array( $this, 'script_loader_tag' ), 10, 2 );
134 }
135 }
136
137 /**
138 * Declares the necessary variables for the reCAPTCHA functionality.
139 *
140 * @return void
141 */
142 protected function declare_variables(): void {
143 $this->recaptcha_type = $this->model->active_type;
144 $this->recaptcha_theme = 'light';
145 $this->recaptcha_size = 'invisible';
146 $this->language = ! empty( $this->model->language ) && 'automatic' !== $this->model->language
147 ? $this->model->language
148 : get_locale();
149
150 // Add the reCAPTCHA keys depending on the reCAPTCHA type.
151 if ( 'v2_checkbox' === $this->recaptcha_type ) {
152 $this->public_key = $this->model->data_v2_checkbox['key'];
153 $this->private_key = $this->model->data_v2_checkbox['secret'];
154 $this->recaptcha_theme = $this->model->data_v2_checkbox['style'];
155 $this->recaptcha_size = $this->model->data_v2_checkbox['size'];
156 } elseif ( 'v2_invisible' === $this->recaptcha_type ) {
157 $this->public_key = $this->model->data_v2_invisible['key'];
158 $this->private_key = $this->model->data_v2_invisible['secret'];
159 } elseif ( 'v3_recaptcha' === $this->recaptcha_type ) {
160 $this->public_key = $this->model->data_v3_recaptcha['key'];
161 $this->private_key = $this->model->data_v3_recaptcha['secret'];
162 }
163 }
164
165 /**
166 * Is it Defender's Google reCAPTCHA page?
167 *
168 * @return bool
169 */
170 protected function is_recaptcha_settings(): bool {
171 $view = defender_get_data_from_request( 'view', 'g' );
172 return 'wdf-advanced-tools' === defender_get_current_page() && 'recaptcha' === $view;
173 }
174
175 /**
176 * Add actions for reCAPTCHA.
177 *
178 * @return void
179 */
180 protected function add_actions() {
181 $extra_conditions = is_admin() && ! ( defined( 'DOING_AJAX' ) && $this->is_recaptcha_settings() );
182 // @since 2.5.6
183 do_action( 'wd_recaptcha_before_actions', $extra_conditions );
184 if ( $extra_conditions ) {
185 return;
186 }
187
188 $display_for_known_users = $this->model->display_for_known_users();
189 $locations = $this->model->locations;
190 // Default login form.
191 if ( in_array( Recaptcha_Component::DEFAULT_LOGIN_FORM, $locations, true ) ) {
192 add_filter( 'authenticate', array( $this, 'validate_login_recaptcha' ), 9999 );
193 add_action( 'login_form', array( $this, 'display_login_recaptcha' ) );
194 add_filter( 'wp_authenticate_user', array( $this, 'validate_captcha_field_on_login' ), 8 );
195 }
196 // Default register form.
197 if ( in_array( Recaptcha_Component::DEFAULT_REGISTER_FORM, $locations, true ) ) {
198 if ( ! is_multisite() ) {
199 add_action( 'register_form', array( $this, 'display_login_recaptcha' ) );
200 add_filter( 'registration_errors', array( $this, 'validate_captcha_field_on_registration' ), 10 );
201 } else {
202 add_action( 'signup_extra_fields', array( $this, 'display_signup_recaptcha' ) );
203 add_action( 'signup_blogform', array( $this, 'display_signup_recaptcha' ) );
204 add_filter(
205 'wpmu_validate_user_signup',
206 array(
207 $this,
208 'validate_captcha_field_on_wpmu_registration',
209 ),
210 10
211 );
212 }
213 }
214 // Default lost password form.
215 if ( in_array( Recaptcha_Component::DEFAULT_LOST_PASSWORD_FORM, $locations, true ) ) {
216 add_action( 'lostpassword_form', array( $this, 'display_login_recaptcha' ) );
217 if ( $this->maybe_validate_captcha_for_lostpassword() ) {
218 add_action( 'lostpassword_post', array( $this, 'validate_captcha_field_on_lostpassword' ) );
219 }
220 }
221 // Default comment form.
222 if ( $display_for_known_users && in_array( Recaptcha_Component::DEFAULT_COMMENT_FORM, $locations, true ) ) {
223 // @since v3.4.0 Change from 'comment_form_after_fields' to 'comment_form_defaults'.
224 add_filter( 'comment_form_defaults', array( $this, 'comment_form_defaults' ), 10 );
225 add_action( 'pre_comment_on_post', array( $this, 'validate_captcha_field_on_comment' ) );
226 // When comments are loaded via Hummingbird's lazy load feature.
227 if ( $this->is_lazy_load_comments_enabled() ) {
228 add_action( 'wp_footer', array( $this, 'add_scripts_for_lazy_load' ) );
229 }
230 }
231 // Todo: move code to related class.
232 // For Woo forms. Mandatory check for the activated Woo before.
233 if ( $this->model->check_woo_locations( $this->is_woo_activated ) ) {
234 $woo_locations = $this->model->woo_checked_locations;
235 // Woo login form.
236 if ( in_array( Woocommerce::WOO_LOGIN_FORM, $woo_locations, true ) ) {
237 add_action( 'woocommerce_login_form', array( $this, 'display_login_recaptcha' ) );
238 add_filter(
239 'woocommerce_process_login_errors',
240 array(
241 $this,
242 'validate_captcha_field_on_woo_login',
243 ),
244 10
245 );
246 }
247 // Woo register form.
248 if ( in_array( Woocommerce::WOO_REGISTER_FORM, $woo_locations, true ) ) {
249 add_action( 'woocommerce_register_form', array( $this, 'display_login_recaptcha' ) );
250 add_filter(
251 'woocommerce_registration_errors',
252 array(
253 $this,
254 'validate_captcha_field_on_woo_registration',
255 ),
256 10
257 );
258 }
259 // Woo lost password form.
260 if ( in_array( Woocommerce::WOO_LOST_PASSWORD_FORM, $woo_locations, true ) ) {
261 add_action( 'woocommerce_lostpassword_form', array( $this, 'display_login_recaptcha' ) );
262 // Use default WP hook because Woo doesn't have own hook, so there's the extra check for Woo form.
263 $post_data = defender_get_data_from_request( null, 'p' );
264 if ( isset( $post_data['wc_reset_password'], $post_data['user_login'] ) ) {
265 add_action( 'lostpassword_post', array( $this, 'validate_captcha_field_on_lostpassword' ) );
266 }
267 }
268 // Woo checkout form.
269 if ( $display_for_known_users && in_array( Woocommerce::WOO_CHECKOUT_FORM, $woo_locations, true ) ) {
270 add_action( 'woocommerce_after_checkout_billing_form', array( $this, 'display_login_recaptcha' ) );
271 add_action(
272 'woocommerce_after_checkout_validation',
273 array(
274 $this,
275 'validate_captcha_field_on_woo_checkout',
276 ),
277 10,
278 2
279 );
280 }
281 }
282 // Todo: move code to related class.
283 // For BuddyPress forms. Mandatory check for the activated BuddyPress before.
284 if ( $this->model->check_buddypress_locations( $this->is_buddypress_activated ) ) {
285 $buddypress_locations = $this->model->buddypress_checked_locations;
286 // Register form.
287 if ( in_array( Buddypress::REGISTER_FORM, $buddypress_locations, true ) ) {
288 add_action( 'bp_before_registration_submit_buttons', array( $this, 'display_buddypress_recaptcha' ) );
289 add_filter(
290 'bp_signup_validate',
291 array(
292 $this,
293 'validate_captcha_field_on_buddypress_registration',
294 ),
295 10
296 );
297 }
298 // Group form.
299 if ( $display_for_known_users && in_array( Buddypress::NEW_GROUP_FORM, $buddypress_locations, true ) ) {
300 add_action( 'bp_after_group_details_creation_step', array( $this, 'display_login_recaptcha' ) );
301 add_action( 'groups_group_before_save', array( $this, 'validate_captcha_field_on_buddypress_group' ) );
302 }
303 }
304 // @since 2.5.6
305 do_action( 'wd_recaptcha_after_actions', $display_for_known_users );
306 }
307
308 /**
309 * Validates the reCAPTCHA response for the login form.
310 *
311 * @param null|WP_Error $error WP_Error object if validation fails, else null.
312 *
313 * @return null|WP_Error WP_Error object if validation fails else null.
314 */
315 public function validate_login_recaptcha( $error ) {
316 // Check if the $_POST array is not empty and if 'g-recaptcha-response' key is also empty.
317 $recaptcha_response = defender_get_data_from_request( 'g-recaptcha-response', 'p' );
318 if ( ! empty( $recaptcha_response ) && empty( $recaptcha_response ) ) {
319 $code = 'recaptcha_error';
320 $message = __( 'Please verify that you are not a robot.', 'defender-security' );
321
322 if ( is_wp_error( $error ) ) {
323 $error->add( $code, $message );
324 } else {
325 // Replace $user with a new WP_Error object with an error message.
326 $error = new WP_Error( $code, $message );
327 }
328 }
329
330 // Return the $error variable.
331 return $error;
332 }
333
334 /**
335 * Modifies the script loader tag for the 'wpdef_recaptcha_api' handle.
336 *
337 * @param string $tag The original script loader tag.
338 * @param string $handle The handle being loaded.
339 *
340 * @return string The modified script loader tag.
341 */
342 public function script_loader_tag( string $tag, string $handle ) {
343 if ( 'wpdef_recaptcha_api' === $handle ) {
344 $tag = str_replace( ' src', ' data-cfasync="false" async="async" defer="defer" src', $tag );
345 }
346
347 return $tag;
348 }
349
350 /**
351 * Returns the API URL for reCAPTCHA based on the recaptcha_type property.
352 *
353 * @return string The API URL for reCAPTCHA. Returns an empty string if the recaptcha_type is not set.
354 */
355 protected function get_api_url(): string {
356 if ( isset( $this->recaptcha_type ) ) {
357 if ( 'v3_recaptcha' === $this->recaptcha_type ) {
358 return sprintf( 'https://www.google.com/recaptcha/api.js?hl=%s&render=%s', $this->language, $this->public_key );
359 } elseif ( in_array( $this->recaptcha_type, array( 'v2_checkbox', 'v2_invisible' ), true ) ) {
360 return sprintf( 'https://www.google.com/recaptcha/api.js?hl=%s&render=explicit', $this->language );
361 }
362 }
363
364 return '';
365 }
366
367 /**
368 * Enqueues the necessary scripts for the reCAPTCHA frontend.
369 *
370 * @return void
371 */
372 public function add_scripts(): void {
373 if ( isset( $this->recaptcha_type ) ) {
374 $this->service->remove_dublicate_scripts();
375 }
376
377 wp_enqueue_script(
378 'wpdef_recaptcha_script',
379 plugins_url( 'assets/js/recaptcha_frontend.js', WP_DEFENDER_FILE ),
380 array(
381 'jquery',
382 'wpdef_recaptcha_api',
383 ),
384 DEFENDER_VERSION,
385 true
386 );
387 // @since 2.5.6
388 do_action( 'wd_recaptcha_extra_assets' );
389
390 $error_text = esc_html__(
391 'More than one reCAPTCHA has been found in the current form. Please remove all unnecessary reCAPTCHA fields to make it work properly.',
392 'defender-security'
393 );
394 $options = array(
395 'hl' => $this->language,
396 'size' => $this->recaptcha_size,
397 'version' => $this->recaptcha_type,
398 'sitekey' => $this->public_key,
399 'error' => sprintf( '<strong>%s</strong>:&nbsp;%s', esc_html__( 'Warning', 'defender-security' ), $error_text ),
400 // For default comment form.
401 'disable' => '',
402 );
403
404 if ( 'v2_checkbox' === $this->recaptcha_type ) {
405 $options['theme'] = $this->recaptcha_theme;
406 }
407
408 wp_localize_script(
409 'wpdef_recaptcha_script',
410 'WPDEF',
411 array(
412 'options' => $options,
413 'vars' => array(
414 'visibility' => ( 'login_footer' === current_filter() ),
415 ),
416 )
417 );
418 }
419
420 /**
421 * Add scripts when comments are lazy loaded.
422 *
423 * @return void
424 * @since 2.6.1
425 */
426 public function add_scripts_for_lazy_load(): void {
427 if (
428 in_array( $this->recaptcha_type, array( 'v2_checkbox', 'v2_invisible' ), true )
429 && ( is_single() || is_page() )
430 && comments_open()
431 ) {
432 if ( ! wp_script_is( 'wpdef_recaptcha_api', 'registered' ) ) {
433 $api_url = $this->get_api_url();
434 $deps = array( 'jquery' );
435 wp_register_script( 'wpdef_recaptcha_api', $api_url, $deps, DEFENDER_VERSION, true );
436 }
437
438 $this->add_scripts();
439 }
440 }
441
442 /**
443 * Display the reCAPTCHA field.
444 *
445 * @return void
446 */
447 public function display_login_recaptcha(): void {
448 if ( 'v2_checkbox' === $this->recaptcha_type ) {
449 $from_width = 302; ?>
450 <style media="screen">
451 .login-action-login #loginform,
452 .login-action-lostpassword #lostpasswordform,
453 .login-action-register #registerform {
454 width: <?php echo (int) $from_width; ?>px !important;
455 }
456
457 #login_error,
458 .message {
459 width: <?php echo (int) $from_width + 20; ?>px !important;
460 }
461
462 .login-action-login #loginform .recaptcha_wrap,
463 .login-action-lostpassword #lostpasswordform .recaptcha_wrap,
464 .login-action-register #registerform .recaptcha_wrap {
465 margin-bottom: 10px;
466 }
467
468 #group-create-body .recaptcha_wrap {
469 margin-top: 15px;
470 }
471 </style>
472 <?php
473 } elseif ( 'v2_invisible' === $this->recaptcha_type ) {
474 ?>
475 <style>
476 .login-action-lostpassword #lostpasswordform .recaptcha_wrap,
477 .login-action-login #loginform .recaptcha_wrap,
478 .login-action-register #registerform .recaptcha_wrap {
479 margin-bottom: 10px;
480 }
481
482 #signup-content .recaptcha_wrap,
483 #group-create-body .recaptcha_wrap {
484 margin-top: 10px;
485 }
486 </style>
487 <?php
488 }
489 $allowed_html = array(
490 'div' => array(
491 'class' => array(),
492 'id' => array(),
493 ),
494 'iframe' => array(
495 'src' => array(),
496 'frameborder' => array(),
497 'scrolling' => array(),
498 'style' => array(),
499 ),
500 'noscript' => array(),
501 'textarea' => array(
502 'name' => array( 'g-recaptcha-response' ),
503 'class' => array( 'g-recaptcha-response' ),
504 'style' => array(),
505 ),
506 'input' => array(
507 'type' => array( 'hidden' ),
508 'class' => array( 'g-recaptcha-response' ),
509 'name' => array( 'g-recaptcha-response' ),
510 ),
511 );
512 echo wp_kses( $this->display_recaptcha(), $allowed_html );
513 }
514
515 /**
516 * Display the output of the recaptcha.
517 *
518 * @return string
519 */
520 protected function display_recaptcha(): string {
521 $deps = null;
522 $content = '<div class="recaptcha_wrap wpdef_recaptcha_' . $this->recaptcha_type . '">';
523 if ( ! $this->private_key || ! $this->public_key || empty( $this->recaptcha_type ) ) {
524 // Display nothing.
525 $content .= '</div>';
526
527 return $content;
528 }
529
530 $api_url = $this->get_api_url();
531
532 // Generate random id value if there's content with pagination plugin for not getting duplicate id values.
533 $id = Crypt::random_int( 0, mt_getrandmax() );
534 if ( in_array( $this->recaptcha_type, array( 'v2_checkbox', 'v2_invisible' ), true ) ) {
535 $content .= '<div id="wpdef_recaptcha_' . $id . '" class="wpdef_recaptcha"></div>
536 <noscript>
537 <div style="width: 302px;">
538 <div style="width: 302px; height: 422px; position: relative;">
539 <div style="width: 302px; height: 422px; position: absolute;">
540 <iframe src="https://www.google.com/recaptcha/api/fallback?k=' . esc_attr( $this->public_key ) . '" frameborder="0" scrolling="no" style="width: 302px; height:422px; border-style: none;"></iframe>
541 </div>
542 </div>
543 <div style="border-style: none; bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px; background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px; height: 60px; width: 300px;">
544 <textarea name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px !important; height: 40px !important; border: 1px solid #c1c1c1 !important; margin: 10px 25px !important; padding: 0px !important; resize: none !important;"></textarea>
545 </div>
546 </div>
547 </noscript>';
548 $deps = array( 'jquery' );
549 } elseif ( 'v3_recaptcha' === $this->recaptcha_type ) {
550 $content .= '<input type="hidden" class="g-recaptcha-response" name="g-recaptcha-response" />';
551 }
552 $content .= '</div>';
553
554 // Register reCAPTCHA script.
555 $locations = $this->model->locations;
556 if ( ! wp_script_is( 'wpdef_recaptcha_api', 'registered' ) ) {
557
558 if ( 'v3_recaptcha' === $this->recaptcha_type ) {
559 wp_register_script( 'wpdef_recaptcha_api', $api_url, array(), DEFENDER_VERSION, false );
560 } else {
561 wp_register_script( 'wpdef_recaptcha_api', $api_url, $deps, DEFENDER_VERSION, true );
562 }
563 add_action( 'wp_footer', array( $this, 'add_scripts' ) );
564 if (
565 in_array( Recaptcha_Component::DEFAULT_LOGIN_FORM, $locations, true )
566 || in_array( Recaptcha_Component::DEFAULT_REGISTER_FORM, $locations, true )
567 || in_array( Recaptcha_Component::DEFAULT_LOST_PASSWORD_FORM, $locations, true )
568 ) {
569 add_action( 'login_footer', array( $this, 'add_scripts' ) );
570 }
571 }
572
573 return $content;
574 }
575
576 /**
577 * Check the current page from is from the Woo plugin.
578 *
579 * @retun bool
580 */
581 protected function is_woocommerce_page(): bool {
582 if ( ! $this->is_woo_activated ) {
583 return false;
584 }
585
586 $traces = debug_backtrace(); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
587 foreach ( $traces as $trace ) {
588 if ( isset( $trace['file'] ) && false !== strpos( $trace['file'], 'woocommerce' ) ) {
589 return true;
590 }
591 }
592
593 return false;
594 }
595
596 /**
597 * Verify the recaptcha code on the Login page.
598 *
599 * @param WP_User|WP_Error $user WP_User or WP_Error object if a previous callback failed authentication.
600 *
601 * @return WP_Error|WP_User
602 */
603 public function validate_captcha_field_on_login( $user ) {
604 if ( $this->is_woocommerce_page() ) {
605 return $user;
606 }
607 // Skip check if connecting to XMLRPC.
608 if ( defined( 'XMLRPC_REQUEST' ) ) {
609 return $user;
610 }
611 // Is Recaptcha-request from 'Ultimate Member' plugin?
612 if ( ! empty( defender_get_data_from_request( 'um_request', 'p' ) ) && function_exists( 'um_recaptcha_validate' ) ) {
613 return $user;
614 }
615
616 if ( ! $this->recaptcha_response( 'default_login' ) ) {
617 if ( is_wp_error( $user ) ) {
618 $user->add( 'invalid_captcha', $this->service->error_message() );
619
620 return $user;
621 }
622 return new WP_Error( 'invalid_captcha', $this->service->error_message() );
623 }
624
625 return $user;
626 }
627
628 /**
629 * Verify the recaptcha code on the Registration page.
630 *
631 * @param WP_Error $errors A WP_Error object containing any errors encountered during registration.
632 *
633 * @return WP_Error
634 */
635 public function validate_captcha_field_on_registration( WP_Error $errors ) {
636 // Skip check if connecting to XMLRPC.
637 if ( defined( 'XMLRPC_REQUEST' ) ) {
638 return $errors;
639 }
640
641 if ( ! $this->recaptcha_response( 'default_registration' ) ) {
642 $errors->add( 'invalid_captcha', $this->service->error_message() );
643 }
644 $_POST['g-recaptcha-response-check'] = true;
645
646 return $errors;
647 }
648
649 /**
650 * Add google recaptcha to the multisite signup form.
651 *
652 * @param WP_Error $errors A WP_Error object possibly containing 'blogname' or 'blog_title' errors.
653 *
654 * @return void
655 */
656 public function display_signup_recaptcha( WP_Error $errors ): void {
657 $error_message = $errors->get_error_message( 'invalid_captcha' );
658 if ( ! empty( $error_message ) ) {
659 printf( '<p class="error">%s</p>', wp_kses_post( $error_message ) );
660 }
661 echo wp_kses_post( $this->display_recaptcha() );
662 }
663
664 /**
665 * Verify the recaptcha code on the multisite signup page.
666 *
667 * @param array $result An array of errors.
668 *
669 * @return array
670 */
671 public function validate_captcha_field_on_wpmu_registration( array $result ): array {
672 global $current_user;
673 if ( is_admin() && ! defined( 'DOING_AJAX' ) && ! empty( $current_user->data->ID ) ) {
674 return $result;
675 }
676
677 if ( ! $this->recaptcha_response( 'wpmu_registration' ) ) {
678 if ( isset( $result['errors'] ) && ! empty( $result['errors'] ) ) {
679 $errors = $result['errors'];
680 } else {
681 $errors = new WP_Error();
682 }
683 $errors->add( 'invalid_captcha', $this->service->error_message() );
684 $result['errors'] = $errors;
685
686 return $result;
687 }
688
689 return $result;
690 }
691
692 /**
693 * Verify the recaptcha code on Woo login page.
694 *
695 * @param WP_Error $errors A WP_Error object containing any errors encountered during login.
696 *
697 * @return WP_Error
698 */
699 public function validate_captcha_field_on_woo_login( WP_Error $errors ) {
700 // Skip check if connecting to XMLRPC.
701 if ( defined( 'XMLRPC_REQUEST' ) ) {
702 return $errors;
703 }
704
705 if ( ! $this->recaptcha_response( 'woo_login' ) ) {
706 // Remove 'Error: ' because Woo has it by default.
707 $message = str_replace(
708 sprintf( '<strong>%s:</strong> ', esc_html__( 'Error', 'defender-security' ) ),
709 '',
710 $this->service->error_message()
711 );
712 $errors->add( 'invalid_captcha', $message );
713 }
714
715 return $errors;
716 }
717
718 /**
719 * Check recaptcha on Woo registration form.
720 *
721 * @param WP_Error $errors A WP_Error object containing any errors encountered during registration.
722 *
723 * @return WP_Error
724 */
725 public function validate_captcha_field_on_woo_registration( WP_Error $errors ) {
726 if ( defined( 'WOOCOMMERCE_CHECKOUT' ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
727 return $errors;
728 }
729 if ( ! $this->recaptcha_response( 'woo_registration' ) ) {
730 // Remove 'Error: ' because Woo has it by default.
731 $message = str_replace(
732 sprintf( '<strong>%s:</strong> ', esc_html__( 'Error', 'defender-security' ) ),
733 '',
734 $this->service->error_message()
735 );
736 $errors->add( 'invalid_captcha', $message );
737 }
738
739 return $errors;
740 }
741
742 /**
743 * Fires before errors are returned from a password reset request.
744 * Without 2nd `$user_data` parameter because it's since WP 5.4.0.
745 *
746 * @param WP_Error $errors A WP_Error object containing any errors encountered during password reset.
747 *
748 * @return void
749 */
750 public function validate_captcha_field_on_lostpassword( WP_Error $errors ): void {
751 if ( ! $this->recaptcha_response( 'default_lost_password' ) ) {
752 $errors->add( 'invalid_captcha', $this->service->error_message() );
753 }
754 }
755
756 /**
757 * Validates the reCAPTCHA field on the WooCommerce checkout form.
758 *
759 * @param array $fields The fields of the checkout form.
760 * @param WP_Error $errors The errors encountered during the checkout process.
761 *
762 * @return WP_Error The updated errors with the reCAPTCHA validation result.
763 */
764 public function validate_captcha_field_on_woo_checkout( $fields, $errors ): WP_Error {
765 if ( ! $this->recaptcha_response( 'woo_checkout' ) ) {
766 // Remove 'Error: ' because Woo has it by default.
767 $message = str_replace(
768 sprintf( '<strong>%s:</strong> ', esc_html__( 'Error', 'defender-security' ) ),
769 '',
770 $this->service->error_message()
771 );
772 $errors->add( 'invalid_captcha', $message );
773 }
774
775 return $errors;
776 }
777
778 /**
779 * Display google recaptcha on comments form.
780 *
781 * @param array $defaults The default comment form arguments.
782 *
783 * @return array
784 */
785 public function comment_form_defaults( array $defaults ) {
786 $defaults['comment_notes_after'] .= '<p>' . $this->display_recaptcha() . '</p>';
787
788 return $defaults;
789 }
790
791 /**
792 * Check JS enabled for comment form.
793 *
794 * @param int $comment_post_id Post ID.
795 *
796 * @return void
797 */
798 public function validate_captcha_field_on_comment( $comment_post_id ) {
799 if ( $this->service->exclude_recaptcha_for_requests() ) {
800 return;
801 }
802 // Skip if it's from WooCommerce review form.
803 if ( 'product' === get_post_type( $comment_post_id ) ) {
804 return;
805 }
806
807 if ( ! $this->recaptcha_response( 'default_comments' ) ) {
808 // @since v2.5.6
809 wp_die(
810 wp_kses_post(
811 apply_filters(
812 'wd_recaptcha_require_valid_comment',
813 $this->service->error_message()
814 )
815 )
816 );
817 }
818 }
819
820 /**
821 * Validates the reCAPTCHA response for a given form.
822 *
823 * @param string $form The form being verified.
824 *
825 * @return bool The result of the reCAPTCHA verification.
826 * @since 2.5.6
827 */
828 protected function recaptcha_response( string $form ): bool {
829 $response = stripslashes( defender_get_data_from_request( 'g-recaptcha-response', 'p' ) );
830 if ( empty( $this->private_key ) || empty( $response ) ) {
831 return false;
832 }
833 // reCAPTCHA response post data.
834 $remote_ip = filter_var( defender_get_data_from_request( 'REMOTE_ADDR', 's' ), FILTER_VALIDATE_IP );
835 /**
836 * Filters to Get the remote IP address.
837 *
838 * @param mixed $remote_ip The remote IP address.
839 *
840 * @since 2.5.6
841 */
842 $remote_ip = (string) apply_filters( 'wd_recaptcha_remote_ip', $remote_ip );
843
844 $post_body = array(
845 'secret' => $this->private_key,
846 'response' => $response,
847 'remoteip' => $remote_ip,
848 );
849
850 $result = $this->service->recaptcha_post_request( $post_body );
851
852 /**
853 * Filters to check the result of a reCAPTCHA verification.
854 *
855 * @param mixed $result The result of the reCAPTCHA verification.
856 * @param mixed $form The form being verified.
857 *
858 * @since 2.5.6
859 */
860 return apply_filters( 'wd_recaptcha_check_result', $result, $form );
861 }
862
863 /**
864 * Display the BuddyPress reCAPTCHA.
865 *
866 * @return void
867 */
868 public function display_buddypress_recaptcha(): void {
869 if ( ! empty( buddypress()->signup->errors['failed_recaptcha_verification'] ) ) {
870 $output = '<div class="error">';
871 $output .= buddypress()->signup->errors['failed_recaptcha_verification'];
872 $output .= '</div>';
873
874 echo wp_kses_post( $output );
875 }
876 echo wp_kses_post( $this->display_recaptcha() );
877 }
878
879 /**
880 * Validates the reCAPTCHA field on the BuddyPress registration form.
881 *
882 * @return void
883 */
884 public function validate_captcha_field_on_buddypress_registration(): void {
885 if ( ! $this->recaptcha_response( 'buddypress_registration' ) ) {
886 buddypress()->signup->errors['failed_recaptcha_verification'] = $this->service->error_message();
887 }
888 }
889
890 /**
891 * Verify BuddyPress group form captcha.
892 *
893 * @return bool|void
894 */
895 public function validate_captcha_field_on_buddypress_group() {
896 if ( ! bp_is_group_creation_step( 'group-details' ) ) {
897 return false;
898 }
899
900 if ( ! $this->recaptcha_response( 'buddypress_create_group' ) ) {
901 bp_core_add_message( $this->service->error_message(), 'error' );
902 bp_core_redirect( bp_get_root_domain() . '/' . bp_get_groups_root_slug() . '/create/step/group-details/' );
903 } else {
904 return false;
905 }
906 }
907
908 /**
909 * Provide data to the frontend via localized script.
910 *
911 * @param array $data Data collection is ready to passed.
912 *
913 * @return array Modified data array with added this controller data.
914 */
915 public function script_data( array $data ): array {
916 $data['recaptcha'] = $this->data_frontend();
917
918 return $data;
919 }
920
921 /**
922 * Save settings.
923 *
924 * @param Request $request The request object containing new settings data.
925 *
926 * @return Response
927 * @defender_route
928 */
929 public function save_settings( Request $request ): Response {
930 $data = $request->get_data_by_model( $this->model );
931 $this->model->import( $data );
932 if ( $this->model->validate() ) {
933 $this->model->save();
934 Config_Hub_Helper::set_clear_active_flag();
935
936 return new Response(
937 true,
938 array_merge(
939 array(
940 'message' => esc_html__( 'Google reCAPTCHA settings saved successfully.', 'defender-security' ),
941 'auto_close' => true,
942 ),
943 $this->data_frontend()
944 )
945 );
946 }
947
948 return new Response(
949 false,
950 // Merge stored data to avoid errors.
951 array_merge(
952 array(
953 'message' => $this->model->get_formatted_errors(),
954 'error_keys' => $this->model->get_error_keys(),
955 ),
956 $this->data_frontend()
957 )
958 );
959 }
960
961 /**
962 * Load the reCAPTCHA preview based on the provided request data.
963 *
964 * @param Request $request The request object containing the data needed to load the preview.
965 *
966 * @return Response The response object with the preview HTML and status.
967 * @defender_route
968 */
969 public function load_recaptcha_preview( Request $request ): Response {
970 $onload = null;
971 $js = null;
972 $data = $request->get_data(
973 array(
974 'captcha_type' => array(
975 'type' => 'string',
976 ),
977 )
978 );
979 $this->recaptcha_type = $data['captcha_type'];
980
981 $model = $this->model;
982 $language = ! empty( $model->language ) && 'automatic' !== $model->language ? $model->language : get_locale();
983
984 $notice = '<div class="sui-notice sui-notice-default">';
985 $notice .= '<div class="sui-notice-content">';
986 $notice .= '<div class="sui-notice-message">';
987 $notice .= '<i class="sui-notice-icon sui-icon-info sui-md" aria-hidden="true"></i>';
988 $notice .= '<p>' . esc_html__( 'Save your API keys to load the reCAPTCHA preview.', 'defender-security' ) . '</p>';
989 $notice .= '</div>';
990 $notice .= '</div>';
991 $notice .= '</div>';
992
993 $theme = 'light';
994 $captcha_size = 'invisible';
995 $data_args = '';
996 if ( 'v2_checkbox' === $this->recaptcha_type ) {
997 $this->public_key = $model->data_v2_checkbox['key'];
998 $theme = $model->data_v2_checkbox['style'];
999 $captcha_size = $model->data_v2_checkbox['size'];
1000 $onload = 'defender_render_admin_captcha_v2';
1001 // Onload method for Recaptcha works only for js 'var'.
1002 $js = "<script>var defender_render_admin_captcha_v2 = function () {
1003 setTimeout( function () {
1004 var captcha_v2 = jQuery( '.defender-g-recaptcha-v2_checkbox' ),
1005 sitekey_v2 = captcha_v2.data('sitekey'),
1006 theme_v2 = captcha_v2.data('theme'),
1007 size_v2 = captcha_v2.data('size')
1008 ;
1009 window.grecaptcha.render( captcha_v2[0], {
1010 sitekey: sitekey_v2,
1011 theme: theme_v2,
1012 size: size_v2,
1013 'error-callback': function() {
1014 jQuery('#v2_checkbox_notice_1').hide();
1015 jQuery('#v2_checkbox_notice_2').show();
1016 }
1017 } );
1018 }, 100 );
1019 };</script>";
1020 } elseif ( 'v2_invisible' === $this->recaptcha_type ) {
1021 $this->public_key = $model->data_v2_invisible['key'];
1022 $onload = 'defender_render_admin_captcha_v2_invisible';
1023 $data_args = 'data-badge="inline" data-callback="setResponse"';
1024 $js = "<script>var defender_render_admin_captcha_v2_invisible = function () {
1025 setTimeout( function () {
1026 var captcha = jQuery( '.defender-g-recaptcha-v2_invisible' ),
1027 sitekey = captcha.data('sitekey'),
1028 theme = captcha.data('theme'),
1029 size = captcha.data('size')
1030 ;
1031 window.grecaptcha.render( captcha[0], {
1032 sitekey: sitekey,
1033 theme: theme,
1034 size: size,
1035 badge: 'inline',
1036 'error-callback': function() {
1037 jQuery('#v2_invisible_notice_1').hide();
1038 jQuery('#v2_invisible_notice_2').show();
1039 }
1040 } );
1041 }, 100 );
1042 };</script>";
1043 } elseif ( 'v3_recaptcha' === $this->recaptcha_type ) {
1044 $this->public_key = $model->data_v3_recaptcha['key'];
1045 $onload = 'defender_render_admin_captcha_v3';
1046 $js = "<script>var defender_render_admin_captcha_v3 = function () {
1047 setTimeout( function () {
1048 var captcha = jQuery( '.defender-g-recaptcha-v3_recaptcha' ),
1049 sitekey = captcha.data('sitekey'),
1050 theme = captcha.data('theme'),
1051 size = captcha.data('size')
1052 ;
1053 window.grecaptcha.render( captcha[0], {
1054 sitekey: sitekey,
1055 theme: theme,
1056 size: size,
1057 badge: 'inline',
1058 'error-callback': function() {
1059 jQuery('#v3_recaptcha_notice_1').hide();
1060 jQuery('#v3_recaptcha_notice_2').show();
1061 }
1062 } );
1063 }, 100 );
1064 };</script>";
1065 }
1066
1067 $html = '';
1068 if ( isset( $this->recaptcha_type ) && ! empty( $this->public_key ) ) {
1069 // Google reCAPTCHA script must be connected with dynamic parameters (onload, render, etc.).
1070 $html .= '<script src="https://www.google.com/recaptcha/api.js?hl=' . esc_attr( $language ) . '&render=explicit&onload=' . $onload . '" async defer></script>' . $js; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
1071
1072 $html .= sprintf(
1073 '<div class="%s" data-sitekey="%s" data-theme="%s" data-size="%s" %s></div>',
1074 'defender-g-recaptcha-' . $this->recaptcha_type,
1075 esc_attr( $this->public_key ),
1076 esc_attr( $theme ),
1077 esc_attr( $captcha_size ),
1078 $data_args
1079 );
1080 } else {
1081 $html .= $notice;
1082 }
1083
1084 return new Response(
1085 true,
1086 array(
1087 'preview' => true,
1088 'html' => $html,
1089 )
1090 );
1091 }
1092
1093 /**
1094 * Removes settings for all submodules.
1095 */
1096 public function remove_settings() {
1097 }
1098
1099 /**
1100 * Delete all the data & the cache.
1101 */
1102 public function remove_data(): void {
1103 $this->model->delete();
1104 }
1105
1106 /**
1107 * Converts the current object state to an array.
1108 *
1109 * @return array The array representation of the object.
1110 */
1111 public function to_array(): array {
1112 return array();
1113 }
1114
1115 /**
1116 * Provides data for the frontend.
1117 *
1118 * @return array An array of data for the frontend.
1119 */
1120 public function data_frontend(): array {
1121 $model = $this->model;
1122 $is_active = $model->is_active();
1123 /**
1124 * Different cases for entered keys and locations:
1125 * success - one default, Woo or BuddyPress location is checked at least,
1126 * warning - default, Woo and BuddyPress locations are unchecked,
1127 * warning - default location is unchecked, also Woo and BuddyPress is deactivated,
1128 * warning - non-entered keys.
1129 */
1130 if ( $is_active ) {
1131 if ( $this->service->enable_any_location( $this->is_woo_activated, $this->is_buddypress_activated ) ) {
1132 switch ( $model->active_type ) {
1133 case 'v2_invisible':
1134 $type = 'V2 Invisible';
1135 break;
1136 case 'v3_recaptcha':
1137 $type = 'V3';
1138 break;
1139 case 'v2_checkbox':
1140 default:
1141 $type = 'V2 Checkbox';
1142 break;
1143 }
1144 $notice_type = 'success';
1145 $notice_text = sprintf(
1146 /* translators: %s: Type. */
1147 esc_html__( 'Google reCAPTCHA is currently active. %s type has been set successfully.', 'defender-security' ),
1148 $type
1149 );
1150 } elseif ( ! $this->is_woo_activated && ! $this->is_buddypress_activated && ! $model->enable_default_location() ) {
1151 $notice_type = 'warning';
1152 $notice_text = sprintf(
1153 /* translators: %s: Type. */
1154 esc_html__( 'Google reCAPTCHA is currently inactive for all forms. You can deploy reCAPTCHA for specific forms in the %s below.', 'defender-security' ),
1155 '<b>' . esc_html__( 'reCAPTCHA Locations', 'defender-security' ) . '</b>'
1156 );
1157 } elseif (
1158 ! $model->enable_default_location()
1159 && (
1160 ( $this->is_woo_activated && ! $model->enable_woo_location() )
1161 || ( $this->is_buddypress_activated && ! $model->enable_buddypress_location() )
1162 )
1163 ) {
1164 $notice_type = 'warning';
1165 $notice_text = sprintf(
1166 /* translators: 1. reCAPTCHA & Woo location. 2. BuddyPress location. */
1167 esc_html__( 'Google reCAPTCHA is currently inactive for all forms. You can deploy reCAPTCHA for specific forms in the %1$s or %2$s settings below.', 'defender-security' ),
1168 '<b>' . esc_html__( 'reCAPTCHA Locations', 'defender-security' ) . '</b>, <b>' . esc_html__( 'WooCommerce', 'defender-security' ) . '</b>',
1169 '<b>' . esc_html__( 'BuddyPress', 'defender-security' ) . '</b>'
1170 );
1171 }
1172 } else {
1173 // Inactive case.
1174 $notice_type = 'warning';
1175 $notice_text = esc_html__(
1176 'Google reCAPTCHA is currently inactive. Enter your Site and Secret keys and save your settings to finish setup.',
1177 'defender-security'
1178 );
1179 }
1180
1181 /**
1182 * Cases:
1183 * Invalid domain for Site Key,
1184 * Google ReCAPTCHA is in localhost,
1185 * Cannot contact reCAPTCHA. Check your connection.
1186 */
1187 $ticket_text = esc_html__(
1188 'If you see any errors in the preview, make sure the keys you’ve entered are valid, and you\'ve listed your domain name while generating the keys.',
1189 'defender-security'
1190 );
1191
1192 if ( ( new WPMUDEV() )->show_support_links() ) {
1193 $ticket_text .= defender_support_ticket_text();
1194 }
1195
1196 return array_merge(
1197 array(
1198 'model' => $model->export(),
1199 'is_active' => $is_active,
1200 'default_message' => $this->default_msg,
1201 'default_locations' => Recaptcha_Component::get_forms(),
1202 'notice_type' => $notice_type,
1203 'notice_text' => $notice_text,
1204 'ticket_text' => $ticket_text,
1205 'is_woo_active' => $this->is_woo_activated,
1206 'woo_locations' => Woocommerce::get_forms(),
1207 'is_buddypress_active' => $this->is_buddypress_activated,
1208 'buddypress_locations' => Buddypress::get_forms(),
1209 ),
1210 $this->dump_routes_and_nonces()
1211 );
1212 }
1213
1214 /**
1215 * Provides data for the dashboard widget.
1216 *
1217 * @return array An array of dashboard widget data.
1218 */
1219 public function dashboard_widget(): array {
1220 $model = $this->model;
1221 $notice_type = ( $model->is_active()
1222 && $this->service->enable_any_location(
1223 $this->is_woo_activated,
1224 $this->is_buddypress_activated
1225 )
1226 )
1227 ? 'success'
1228 : 'warning';
1229
1230 return array(
1231 'model' => $model->export(),
1232 'notice_type' => $notice_type,
1233 );
1234 }
1235
1236 /**
1237 * Imports data into the model.
1238 *
1239 * @param array $data Data to be imported into the model.
1240 */
1241 public function import_data( array $data ) {
1242 $model = $this->model;
1243
1244 $model->import( $data );
1245 if ( $model->validate() ) {
1246 $model->save();
1247 }
1248 }
1249
1250 /**
1251 * Exports strings.
1252 *
1253 * @return array An array of strings.
1254 */
1255 public function export_strings(): array {
1256 return array( $this->model->is_active() ? esc_html__( 'Active', 'defender-security' ) : esc_html__( 'Inactive', 'defender-security' ) );
1257 }
1258
1259 /**
1260 * Maybe validate reCaptcha for lost password.
1261 *
1262 * @return bool
1263 * @since 3.2.0
1264 */
1265 protected function maybe_validate_captcha_for_lostpassword(): bool {
1266 $post_data = defender_get_data_from_request( null, 'p' );
1267 $action = $post_data['action'] ?? '';
1268
1269 return ! $this->is_woocommerce_page() &&
1270 ! isset( $post_data['wc_reset_password'], $post_data['user_login'] ) &&
1271 ! ( is_admin() && 'send-password-reset' === $action ) &&
1272 'pp_ajax_passwordreset' !== $action;
1273 }
1274
1275 /**
1276 * Enable/disable module.
1277 *
1278 * @param Request $request The request object.
1279 *
1280 * @return Response
1281 * @defender_route
1282 * @since 3.12.0
1283 */
1284 public function toggle_module( Request $request ): Response {
1285 $data = $request->get_data(
1286 array(
1287 'enabled' => array(
1288 'type' => 'boolean',
1289 ),
1290 )
1291 );
1292
1293 $this->model->enabled = $data['enabled'];
1294 $this->model->save();
1295
1296 Config_Hub_Helper::set_clear_active_flag();
1297
1298 return new Response(
1299 true,
1300 array_merge(
1301 array(
1302 'message' => esc_html__( 'Google reCAPTCHA settings saved successfully.', 'defender-security' ),
1303 'auto_close' => true,
1304 ),
1305 $this->data_frontend()
1306 )
1307 );
1308 }
1309 }
1310