PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / trunk
Defender Security – Malware Scanner, Login Security & Firewall vtrunk
6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 3.1.1 All 140 releases
defender-security / src / controller / class-firewall.php

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

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