PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.6.0
Defender Security – Malware Scanner, Login Security & Firewall v5.6.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 5.6.0, at src/controller/class-firewall.php

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