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

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

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