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

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