PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / trunk
Defender Security – Malware Scanner, Login Security & Firewall vtrunk
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 3.1.1 All 140 releases
defender-security / src / controller / class-captcha.php

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

577 lines 17.0 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_Defender\Event;
11 use Calotes\Component\Request;
12 use Calotes\Component\Response;
13 use WP_Defender\Behavior\WPMUDEV;
14 use WP_Defender\Integrations\Hummingbird;
15 use WP_Defender\Integrations\Buddypress;
16 use WP_Defender\Integrations\Woocommerce;
17 use WP_Defender\Component\Config\Config_Hub_Helper;
18 use WP_Defender\Component\Captcha as Captcha_Component;
19 use WP_Defender\Model\Setting\Captcha as Captcha_Model;
20
21 /**
22 * Handles Captcha related actions.
23 *
24 * @since 2.5.4
25 */
26 class Captcha extends Event {
27
28 /**
29 * The model for handling the data.
30 *
31 * @var Captcha_Model
32 */
33 public $model;
34 /**
35 * Service for handling logic.
36 *
37 * @var Captcha_Component
38 */
39 protected $service;
40
41 /**
42 * Is Woo activated.
43 *
44 * @var bool
45 */
46 private $is_woo_activated;
47
48 /**
49 * Is BuddyPress activated.
50 *
51 * @var bool
52 */
53 private $is_buddypress_activated;
54
55 /**
56 * Buddypress integration module.
57 *
58 * @var Buddypress|null
59 */
60 private ?Buddypress $buddypress;
61
62 /**
63 * Woocommerce integration module.
64 *
65 * @var Woocommerce|null
66 */
67 private ?Woocommerce $woo;
68
69 /**
70 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
71 */
72 public function __construct() {
73 $this->model = wd_di()->get( Captcha_Model::class );
74
75 $this->service = new Captcha_Component( $this->model );
76 $this->register_routes();
77 $this->woo = wd_di()->get( Woocommerce::class );
78 $this->is_woo_activated = $this->woo->is_activated();
79 $this->buddypress = wd_di()->get( Buddypress::class );
80 $this->is_buddypress_activated = $this->buddypress->is_activated();
81
82 if ( $this->model->is_active() // No need the check by Woo and Buddypress are activated because we use this below.
83 && $this->service->enable_any_location( $this->is_woo_activated, $this->is_buddypress_activated ) && ! $this->service->exclude_captcha_for_requests() ) {
84 $this->add_actions();
85
86 add_filter( 'script_loader_tag', array( $this->service, 'script_loader_tag' ), 10, 2 );
87 }
88 }
89
90 /**
91 * Add actions for CAPTCHA.
92 *
93 * @return void
94 */
95 protected function add_actions() {
96 $extra_conditions = is_admin() && ! ( defined( 'DOING_AJAX' ) && $this->is_captcha_settings() );
97 // Since v5.7.0.
98 do_action( 'wd_captcha_before_actions', $extra_conditions );
99
100 do_action_deprecated( 'wd_recaptcha_before_actions', array( $extra_conditions ), '5.7.0', 'wd_captcha_before_actions', __( 'This hook is deprecated and will be removed in future versions.', 'defender-security' ) );
101 if ( $extra_conditions ) {
102 return;
103 }
104
105 $display_for_known_users = $this->model->display_for_known_users();
106 $locations = $this->model->locations;
107 if ( in_array( Captcha_Component::DEFAULT_LOGIN_FORM, $locations, true ) || in_array( Captcha_Component::DEFAULT_REGISTER_FORM, $locations, true ) || in_array( Captcha_Component::DEFAULT_LOST_PASSWORD_FORM, $locations, true ) ) {
108 add_filter( 'cfturnstile_widget_disable', '__return_true' );
109 add_filter( 'easy_cloudflare_turnstile_render_list', '__return_empty_array' );
110 add_filter( 'easy_cloudflare_turnstile_verify_list', '__return_empty_array' );
111 add_action(
112 'login_enqueue_scripts',
113 array(
114 $this->service,
115 'remove_duplicate_captcha_scripts',
116 ),
117 PHP_INT_MAX
118 );
119 }
120 // Default login form.
121 if ( in_array( Captcha_Component::DEFAULT_LOGIN_FORM, $locations, true ) ) {
122 add_filter( 'authenticate', array( $this->service, 'validate_login_captcha' ), 9999 );
123 add_action( 'login_form', array( $this->service, 'display_login_captcha' ) );
124 add_filter( 'wp_authenticate_user', array( $this->service, 'validate_captcha_field_on_login' ), 8 );
125 }
126 // Default register form.
127 if ( in_array( Captcha_Component::DEFAULT_REGISTER_FORM, $locations, true ) ) {
128 if ( ! is_multisite() ) {
129 add_action( 'register_form', array( $this->service, 'display_login_captcha' ) );
130 add_filter(
131 'registration_errors',
132 array(
133 $this->service,
134 'validate_captcha_field_on_registration',
135 ),
136 10
137 );
138 } else {
139 add_action( 'signup_extra_fields', array( $this->service, 'display_signup_captcha' ) );
140 add_action( 'signup_blogform', array( $this->service, 'display_signup_captcha' ) );
141 add_filter(
142 'wpmu_validate_user_signup',
143 array(
144 $this->service,
145 'validate_captcha_field_on_wpmu_registration',
146 ),
147 10
148 );
149 }
150 }
151 // Default lost password form.
152 if ( in_array( Captcha_Component::DEFAULT_LOST_PASSWORD_FORM, $locations, true ) ) {
153 add_action( 'lostpassword_form', array( $this->service, 'display_login_captcha' ) );
154 if ( $this->maybe_validate_captcha_for_lostpassword() ) {
155 add_action( 'lostpassword_post', array( $this->service, 'validate_captcha_field_on_lostpassword' ) );
156 }
157 }
158 // Default comment form.
159 if ( $display_for_known_users && in_array( Captcha_Component::DEFAULT_COMMENT_FORM, $locations, true ) ) {
160 // @since v3.4.0 Change from 'comment_form_after_fields' to 'comment_form_defaults'.
161 add_filter( 'comment_form_defaults', array( $this->service, 'comment_form_defaults' ), 10 );
162 add_action( 'pre_comment_on_post', array( $this->service, 'validate_captcha_field_on_comment' ) );
163 // When comments are loaded via Hummingbird's lazy load feature.
164 if ( wd_di()->get( Hummingbird::class )->is_lazy_load_comments_enabled() ) {
165 add_action( 'wp_footer', array( $this->service, 'add_scripts_for_lazy_load' ) );
166 }
167 }
168 // Todo: move code to related class.
169 // For Woo forms. Mandatory check for the activated Woo before.
170 if ( $this->model->check_woo_locations( $this->is_woo_activated ) ) {
171 $woo_locations = $this->model->woo_checked_locations;
172 // Woo login form.
173 if ( in_array( Woocommerce::WOO_LOGIN_FORM, $woo_locations, true ) ) {
174 add_action( 'woocommerce_login_form', array( $this->service, 'display_login_captcha' ) );
175 add_filter(
176 'woocommerce_process_login_errors',
177 array(
178 $this->service,
179 'validate_captcha_field_on_woo_login',
180 ),
181 10
182 );
183 }
184 // Woo register form.
185 if ( in_array( Woocommerce::WOO_REGISTER_FORM, $woo_locations, true ) ) {
186 add_action( 'woocommerce_register_form', array( $this->service, 'display_login_captcha' ) );
187 add_filter(
188 'woocommerce_registration_errors',
189 array(
190 $this->service,
191 'validate_captcha_field_on_woo_registration',
192 ),
193 10
194 );
195 }
196 // Woo lost password form.
197 if ( in_array( Woocommerce::WOO_LOST_PASSWORD_FORM, $woo_locations, true ) ) {
198 add_action( 'woocommerce_lostpassword_form', array( $this->service, 'display_login_captcha' ) );
199 // Use default WP hook because Woo doesn't have own hook, so there's the extra check for Woo form.
200 $post_data = defender_get_data_from_request( null, 'p' );
201 if ( isset( $post_data['wc_reset_password'], $post_data['user_login'] ) ) {
202 add_action(
203 'lostpassword_post',
204 array(
205 $this->service,
206 'validate_captcha_field_on_lostpassword',
207 )
208 );
209 }
210 }
211 // Woo checkout form.
212 if ( $display_for_known_users && in_array( Woocommerce::WOO_CHECKOUT_FORM, $woo_locations, true ) ) {
213 add_action(
214 'woocommerce_after_checkout_billing_form',
215 array(
216 $this->service,
217 'display_login_captcha',
218 )
219 );
220 add_action(
221 'woocommerce_after_checkout_validation',
222 array(
223 $this->service,
224 'validate_captcha_field_on_woo_checkout',
225 ),
226 10,
227 2
228 );
229 }
230 }
231 // For BuddyPress forms. Mandatory check for the activated BuddyPress before.
232 if ( $this->model->check_buddypress_locations( $this->is_buddypress_activated ) ) {
233 $buddypress_locations = $this->model->buddypress_checked_locations;
234 // Register form.
235 if ( in_array( Buddypress::REGISTER_FORM, $buddypress_locations, true ) ) {
236 add_action(
237 'bp_before_registration_submit_buttons',
238 array(
239 $this->service,
240 'display_buddypress_recaptcha',
241 )
242 );
243 add_action(
244 'bp_signup_validate',
245 array(
246 $this->service,
247 'validate_captcha_field_on_buddypress_registration',
248 ),
249 10
250 );
251 }
252 // Group form.
253 if ( $display_for_known_users && in_array( Buddypress::NEW_GROUP_FORM, $buddypress_locations, true ) ) {
254 add_action( 'bp_after_group_details_creation_step', array( $this->service, 'display_login_captcha' ) );
255 add_action(
256 'groups_group_before_save',
257 array(
258 $this->service,
259 'validate_captcha_field_on_buddypress_group',
260 )
261 );
262 }
263 }
264 // Since v5.7.0.
265 do_action( 'wd_captcha_after_actions', $display_for_known_users );
266
267 do_action_deprecated( 'wd_recaptcha_after_actions', array( $display_for_known_users ), '5.7.0', 'wd_captcha_after_actions', __( 'This hook is deprecated and will be removed in a future version.', 'defender-security' ) );
268 }
269
270 /**
271 * Is it Defender's CAPTCHA page?
272 *
273 * @return bool
274 */
275 protected function is_captcha_settings(): bool {
276 $view = defender_get_data_from_request( 'view', 'g' );
277
278 return 'wdf-advanced-tools' === defender_get_current_page() && 'captcha' === $view;
279 }
280
281 /**
282 * Maybe validate reCaptcha for lost password.
283 *
284 * @return bool
285 * @since 3.2.0
286 */
287 protected function maybe_validate_captcha_for_lostpassword(): bool {
288 $post_data = defender_get_data_from_request( null, 'p' );
289 $action = $post_data['action'] ?? '';
290
291 return ! $this->is_woocommerce_page() && ! isset( $post_data['wc_reset_password'], $post_data['user_login'] ) && ! ( is_admin() && 'send-password-reset' === $action ) && 'pp_ajax_passwordreset' !== $action;
292 }
293
294 /**
295 * Check the current page from is from the Woo plugin.
296 *
297 * @return bool
298 */
299 protected function is_woocommerce_page(): bool {
300 if ( ! $this->is_woo_activated ) {
301 return false;
302 }
303
304 $traces = debug_backtrace(); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
305 foreach ( $traces as $trace ) {
306 if ( isset( $trace['file'] ) && false !== strpos( $trace['file'], 'woocommerce' ) ) {
307 return true;
308 }
309 }
310
311 return false;
312 }
313
314 /**
315 * Provides data for the frontend.
316 *
317 * @return array An array of data for the frontend.
318 */
319 public function data_frontend(): array {
320 $model = $this->model;
321 $is_active = $model->is_active();
322 $notice_data = $this->service->get_provider()->get_notice_data(
323 $this->is_woo_activated,
324 $this->is_buddypress_activated
325 );
326
327 /**
328 * Cases:
329 * Invalid domain for Site Key,
330 * Google ReCAPTCHA is in localhost,
331 * Cannot contact reCAPTCHA. Check your connection.
332 */
333 $ticket_text = esc_html__( '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.', 'defender-security' );
334
335 if ( ( new WPMUDEV() )->show_support_links() ) {
336 $ticket_text .= defender_support_ticket_text();
337 }
338
339 return array_merge(
340 array(
341 'model' => $model->export(),
342 'active_captcha' => $model->get_active_captcha_data(),
343 'is_active' => $is_active,
344 'default_message' => $this->model->get_default_values()['message'],
345 'default_turnstile_message' => $this->model->get_default_values()['turnstile_message'],
346 'default_locations' => Captcha_Component::get_forms(),
347 'notice_type' => $notice_data['notice_type'],
348 'notice_text' => $notice_data['notice_text'],
349 'ticket_text' => $ticket_text,
350 'is_woo_active' => $this->woo->is_activated(),
351 'woo_locations' => Woocommerce::get_forms(),
352 'is_buddypress_active' => $this->buddypress->is_activated(),
353 'buddypress_locations' => Buddypress::get_forms(),
354 ),
355 $this->dump_routes_and_nonces()
356 );
357 }
358
359 /**
360 * Save settings.
361 *
362 * @param Request $request The request object containing new settings data.
363 *
364 * @return Response
365 * @defender_route
366 */
367 public function save_settings( Request $request ): Response {
368 $data = $request->get_data_by_model( $this->model );
369 $this->model->import( $data );
370 if ( Captcha_Model::TURNSTILE === $this->model->provider ) {
371 $this->model->active_type = Captcha_Model::TURNSTILE;
372 }
373 if ( $this->model->validate() ) {
374 $this->model->save();
375 Config_Hub_Helper::set_clear_active_flag();
376
377 return new Response(
378 true,
379 array_merge(
380 array(
381 'message' => esc_html__( 'Settings saved successfully!', 'defender-security' ),
382 'auto_close' => true,
383 ),
384 $this->data_frontend()
385 )
386 );
387 }
388
389 return new Response(
390 false,
391 // Merge stored data to avoid errors.
392 array_merge(
393 array(
394 'message' => $this->model->get_formatted_errors(),
395 'error_keys' => $this->model->get_error_keys(),
396 ),
397 $this->data_frontend()
398 )
399 );
400 }
401
402 /**
403 * Verify a CAPTCHA response token.
404 *
405 * @param Request $request The request object containing token and key data.
406 *
407 * @return Response
408 * @defender_route
409 */
410 public function verify_response_token( Request $request ): Response {
411 $data = $request->get_data(
412 array(
413 'token' => array(
414 'type' => 'string',
415 'sanitize' => 'sanitize_text_field',
416 ),
417 'key' => array(
418 'type' => 'string',
419 'sanitize' => 'sanitize_text_field',
420 ),
421 )
422 );
423
424 $token = $data['token'] ?? '';
425 $key = $data['key'] ?? '';
426 if ( '' === $token || '' === $key ) {
427 return new Response(
428 false,
429 array(
430 'message' => esc_html__( 'Missing CAPTCHA verification data.', 'defender-security' ),
431 )
432 );
433 }
434
435 $provider = $this->service->get_provider();
436 $response = $provider->verify_response_token(
437 $token,
438 $provider->get_verify_url(),
439 array(
440 'secret' => $key,
441 )
442 );
443
444 $success = (bool) ( $response['success'] ?? false );
445 if ( $success ) {
446 if ( Captcha_Model::TURNSTILE === $this->model->provider ) {
447 $data = $this->model->data_turnstile;
448 $data['verified'] = true;
449 $this->model->data_turnstile = $data;
450 } elseif ( 'v2_checkbox' === $this->model->active_type ) {
451 $data = $this->model->data_v2_checkbox;
452 $data['verified'] = true;
453 $this->model->data_v2_checkbox = $data;
454 } elseif ( 'v2_invisible' === $this->model->active_type ) {
455 $data = $this->model->data_v2_invisible;
456 $data['verified'] = true;
457 $this->model->data_v2_invisible = $data;
458 } elseif ( 'v3_recaptcha' === $this->model->active_type ) {
459 $data = $this->model->data_v3_recaptcha;
460 $data['verified'] = true;
461 $this->model->data_v3_recaptcha = $data;
462 }
463 $this->model->save();
464 }
465
466 return new Response(
467 $success,
468 array(
469 'success' => $success,
470 )
471 );
472 }
473
474 /**
475 * Removes settings for all submodules.
476 */
477 public function remove_settings() {
478 }
479
480 /**
481 * Delete all the data & the cache.
482 */
483 public function remove_data(): void {
484 $this->model->delete();
485 }
486
487 /**
488 * Converts the current object state to an array.
489 *
490 * @return array The array representation of the object.
491 */
492 public function to_array(): array {
493 return array();
494 }
495
496 /**
497 * Provides data for the dashboard widget.
498 *
499 * @return array An array of dashboard widget data.
500 */
501 public function dashboard_widget(): array {
502 $model = $this->model;
503 $notice_type = ( $model->is_active() && $this->service->enable_any_location( $this->is_woo_activated, $this->is_buddypress_activated ) ) ? 'success' : 'warning';
504
505 return array(
506 'model' => $model->export(),
507 'notice_type' => $notice_type,
508 );
509 }
510
511 /**
512 * Imports data into the model.
513 *
514 * @param array $data Data to be imported into the model.
515 */
516 public function import_data( array $data ) {
517 $model = $this->model;
518
519 $model->import( $data );
520 if ( $model->validate() ) {
521 $model->save();
522 }
523 }
524
525 /**
526 * Exports strings.
527 *
528 * @return array An array of strings.
529 */
530 public function export_strings(): array {
531 return array( $this->model->is_active() ? esc_html__( 'Active', 'defender-security' ) : esc_html__( 'Inactive', 'defender-security' ) );
532 }
533
534 /**
535 * Enable/disable module.
536 *
537 * @param Request $request The request object.
538 *
539 * @return Response
540 * @defender_route
541 * @since 3.12.0
542 */
543 public function toggle_module( Request $request ): Response {
544 $data = $request->get_data(
545 array(
546 'enabled' => array(
547 'type' => 'boolean',
548 ),
549 )
550 );
551 $prev_state = $this->model->enabled;
552 $this->model->enabled = $data['enabled'];
553 $this->model->save();
554 $message = esc_html__( 'Settings saved successfully!', 'defender-security' );
555 if ( $prev_state !== $data['enabled'] ) {
556 if ( $data['enabled'] ) {
557 $message = esc_html__( 'CAPTCHA module is enabled successfully!', 'defender-security' );
558 } else {
559 $message = esc_html__( 'CAPTCHA module is disabled successfully!', 'defender-security' );
560 }
561 }
562
563 Config_Hub_Helper::set_clear_active_flag();
564
565 return new Response(
566 true,
567 array_merge(
568 array(
569 'message' => $message,
570 'auto_close' => true,
571 ),
572 $this->data_frontend()
573 )
574 );
575 }
576 }
577