PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.8.0
Defender Security – Malware Scanner, Login Security & Firewall v5.8.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.8.0, at src/controller/class-captcha.php

1,449 lines 44.8 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 = '' !== $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 esc_attr( $from_width ); ?>px !important;
516 }
517
518 #login_error,
519 .message {
520 width: <?php echo esc_attr( $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 echo wp_kses( $this->display_recaptcha(), $this->get_allowed_html() );
551 }
552
553 /**
554 * Display the output of the recaptcha.
555 *
556 * @return string
557 */
558 protected function display_recaptcha(): string {
559 $deps = null;
560 $content = '<div class="captcha_wrap wpdef_captcha_' . $this->recaptcha_type . '">';
561 if ( ! $this->private_key || ! $this->public_key || '' === $this->recaptcha_type ) {
562 // Display nothing.
563 $content .= '</div>';
564
565 return $content;
566 }
567
568 $api_url = $this->get_api_url();
569
570 // Generate random id value if there's content with pagination plugin for not getting duplicate id values.
571 $id = Crypt::random_int( 0, mt_getrandmax() );
572 if ( in_array( $this->recaptcha_type, array( 'v2_checkbox', 'v2_invisible' ), true ) ) {
573 $content .= '<div id="wpdef_recaptcha_' . $id . '" class="wpdef_recaptcha"></div>
574 <noscript>
575 <div style="width: 302px;">
576 <div style="width: 302px; height: 422px; position: relative;">
577 <div style="width: 302px; height: 422px; position: absolute;">
578 <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>
579 </div>
580 </div>
581 <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;">
582 <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>
583 </div>
584 </div>
585 </noscript>';
586 $deps = array( 'jquery' );
587 } elseif ( 'v3_recaptcha' === $this->recaptcha_type ) {
588 $content .= '<input type="hidden" class="g-recaptcha-response" name="g-recaptcha-response" />';
589 } elseif ( 'turnstile' === $this->recaptcha_type ) {
590 $content .= sprintf( '<div id="wpdef_turnstile_%s"></div>', $id );
591 $content .= '<noscript>';
592 $content .= '<div style="padding: 10px; border: 1px solid #cccccc; background-color: #f9f9f9;margin-top: 10px;">';
593 $content .= sprintf( '<p>%s</p>', esc_html__( 'Please enable JavaScript to complete the security verification.', 'defender-security' ) );
594 $content .= '</div>';
595 $content .= '</noscript>';
596 }
597 $content .= '</div>';
598
599 // Register reCAPTCHA script.
600 $locations = $this->model->locations;
601 if ( ! wp_script_is( 'wpdef_captcha_api', 'registered' ) ) {
602
603 if ( 'v3_recaptcha' === $this->recaptcha_type ) {
604 wp_register_script( 'wpdef_captcha_api', $api_url, array(), DEFENDER_VERSION, false );
605 } else {
606 wp_register_script( 'wpdef_captcha_api', $api_url, $deps, $this->service->is_recaptcha_active_provider() ? DEFENDER_VERSION : null, true );
607 }
608 add_action( 'wp_footer', array( $this, 'add_captcha_scripts' ) );
609 if (
610 in_array( Captcha_Component::DEFAULT_LOGIN_FORM, $locations, true )
611 || in_array( Captcha_Component::DEFAULT_REGISTER_FORM, $locations, true )
612 || in_array( Captcha_Component::DEFAULT_LOST_PASSWORD_FORM, $locations, true )
613 ) {
614 add_action( 'login_footer', array( $this, 'add_captcha_scripts' ) );
615 }
616 }
617
618 return $content;
619 }
620
621 /**
622 * Check the current page from is from the Woo plugin.
623 *
624 * @return bool
625 */
626 protected function is_woocommerce_page(): bool {
627 if ( ! $this->is_woo_activated ) {
628 return false;
629 }
630
631 $traces = debug_backtrace(); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
632 foreach ( $traces as $trace ) {
633 if ( isset( $trace['file'] ) && false !== strpos( $trace['file'], 'woocommerce' ) ) {
634 return true;
635 }
636 }
637
638 return false;
639 }
640
641 /**
642 * Verify the captcha code on the Login page.
643 *
644 * @param WP_User|WP_Error $user WP_User or WP_Error object if a previous callback failed authentication.
645 *
646 * @return WP_Error|WP_User
647 */
648 public function validate_captcha_field_on_login( $user ) {
649 // Only Recaptcha type has Integration fields in v5.7.0.
650 if ( $this->service->is_recaptcha_active_provider() && $this->is_woocommerce_page() ) {
651 return $user;
652 }
653 // Skip check if connecting to XMLRPC.
654 if ( defined( 'XMLRPC_REQUEST' ) ) {
655 return $user;
656 }
657 // Skip turnstile verification for current sprint plan.
658 if ( $this->service->should_skip_turnstile_check() ) {
659 return $user;
660 }
661 // Is Recaptcha-request from 'Ultimate Member' plugin?
662 $um_request = defender_get_data_from_request( 'um_request', 'p' );
663 if ( $this->service->is_recaptcha_active_provider() &&
664 is_string( $um_request ) &&
665 '' !== $um_request &&
666 function_exists( 'um_recaptcha_validate' )
667 ) {
668 return $user;
669 }
670
671 if ( ! $this->captcha_response( 'default_login' ) ) {
672 if ( is_wp_error( $user ) ) {
673 $user->add( 'invalid_captcha', $this->service->error_message() );
674
675 return $user;
676 }
677 return new WP_Error( 'invalid_captcha', $this->service->error_message() );
678 }
679
680 return $user;
681 }
682
683 /**
684 * Verify the recaptcha code on the Registration page.
685 *
686 * @param WP_Error $errors A WP_Error object containing any errors encountered during registration.
687 *
688 * @return WP_Error
689 */
690 public function validate_captcha_field_on_registration( WP_Error $errors ) {
691 // Skip check if connecting to XMLRPC.
692 if ( defined( 'XMLRPC_REQUEST' ) ) {
693 return $errors;
694 }
695
696 if ( ! $this->captcha_response( 'default_registration' ) ) {
697 $errors->add( 'invalid_captcha', $this->service->error_message() );
698 }
699 $_POST['g-recaptcha-response-check'] = true;
700
701 return $errors;
702 }
703
704 /**
705 * Add captcha to the multisite signup form.
706 *
707 * @param WP_Error $errors A WP_Error object possibly containing 'blogname' or 'blog_title' errors.
708 *
709 * @return void
710 */
711 public function display_signup_captcha( WP_Error $errors ): void {
712 $error_message = $errors->get_error_message( 'invalid_captcha' );
713 if ( is_string( $error_message ) && '' !== $error_message ) {
714 printf( '<p class="error">%s</p>', wp_kses_post( $error_message ) );
715 }
716 echo wp_kses( $this->display_recaptcha(), $this->get_allowed_html() );
717 }
718
719 /**
720 * Verify the recaptcha code on the multisite signup page.
721 *
722 * @param array $result An array of errors.
723 *
724 * @return array
725 */
726 public function validate_captcha_field_on_wpmu_registration( array $result ): array {
727 global $current_user;
728 if ( is_admin() && ! defined( 'DOING_AJAX' ) && isset( $current_user->data->ID ) && 0 < (int) $current_user->data->ID ) {
729 return $result;
730 }
731
732 // Skip if BuddyPress is handling the registration.
733 if (
734 $this->is_buddypress_activated &&
735 $this->service->is_recaptcha_active_provider() &&
736 in_array( Buddypress::REGISTER_FORM, $this->model->buddypress_checked_locations, true )
737 ) {
738 return $result;
739 }
740
741 if ( ! $this->captcha_response( 'wpmu_registration' ) ) {
742 if ( isset( $result['errors'] ) && $result['errors'] instanceof \WP_Error ) {
743 $errors = $result['errors'];
744 } else {
745 $errors = new WP_Error();
746 }
747 $errors->add( 'invalid_captcha', $this->service->error_message() );
748 $result['errors'] = $errors;
749
750 return $result;
751 }
752
753 return $result;
754 }
755
756 /**
757 * Verify the recaptcha code on Woo login page.
758 *
759 * @param WP_Error $errors A WP_Error object containing any errors encountered during login.
760 *
761 * @return WP_Error
762 */
763 public function validate_captcha_field_on_woo_login( WP_Error $errors ) {
764 // Skip check if connecting to XMLRPC.
765 if ( defined( 'XMLRPC_REQUEST' ) ) {
766 return $errors;
767 }
768
769 if ( ! $this->captcha_response( 'woo_login' ) ) {
770 // Remove 'Error: ' because Woo has it by default.
771 $message = str_replace(
772 sprintf( '<strong>%s:</strong> ', esc_html__( 'Error', 'defender-security' ) ),
773 '',
774 $this->service->error_message()
775 );
776 $errors->add( 'invalid_captcha', $message );
777 }
778
779 return $errors;
780 }
781
782 /**
783 * Check recaptcha on Woo registration form.
784 *
785 * @param WP_Error $errors A WP_Error object containing any errors encountered during registration.
786 *
787 * @return WP_Error
788 */
789 public function validate_captcha_field_on_woo_registration( WP_Error $errors ) {
790 if ( defined( 'WOOCOMMERCE_CHECKOUT' ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
791 return $errors;
792 }
793 if ( ! $this->captcha_response( 'woo_registration' ) ) {
794 // Remove 'Error: ' because Woo has it by default.
795 $message = str_replace(
796 sprintf( '<strong>%s:</strong> ', esc_html__( 'Error', 'defender-security' ) ),
797 '',
798 $this->service->error_message()
799 );
800 $errors->add( 'invalid_captcha', $message );
801 }
802
803 return $errors;
804 }
805
806 /**
807 * Fires before errors are returned from a password reset request.
808 * Without 2nd `$user_data` parameter because it's since WP 5.4.0.
809 *
810 * @param WP_Error $errors A WP_Error object containing any errors encountered during password reset.
811 *
812 * @return void
813 */
814 public function validate_captcha_field_on_lostpassword( WP_Error $errors ): void {
815 if ( ! $this->captcha_response( 'default_lost_password' ) ) {
816 $errors->add( 'invalid_captcha', $this->service->error_message() );
817 }
818 }
819
820 /**
821 * Validates the reCAPTCHA field on the WooCommerce checkout form.
822 *
823 * @param array $fields The fields of the checkout form.
824 * @param WP_Error $errors The errors encountered during the checkout process.
825 *
826 * @return WP_Error The updated errors with the reCAPTCHA validation result.
827 */
828 public function validate_captcha_field_on_woo_checkout( $fields, $errors ): WP_Error {
829 if ( ! $this->captcha_response( 'woo_checkout' ) ) {
830 // Remove 'Error: ' because Woo has it by default.
831 $message = str_replace(
832 sprintf( '<strong>%s:</strong> ', esc_html__( 'Error', 'defender-security' ) ),
833 '',
834 $this->service->error_message()
835 );
836 $errors->add( 'invalid_captcha', $message );
837 }
838
839 return $errors;
840 }
841
842 /**
843 * Display google recaptcha on comments form.
844 *
845 * @param array $defaults The default comment form arguments.
846 *
847 * @return array
848 */
849 public function comment_form_defaults( array $defaults ) {
850 $defaults['comment_notes_after'] .= '<p>' . $this->display_recaptcha() . '</p>';
851
852 return $defaults;
853 }
854
855 /**
856 * Check JS enabled for comment form.
857 *
858 * @param int $comment_post_id Post ID.
859 *
860 * @return void
861 */
862 public function validate_captcha_field_on_comment( $comment_post_id ) {
863 if ( $this->service->exclude_captcha_for_requests() ) {
864 return;
865 }
866 // Skip if it's from WooCommerce review form.
867 if ( 'product' === get_post_type( $comment_post_id ) ) {
868 return;
869 }
870
871 if ( ! $this->captcha_response( 'default_comments' ) ) {
872 // @since v2.5.6
873 wp_die(
874 wp_kses_post(
875 apply_filters(
876 'wd_recaptcha_require_valid_comment',
877 $this->service->error_message()
878 )
879 )
880 );
881 }
882 }
883
884 /**
885 * Validates the reCAPTCHA response for a given form.
886 *
887 * @param string $form The form being verified.
888 *
889 * @return bool The result of the reCAPTCHA verification.
890 * @since 2.5.6
891 */
892 protected function captcha_response( string $form ): bool {
893 $response_field = 'turnstile' === $this->recaptcha_type ? 'wpdef-turnstile-response' : 'g-recaptcha-response';
894 $response = stripslashes( defender_get_data_from_request( $response_field, 'p' ) );
895 if ( ! is_string( $this->private_key ) || '' === $this->private_key || '' === $response ) {
896 return false;
897 }
898 // reCAPTCHA response post data.
899 $remote_ip = filter_var( defender_get_data_from_request( 'REMOTE_ADDR', 's' ), FILTER_VALIDATE_IP );
900 /**
901 * Filters to Get the remote IP address.
902 *
903 * @param mixed $remote_ip The remote IP address.
904 *
905 * @since 2.5.6
906 */
907 $remote_ip = (string) apply_filters( 'wd_recaptcha_remote_ip', $remote_ip );
908
909 $post_body = array(
910 'secret' => $this->private_key,
911 'response' => $response,
912 'remoteip' => $remote_ip,
913 );
914
915 $result = $this->service->captcha_post_request( $post_body );
916
917 /**
918 * Filters to check the result of a reCAPTCHA verification.
919 *
920 * @param mixed $result The result of the reCAPTCHA verification.
921 * @param mixed $form The form being verified.
922 *
923 * @since 2.5.6
924 */
925 return apply_filters( 'wd_recaptcha_check_result', $result, $form );
926 }
927
928 /**
929 * Display the BuddyPress reCAPTCHA.
930 *
931 * @return void
932 */
933 public function display_buddypress_recaptcha(): void {
934 $error = '';
935 $obj = buddypress();
936 if ( isset( $obj->signup->errors['failed_recaptcha_verification'] ) && '' !== $obj->signup->errors['failed_recaptcha_verification'] ) {
937 $error = $obj->signup->errors['failed_recaptcha_verification'];
938 }
939
940 if ( '' !== $error ) {
941 $output = '<div class="error">';
942 $output .= $error;
943 $output .= '</div>';
944
945 echo wp_kses_post( $output );
946 }
947
948 echo wp_kses( $this->display_recaptcha(), $this->get_allowed_html() );
949 }
950
951 /**
952 * Validates the reCAPTCHA field on the BuddyPress registration form.
953 *
954 * @return void
955 */
956 public function validate_captcha_field_on_buddypress_registration(): void {
957 if ( ! $this->captcha_response( 'buddypress_registration' ) ) {
958 buddypress()->signup->errors['failed_recaptcha_verification'] = $this->service->error_message();
959 }
960 }
961
962 /**
963 * Verify BuddyPress group form captcha.
964 *
965 * @return bool|void
966 */
967 public function validate_captcha_field_on_buddypress_group() {
968 if ( ! bp_is_group_creation_step( 'group-details' ) ) {
969 return false;
970 }
971
972 if ( ! $this->captcha_response( 'buddypress_create_group' ) ) {
973 bp_core_add_message( $this->service->error_message(), 'error' );
974 bp_core_redirect( bp_get_root_domain() . '/' . bp_get_groups_root_slug() . '/create/step/group-details/' );
975 } else {
976 return false;
977 }
978 }
979
980 /**
981 * Provide data to the frontend via localized script.
982 *
983 * @param array $data Data collection is ready to passed.
984 *
985 * @return array Modified data array with added this controller data.
986 */
987 public function script_data( array $data ): array {
988 $data['captcha'] = $this->data_frontend();
989
990 return $data;
991 }
992
993 /**
994 * Save settings.
995 *
996 * @param Request $request The request object containing new settings data.
997 *
998 * @return Response
999 * @defender_route
1000 */
1001 public function save_settings( Request $request ): Response {
1002 $data = $request->get_data_by_model( $this->model );
1003 $this->model->import( $data );
1004 if ( ! $this->service->is_recaptcha_active_provider() ) {
1005 $valid_token = $request->get_data(
1006 array(
1007 'token' => array(
1008 'type' => 'string',
1009 ),
1010 )
1011 );
1012 $token = $valid_token['token'] ?? '';
1013 $is_valid = $this->service->verify_turnstile_key( $token );
1014 if ( $is_valid ) {
1015 $this->model->active_type = 'turnstile';
1016 } else {
1017 return new Response(
1018 false,
1019 // Merge stored data to avoid errors.
1020 array_merge(
1021 array(
1022 'message' => __( 'Please make sure your Cloudflare API keys are correct and that Preview Widget verification is successful.', 'defender-security' ),
1023 'error_keys' => array( 'data_turnstile.secret' ),
1024 ),
1025 $this->data_frontend()
1026 )
1027 );
1028 }
1029 }
1030 if ( $this->model->validate() ) {
1031 $this->model->save();
1032 Config_Hub_Helper::set_clear_active_flag();
1033
1034 return new Response(
1035 true,
1036 array_merge(
1037 array(
1038 'message' => esc_html__( 'Settings saved successfully!', 'defender-security' ),
1039 'auto_close' => true,
1040 ),
1041 $this->data_frontend()
1042 )
1043 );
1044 }
1045
1046 return new Response(
1047 false,
1048 // Merge stored data to avoid errors.
1049 array_merge(
1050 array(
1051 'message' => $this->model->get_formatted_errors(),
1052 'error_keys' => $this->model->get_error_keys(),
1053 ),
1054 $this->data_frontend()
1055 )
1056 );
1057 }
1058
1059 /**
1060 * Load the CAPTCHA preview based on the provided request data.
1061 *
1062 * @param Request $request The request object containing the data needed to load the preview.
1063 *
1064 * @return Response The response object with the preview HTML and status.
1065 * @defender_route
1066 */
1067 public function load_captcha_preview( Request $request ): Response {
1068 $onload = null;
1069 $js = null;
1070 $data = $request->get_data(
1071 array(
1072 'captcha_type' => array(
1073 'type' => 'string',
1074 ),
1075 )
1076 );
1077 $this->recaptcha_type = $data['captcha_type'];
1078
1079 $model = $this->model;
1080
1081 if ( 'turnstile' === $this->recaptcha_type ) {
1082 return new Response(
1083 $model->is_active(),
1084 array(
1085 'preview' => $model->is_active(),
1086 )
1087 );
1088 }
1089 $language = '' !== $model->language && 'automatic' !== $model->language ? $model->language : get_locale();
1090
1091 $notice = '<div class="sui-notice sui-notice-default">';
1092 $notice .= '<div class="sui-notice-content">';
1093 $notice .= '<div class="sui-notice-message">';
1094 $notice .= '<i class="sui-notice-icon sui-icon-info sui-md" aria-hidden="true"></i>';
1095 $notice .= '<p>' . esc_html__( 'Save your API keys to load the reCAPTCHA preview.', 'defender-security' ) . '</p>';
1096 $notice .= '</div>';
1097 $notice .= '</div>';
1098 $notice .= '</div>';
1099
1100 $theme = 'light';
1101 $captcha_size = 'invisible';
1102 $data_args = '';
1103 if ( 'v2_checkbox' === $this->recaptcha_type ) {
1104 $this->public_key = $model->data_v2_checkbox['key'];
1105 $theme = $model->data_v2_checkbox['style'];
1106 $captcha_size = $model->data_v2_checkbox['size'];
1107 $onload = 'defender_render_admin_captcha_v2';
1108 // Onload method for Recaptcha works only for js 'var'.
1109 $js = "<script>var defender_render_admin_captcha_v2 = function () {
1110 setTimeout( function () {
1111 var captcha_v2 = jQuery( '.defender-g-recaptcha-v2_checkbox' ),
1112 sitekey_v2 = captcha_v2.data('sitekey'),
1113 theme_v2 = captcha_v2.data('theme'),
1114 size_v2 = captcha_v2.data('size');
1115 if (captcha_v2[0] && captcha_v2[0].hasChildNodes()) {
1116 return;
1117 }
1118 window.grecaptcha.render( captcha_v2[0], {
1119 sitekey: sitekey_v2,
1120 theme: theme_v2,
1121 size: size_v2,
1122 'error-callback': function() {
1123 jQuery('#v2_checkbox_notice_1').hide();
1124 jQuery('#v2_checkbox_notice_2').show();
1125 }
1126 } );
1127 }, 100 );
1128 };</script>";
1129 } elseif ( 'v2_invisible' === $this->recaptcha_type ) {
1130 $this->public_key = $model->data_v2_invisible['key'];
1131 $onload = 'defender_render_admin_captcha_v2_invisible';
1132 $data_args = 'data-badge="inline" data-callback="setResponse"';
1133 $js = "<script>var defender_render_admin_captcha_v2_invisible = function () {
1134 setTimeout( function () {
1135 var captcha = jQuery( '.defender-g-recaptcha-v2_invisible' ),
1136 sitekey = captcha.data('sitekey'),
1137 theme = captcha.data('theme'),
1138 size = captcha.data('size');
1139 if (captcha[0] && captcha[0].hasChildNodes()) {
1140 return;
1141 }
1142 window.grecaptcha.render( captcha[0], {
1143 sitekey: sitekey,
1144 theme: theme,
1145 size: size,
1146 badge: 'inline',
1147 'error-callback': function() {
1148 jQuery('#v2_invisible_notice_1').hide();
1149 jQuery('#v2_invisible_notice_2').show();
1150 }
1151 } );
1152 }, 100 );
1153 };</script>";
1154 } elseif ( 'v3_recaptcha' === $this->recaptcha_type ) {
1155 $this->public_key = $model->data_v3_recaptcha['key'];
1156 $onload = 'defender_render_admin_captcha_v3';
1157 $js = "<script>var defender_render_admin_captcha_v3 = function () {
1158 setTimeout( function () {
1159 var captcha = jQuery( '.defender-g-recaptcha-v3_recaptcha' ),
1160 sitekey = captcha.data('sitekey'),
1161 theme = captcha.data('theme'),
1162 size = captcha.data('size');
1163 if (captcha[0] && captcha[0].hasChildNodes()) {
1164 return;
1165 }
1166 window.grecaptcha.render( captcha[0], {
1167 sitekey: sitekey,
1168 theme: theme,
1169 size: size,
1170 badge: 'inline',
1171 'error-callback': function() {
1172 jQuery('#v3_recaptcha_notice_1').hide();
1173 jQuery('#v3_recaptcha_notice_2').show();
1174 }
1175 } );
1176 }, 100 );
1177 };</script>";
1178 }
1179
1180 $html = '';
1181 if ( isset( $this->recaptcha_type ) && '' !== $this->public_key ) {
1182 // Google reCAPTCHA script must be connected with dynamic parameters (onload, render, etc.).
1183 $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
1184
1185 $html .= sprintf(
1186 '<div class="%s" data-sitekey="%s" data-theme="%s" data-size="%s" %s></div>',
1187 'defender-g-recaptcha-' . $this->recaptcha_type,
1188 esc_attr( $this->public_key ),
1189 esc_attr( $theme ),
1190 esc_attr( $captcha_size ),
1191 $data_args
1192 );
1193 } else {
1194 $html .= $notice;
1195 }
1196
1197 return new Response(
1198 true,
1199 array(
1200 'preview' => true,
1201 'html' => $html,
1202 )
1203 );
1204 }
1205
1206 /**
1207 * Removes settings for all submodules.
1208 */
1209 public function remove_settings() {
1210 }
1211
1212 /**
1213 * Delete all the data & the cache.
1214 */
1215 public function remove_data(): void {
1216 $this->model->delete();
1217 }
1218
1219 /**
1220 * Converts the current object state to an array.
1221 *
1222 * @return array The array representation of the object.
1223 */
1224 public function to_array(): array {
1225 return array();
1226 }
1227
1228 /**
1229 * Provides data for the frontend.
1230 *
1231 * @return array An array of data for the frontend.
1232 */
1233 public function data_frontend(): array {
1234 $model = $this->model;
1235 $is_active = $model->is_active();
1236 /**
1237 * Different cases for entered keys and locations:
1238 * success - one default, Woo or BuddyPress location is checked at least,
1239 * warning - default, Woo and BuddyPress locations are unchecked,
1240 * warning - default location is unchecked, also Woo and BuddyPress is deactivated,
1241 * warning - non-entered keys.
1242 */
1243 if ( $is_active ) {
1244 if ( $this->service->enable_any_location( $this->is_woo_activated, $this->is_buddypress_activated ) ) {
1245 $notice_type = 'success';
1246 $notice_text = false;
1247 } elseif ( ! $this->is_woo_activated && ! $this->is_buddypress_activated && ! $model->enable_default_location() ) {
1248 $notice_type = 'warning';
1249 $notice_text = sprintf(
1250 /* translators: 1. Captcha Provider name. 2. Captcha Type */
1251 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' ),
1252 'turnstile' === $model->provider ? esc_html__( 'Cloudflare Turnstile', 'defender-security' ) : esc_html__( 'Google reCAPTCHA', 'defender-security' ),
1253 'turnstile' === $model->provider ? esc_html__( 'Turnstile', 'defender-security' ) : esc_html__( 'reCAPTCHA', 'defender-security' ),
1254 );
1255 } elseif (
1256 ! $model->enable_default_location()
1257 && (
1258 ( $this->is_woo_activated && ! $model->enable_woo_location() )
1259 || ( $this->is_buddypress_activated && ! $model->enable_buddypress_location() )
1260 )
1261 ) {
1262 $notice_type = 'warning';
1263 $notice_text = sprintf(
1264 /* translators: 1. reCAPTCHA & Woo location. 2. BuddyPress location. */
1265 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' ),
1266 '<b>' . esc_html__( 'reCAPTCHA Locations', 'defender-security' ) . '</b>, <b>' . esc_html__( 'WooCommerce', 'defender-security' ) . '</b>',
1267 '<b>' . esc_html__( 'BuddyPress', 'defender-security' ) . '</b>'
1268 );
1269 }
1270 } else {
1271 // Inactive case.
1272 $notice_type = 'error';
1273 $notice_text = esc_html__(
1274 'Configure a CAPTCHA provider to verify users and prevent automated bots. Both reCAPTCHA and Cloudflare Turnstile are supported.',
1275 'defender-security'
1276 );
1277 }
1278
1279 /**
1280 * Cases:
1281 * Invalid domain for Site Key,
1282 * Google ReCAPTCHA is in localhost,
1283 * Cannot contact reCAPTCHA. Check your connection.
1284 */
1285 $ticket_text = esc_html__(
1286 '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.',
1287 'defender-security'
1288 );
1289
1290 if ( ( new WPMUDEV() )->show_support_links() ) {
1291 $ticket_text .= defender_support_ticket_text();
1292 }
1293
1294 return array_merge(
1295 array(
1296 'model' => $model->export(),
1297 'is_active' => $is_active,
1298 'default_message' => $this->default_msg,
1299 'default_turnstile_message' => $this->model->get_default_values()['turnstile_message'],
1300 'default_locations' => Captcha_Component::get_forms(),
1301 'notice_type' => $notice_type,
1302 'notice_text' => $notice_text,
1303 'ticket_text' => $ticket_text,
1304 'is_woo_active' => $this->is_woo_activated,
1305 'woo_locations' => Woocommerce::get_forms(),
1306 'is_buddypress_active' => $this->is_buddypress_activated,
1307 'buddypress_locations' => Buddypress::get_forms(),
1308 ),
1309 $this->dump_routes_and_nonces()
1310 );
1311 }
1312
1313 /**
1314 * Provides data for the dashboard widget.
1315 *
1316 * @return array An array of dashboard widget data.
1317 */
1318 public function dashboard_widget(): array {
1319 $model = $this->model;
1320 $notice_type = ( $model->is_active()
1321 && $this->service->enable_any_location(
1322 $this->is_woo_activated,
1323 $this->is_buddypress_activated
1324 )
1325 )
1326 ? 'success'
1327 : 'warning';
1328
1329 return array(
1330 'model' => $model->export(),
1331 'notice_type' => $notice_type,
1332 );
1333 }
1334
1335 /**
1336 * Imports data into the model.
1337 *
1338 * @param array $data Data to be imported into the model.
1339 */
1340 public function import_data( array $data ) {
1341 $model = $this->model;
1342
1343 $model->import( $data );
1344 if ( $model->validate() ) {
1345 $model->save();
1346 }
1347 }
1348
1349 /**
1350 * Exports strings.
1351 *
1352 * @return array An array of strings.
1353 */
1354 public function export_strings(): array {
1355 return array( $this->model->is_active() ? esc_html__( 'Active', 'defender-security' ) : esc_html__( 'Inactive', 'defender-security' ) );
1356 }
1357
1358 /**
1359 * Maybe validate reCaptcha for lost password.
1360 *
1361 * @return bool
1362 * @since 3.2.0
1363 */
1364 protected function maybe_validate_captcha_for_lostpassword(): bool {
1365 $post_data = defender_get_data_from_request( null, 'p' );
1366 $action = $post_data['action'] ?? '';
1367
1368 return ! $this->is_woocommerce_page() &&
1369 ! isset( $post_data['wc_reset_password'], $post_data['user_login'] ) &&
1370 ! ( is_admin() && 'send-password-reset' === $action ) &&
1371 'pp_ajax_passwordreset' !== $action;
1372 }
1373
1374 /**
1375 * Enable/disable module.
1376 *
1377 * @param Request $request The request object.
1378 *
1379 * @return Response
1380 * @defender_route
1381 * @since 3.12.0
1382 */
1383 public function toggle_module( Request $request ): Response {
1384 $data = $request->get_data(
1385 array(
1386 'enabled' => array(
1387 'type' => 'boolean',
1388 ),
1389 )
1390 );
1391 $prev_state = $this->model->enabled;
1392 $this->model->enabled = $data['enabled'];
1393 $this->model->save();
1394 $message = esc_html__( 'Settings saved successfully!', 'defender-security' );
1395 if ( $prev_state !== $data['enabled'] ) {
1396 if ( $data['enabled'] ) {
1397 $message = esc_html__( 'CAPTCHA module is enabled successfully!', 'defender-security' );
1398 } else {
1399 $message = esc_html__( 'CAPTCHA module is disabled successfully!', 'defender-security' );
1400 }
1401 }
1402
1403 Config_Hub_Helper::set_clear_active_flag();
1404
1405 return new Response(
1406 true,
1407 array_merge(
1408 array(
1409 'message' => $message,
1410 'auto_close' => true,
1411 ),
1412 $this->data_frontend()
1413 )
1414 );
1415 }
1416
1417 /**
1418 * Get allowed HTML tags and attributes for reCaptcha output sanitization.
1419 *
1420 * @return array
1421 */
1422 private function get_allowed_html(): array {
1423 return array(
1424 'div' => array(
1425 'class' => array(),
1426 'id' => array(),
1427 'style' => array(),
1428 ),
1429 'iframe' => array(
1430 'src' => array(),
1431 'frameborder' => array(),
1432 'scrolling' => array(),
1433 'style' => array(),
1434 ),
1435 'noscript' => array(),
1436 'textarea' => array(
1437 'name' => array( 'g-recaptcha-response' ),
1438 'class' => array( 'g-recaptcha-response' ),
1439 'style' => array(),
1440 ),
1441 'input' => array(
1442 'type' => array( 'hidden' ),
1443 'class' => array( 'g-recaptcha-response' ),
1444 'name' => array( 'g-recaptcha-response' ),
1445 ),
1446 );
1447 }
1448 }
1449