PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.1.0
Defender Security – Malware Scanner, Login Security & Firewall v6.1.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 6.1.0, at src/controller/class-captcha.php

567 lines 16.4 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 $data_key = Captcha_Model::TURNSTILE === $this->model->provider
447 ? 'data_turnstile'
448 : 'data_' . $this->model->active_type;
449
450 $data = $this->model->{$data_key};
451 $data['verified'] = true;
452 $this->model->{$data_key} = $data;
453 $this->model->save();
454 }
455
456 return new Response(
457 $success,
458 array(
459 'success' => $success,
460 )
461 );
462 }
463
464 /**
465 * Removes settings for all submodules.
466 */
467 public function remove_settings() {
468 }
469
470 /**
471 * Delete all the data & the cache.
472 */
473 public function remove_data(): void {
474 $this->model->delete();
475 }
476
477 /**
478 * Converts the current object state to an array.
479 *
480 * @return array The array representation of the object.
481 */
482 public function to_array(): array {
483 return array();
484 }
485
486 /**
487 * Provides data for the dashboard widget.
488 *
489 * @return array An array of dashboard widget data.
490 */
491 public function dashboard_widget(): array {
492 $model = $this->model;
493 $notice_type = ( $model->is_active() && $this->service->enable_any_location( $this->is_woo_activated, $this->is_buddypress_activated ) ) ? 'success' : 'warning';
494
495 return array(
496 'model' => $model->export(),
497 'notice_type' => $notice_type,
498 );
499 }
500
501 /**
502 * Imports data into the model.
503 *
504 * @param array $data Data to be imported into the model.
505 */
506 public function import_data( array $data ) {
507 $model = $this->model;
508
509 $model->import( $data );
510 if ( $model->validate() ) {
511 $model->save();
512 }
513 }
514
515 /**
516 * Exports strings.
517 *
518 * @return array An array of strings.
519 */
520 public function export_strings(): array {
521 return array( $this->model->is_active() ? esc_html__( 'Active', 'defender-security' ) : esc_html__( 'Inactive', 'defender-security' ) );
522 }
523
524 /**
525 * Enable/disable module.
526 *
527 * @param Request $request The request object.
528 *
529 * @return Response
530 * @defender_route
531 * @since 3.12.0
532 */
533 public function toggle_module( Request $request ): Response {
534 $data = $request->get_data(
535 array(
536 'enabled' => array(
537 'type' => 'boolean',
538 ),
539 )
540 );
541 $prev_state = $this->model->enabled;
542 $this->model->enabled = $data['enabled'];
543 $this->model->save();
544 $message = esc_html__( 'Settings saved successfully!', 'defender-security' );
545 if ( $prev_state !== $data['enabled'] ) {
546 if ( $data['enabled'] ) {
547 $message = esc_html__( 'CAPTCHA module is enabled successfully!', 'defender-security' );
548 } else {
549 $message = esc_html__( 'CAPTCHA module is disabled successfully!', 'defender-security' );
550 }
551 }
552
553 Config_Hub_Helper::set_clear_active_flag();
554
555 return new Response(
556 true,
557 array_merge(
558 array(
559 'message' => $message,
560 'auto_close' => true,
561 ),
562 $this->data_frontend()
563 )
564 );
565 }
566 }
567