PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.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.3.0, at src/controller/class-firewall.php

1,438 lines 45.3 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 WP_DEFENDER_VENDOR\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/core-ui.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 = isset( $data['user_data'] ) ? $data['user_data'] : '';
533 $ips = $this->get_user_ip();
534 // Check if at least one IP is blocked.
535 $blocked_ip = $this->service->get_blocked_ip( $ips );
536 // If nothing, just return.
537 if ( '' === $blocked_ip ) {
538 return new Response( false, array() );
539 }
540 // Count every verification attempt and stop before account lookup or side effects when the limit is reached.
541 if ( $this->check_attempt_counter_by( $blocked_ip ) ) {
542 return new Response( false, array() );
543 }
544
545 // Always return the same response for accepted attempts to avoid exposing account existence or role.
546 if ( ! is_string( $maybe_email ) || '' === trim( $maybe_email ) ) {
547 return new Response( true, array() );
548 }
549 // Maybe is it a user email?
550 $user = get_user_by( 'email', $maybe_email );
551 if ( ! is_object( $user ) ) {
552 // Maybe is it a username?
553 $user = get_user_by( 'login', $maybe_email );
554 if ( ! is_object( $user ) ) {
555 return new Response( true, array() );
556 }
557 }
558 // Send email only for admins.
559 if ( ! $this->is_admin( $user ) ) {
560 return new Response( true, array() );
561 }
562 // Create Unlockout records.
563 $arr_uids = array();
564 foreach ( $ips as $ip ) {
565 if ( Unlockout::has_pending_request( $ip, $user->user_email ) ) {
566 continue;
567 }
568 // Collect blocked IP's.
569 $created_id = wd_di()->get( Unlockout::class )->create( $ip, $user->user_email );
570 if ( $created_id ) {
571 $arr_uids[] = $created_id;
572 }
573 }
574
575 if ( array() !== $arr_uids ) {
576 $this->send_unlock_email( $user->user_email, $user->user_login, $arr_uids );
577 }
578
579 return new Response( true, array() );
580 }
581
582 /**
583 * Send again if the attempt limit has not expired.
584 *
585 * @return Response
586 * @defender_route
587 * @is_public
588 * @throws InvalidDatabaseException When unexpected data is found in the database.
589 */
590 public function send_again(): Response {
591 // Check if at least one IP is blocked.
592 $blocked_ip = $this->service->get_blocked_ip( $this->get_user_ip() );
593 if ( '' === $blocked_ip ) {
594 return new Response( false, array() );
595 }
596 $request_count = get_transient( $this->check_ip_by_remote_addr( $blocked_ip ) );
597 $is_expired = false !== $request_count && $request_count >= Unlock_Me::get_attempt_limit();
598
599 return new Response(
600 ! $is_expired,
601 array()
602 );
603 }
604
605 /**
606 * Sends an unlock email to the user.
607 *
608 * @param string $user_email The email address of the user.
609 * @param string $user_login The login name of the user.
610 * @param array $arr_uids The array of unique IDs.
611 *
612 * @return bool True if the email is sent successfully, false otherwise.
613 */
614 protected function send_unlock_email( $user_email, $user_login, $arr_uids ): bool {
615 $headers = wd_di()->get( Mail::class )->get_headers(
616 defender_noreply_email( 'wd_unlock_noreply_email' ),
617 Unlock_Me::SLUG_UNLOCK
618 );
619 $subject = __( 'Request to Unblock IP Address', 'defender-security' );
620 $subject = wp_specialchars_decode( $subject, ENT_QUOTES );
621
622 $content_body = $this->render_partial(
623 'email/unlockout',
624 array(
625 'subject' => $subject,
626 'name' => $user_login,
627 'unlocked_link' => Unlock_Me::create_url( $user_email, $user_login, $arr_uids ),
628 'generated_time' => $this->get_local_human_date( time() ),
629 ),
630 false
631 );
632 $content = $this->render_partial(
633 'email/index',
634 array(
635 'title' => esc_html__( 'Firewall', 'defender-security' ),
636 'content_body' => $content_body,
637 'unsubscribe_link' => '',
638 ),
639 false
640 );
641
642 // Send email.
643 return wp_mail( $user_email, $subject, $content, $headers );
644 }
645
646 /**
647 * Run actions for locked entities.
648 *
649 * @param string $message The message to show.
650 * @param int $remaining_time Remaining countdown time in seconds.
651 * @param string $reason Block's reason.
652 * @param array $ips Array of blocked IP's.
653 * @param bool $discourage_crawlers Whether to discourage crawlers with a noindex meta tag.
654 *
655 * @return void
656 */
657 public function actions_for_blocked(
658 string $message,
659 int $remaining_time = 0,
660 string $reason = '',
661 array $ips = array(),
662 bool $discourage_crawlers = false
663 ): void {
664 $action = HTTP::get( 'action', false );
665
666 if ( defender_base_action() === $action ) {
667 $nonce = HTTP::get( '_def_nonce', false );
668 $route = HTTP::get( 'route', '' );
669 $route = wp_unslash( $route );
670 if ( wp_verify_nonce( $nonce, $route ) ) {
671 return;
672 }
673 }
674 // Maybe unblock the request?
675 if ( Unlock_Me::SLUG_UNLOCK === $action && wd_di()->get( Unlock_Me::class )->maybe_unlock() ) {
676 return;
677 }
678 // Create a Lockout cookie to avoid caching in real case.
679 if ( 'demo' !== $reason ) {
680 // We follow the default naming process to find the required cookie later.
681 $cookie_name = str_replace( '.', '_', $ips[0] );
682 $cookie_name = 'wpdef_lockout_' . $cookie_name;
683 if ( ! isset( $_COOKIE[ $cookie_name ] ) ) {
684 setcookie( $cookie_name, true, time() + HOUR_IN_SECONDS, '/', '', is_ssl(), true );
685 }
686 }
687
688 $global_service = wd_di()->get( Global_IP_Component::class );
689 if ( Global_IP_Component::REASON_SLUG === $reason ) {
690 $global_service->log_event( $ips[0] );
691 }
692
693 $antibot_service = wd_di()->get( Antibot_Global_Firewall_Component::class );
694 if ( Antibot_Global_Firewall_Component::REASON_SLUG === $reason ) {
695 $antibot_service->log_ip_message( 'Blocked IP(s): ' . implode( ', ', $ips ) );
696 }
697
698 ob_start();
699
700 if ( ! headers_sent() ) {
701 if ( ! defined( 'DONOTCACHEPAGE' ) ) {
702 define( 'DONOTCACHEPAGE', true );
703 }
704
705 header( 'HTTP/1.0 403 Forbidden' );
706 header( 'Cache-Control: no-cache, no-store, must-revalidate, max-age=0' ); // HTTP 1.1.
707 header( 'Pragma: no-cache' ); // HTTP 1.0.
708 header( 'Expires: ' . wp_date( 'D, d M Y H:i:s', time() - 3600 ) . ' GMT' ); // Proxies.
709 header( 'Clear-Site-Data: "cache"' ); // Clear cache of the current request.
710
711 $global_ip_lockout = wd_di()->get( Global_Ip_Lockout::class );
712 $is_displayed = Unlock_Me::is_displayed( $reason, $ips );
713 $is_displayed_agf = $antibot_service->is_displayed( $ips );
714 $allow_self_unlock = $global_ip_lockout->allow_self_unlock;
715 $hide_btn_agf = $is_displayed_agf && ! $allow_self_unlock;
716 $malicious_bot = wd_di()->get( Malicious_Bot::class );
717 $discourage_crawlers = ! $discourage_crawlers && $malicious_bot->is_hash_request() ? true : $discourage_crawlers;
718 $params = array(
719 'message' => ! $hide_btn_agf ? $message : '',
720 'remaining_time' => $remaining_time,
721 'is_unlock_me' => $is_displayed,
722 'is_unlock_me_agf' => $is_displayed_agf,
723 'module_name_agf' => Antibot_Global_Firewall_Setting::get_module_name(),
724 'hide_btn_agf' => $hide_btn_agf,
725 'discourage_crawlers' => $discourage_crawlers,
726 );
727
728 // For AntiBot Global Firewall "Unlock me captcha".
729 if ( $is_displayed_agf && $allow_self_unlock ) {
730 $altcha_challenge = wd_di()->get( Altcha_Handler::class )->create_challenge();
731 $collection = $this->dump_routes_and_nonces();
732 $routes = $collection['routes'];
733 $nonces = $collection['nonces'];
734 $args = array(
735 'action' => defender_base_action(),
736 '_def_nonce' => $nonces['agf_unlock_user'],
737 'route' => $this->check_route( $routes['agf_unlock_user'] ),
738 );
739
740 $params['action_agf_unlock_user'] = add_query_arg( $args, admin_url( 'admin-ajax.php' ) );
741 $params['button_title'] = Antibot_Global_Firewall_Component::get_button_text();
742 $params['altcha'] = $altcha_challenge;
743 } elseif ( $is_displayed ) { // Only for "Unlock me".
744 $collection = $this->dump_routes_and_nonces();
745 $routes = $collection['routes'];
746 $nonces = $collection['nonces'];
747 // Prepare data.
748 $args = array(
749 'action' => defender_base_action(),
750 '_def_nonce' => $nonces['verify_blocked_user'],
751 'route' => $this->check_route( $routes['verify_blocked_user'] ),
752 );
753 $params['action_verify_blocked_user'] = add_query_arg( $args, admin_url( 'admin-ajax.php' ) );
754 // Rewrite args for another action.
755 $args['_def_nonce'] = $nonces['send_again'];
756 $args['route'] = $this->check_route( $routes['send_again'] );
757 $params['action_send_again'] = add_query_arg( $args, admin_url( 'admin-ajax.php' ) );
758
759 $params['button_title'] = Unlock_Me::get_feature_title();
760 $button_disabled = false;
761 if ( array() !== $ips ) {
762 // Get IP's.
763 $request_count = get_transient( $this->check_ip_by_remote_addr( $ips[0] ) );
764 $button_disabled = false !== $request_count && $request_count >= Unlock_Me::get_attempt_limit();
765 }
766
767 $params['button_disabled'] = $button_disabled;
768 }
769
770 $this->render_partial(
771 'ip-lockout/locked',
772 $params
773 );
774 }
775
776 /**
777 * Ignore WordPress.Security.EscapeOutput.OutputNotEscaped
778 * Why?
779 * Escaping this content would break the page.
780 */
781 echo ob_get_clean(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
782 exit();
783 }
784
785 /**
786 * We will check and prevent the access if the current IP is blacklist, or get temporary banned.
787 *
788 * @param string $ip The IP to check.
789 *
790 * @return void|string
791 * @throws InvalidDatabaseException When unexpected data is found in the database.
792 */
793 public function maybe_lockout( $ip ) {
794 do_action( 'wd_before_lockout', $ip );
795
796 if ( $this->service->skip_priority_lockout_checks( $ip ) ) {
797 return;
798 }
799
800 $is_blocklisted = $this->service->is_blocklisted_ip( $ip );
801 if ( $is_blocklisted['result'] ) {
802 // Get Blacklist_Lockout instance.
803 $blacklist_model = wd_di()->get( Blacklist_Model::class );
804 // This one is get blacklisted.
805 $this->actions_for_blocked(
806 $blacklist_model->ip_lockout_message,
807 0,
808 $is_blocklisted['reason'],
809 array( $ip )
810 );
811 }
812 // Get an instance of UA component.
813 $service_ua = wd_di()->get( User_Agent_Component::class );
814
815 if ( $service_ua->is_active_component() ) {
816 $user_agent = $service_ua->sanitize_user_agent();
817 if ( $service_ua->is_bad_post( $user_agent ) ) {
818 $service_ua->block_user_agent_or_ip( $user_agent, $ip, User_Agent_Component::REASON_BAD_POST );
819
820 return $service_ua->get_message();
821 }
822 if ( '' !== trim( $user_agent )
823 /**
824 * Apply additional checks for user agent before determining if it is a bad user agent.
825 *
826 * @param bool $is_bad_user_agent The result of checking if the user agent is bad.
827 * @param string $user_agent The user agent string to be checked.
828 * @param string $ip The IP address associated with the user agent.
829 *
830 * @return bool The final result after applying additional checks.
831 * @since 3.1.0
832 */
833 && apply_filters(
834 'wd_user_agent_additional_check',
835 $service_ua->is_bad_user_agent( $user_agent ),
836 $user_agent,
837 $ip
838 )
839 ) {
840 // Todo: if we use a hook then we should extend cases with a custom reason and send it for log.
841 $service_ua->block_user_agent_or_ip( $user_agent, $ip, User_Agent_Component::REASON_BAD_USER_AGENT );
842
843 return $service_ua->get_message();
844 }
845 }
846
847 $notfound_lockout = wd_di()->get( Notfound_Lockout::class );
848 if ( $notfound_lockout->enabled && false === $notfound_lockout->detect_logged && is_user_logged_in() ) {
849 /**
850 * We don't need to check the IP if:
851 * the current user can logged-in and isn't from blacklisted,
852 * the option detect_404_logged is disabled.
853 */
854 return;
855 }
856 // Check blacklist.
857 $model = Lockout_Ip::get( $ip );
858 if ( is_object( $model ) && $model->is_locked() ) {
859 $remaining_time = $model->remaining_release_time();
860 $this->actions_for_blocked( $model->lockout_message, $remaining_time, 'blacklist', array( $ip ) );
861 }
862 }
863
864 /**
865 * Remove all IP logs.
866 *
867 * @return Response
868 * @defender_route
869 */
870 public function empty_logs(): Response {
871 if ( Lockout_Log::truncate() ) {
872 $this->log( 'Logs have been successfully deleted.', self::FIREWALL_LOG );
873
874 return new Response(
875 true,
876 array(
877 'message' => esc_html__( 'Your logs have been successfully deleted.', 'defender-security' ),
878 'interval' => 1,
879 )
880 );
881 }
882
883 return new Response(
884 false,
885 array(
886 'message' => esc_html__( 'Failed remove!', 'defender-security' ),
887 )
888 );
889 }
890
891 /**
892 * Return summary data.
893 *
894 * @return array
895 */
896 public function get_summary(): array {
897 $summary = Lockout_Log::get_summary();
898
899 return array(
900 'lockout_last' => isset( $summary['lockout_last'] ) ?
901 $this->format_date_time( $summary['lockout_last'] ) :
902 esc_html__( 'Never', 'defender-security' ),
903 'lockout_today' => $summary['lockout_today'] ?? 0,
904 'lockout_this_month' => $summary['lockout_this_month'] ?? 0,
905 'lockout_login_today' => $summary['lockout_login_today'] ?? 0,
906 'lockout_login_this_week' => $summary['lockout_login_this_week'] ?? 0,
907 'lockout_login_this_month' => $summary['lockout_login_this_month'] ?? 0,
908 'lockout_404_today' => $summary['lockout_404_today'] ?? 0,
909 'lockout_404_this_week' => $summary['lockout_404_this_week'] ?? 0,
910 'lockout_404_this_month' => $summary['lockout_404_this_month'] ?? 0,
911 'lockout_ua_today' => $summary['lockout_ua_today'] ?? 0,
912 'lockout_ua_this_week' => $summary['lockout_ua_this_week'] ?? 0,
913 'lockout_ua_this_month' => $summary['lockout_ua_this_month'] ?? 0,
914 );
915 }
916
917 /**
918 * Removes settings for all submodules.
919 */
920 public function remove_settings(): void {
921 ( new Login_Lockout_Model() )->delete();
922 ( new Blacklist_Model() )->delete();
923 ( new Notfound_Lockout() )->delete();
924 ( new Firewall_Settings() )->delete();
925 ( new User_Agent_Lockout() )->delete();
926 ( new Global_Ip_Lockout() )->delete();
927 ( new Antibot_Global_Firewall_Setting() )->delete();
928 }
929
930 /**
931 * Delete all the data & the cache.
932 */
933 public function remove_data(): void {
934 Lockout_Log::truncate();
935 // Remove cached data.
936 Array_Cache::remove( 'countries', 'ip_lockout' );
937 // Remove Global IP data.
938 ( new Global_Ip() )->remove_data();
939 // Remove AntiBot Global Firewall data.
940 wd_di()->get( Antibot_Global_Firewall::class )->remove_data();
941 // Remove Malicious Bot data.
942 wd_di()->get( Malicious_Bot::class )->remove_data();
943 // Remove Fake Bot data.
944 wd_di()->get( Fake_Bot_Detection::class )->remove_data();
945 // Remove Firewall Logs data.
946 wd_di()->get( Firewall_Logs::class )->remove_data();
947 // Remove WAF data.
948 wd_di()->get( WAF::class )->remove_data();
949 // Clear Trusted Proxy data.
950 $trusted_proxy_preset = wd_di()->get( Trusted_Proxy_Preset::class );
951 foreach ( array_keys( Firewall_Service::trusted_proxy_presets() ) as $preset ) {
952 $trusted_proxy_preset->set_proxy_preset( $preset );
953 $trusted_proxy_preset->delete_ips();
954 }
955 // Remove Unlockouts.
956 Unlockout::truncate();
957 Smart_Ip_Detection::remove_header();
958
959 delete_site_option( Firewall_Service::WHITELIST_SERVER_PUBLIC_IP_OPTION );
960 delete_site_option( Main_Wp::WHITELIST_DASHBOARD_PUBLIC_IP_OPTION );
961 }
962
963 /**
964 * Provides data for the frontend.
965 *
966 * @return array An array of data for the frontend.
967 */
968 public function data_frontend(): array {
969 $user_ip = $this->get_user_ip();
970 $http_ip_header_value = $this->get_user_ip_header();
971 $summary_data = $this->get_summary();
972
973 $data = array(
974 'login' => array(
975 'month' => $summary_data['lockout_login_this_month'],
976 'week' => $summary_data['lockout_login_this_week'],
977 'day' => $summary_data['lockout_login_today'],
978 ),
979 'nf' => array(
980 'month' => $summary_data['lockout_404_this_month'],
981 'week' => $summary_data['lockout_404_this_week'],
982 'day' => $summary_data['lockout_404_today'],
983 ),
984 'ua' => array(
985 'month' => $summary_data['lockout_ua_this_month'],
986 'week' => $summary_data['lockout_ua_this_week'],
987 'day' => $summary_data['lockout_ua_today'],
988 ),
989 'month' => $summary_data['lockout_this_month'],
990 'day' => $summary_data['lockout_today'],
991 'last_lockout' => $summary_data['lockout_last'],
992 'settings' => $this->model->export(),
993 'user_ip' => implode( ', ', $user_ip ),
994 'user_ip_header' => $http_ip_header_value,
995 'trusted_proxy_presets' => Firewall_Service::trusted_proxy_presets(),
996 'global_ip' => wd_di()->get( \WP_Defender\Controller\Global_Ip::class )->data_frontend(),
997 'hub_connector' => wd_di()->get( Hub_Connector::class )->data_frontend(),
998 'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
999 'waf' => wd_di()->get( WAF::class )->data_frontend(),
1000 'loginLockout' => wd_di()->get( Login_Lockout::class )->data_frontend(),
1001 'nfLockout' => wd_di()->get( Nf_Lockout::class )->data_frontend(),
1002 'uaLockout' => wd_di()->get( UA_Lockout::class )->data_frontend(),
1003 'banning' => wd_di()->get( Blacklist::class )->data_frontend(),
1004 'logs' => wd_di()->get( Firewall_Logs::class )->data_frontend(),
1005 );
1006
1007 return array_merge( $data, $this->dump_routes_and_nonces() );
1008 }
1009
1010 /**
1011 * Provides data for the dashboard widget.
1012 *
1013 * @return array An array of dashboard widget data.
1014 */
1015 public function dashboard_widget(): array {
1016 return array(
1017 'countries' => wd_di()->get( Blacklist_Lockout::class )->get_top_countries_blocked(),
1018 );
1019 }
1020
1021 /**
1022 * Imports data into the model.
1023 *
1024 * @param array $data Data to be imported into the model.
1025 */
1026 public function import_data( array $data ) {
1027 $model = $this->model;
1028
1029 $model->import( $data );
1030 if ( $model->validate() ) {
1031 $model->save();
1032 }
1033 }
1034
1035 /**
1036 * Exports strings.
1037 *
1038 * @return array An array of strings.
1039 */
1040 public function export_strings(): array {
1041 $strings = array();
1042 // Login lockout.
1043 $strings[] = Login_Lockout_Model::get_module_name() . ' '
1044 . Login_Lockout_Model::get_module_state( ( new Login_Lockout_Model() )->enabled );
1045 // Notfound lockout.
1046 $strings[] = Notfound_Lockout::get_module_name() . ' '
1047 . Notfound_Lockout::get_module_state( ( new Notfound_Lockout() )->enabled );
1048 // Global IP lockout.
1049 $strings[] = Global_Ip_Lockout::get_module_name() . ' '
1050 . Global_Ip_Lockout::get_module_state( ( new Global_Ip_Lockout() )->enabled );
1051 // AntiBot Global Firewall.
1052 $strings[] = Antibot_Global_Firewall_Setting::get_module_name() . ' '
1053 . Antibot_Global_Firewall_Setting::get_module_state( ( new Antibot_Global_Firewall_Setting() )->enabled );
1054 // UA lockout.
1055 $strings[] = User_Agent_Lockout::get_module_name() . ' '
1056 . User_Agent_Lockout::get_module_state( ( new User_Agent_Lockout() )->enabled );
1057 // Notifications and reports.
1058 if ( 'enabled' === ( new Firewall_Notification() )->status ) {
1059 $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
1060 }
1061 $strings[] = sprintf(
1062 /* translators: %s: Html for Pro-tag. */
1063 esc_html__( 'Email report inactive %s', 'defender-security' ),
1064 '<span class="sui-tag sui-tag-pro">Pro</span>'
1065 );
1066 $strings[] = sprintf(
1067 /* translators: %s: Html for Pro-tag. */
1068 esc_html__( 'Web Application Firewall (WAF) inactive %s', 'defender-security' ),
1069 '<span class="sui-tag sui-tag-pro">Pro</span>'
1070 );
1071
1072 return $strings;
1073 }
1074
1075 /**
1076 * Generates configuration strings based on the provided configuration.
1077 *
1078 * @param array $config Configuration data.
1079 *
1080 * @return array Returns an array of configuration strings.
1081 */
1082 public function config_strings( array $config ): array {
1083 $strings = array();
1084 // Login lockout.
1085 if ( isset( $config['login_protection'] ) ) {
1086 $strings[] = Login_Lockout_Model::get_module_name() . ' '
1087 . Login_Lockout_Model::get_module_state( (bool) $config['login_protection'] );
1088 }
1089 // NF lockout.
1090 if ( isset( $config['detect_404'] ) ) {
1091 $strings[] = Notfound_Lockout::get_module_name() . ' '
1092 . Notfound_Lockout::get_module_state( (bool) $config['detect_404'] );
1093 }
1094 // Custom IP List.
1095 if ( isset( $config['global_ip_list'] ) ) {
1096 $strings[] = Global_Ip_Lockout::get_module_name() . ' '
1097 . Global_Ip_Lockout::get_module_state( (bool) $config['global_ip_list'] );
1098 }
1099 // AntiBot Global Firewall.
1100 if ( isset( $config['antibot'] ) ) {
1101 $strings[] = Antibot_Global_Firewall_Setting::get_module_name() . ' '
1102 . Antibot_Global_Firewall_Setting::get_module_state( (bool) $config['antibot'] );
1103 }
1104 // UA lockout.
1105 if ( isset( $config['ua_banning_enabled'] ) ) {
1106 $strings[] = User_Agent_Lockout::get_module_name() . ' '
1107 . User_Agent_Lockout::get_module_state( (bool) $config['ua_banning_enabled'] );
1108 }
1109 // Notifications.
1110 if ( isset( $config['notification'] ) && 'enabled' === $config['notification'] ) {
1111 $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
1112 }
1113 $strings[] = sprintf(
1114 /* translators: %s: Html for Pro-tag. */
1115 esc_html__( 'Email report inactive %s', 'defender-security' ),
1116 '<span class="sui-tag sui-tag-pro">Pro</span>'
1117 );
1118 $strings[] = sprintf(
1119 /* translators: %s: Html for Pro-tag. */
1120 esc_html__( 'Web Application Firewall (WAF) inactive %s', 'defender-security' ),
1121 '<span class="sui-tag sui-tag-pro">Pro</span>'
1122 );
1123
1124 return $strings;
1125 }
1126
1127 /**
1128 * Schedule cleanup blocklist ips event.
1129 *
1130 * @return void
1131 */
1132 private function schedule_cleanup_blocklist_ips_event() {
1133 // Sometimes multiple requests come at the same time. So we will only count the web requests.
1134 if ( defined( 'DOING_AJAX' ) || defined( 'DOING_CRON' ) ) {
1135 return;
1136 }
1137
1138 $interval = $this->model->ip_blocklist_cleanup_interval;
1139 if ( ! $interval || 'never' === $interval ) {
1140 return;
1141 }
1142
1143 $interval_map = array(
1144 'daily' => DAY_IN_SECONDS,
1145 'weekly' => WEEK_IN_SECONDS,
1146 'monthly' => MONTH_IN_SECONDS,
1147 );
1148 /**
1149 * Network Cron Manager
1150 *
1151 * @var Network_Cron_Manager $network_cron_manager
1152 */
1153 $network_cron_manager = wd_di()->get( Network_Cron_Manager::class );
1154 $network_cron_manager->register_callback(
1155 'firewall_cleanup_temp_blocklist_ips',
1156 array( $this->service, 'firewall_clean_up_temporary_ip_blocklist' ),
1157 $interval_map[ $interval ],
1158 time() + 15
1159 );
1160
1161 $clear = get_site_option( 'wpdef_clear_schedule_firewall_cleanup_temp_blocklist_ips', false );
1162 if ( true === $clear ) {
1163 wp_clear_scheduled_hook( 'firewall_cleanup_temp_blocklist_ips' );
1164 }
1165 }
1166
1167 /**
1168 * Maybe add a filter to extend mime types.
1169 *
1170 * @return void
1171 * @since 2.6.3
1172 */
1173 public function maybe_extend_mime_types(): void {
1174 if ( is_admin() ) {
1175 $server = defender_get_data_from_request( null, 's' );
1176 $current_url = set_url_scheme( 'http://' . $server['HTTP_HOST'] . $server['REQUEST_URI'] );
1177 $current_query = wp_parse_url( $current_url, PHP_URL_QUERY );
1178 $current_query = $current_query ?? '';
1179 $referer_url = isset( $server['HTTP_REFERER'] ) && '' !== trim( $server['HTTP_REFERER'] ) ?
1180 filter_var( $server['HTTP_REFERER'], FILTER_SANITIZE_URL ) :
1181 '';
1182 $referer_query = wp_parse_url( $referer_url, PHP_URL_QUERY );
1183 $referer_query = $referer_query ?? '';
1184
1185 parse_str( $current_query, $current_queries );
1186 parse_str( $referer_query, $referer_queries );
1187
1188 if (
1189 ( preg_match( '#^' . network_admin_url() . '#i', $current_url ) &&
1190 isset( $current_queries['page'] ) && '' !== trim( $current_queries['page'] ) && $this->slug === $current_queries['page']
1191 ) ||
1192 ( preg_match( '#^' . network_admin_url() . '#i', $referer_url ) &&
1193 isset( $referer_queries['page'] ) && '' !== trim( $referer_queries['page'] ) && $this->slug === $referer_queries['page']
1194 )
1195 ) {
1196 // Add action hook here.
1197 add_filter( 'upload_mimes', array( $this, 'extend_mime_types' ) );
1198 }
1199 }
1200 }
1201
1202 /**
1203 * Filter list of allowed mime types and file extensions.
1204 *
1205 * @param array $types List of mime types.
1206 *
1207 * @return array
1208 */
1209 public function extend_mime_types( array $types ) {
1210 if ( ! isset( $types['csv'] ) || ! is_string( $types['csv'] ) || '' === trim( $types['csv'] ) ) {
1211 $types['csv'] = 'text/csv';
1212 }
1213
1214 return $types;
1215 }
1216
1217 /**
1218 * Remove all lockouts.
1219 *
1220 * @return Response
1221 * @defender_route
1222 * @since 3.3.0
1223 */
1224 public function empty_lockouts() {
1225 $ip_deleted = Lockout_Ip::truncate();
1226 $log_deleted = Lockout_Log::delete_lockout_records();
1227
1228 if ( false !== $ip_deleted && false !== $log_deleted ) {
1229 $this->log( 'Deleted lockout records successfully.', self::FIREWALL_LOG );
1230
1231 return new Response(
1232 true,
1233 array(
1234 'message' => esc_html__( 'Deleted lockout records successfully.', 'defender-security' ),
1235 'interval' => 1,
1236 )
1237 );
1238 }
1239
1240 return new Response(
1241 false,
1242 array(
1243 'message' => esc_html__( 'Failed remove!', 'defender-security' ),
1244 )
1245 );
1246 }
1247
1248 /**
1249 * Sync IP and it's HTTP header.
1250 *
1251 * @param Request $request The request object.
1252 *
1253 * @return Response
1254 * @defender_route
1255 */
1256 public function sync_ip_header( Request $request ): Response {
1257 $data = $request->get_data();
1258
1259 if ( 'automatic' === $data['ip_detection_type'] ) {
1260 $this->service_sid->smart_ip_detection_ping( true );
1261
1262 $ip_detail = $this->service_sid->get_smart_ip_detection_details();
1263 $user_ip = isset( $ip_detail[0] ) ? $ip_detail[0] : '';
1264 $user_ip_header = isset( $ip_detail[1] ) ? $ip_detail[1] : '';
1265 } else {
1266 $remote_addr = wd_di()->get( Remote_Address::class );
1267 $remote_addr->set_http_ip_header( $data['selected_http_header'] );
1268
1269 $user_ip = $remote_addr->get_ip_address();
1270 $user_ip_header = $remote_addr->get_http_ip_header_value( $data['selected_http_header'] );
1271 }
1272
1273 $data = array(
1274 'user_ip' => is_array( $user_ip ) ? implode( ', ', $user_ip ) : $user_ip,
1275 'user_ip_header' => $user_ip_header,
1276 );
1277
1278 return new Response(
1279 true,
1280 $data
1281 );
1282 }
1283
1284 /**
1285 * Prints inline Emoji detection script on specific admin pages only.
1286 * The conflict happens when other plugins work with emoji flags.
1287 *
1288 * @return void
1289 * @since 3.7.0
1290 */
1291 public function print_emoji_script(): void {
1292 $allowed_pages = array(
1293 $this->slug,
1294 wd_di()->get( \WP_Defender\Controller\Dashboard::class )->slug,
1295 );
1296
1297 if ( in_array( HTTP::get( 'page' ), $allowed_pages, true ) ) {
1298 if ( ! function_exists( 'print_emoji_detection_script' ) ) {
1299 include_once ABSPATH . WPINC . '/formatting.php';
1300 }
1301
1302 remove_filter( 'emoji_svg_url', '__return_false' );
1303 print_emoji_detection_script();
1304 }
1305 }
1306
1307 /**
1308 * Gather IP(s) from various headers and check if any IP is blacklisted, or temporary banned.
1309 *
1310 * @return void
1311 * @since 4.4.2
1312 */
1313 public function maybe_lockout_gathered_ips(): void {
1314 $msg = '';
1315 $ips = $this->service->get_user_ip();
1316
1317 if ( array() !== $ips ) {
1318 foreach ( $ips as $ip ) {
1319 $result = $this->maybe_lockout( $ip );
1320 if ( '' === $msg && is_string( $result ) && '' !== trim( $result ) ) {
1321 $msg = $result;
1322 }
1323 }
1324 }
1325
1326 if ( '' !== $msg ) {
1327 $this->actions_for_blocked( $msg, 0, 'blacklist', $ips );
1328 }
1329 }
1330
1331 /**
1332 * Clean up old records.
1333 *
1334 * @return void
1335 * @since 4.6.0
1336 */
1337 public function clean_up_unlockout(): void {
1338 $timestamp = $this->local_to_utc( Unlock_Me::get_expired_time() );
1339 Unlockout::remove_records( $timestamp, 100 );
1340 }
1341
1342 /**
1343 * Handle the request to detect the IP header.
1344 *
1345 * @return void
1346 */
1347 public function handle_detect_ip_header(): void {
1348 $nonce = defender_get_data_from_request( 'nonce', 'g' );
1349 $nonce_ctx = Smart_Ip_Detection::get_nonce_context();
1350
1351 if ( ! is_string( $nonce ) || '' === trim( $nonce ) || get_transient( $nonce_ctx ) !== $nonce ) {
1352 wp_send_json_error( __( 'Invalid nonce.', 'defender-security' ) );
1353 }
1354
1355 delete_transient( $nonce_ctx );
1356
1357 $result = $this->service_sid->smart_ip_detect_header();
1358 if ( is_wp_error( $result ) ) {
1359 wp_send_json_error( $result->get_error_message() );
1360 } else {
1361 wp_send_json_success(
1362 isset( $result['message'] )
1363 ? $result['message']
1364 : esc_html__( 'IP detection process completed.', 'defender-security' )
1365 );
1366 }
1367 }
1368
1369 /**
1370 * Unlock user from AntiBot Global Firewall blocklist.
1371 *
1372 * @return Response
1373 * @defender_route
1374 * @is_public
1375 */
1376 public function agf_unlock_user(): Response {
1377 $user_ips = $this->get_user_ip(); // Get all IPs belonging to the user.
1378 $attempts_key_prefix = 'wp_defender_agf_unlock_attempts_'; // Prefix for each transient key.
1379
1380 // Load attempts data and check limits in a single loop.
1381 foreach ( $user_ips as $ip ) {
1382 $key = $attempts_key_prefix . $ip;
1383 $attempts_data = get_transient( $key );
1384 $timestamps = is_array( $attempts_data ) ? $attempts_data : array();
1385
1386 // Check if the IP has reached the limit.
1387 if ( count( $timestamps ) >= Unlock_Me::get_attempt_limit() && ( time() - end( $timestamps ) ) < DAY_IN_SECONDS ) {
1388 $this->log( 'Verification attempt limit reached for IP: ' . $ip, Altcha_Handler::LOG_FILE_NAME );
1389
1390 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' ) ) );
1391 }
1392 }
1393
1394 // Retrieve captcha payload data.
1395 $captcha_checkbox = defender_get_data_from_request( 'captcha', 'r' );
1396 $captcha_payload = array(
1397 'algorithm' => defender_get_data_from_request( 'algorithm', 'r' ),
1398 'challenge' => defender_get_data_from_request( 'challenge', 'r' ),
1399 'salt' => defender_get_data_from_request( 'salt', 'r' ),
1400 'signature' => defender_get_data_from_request( 'signature', 'r' ),
1401 'number' => defender_get_data_from_request( 'solution', 'r' ),
1402 );
1403
1404 // 'number' should be an integer.
1405 $captcha_payload['number'] = is_numeric( $captcha_payload['number'] ) ? (int) $captcha_payload['number'] : null;
1406
1407 // Successful verification of captcha.
1408 if ( '0' === $captcha_checkbox && wd_di()->get( Altcha_Handler::class )->verify_solution( $captcha_payload ) ) {
1409 // Reset attempt data for all IPs in a batch.
1410 foreach ( $user_ips as $ip ) {
1411 delete_transient( $attempts_key_prefix . $ip );
1412 }
1413
1414 $this->log( 'Captcha verified successfully. IP(s): ' . implode( ', ', $user_ips ), Altcha_Handler::LOG_FILE_NAME );
1415
1416 $unlock_result = wd_di()->get( Antibot_Global_Firewall_Model::class )->unlock_ips( $user_ips );
1417 if ( false !== $unlock_result ) {
1418 wd_di()->get( Antibot_Global_Firewall_Component::class )->log_ip_message( 'Successfully unlocked IP(s): ' . implode( ', ', $user_ips ) );
1419 }
1420
1421 return new Response( true, array() );
1422 }
1423
1424 // Verification failed: increment attempt count for all IPs.
1425 $current_time = time();
1426 foreach ( $user_ips as $ip ) {
1427 $attempts_key = $attempts_key_prefix . $ip;
1428 $timestamps = get_transient( $attempts_key ) ?? array();
1429 $timestamps[] = $current_time; // Add the current timestamp.
1430 set_transient( $attempts_key, $timestamps, DAY_IN_SECONDS );
1431 }
1432
1433 $this->log( 'Captcha verification failed for IP(s): ' . implode( ', ', $user_ips ), Altcha_Handler::LOG_FILE_NAME );
1434
1435 return new Response( false, array( 'message' => esc_html__( 'Captcha verification failed. Please try again.', 'defender-security' ) ) );
1436 }
1437 }
1438