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

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