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-firewall.php

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

1,429 lines 44.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles IP lockouts, notifications, and settings related to the firewall features.
4 *
5 * @package WP_Defender\Controller
6 */
7
8 namespace WP_Defender\Controller;
9
10 use Exception;
11 use WP_Defender\Event;
12 use Calotes\Helper\HTTP;
13 use WP_Defender\Traits\IP;
14 use Calotes\Component\Request;
15 use Calotes\Component\Response;
16 use Calotes\Helper\Array_Cache;
17 use WP_Defender\Component\Network_Cron_Manager;
18 use WP_Defender\Model\Setting\Antibot_Global_Firewall_Setting;
19 use WP_Defender\Component\Mail;
20 use WP_Defender\Traits\Formats;
21 use WP_Defender\Model\Unlockout;
22 use WP_Defender\Behavior\WPMUDEV;
23 use WP_Defender\Model\Lockout_Ip;
24 use WP_Defender\Model\Lockout_Log;
25 use WP_Defender\Component\Unlock_Me;
26 use WP_Defender\Component\IP\Antibot_Global_Firewall as Antibot_Global_Firewall_Component;
27 use WP_Defender\Component\IP\Global_IP as Global_IP_Component;
28 use WP_Defender\Component\Blacklist_Lockout;
29 use WP_Defender\Component\Http\Remote_Address;
30 use MaxMind\Db\Reader\InvalidDatabaseException;
31 use WP_Defender\Model\Setting\Notfound_Lockout;
32 use WP_Defender\Model\Setting\Global_Ip_Lockout;
33 use WP_Defender\Model\Setting\User_Agent_Lockout;
34 use WP_Defender\Component\Config\Config_Hub_Helper;
35 use WP_Defender\Model\Notification\Firewall_Report;
36 use WP_Defender\Component\Firewall as Firewall_Service;
37 use WP_Defender\Model\Notification\Firewall_Notification;
38 use WP_Defender\Model\Setting\Firewall as Firewall_Settings;
39 use WP_Defender\Component\User_Agent as User_Agent_Component;
40 use WP_Defender\Model\Setting\Blacklist_Lockout as Blacklist_Model;
41 use WP_Defender\Model\Setting\Login_Lockout as Login_Lockout_Model;
42 use WP_Defender\Component\Trusted_Proxy_Preset\Trusted_Proxy_Preset;
43 use WP_Defender\Component\Smart_Ip_Detection;
44 use WP_Defender\Helper\Analytics\Firewall as Firewall_Analytics;
45 use WP_Defender\Model\Antibot_Global_Firewall as Antibot_Global_Firewall_Model;
46 use WP_Defender\Component\Altcha_Handler;
47 use WP_Defender\Controller\Hub_Connector;
48 use WP_Defender\Integrations\Main_Wp;
49
50 /**
51 * Handles IP lockouts, notifications, and settings related to the firewall features.
52 */
53 class Firewall extends Event {
54
55 use IP;
56 use Formats;
57
58 public const FIREWALL_LOG = 'firewall.log';
59 /**
60 * The slug identifier for this controller.
61 *
62 * @var string
63 */
64 protected $slug = 'wdf-ip-lockout';
65
66 /**
67 * The model for handling the data.
68 *
69 * @var Firewall_Settings
70 */
71 protected $model;
72
73 /**
74 * Service for handling logic.
75 *
76 * @var Firewall_Service
77 */
78 public $service;
79
80 /**
81 * Service for handling Smart IP Detection.
82 *
83 * @var Smart_Ip_Detection
84 */
85 public $service_sid;
86
87 /**
88 * The WPMUDEV instance used for interacting with WPMUDEV services.
89 *
90 * @var WPMUDEV
91 */
92 private $wpmudev;
93
94 /**
95 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
96 */
97 public function __construct() {
98 $this->wpmudev = wd_di()->get( WPMUDEV::class );
99
100 $title = esc_html__( 'Firewalls', 'defender-security' );
101 $this->register_page(
102 $this->get_title( $title ),
103 $this->slug,
104 array( $this, 'main_view' ),
105 $this->parent_slug,
106 null,
107 $this->menu_title( $title )
108 );
109 $this->model = wd_di()->get( Firewall_Settings::class );
110 $this->service = wd_di()->get( Firewall_Service::class );
111 $this->service_sid = wd_di()->get( Smart_Ip_Detection::class );
112 $this->register_routes();
113 $this->maybe_show_demo_lockout();
114 $this->maybe_lockout_gathered_ips();
115 // Todo: pass $ip as argument to Login_Lockout/Nf_Lockout.
116 wd_di()->get( Login_Lockout::class );
117 wd_di()->get( Nf_Lockout::class );
118 wd_di()->get( Blacklist::class );
119 wd_di()->get( Firewall_Logs::class );
120 wd_di()->get( UA_Lockout::class );
121 wd_di()->get( Global_Ip::class );
122 wd_di()->get( Antibot_Global_Firewall::class );
123 wd_di()->get( Malicious_Bot::class );
124 wd_di()->get( Fake_Bot_Detection::class );
125
126 // Integrate MainWP plugin.
127 wd_di()->get( Main_Wp::class );
128
129 /**
130 * Network Cron Manager
131 *
132 * @var Network_Cron_Manager $network_cron_manager
133 */
134 $network_cron_manager = wd_di()->get( Network_Cron_Manager::class );
135 $network_cron_manager->register_callback(
136 'firewall_clean_up_logs',
137 array( $this->service, 'firewall_clean_up_logs' ),
138 HOUR_IN_SECONDS,
139 time() + 10
140 );
141 $network_cron_manager->register_callback(
142 'wpdef_firewall_clean_up_lockout',
143 array( $this->service, 'firewall_clean_up_lockout' ),
144 WEEK_IN_SECONDS,
145 time() + 10
146 );
147 $network_cron_manager->register_callback(
148 'wpdef_firewall_clean_up_unlockout',
149 array( $this, 'clean_up_unlockout' ),
150 WEEK_IN_SECONDS,
151 time() + 20
152 );
153 $network_cron_manager->register_callback(
154 'wpdef_firewall_fetch_trusted_proxy_preset_ips',
155 array( $this->service, 'update_trusted_proxy_preset_ips' ),
156 DAY_IN_SECONDS
157 );
158 if ( $this->service_sid->is_smart_ip_detection_enabled() ) {
159 $network_cron_manager->register_callback(
160 'wpdef_smart_ip_detection_ping',
161 array( $this->service_sid, 'smart_ip_detection_ping' ),
162 WEEK_IN_SECONDS
163 );
164 }
165 $network_cron_manager->register_callback(
166 'wpdef_firewall_whitelist_server_public_ip',
167 array( $this->service, 'set_whitelist_server_public_ip' ),
168 12 * HOUR_IN_SECONDS,
169 time() + 15
170 );
171 // Additional hooks.
172 add_action( 'defender_enqueue_assets', array( $this, 'enqueue_assets' ), 11 );
173 add_action( 'admin_print_scripts', array( $this, 'print_emoji_script' ) );
174
175 $this->maybe_extend_mime_types();
176 // Schedule cleanup blocklist ips event.
177 $this->schedule_cleanup_blocklist_ips_event();
178 add_action( 'wp_ajax_' . Smart_Ip_Detection::ACTION_PING, array( $this, 'handle_detect_ip_header' ) );
179 add_action( 'wp_ajax_nopriv_' . Smart_Ip_Detection::ACTION_PING, array( $this, 'handle_detect_ip_header' ) );
180 }
181
182 /**
183 * Return the title of the page.
184 *
185 * @param string $default_text The original menu title.
186 *
187 * @return string
188 */
189 public function get_title( $default_text ): string {
190 return $default_text;
191 }
192
193 /**
194 * Get menu title.
195 *
196 * @param string $default_text The original menu title.
197 *
198 * @return string
199 */
200 protected function menu_title( string $default_text ): string {
201 return $default_text;
202 }
203
204 /**
205 * Sets up the preset firewall configuration.
206 *
207 * @param bool $state The feature state.
208 *
209 * @return void
210 */
211 public function preset_firewall( bool $state ) {
212 // For every lockout.
213 $ll = wd_di()->get( Login_Lockout_Model::class );
214 $nf = wd_di()->get( Notfound_Lockout::class );
215 $ua = wd_di()->get( User_Agent_Lockout::class );
216
217 $ll->enabled = $state;
218 $ll->save();
219 $nf->enabled = $state;
220 $nf->save();
221
222 $old_malicious_bot_enabled = $ua->malicious_bot_enabled;
223 $old_fake_bots_enabled = $ua->fake_bots_enabled;
224 $ua->enabled = $state;
225 if ( $state ) {
226 $ua->malicious_bot_enabled = true;
227 $ua->fake_bots_enabled = true;
228 } else {
229 $ua->malicious_bot_enabled = false;
230 $ua->fake_bots_enabled = false;
231 }
232 $ua->save();
233
234 if ( $state && ! $old_malicious_bot_enabled ) {
235 wd_di()->get( Malicious_Bot::class )->rotate_hash();
236 } elseif ( ! $state ) {
237 if ( $old_malicious_bot_enabled ) {
238 wd_di()->get( Malicious_Bot::class )->remove_data();
239 }
240 if ( $old_fake_bots_enabled ) {
241 wd_di()->get( Fake_Bot_Detection::class )->remove_data();
242 }
243 }
244
245 Config_Hub_Helper::set_clear_active_flag();
246 }
247
248 /**
249 * Enable/disable lockout modules.
250 *
251 * @param Request $request The request object.
252 *
253 * @return Response
254 * @defender_route
255 */
256 public function toggle_lockout_modules( Request $request ): Response {
257 $data = $request->get_data(
258 array(
259 'enabled' => array(
260 'type' => 'boolean',
261 ),
262 )
263 );
264 $this->preset_firewall( $data['enabled'] );
265
266 return new Response( true, $this->to_array() );
267 }
268
269 /**
270 * Render the view page.
271 *
272 * @return void
273 */
274 public function main_view(): void {
275 $this->render( 'main' );
276 }
277
278 /**
279 * Save settings.
280 *
281 * @param Request $request The request object containing new settings data.
282 *
283 * @return Response
284 * @defender_route
285 */
286 public function save_settings( Request $request ): Response {
287 $data = $request->get_data_by_model( $this->model );
288 // Before updating Trusted Proxy Preset (TPP) IP's, check the current option is a custom header, no blank TPP value and there's TPP change.
289 $is_preset_update = false;
290 if (
291 in_array(
292 $data['http_ip_header'],
293 Firewall_Service::custom_http_headers(),
294 true
295 )
296 && isset( $data['trusted_proxy_preset'] )
297 && is_string( $data['trusted_proxy_preset'] )
298 && '' !== $data['trusted_proxy_preset']
299 && $data['trusted_proxy_preset'] !== $this->model->trusted_proxy_preset
300 ) {
301 $is_preset_update = true;
302 }
303
304 $is_ip_detection_type_changed = false;
305 if ( 'automatic' === $data['ip_detection_type'] && $this->model->ip_detection_type !== $data['ip_detection_type'] ) {
306 $is_ip_detection_type_changed = true;
307 }
308
309 $is_http_ip_header_changed = false;
310 if ( $this->model->http_ip_header !== $data['http_ip_header'] ) {
311 $is_http_ip_header_changed = true;
312 }
313
314 $this->model->import( $data );
315 if ( $this->model->validate() ) {
316 $this->service->update_cron_schedule_interval( $data['ip_blocklist_cleanup_interval'] );
317 $this->model->save();
318 Config_Hub_Helper::set_clear_active_flag();
319 // Fetch trusted proxy ips.
320 if ( $is_preset_update ) {
321 $this->service->update_trusted_proxy_preset_ips();
322 }
323
324 if ( $is_ip_detection_type_changed ) {
325 $this->service_sid->smart_ip_detection_ping();
326 }
327 // Maybe track.
328 if ( ( $is_ip_detection_type_changed || $is_http_ip_header_changed )
329 && ! defender_is_wp_cli()
330 ) {
331 $firewall_analytics = wd_di()->get( Firewall_Analytics::class );
332 $detection_method = Firewall_Analytics::get_detection_method_label(
333 $data['ip_detection_type'],
334 $data['http_ip_header']
335 );
336
337 $firewall_analytics->track_feature(
338 Firewall_Analytics::EVENT_IP_DETECTION,
339 array( Firewall_Analytics::PROP_IP_DETECTION => $detection_method )
340 );
341 }
342
343 return new Response(
344 true,
345 array(
346 'message' => esc_html__( 'Your settings have been updated.', 'defender-security' ),
347 'auto_close' => true,
348 )
349 );
350 }
351
352 return new Response(
353 false,
354 array(
355 'message' => $this->model->get_formatted_errors(),
356 )
357 );
358 }
359
360 /**
361 * Converts the current object to an array representation.
362 *
363 * @return array The array representation of the object.
364 */
365 public function to_array(): array {
366 $il = wd_di()->get( Login_Lockout_Model::class );
367 $nf = wd_di()->get( Notfound_Lockout::class );
368 $ua = wd_di()->get( User_Agent_Lockout::class );
369
370 return array_merge(
371 array(
372 'summary' => array(
373 'ip' => array(
374 'week' => Lockout_Log::count_login_lockout_last_7_days(),
375 ),
376 'nf' => array(
377 'week' => Lockout_Log::count_404_lockout_last_7_days(),
378 ),
379 'ua' => array(
380 'week' => Lockout_Log::count_ua_lockout_last_7_days(),
381 ),
382 'lastLockout' => Lockout_Log::get_last_lockout_date(),
383 ),
384 'notification' => true,
385 'enabled' => $nf->enabled || $il->enabled || $ua->enabled,
386 'enable_login' => $il->enabled,
387 'enable_404' => $nf->enabled,
388 'enable_ua' => $ua->enabled,
389 ),
390 $this->dump_routes_and_nonces()
391 );
392 }
393
394 /**
395 * Enqueues scripts and styles for this page.
396 * Only enqueues assets if the page is active.
397 */
398 public function enqueue_assets(): void {
399 if ( ! $this->is_page_active() ) {
400 return;
401 }
402
403 add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
404 wp_enqueue_media();
405
406 $handle = 'defender-ui-firewalls';
407 wp_enqueue_script(
408 $handle,
409 WP_DEFENDER_BASE_URL . 'assets/js/firewalls-ui.js',
410 array( 'def-vue', 'def-manifest', 'def-core-ui', 'defender', 'wp-i18n' ),
411 DEFENDER_VERSION,
412 true
413 );
414 wp_set_script_translations( $handle, 'wpdef' );
415
416 wp_localize_script(
417 $handle,
418 'defenderUIData',
419 array_merge(
420 $this->get_shared_data(),
421 $this->data_frontend()
422 )
423 );
424
425 wp_enqueue_style(
426 $handle,
427 WP_DEFENDER_BASE_URL . 'assets/css/showcase.css',
428 array(),
429 DEFENDER_VERSION
430 );
431
432 $this->enqueue_main_assets();
433 // Keep this action for backward compatibility with existing hooks.
434 do_action( 'defender_ip_lockout_action_assets' );
435 }
436
437 /**
438 * Add a Firewalls page marker class for scoped admin CSS overrides.
439 *
440 * @param string $classes Current admin body classes.
441 *
442 * @return string
443 */
444 public function admin_body_class( string $classes ): string {
445 return trim( $classes . ' wdf-firewalls-react-page' );
446 }
447
448 /**
449 * Renders the preview of lockout screen.
450 *
451 * @return void
452 */
453 private function maybe_show_demo_lockout(): void {
454 $is_test = HTTP::get( 'def-lockout-demo', 0 );
455 if ( 1 === (int) $is_test ) {
456 $type = HTTP::get( 'type' );
457
458 $remaining_time = 0;
459
460 switch ( $type ) {
461 case 'login':
462 $settings = wd_di()->get( Login_Lockout_Model::class );
463 $message = $settings->lockout_message;
464 $remaining_time = 3600;
465 break;
466 case '404':
467 $settings = wd_di()->get( Notfound_Lockout::class );
468 $message = $settings->lockout_message;
469 $remaining_time = 3600;
470 break;
471 case 'blocklist':
472 $settings = wd_di()->get( Blacklist_Model::class );
473 $message = $settings->ip_lockout_message;
474 break;
475 case User_Agent_Lockout::get_module_slug():
476 $settings = wd_di()->get( User_Agent_Lockout::class );
477 $message = $settings->message;
478 $remaining_time = 3600;
479 break;
480 default:
481 $message = esc_html__( 'Demo', 'defender-security' );
482 break;
483 }
484
485 $this->actions_for_blocked( $message, $remaining_time, 'demo', $this->get_user_ip() );
486 exit;
487 }
488 }
489
490 /**
491 * Checks the attempt counter for a blocked IP address.
492 *
493 * @param string $blocked_ip The blocked IP address to check the attempt counter for.
494 *
495 * @return bool
496 */
497 private function check_attempt_counter_by( $blocked_ip ): bool {
498 $blocked_ip = $this->check_ip_by_remote_addr( $blocked_ip );
499 $request_count = get_transient( $blocked_ip );
500 $disabled = false;
501 if ( false === $request_count ) {
502 set_transient( $blocked_ip, 1, Unlock_Me::EXPIRED_COUNTER_TIME );
503 } elseif ( (int) $request_count >= Unlock_Me::get_attempt_limit() ) {
504 $disabled = true;
505 } else {
506 ++$request_count;
507 set_transient( $blocked_ip, $request_count, Unlock_Me::EXPIRED_COUNTER_TIME );
508 }
509
510 return $disabled;
511 }
512
513 /**
514 * Verify if the user is blocked.
515 *
516 * @param Request $request The request object.
517 *
518 * @return Response
519 * @defender_route
520 * @is_public
521 * @throws InvalidDatabaseException When unexpected data is found in the database.
522 */
523 public function verify_blocked_user( Request $request ): Response {
524 $data = $request->get_data(
525 array(
526 'user_data' => array(
527 'type' => 'string',
528 'sanitize' => 'sanitize_text_field',
529 ),
530 )
531 );
532 $maybe_email = ! empty( $data['user_data'] ) ? $data['user_data'] : '';
533 if ( ! is_string( $maybe_email ) || '' === trim( $maybe_email ) ) {
534 return new Response( false, array() );
535 }
536 $ips = $this->get_user_ip();
537 // Check if at least one IP is blocked.
538 $blocked_ip = $this->service->get_blocked_ip( $ips );
539 // If nothing, just return.
540 if ( '' === $blocked_ip ) {
541 return new Response( false, array() );
542 }
543 // Maybe is it a user email?
544 $user = get_user_by( 'email', $maybe_email );
545 if ( ! is_object( $user ) ) {
546 // Maybe is it a username?
547 $user = get_user_by( 'login', $maybe_email );
548 if ( ! is_object( $user ) ) {
549 $this->check_attempt_counter_by( $blocked_ip );
550
551 return new Response( false, array() );
552 }
553 }
554 // Send email only for admins.
555 if ( ! $this->is_admin( $user ) ) {
556 // No need to count attempts for existed user but non-admin.
557 return new Response( false, array() );
558 }
559 // Create Unlockout records.
560 $arr_uids = array();
561 foreach ( $ips as $ip ) {
562 // Collect blocked IP's.
563 $created_id = wd_di()->get( Unlockout::class )->create( $ip, $user->user_email );
564 if ( $created_id ) {
565 $arr_uids[] = $created_id;
566 }
567 }
568
569 $this->send_unlock_email( $user->user_email, $user->user_login, $arr_uids );
570
571 return new Response( true, array() );
572 }
573
574 /**
575 * Send again if the attempt limit has not expired.
576 *
577 * @return Response
578 * @defender_route
579 * @is_public
580 * @throws InvalidDatabaseException When unexpected data is found in the database.
581 */
582 public function send_again(): Response {
583 // Check if at least one IP is blocked.
584 $blocked_ip = $this->service->get_blocked_ip( $this->get_user_ip() );
585 if ( '' === $blocked_ip ) {
586 return new Response( false, array() );
587 }
588 $request_count = get_transient( $this->check_ip_by_remote_addr( $blocked_ip ) );
589 $is_expired = false !== $request_count && $request_count >= Unlock_Me::get_attempt_limit();
590
591 return new Response(
592 ! $is_expired,
593 array()
594 );
595 }
596
597 /**
598 * Sends an unlock email to the user.
599 *
600 * @param string $user_email The email address of the user.
601 * @param string $user_login The login name of the user.
602 * @param array $arr_uids The array of unique IDs.
603 *
604 * @return bool True if the email is sent successfully, false otherwise.
605 */
606 protected function send_unlock_email( $user_email, $user_login, $arr_uids ): bool {
607 $headers = wd_di()->get( Mail::class )->get_headers(
608 defender_noreply_email( 'wd_unlock_noreply_email' ),
609 Unlock_Me::SLUG_UNLOCK
610 );
611 $subject = esc_html__( 'Request to Unblock IP Address', 'defender-security' );
612
613 $content_body = $this->render_partial(
614 'email/unlockout',
615 array(
616 'subject' => $subject,
617 'name' => $user_login,
618 'unlocked_link' => Unlock_Me::create_url( $user_email, $user_login, $arr_uids ),
619 'generated_time' => $this->get_local_human_date( time() ),
620 ),
621 false
622 );
623 $content = $this->render_partial(
624 'email/index',
625 array(
626 'title' => esc_html__( 'Firewall', 'defender-security' ),
627 'content_body' => $content_body,
628 'unsubscribe_link' => '',
629 ),
630 false
631 );
632
633 // Send email.
634 return wp_mail( $user_email, $subject, $content, $headers );
635 }
636
637 /**
638 * Run actions for locked entities.
639 *
640 * @param string $message The message to show.
641 * @param int $remaining_time Remaining countdown time in seconds.
642 * @param string $reason Block's reason.
643 * @param array $ips Array of blocked IP's.
644 * @param bool $discourage_crawlers Whether to discourage crawlers with a noindex meta tag.
645 *
646 * @return void
647 */
648 public function actions_for_blocked(
649 string $message,
650 int $remaining_time = 0,
651 string $reason = '',
652 array $ips = array(),
653 bool $discourage_crawlers = false
654 ): void {
655 $action = HTTP::get( 'action', false );
656
657 if ( defender_base_action() === $action ) {
658 $nonce = HTTP::get( '_def_nonce', false );
659 $route = HTTP::get( 'route', '' );
660 $route = wp_unslash( $route );
661 if ( wp_verify_nonce( $nonce, $route ) ) {
662 return;
663 }
664 }
665 // Maybe unblock the request?
666 if ( Unlock_Me::SLUG_UNLOCK === $action && wd_di()->get( Unlock_Me::class )->maybe_unlock() ) {
667 return;
668 }
669 // Create a Lockout cookie to avoid caching in real case.
670 if ( 'demo' !== $reason ) {
671 // We follow the default naming process to find the required cookie later.
672 $cookie_name = str_replace( '.', '_', $ips[0] );
673 $cookie_name = 'wpdef_lockout_' . $cookie_name;
674 if ( ! isset( $_COOKIE[ $cookie_name ] ) ) {
675 setcookie( $cookie_name, true, time() + HOUR_IN_SECONDS, '/', '', is_ssl(), true );
676 }
677 }
678
679 $global_service = wd_di()->get( Global_IP_Component::class );
680 if ( Global_IP_Component::REASON_SLUG === $reason ) {
681 $global_service->log_event( $ips[0] );
682 }
683
684 $antibot_service = wd_di()->get( Antibot_Global_Firewall_Component::class );
685 if ( Antibot_Global_Firewall_Component::REASON_SLUG === $reason ) {
686 $antibot_service->log_ip_message( 'Blocked IP(s): ' . implode( ', ', $ips ) );
687 }
688
689 ob_start();
690
691 if ( ! headers_sent() ) {
692 if ( ! defined( 'DONOTCACHEPAGE' ) ) {
693 define( 'DONOTCACHEPAGE', true );
694 }
695
696 header( 'HTTP/1.0 403 Forbidden' );
697 header( 'Cache-Control: no-cache, no-store, must-revalidate, max-age=0' ); // HTTP 1.1.
698 header( 'Pragma: no-cache' ); // HTTP 1.0.
699 header( 'Expires: ' . wp_date( 'D, d M Y H:i:s', time() - 3600 ) . ' GMT' ); // Proxies.
700 header( 'Clear-Site-Data: "cache"' ); // Clear cache of the current request.
701
702 $global_ip_lockout = wd_di()->get( Global_Ip_Lockout::class );
703 $is_displayed = Unlock_Me::is_displayed( $reason, $ips );
704 $is_displayed_agf = $antibot_service->is_displayed( $ips );
705 $allow_self_unlock = $global_ip_lockout->allow_self_unlock;
706 $hide_btn_agf = $is_displayed_agf && ! $allow_self_unlock;
707 $malicious_bot = wd_di()->get( Malicious_Bot::class );
708 $discourage_crawlers = ! $discourage_crawlers && $malicious_bot->is_hash_request() ? true : $discourage_crawlers;
709 $params = array(
710 'message' => ! $hide_btn_agf ? $message : '',
711 'remaining_time' => $remaining_time,
712 'is_unlock_me' => $is_displayed,
713 'is_unlock_me_agf' => $is_displayed_agf,
714 'module_name_agf' => Antibot_Global_Firewall_Setting::get_module_name(),
715 'hide_btn_agf' => $hide_btn_agf,
716 'discourage_crawlers' => $discourage_crawlers,
717 );
718
719 // For AntiBot Global Firewall "Unlock me captcha".
720 if ( $is_displayed_agf && $allow_self_unlock ) {
721 $altcha_challenge = wd_di()->get( Altcha_Handler::class )->create_challenge();
722 $collection = $this->dump_routes_and_nonces();
723 $routes = $collection['routes'];
724 $nonces = $collection['nonces'];
725 $args = array(
726 'action' => defender_base_action(),
727 '_def_nonce' => $nonces['agf_unlock_user'],
728 'route' => $this->check_route( $routes['agf_unlock_user'] ),
729 );
730
731 $params['action_agf_unlock_user'] = add_query_arg( $args, admin_url( 'admin-ajax.php' ) );
732 $params['button_title'] = Antibot_Global_Firewall_Component::get_button_text();
733 $params['altcha'] = $altcha_challenge;
734 } elseif ( $is_displayed ) { // Only for "Unlock me".
735 $collection = $this->dump_routes_and_nonces();
736 $routes = $collection['routes'];
737 $nonces = $collection['nonces'];
738 // Prepare data.
739 $args = array(
740 'action' => defender_base_action(),
741 '_def_nonce' => $nonces['verify_blocked_user'],
742 'route' => $this->check_route( $routes['verify_blocked_user'] ),
743 );
744 $params['action_verify_blocked_user'] = add_query_arg( $args, admin_url( 'admin-ajax.php' ) );
745 // Rewrite args for another action.
746 $args['_def_nonce'] = $nonces['send_again'];
747 $args['route'] = $this->check_route( $routes['send_again'] );
748 $params['action_send_again'] = add_query_arg( $args, admin_url( 'admin-ajax.php' ) );
749
750 $params['button_title'] = Unlock_Me::get_feature_title();
751 $button_disabled = false;
752 if ( array() !== $ips ) {
753 // Get IP's.
754 $request_count = get_transient( $this->check_ip_by_remote_addr( $ips[0] ) );
755 $button_disabled = false !== $request_count && $request_count >= Unlock_Me::get_attempt_limit();
756 }
757
758 $params['button_disabled'] = $button_disabled;
759 }
760
761 $this->render_partial(
762 'ip-lockout/locked',
763 $params
764 );
765 }
766
767 /**
768 * Ignore WordPress.Security.EscapeOutput.OutputNotEscaped
769 * Why?
770 * Escaping this content would break the page.
771 */
772 echo ob_get_clean(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
773 exit();
774 }
775
776 /**
777 * We will check and prevent the access if the current IP is blacklist, or get temporary banned.
778 *
779 * @param string $ip The IP to check.
780 *
781 * @return void|string
782 * @throws InvalidDatabaseException When unexpected data is found in the database.
783 */
784 public function maybe_lockout( $ip ) {
785 do_action( 'wd_before_lockout', $ip );
786
787 if ( $this->service->skip_priority_lockout_checks( $ip ) ) {
788 return;
789 }
790
791 $is_blocklisted = $this->service->is_blocklisted_ip( $ip );
792 if ( $is_blocklisted['result'] ) {
793 // Get Blacklist_Lockout instance.
794 $blacklist_model = wd_di()->get( Blacklist_Model::class );
795 // This one is get blacklisted.
796 $this->actions_for_blocked(
797 $blacklist_model->ip_lockout_message,
798 0,
799 $is_blocklisted['reason'],
800 array( $ip )
801 );
802 }
803 // Get an instance of UA component.
804 $service_ua = wd_di()->get( User_Agent_Component::class );
805
806 if ( $service_ua->is_active_component() ) {
807 $user_agent = $service_ua->sanitize_user_agent();
808 if ( $service_ua->is_bad_post( $user_agent ) ) {
809 $service_ua->block_user_agent_or_ip( $user_agent, $ip, User_Agent_Component::REASON_BAD_POST );
810
811 return $service_ua->get_message();
812 }
813 if ( '' !== trim( $user_agent )
814 /**
815 * Apply additional checks for user agent before determining if it is a bad user agent.
816 *
817 * @param bool $is_bad_user_agent The result of checking if the user agent is bad.
818 * @param string $user_agent The user agent string to be checked.
819 * @param string $ip The IP address associated with the user agent.
820 *
821 * @return bool The final result after applying additional checks.
822 * @since 3.1.0
823 */
824 && apply_filters(
825 'wd_user_agent_additional_check',
826 $service_ua->is_bad_user_agent( $user_agent ),
827 $user_agent,
828 $ip
829 )
830 ) {
831 // Todo: if we use a hook then we should extend cases with a custom reason and send it for log.
832 $service_ua->block_user_agent_or_ip( $user_agent, $ip, User_Agent_Component::REASON_BAD_USER_AGENT );
833
834 return $service_ua->get_message();
835 }
836 }
837
838 $notfound_lockout = wd_di()->get( Notfound_Lockout::class );
839 if ( $notfound_lockout->enabled && false === $notfound_lockout->detect_logged && is_user_logged_in() ) {
840 /**
841 * We don't need to check the IP if:
842 * the current user can logged-in and isn't from blacklisted,
843 * the option detect_404_logged is disabled.
844 */
845 return;
846 }
847 // Check blacklist.
848 $model = Lockout_Ip::get( $ip );
849 if ( is_object( $model ) && $model->is_locked() ) {
850 $remaining_time = $model->remaining_release_time();
851 $this->actions_for_blocked( $model->lockout_message, $remaining_time, 'blacklist', array( $ip ) );
852 }
853 }
854
855 /**
856 * Remove all IP logs.
857 *
858 * @return Response
859 * @defender_route
860 */
861 public function empty_logs(): Response {
862 if ( Lockout_Log::truncate() ) {
863 $this->log( 'Logs have been successfully deleted.', self::FIREWALL_LOG );
864
865 return new Response(
866 true,
867 array(
868 'message' => esc_html__( 'Your logs have been successfully deleted.', 'defender-security' ),
869 'interval' => 1,
870 )
871 );
872 }
873
874 return new Response(
875 false,
876 array(
877 'message' => esc_html__( 'Failed remove!', 'defender-security' ),
878 )
879 );
880 }
881
882 /**
883 * Return summary data.
884 *
885 * @return array
886 */
887 public function get_summary(): array {
888 $summary = Lockout_Log::get_summary();
889
890 return array(
891 'lockout_last' => isset( $summary['lockout_last'] ) ?
892 $this->format_date_time( $summary['lockout_last'] ) :
893 esc_html__( 'Never', 'defender-security' ),
894 'lockout_today' => $summary['lockout_today'] ?? 0,
895 'lockout_this_month' => $summary['lockout_this_month'] ?? 0,
896 'lockout_login_today' => $summary['lockout_login_today'] ?? 0,
897 'lockout_login_this_week' => $summary['lockout_login_this_week'] ?? 0,
898 'lockout_login_this_month' => $summary['lockout_login_this_month'] ?? 0,
899 'lockout_404_today' => $summary['lockout_404_today'] ?? 0,
900 'lockout_404_this_week' => $summary['lockout_404_this_week'] ?? 0,
901 'lockout_404_this_month' => $summary['lockout_404_this_month'] ?? 0,
902 'lockout_ua_today' => $summary['lockout_ua_today'] ?? 0,
903 'lockout_ua_this_week' => $summary['lockout_ua_this_week'] ?? 0,
904 'lockout_ua_this_month' => $summary['lockout_ua_this_month'] ?? 0,
905 );
906 }
907
908 /**
909 * Removes settings for all submodules.
910 */
911 public function remove_settings(): void {
912 ( new Login_Lockout_Model() )->delete();
913 ( new Blacklist_Model() )->delete();
914 ( new Notfound_Lockout() )->delete();
915 ( new Firewall_Settings() )->delete();
916 ( new User_Agent_Lockout() )->delete();
917 ( new Global_Ip_Lockout() )->delete();
918 ( new Antibot_Global_Firewall_Setting() )->delete();
919 }
920
921 /**
922 * Delete all the data & the cache.
923 */
924 public function remove_data(): void {
925 Lockout_Log::truncate();
926 // Remove cached data.
927 Array_Cache::remove( 'countries', 'ip_lockout' );
928 // Remove Global IP data.
929 ( new Global_Ip() )->remove_data();
930 // Remove AntiBot Global Firewall data.
931 wd_di()->get( Antibot_Global_Firewall::class )->remove_data();
932 // Remove Malicious Bot data.
933 wd_di()->get( Malicious_Bot::class )->remove_data();
934 // Remove Fake Bot data.
935 wd_di()->get( Fake_Bot_Detection::class )->remove_data();
936 // Remove Firewall Logs data.
937 wd_di()->get( Firewall_Logs::class )->remove_data();
938 // Remove WAF data.
939 wd_di()->get( WAF::class )->remove_data();
940 // Clear Trusted Proxy data.
941 $trusted_proxy_preset = wd_di()->get( Trusted_Proxy_Preset::class );
942 foreach ( array_keys( Firewall_Service::trusted_proxy_presets() ) as $preset ) {
943 $trusted_proxy_preset->set_proxy_preset( $preset );
944 $trusted_proxy_preset->delete_ips();
945 }
946 // Remove Unlockouts.
947 Unlockout::truncate();
948 Smart_Ip_Detection::remove_header();
949
950 delete_site_option( Firewall_Service::WHITELIST_SERVER_PUBLIC_IP_OPTION );
951 delete_site_option( Main_Wp::WHITELIST_DASHBOARD_PUBLIC_IP_OPTION );
952 }
953
954 /**
955 * Provides data for the frontend.
956 *
957 * @return array An array of data for the frontend.
958 */
959 public function data_frontend(): array {
960 $user_ip = $this->get_user_ip();
961 $http_ip_header_value = $this->get_user_ip_header();
962 $summary_data = $this->get_summary();
963
964 $data = array(
965 'login' => array(
966 'month' => $summary_data['lockout_login_this_month'],
967 'week' => $summary_data['lockout_login_this_week'],
968 'day' => $summary_data['lockout_login_today'],
969 ),
970 'nf' => array(
971 'month' => $summary_data['lockout_404_this_month'],
972 'week' => $summary_data['lockout_404_this_week'],
973 'day' => $summary_data['lockout_404_today'],
974 ),
975 'ua' => array(
976 'month' => $summary_data['lockout_ua_this_month'],
977 'week' => $summary_data['lockout_ua_this_week'],
978 'day' => $summary_data['lockout_ua_today'],
979 ),
980 'month' => $summary_data['lockout_this_month'],
981 'day' => $summary_data['lockout_today'],
982 'last_lockout' => $summary_data['lockout_last'],
983 'settings' => $this->model->export(),
984 'user_ip' => implode( ', ', $user_ip ),
985 'user_ip_header' => $http_ip_header_value,
986 'trusted_proxy_presets' => Firewall_Service::trusted_proxy_presets(),
987 'global_ip' => wd_di()->get( \WP_Defender\Controller\Global_Ip::class )->data_frontend(),
988 'hub_connector' => wd_di()->get( Hub_Connector::class )->data_frontend(),
989 'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
990 'waf' => wd_di()->get( WAF::class )->data_frontend(),
991 'loginLockout' => wd_di()->get( Login_Lockout::class )->data_frontend(),
992 'nfLockout' => wd_di()->get( Nf_Lockout::class )->data_frontend(),
993 'uaLockout' => wd_di()->get( UA_Lockout::class )->data_frontend(),
994 'banning' => wd_di()->get( Blacklist::class )->data_frontend(),
995 'logs' => wd_di()->get( Firewall_Logs::class )->data_frontend(),
996 );
997
998 return array_merge( $data, $this->dump_routes_and_nonces() );
999 }
1000
1001 /**
1002 * Provides data for the dashboard widget.
1003 *
1004 * @return array An array of dashboard widget data.
1005 */
1006 public function dashboard_widget(): array {
1007 return array(
1008 'countries' => wd_di()->get( Blacklist_Lockout::class )->get_top_countries_blocked(),
1009 );
1010 }
1011
1012 /**
1013 * Imports data into the model.
1014 *
1015 * @param array $data Data to be imported into the model.
1016 */
1017 public function import_data( array $data ) {
1018 $model = $this->model;
1019
1020 $model->import( $data );
1021 if ( $model->validate() ) {
1022 $model->save();
1023 }
1024 }
1025
1026 /**
1027 * Exports strings.
1028 *
1029 * @return array An array of strings.
1030 */
1031 public function export_strings(): array {
1032 $strings = array();
1033 // Login lockout.
1034 $strings[] = Login_Lockout_Model::get_module_name() . ' '
1035 . Login_Lockout_Model::get_module_state( ( new Login_Lockout_Model() )->enabled );
1036 // Notfound lockout.
1037 $strings[] = Notfound_Lockout::get_module_name() . ' '
1038 . Notfound_Lockout::get_module_state( ( new Notfound_Lockout() )->enabled );
1039 // Global IP lockout.
1040 $strings[] = Global_Ip_Lockout::get_module_name() . ' '
1041 . Global_Ip_Lockout::get_module_state( ( new Global_Ip_Lockout() )->enabled );
1042 // AntiBot Global Firewall.
1043 $strings[] = Antibot_Global_Firewall_Setting::get_module_name() . ' '
1044 . Antibot_Global_Firewall_Setting::get_module_state( ( new Antibot_Global_Firewall_Setting() )->enabled );
1045 // UA lockout.
1046 $strings[] = User_Agent_Lockout::get_module_name() . ' '
1047 . User_Agent_Lockout::get_module_state( ( new User_Agent_Lockout() )->enabled );
1048 // Notifications and reports.
1049 if ( 'enabled' === ( new Firewall_Notification() )->status ) {
1050 $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
1051 }
1052 $strings[] = sprintf(
1053 /* translators: %s: Html for Pro-tag. */
1054 esc_html__( 'Email report inactive %s', 'defender-security' ),
1055 '<span class="sui-tag sui-tag-pro">Pro</span>'
1056 );
1057 $strings[] = sprintf(
1058 /* translators: %s: Html for Pro-tag. */
1059 esc_html__( 'Web Application Firewall (WAF) inactive %s', 'defender-security' ),
1060 '<span class="sui-tag sui-tag-pro">Pro</span>'
1061 );
1062
1063 return $strings;
1064 }
1065
1066 /**
1067 * Generates configuration strings based on the provided configuration.
1068 *
1069 * @param array $config Configuration data.
1070 *
1071 * @return array Returns an array of configuration strings.
1072 */
1073 public function config_strings( array $config ): array {
1074 $strings = array();
1075 // Login lockout.
1076 if ( isset( $config['login_protection'] ) ) {
1077 $strings[] = Login_Lockout_Model::get_module_name() . ' '
1078 . Login_Lockout_Model::get_module_state( (bool) $config['login_protection'] );
1079 }
1080 // NF lockout.
1081 if ( isset( $config['detect_404'] ) ) {
1082 $strings[] = Notfound_Lockout::get_module_name() . ' '
1083 . Notfound_Lockout::get_module_state( (bool) $config['detect_404'] );
1084 }
1085 // Custom IP List.
1086 if ( isset( $config['global_ip_list'] ) ) {
1087 $strings[] = Global_Ip_Lockout::get_module_name() . ' '
1088 . Global_Ip_Lockout::get_module_state( (bool) $config['global_ip_list'] );
1089 }
1090 // AntiBot Global Firewall.
1091 if ( isset( $config['antibot'] ) ) {
1092 $strings[] = Antibot_Global_Firewall_Setting::get_module_name() . ' '
1093 . Antibot_Global_Firewall_Setting::get_module_state( (bool) $config['antibot'] );
1094 }
1095 // UA lockout.
1096 if ( isset( $config['ua_banning_enabled'] ) ) {
1097 $strings[] = User_Agent_Lockout::get_module_name() . ' '
1098 . User_Agent_Lockout::get_module_state( (bool) $config['ua_banning_enabled'] );
1099 }
1100 // Notifications.
1101 if ( isset( $config['notification'] ) && 'enabled' === $config['notification'] ) {
1102 $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
1103 }
1104 $strings[] = sprintf(
1105 /* translators: %s: Html for Pro-tag. */
1106 esc_html__( 'Email report inactive %s', 'defender-security' ),
1107 '<span class="sui-tag sui-tag-pro">Pro</span>'
1108 );
1109 $strings[] = sprintf(
1110 /* translators: %s: Html for Pro-tag. */
1111 esc_html__( 'Web Application Firewall (WAF) inactive %s', 'defender-security' ),
1112 '<span class="sui-tag sui-tag-pro">Pro</span>'
1113 );
1114
1115 return $strings;
1116 }
1117
1118 /**
1119 * Schedule cleanup blocklist ips event.
1120 *
1121 * @return void
1122 */
1123 private function schedule_cleanup_blocklist_ips_event() {
1124 // Sometimes multiple requests come at the same time. So we will only count the web requests.
1125 if ( defined( 'DOING_AJAX' ) || defined( 'DOING_CRON' ) ) {
1126 return;
1127 }
1128
1129 $interval = $this->model->ip_blocklist_cleanup_interval;
1130 if ( ! $interval || 'never' === $interval ) {
1131 return;
1132 }
1133
1134 $interval_map = array(
1135 'daily' => DAY_IN_SECONDS,
1136 'weekly' => WEEK_IN_SECONDS,
1137 'monthly' => MONTH_IN_SECONDS,
1138 );
1139 /**
1140 * Network Cron Manager
1141 *
1142 * @var Network_Cron_Manager $network_cron_manager
1143 */
1144 $network_cron_manager = wd_di()->get( Network_Cron_Manager::class );
1145 $network_cron_manager->register_callback(
1146 'firewall_cleanup_temp_blocklist_ips',
1147 array( $this->service, 'firewall_clean_up_temporary_ip_blocklist' ),
1148 $interval_map[ $interval ],
1149 time() + 15
1150 );
1151
1152 $clear = get_site_option( 'wpdef_clear_schedule_firewall_cleanup_temp_blocklist_ips', false );
1153 if ( true === $clear ) {
1154 wp_clear_scheduled_hook( 'firewall_cleanup_temp_blocklist_ips' );
1155 }
1156 }
1157
1158 /**
1159 * Maybe add a filter to extend mime types.
1160 *
1161 * @return void
1162 * @since 2.6.3
1163 */
1164 public function maybe_extend_mime_types(): void {
1165 if ( is_admin() ) {
1166 $server = defender_get_data_from_request( null, 's' );
1167 $current_url = set_url_scheme( 'http://' . $server['HTTP_HOST'] . $server['REQUEST_URI'] );
1168 $current_query = wp_parse_url( $current_url, PHP_URL_QUERY );
1169 $current_query = $current_query ?? '';
1170 $referer_url = isset( $server['HTTP_REFERER'] ) && '' !== trim( $server['HTTP_REFERER'] ) ?
1171 filter_var( $server['HTTP_REFERER'], FILTER_SANITIZE_URL ) :
1172 '';
1173 $referer_query = wp_parse_url( $referer_url, PHP_URL_QUERY );
1174 $referer_query = $referer_query ?? '';
1175
1176 parse_str( $current_query, $current_queries );
1177 parse_str( $referer_query, $referer_queries );
1178
1179 if (
1180 ( preg_match( '#^' . network_admin_url() . '#i', $current_url ) &&
1181 isset( $current_queries['page'] ) && '' !== trim( $current_queries['page'] ) && $this->slug === $current_queries['page']
1182 ) ||
1183 ( preg_match( '#^' . network_admin_url() . '#i', $referer_url ) &&
1184 isset( $referer_queries['page'] ) && '' !== trim( $referer_queries['page'] ) && $this->slug === $referer_queries['page']
1185 )
1186 ) {
1187 // Add action hook here.
1188 add_filter( 'upload_mimes', array( $this, 'extend_mime_types' ) );
1189 }
1190 }
1191 }
1192
1193 /**
1194 * Filter list of allowed mime types and file extensions.
1195 *
1196 * @param array $types List of mime types.
1197 *
1198 * @return array
1199 */
1200 public function extend_mime_types( array $types ) {
1201 if ( ! isset( $types['csv'] ) || ! is_string( $types['csv'] ) || '' === trim( $types['csv'] ) ) {
1202 $types['csv'] = 'text/csv';
1203 }
1204
1205 return $types;
1206 }
1207
1208 /**
1209 * Remove all lockouts.
1210 *
1211 * @return Response
1212 * @defender_route
1213 * @since 3.3.0
1214 */
1215 public function empty_lockouts() {
1216 $ip_deleted = Lockout_Ip::truncate();
1217 $log_deleted = Lockout_Log::delete_lockout_records();
1218
1219 if ( $ip_deleted !== false && $log_deleted !== false ) {
1220 $this->log( 'Deleted lockout records successfully.', self::FIREWALL_LOG );
1221
1222 return new Response(
1223 true,
1224 array(
1225 'message' => esc_html__( 'Deleted lockout records successfully.', 'defender-security' ),
1226 'interval' => 1,
1227 )
1228 );
1229 }
1230
1231 return new Response(
1232 false,
1233 array(
1234 'message' => esc_html__( 'Failed remove!', 'defender-security' ),
1235 )
1236 );
1237 }
1238
1239 /**
1240 * Sync IP and it's HTTP header.
1241 *
1242 * @param Request $request The request object.
1243 *
1244 * @return Response
1245 * @defender_route
1246 */
1247 public function sync_ip_header( Request $request ): Response {
1248 $data = $request->get_data();
1249
1250 if ( 'automatic' === $data['ip_detection_type'] ) {
1251 $this->service_sid->smart_ip_detection_ping( true );
1252
1253 $ip_detail = $this->service_sid->get_smart_ip_detection_details();
1254 $user_ip = isset( $ip_detail[0] ) ? $ip_detail[0] : '';
1255 $user_ip_header = isset( $ip_detail[1] ) ? $ip_detail[1] : '';
1256 } else {
1257 $remote_addr = wd_di()->get( Remote_Address::class );
1258 $remote_addr->set_http_ip_header( $data['selected_http_header'] );
1259
1260 $user_ip = $remote_addr->get_ip_address();
1261 $user_ip_header = $remote_addr->get_http_ip_header_value( $data['selected_http_header'] );
1262 }
1263
1264 $data = array(
1265 'user_ip' => is_array( $user_ip ) ? implode( ', ', $user_ip ) : $user_ip,
1266 'user_ip_header' => $user_ip_header,
1267 );
1268
1269 return new Response(
1270 true,
1271 $data
1272 );
1273 }
1274
1275 /**
1276 * Prints inline Emoji detection script on specific admin pages only.
1277 * The conflict happens when other plugins work with emoji flags.
1278 *
1279 * @return void
1280 * @since 3.7.0
1281 */
1282 public function print_emoji_script(): void {
1283 $allowed_pages = array(
1284 $this->slug,
1285 wd_di()->get( \WP_Defender\Controller\Dashboard::class )->slug,
1286 );
1287
1288 if ( in_array( HTTP::get( 'page' ), $allowed_pages, true ) ) {
1289 if ( ! function_exists( 'print_emoji_detection_script' ) ) {
1290 include_once ABSPATH . WPINC . '/formatting.php';
1291 }
1292
1293 remove_filter( 'emoji_svg_url', '__return_false' );
1294 print_emoji_detection_script();
1295 }
1296 }
1297
1298 /**
1299 * Gather IP(s) from various headers and check if any IP is blacklisted, or temporary banned.
1300 *
1301 * @return void
1302 * @since 4.4.2
1303 */
1304 public function maybe_lockout_gathered_ips(): void {
1305 $msg = '';
1306 $ips = $this->service->get_user_ip();
1307
1308 if ( array() !== $ips ) {
1309 foreach ( $ips as $ip ) {
1310 $result = $this->maybe_lockout( $ip );
1311 if ( '' === $msg && is_string( $result ) && '' !== trim( $result ) ) {
1312 $msg = $result;
1313 }
1314 }
1315 }
1316
1317 if ( '' !== $msg ) {
1318 $this->actions_for_blocked( $msg, 0, 'blacklist', $ips );
1319 }
1320 }
1321
1322 /**
1323 * Clean up old records.
1324 *
1325 * @return void
1326 * @since 4.6.0
1327 */
1328 public function clean_up_unlockout(): void {
1329 $timestamp = $this->local_to_utc( Unlock_Me::get_expired_time() );
1330 Unlockout::remove_records( $timestamp, 100 );
1331 }
1332
1333 /**
1334 * Handle the request to detect the IP header.
1335 *
1336 * @return void
1337 */
1338 public function handle_detect_ip_header(): void {
1339 $nonce = defender_get_data_from_request( 'nonce', 'g' );
1340 $nonce_ctx = Smart_Ip_Detection::get_nonce_context();
1341
1342 if ( ! is_string( $nonce ) || '' === trim( $nonce ) || get_transient( $nonce_ctx ) !== $nonce ) {
1343 wp_send_json_error( __( 'Invalid nonce.', 'defender-security' ) );
1344 }
1345
1346 delete_transient( $nonce_ctx );
1347
1348 $result = $this->service_sid->smart_ip_detect_header();
1349 if ( is_wp_error( $result ) ) {
1350 wp_send_json_error( $result->get_error_message() );
1351 } else {
1352 wp_send_json_success(
1353 isset( $result['message'] )
1354 ? $result['message']
1355 : esc_html__( 'IP detection process completed.', 'defender-security' )
1356 );
1357 }
1358 }
1359
1360 /**
1361 * Unlock user from AntiBot Global Firewall blocklist.
1362 *
1363 * @return Response
1364 * @defender_route
1365 * @is_public
1366 */
1367 public function agf_unlock_user(): Response {
1368 $user_ips = $this->get_user_ip(); // Get all IPs belonging to the user.
1369 $attempts_key_prefix = 'wp_defender_agf_unlock_attempts_'; // Prefix for each transient key.
1370
1371 // Load attempts data and check limits in a single loop.
1372 foreach ( $user_ips as $ip ) {
1373 $key = $attempts_key_prefix . $ip;
1374 $attempts_data = get_transient( $key );
1375 $timestamps = is_array( $attempts_data ) ? $attempts_data : array();
1376
1377 // Check if the IP has reached the limit.
1378 if ( count( $timestamps ) >= Unlock_Me::get_attempt_limit() && ( time() - end( $timestamps ) ) < DAY_IN_SECONDS ) {
1379 $this->log( 'Verification attempt limit reached for IP: ' . $ip, Altcha_Handler::LOG_FILE_NAME );
1380
1381 return new Response( false, array( 'message' => esc_html__( 'You have reached the maximum limit of verification attempts. Please try again later or contact your web administrator for assistance.', 'defender-security' ) ) );
1382 }
1383 }
1384
1385 // Retrieve captcha payload data.
1386 $captcha_checkbox = defender_get_data_from_request( 'captcha', 'r' );
1387 $captcha_payload = array(
1388 'algorithm' => defender_get_data_from_request( 'algorithm', 'r' ),
1389 'challenge' => defender_get_data_from_request( 'challenge', 'r' ),
1390 'salt' => defender_get_data_from_request( 'salt', 'r' ),
1391 'signature' => defender_get_data_from_request( 'signature', 'r' ),
1392 'number' => defender_get_data_from_request( 'solution', 'r' ),
1393 );
1394
1395 // 'number' should be an integer.
1396 $captcha_payload['number'] = is_numeric( $captcha_payload['number'] ) ? (int) $captcha_payload['number'] : null;
1397
1398 // Successful verification of captcha.
1399 if ( '0' === $captcha_checkbox && wd_di()->get( Altcha_Handler::class )->verify_solution( $captcha_payload ) ) {
1400 // Reset attempt data for all IPs in a batch.
1401 foreach ( $user_ips as $ip ) {
1402 delete_transient( $attempts_key_prefix . $ip );
1403 }
1404
1405 $this->log( 'Captcha verified successfully. IP(s): ' . implode( ', ', $user_ips ), Altcha_Handler::LOG_FILE_NAME );
1406
1407 $unlock_result = wd_di()->get( Antibot_Global_Firewall_Model::class )->unlock_ips( $user_ips );
1408 if ( false !== $unlock_result ) {
1409 wd_di()->get( Antibot_Global_Firewall_Component::class )->log_ip_message( 'Successfully unlocked IP(s): ' . implode( ', ', $user_ips ) );
1410 }
1411
1412 return new Response( true, array() );
1413 }
1414
1415 // Verification failed: increment attempt count for all IPs.
1416 $current_time = time();
1417 foreach ( $user_ips as $ip ) {
1418 $attempts_key = $attempts_key_prefix . $ip;
1419 $timestamps = get_transient( $attempts_key ) ?? array();
1420 $timestamps[] = $current_time; // Add the current timestamp.
1421 set_transient( $attempts_key, $timestamps, DAY_IN_SECONDS );
1422 }
1423
1424 $this->log( 'Captcha verification failed for IP(s): ' . implode( ', ', $user_ips ), Altcha_Handler::LOG_FILE_NAME );
1425
1426 return new Response( false, array( 'message' => esc_html__( 'Captcha verification failed. Please try again.', 'defender-security' ) ) );
1427 }
1428 }
1429