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

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