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

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