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

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