File: /var/www/html/wpdeskera/wp-content/plugins/defender-security/src/controller/class-firewall.php
<?php
/**
* Handles IP lockouts, notifications, and settings related to the firewall features.
*
* @package WP_Defender\Controller
*/
namespace WP_Defender\Controller;
use Exception;
use WP_Defender\Event;
use Calotes\Helper\HTTP;
use WP_Defender\Traits\IP;
use Calotes\Component\Request;
use Calotes\Component\Response;
use Calotes\Helper\Array_Cache;
use WP_Defender\Controller\Dashboard;
use WP_Defender\Model\Setting\Antibot_Global_Firewall_Setting;
use WP_Defender\Component\Mail;
use WP_Defender\Traits\Formats;
use WP_Defender\Model\Unlockout;
use WP_Defender\Behavior\WPMUDEV;
use WP_Defender\Model\Lockout_Ip;
use WP_Defender\Model\Lockout_Log;
use WP_Defender\Component\Unlock_Me;
use WP_Defender\Component\IP\Antibot_Global_Firewall as Antibot_Global_Firewall_Component;
use WP_Defender\Component\IP\Global_IP as Global_IP_Component;
use WP_Defender\Component\Blacklist_Lockout;
use WP_Defender\Component\Http\Remote_Address;
use MaxMind\Db\Reader\InvalidDatabaseException;
use WP_Defender\Model\Setting\Notfound_Lockout;
use WP_Defender\Model\Setting\Global_Ip_Lockout;
use WP_Defender\Model\Setting\User_Agent_Lockout;
use WP_Defender\Component\Config\Config_Hub_Helper;
use WP_Defender\Model\Notification\Firewall_Report;
use WP_Defender\Component\Firewall as Firewall_Service;
use WP_Defender\Model\Notification\Firewall_Notification;
use WP_Defender\Model\Setting\Firewall as Firewall_Settings;
use WP_Defender\Component\User_Agent as User_Agent_Component;
use WP_Defender\Model\Setting\Blacklist_Lockout as Blacklist_Model;
use WP_Defender\Model\Setting\Login_Lockout as Login_Lockout_Model;
use WP_Defender\Component\Trusted_Proxy_Preset\Trusted_Proxy_Preset;
use WP_Defender\Component\Smart_Ip_Detection;
use WP_Defender\Helper\Analytics\Firewall as Firewall_Analytics;
use WP_Defender\Model\Antibot_Global_Firewall as Antibot_Global_Firewall_Model;
use WP_Defender\Component\Altcha_Handler;
use WP_Defender\Controller\Hub_Connector;
use WP_Defender\Integrations\Main_Wp;
/**
* Handles IP lockouts, notifications, and settings related to the firewall features.
*/
class Firewall extends Event {
use IP;
use Formats;
public const FIREWALL_LOG = 'firewall.log';
/**
* The slug identifier for this controller.
*
* @var string
*/
protected $slug = 'wdf-ip-lockout';
/**
* The model for handling the data.
*
* @var Firewall_Settings
*/
protected $model;
/**
* Service for handling logic.
*
* @var Firewall_Service
*/
public $service;
/**
* Service for handling Smart IP Detection.
*
* @var Smart_Ip_Detection
*/
public $service_sid;
/**
* Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
*/
public function __construct() {
$title = esc_html__( 'Firewall', 'defender-security' );
$this->register_page(
$title,
$this->slug,
array( $this, 'main_view' ),
$this->parent_slug,
null,
$this->menu_title( $title )
);
$this->model = wd_di()->get( Firewall_Settings::class );
$this->service = wd_di()->get( Firewall_Service::class );
$this->service_sid = wd_di()->get( Smart_Ip_Detection::class );
$this->register_routes();
$this->maybe_show_demo_lockout();
$this->maybe_lockout_gathered_ips();
// Todo: pass $ip as argument to Login_Lockout/Nf_Lockout.
wd_di()->get( Login_Lockout::class );
wd_di()->get( Nf_Lockout::class );
wd_di()->get( Blacklist::class );
wd_di()->get( Firewall_Logs::class );
wd_di()->get( UA_Lockout::class );
wd_di()->get( Global_Ip::class );
wd_di()->get( Antibot_Global_Firewall::class );
wd_di()->get( Bot_Trap::class );
// Integrate MainWP plugin.
wd_di()->get( Main_Wp::class );
// We will schedule the time to clean up old firewall logs.
if ( ! wp_next_scheduled( 'firewall_clean_up_logs' ) ) {
wp_schedule_event( time() + 10, 'hourly', 'firewall_clean_up_logs' );
}
// Schedule cleanup blocklist ips event.
$this->schedule_cleanup_blocklist_ips_event();
add_action( 'firewall_clean_up_logs', array( $this, 'clean_up_firewall_logs' ) );
add_action( 'firewall_cleanup_temp_blocklist_ips', array( $this, 'clean_up_temporary_ip_blocklist' ) );
// Clean unwanted records from lockout table.
if ( ! wp_next_scheduled( 'wpdef_firewall_clean_up_lockout' ) ) {
wp_schedule_event( time() + 10, 'weekly', 'wpdef_firewall_clean_up_lockout' );
}
add_action( 'wpdef_firewall_clean_up_lockout', array( $this, 'clean_up_firewall_lockout' ) );
// Clean old Unlockouts.
if ( ! wp_next_scheduled( 'wpdef_firewall_clean_up_unlockout' ) ) {
wp_schedule_event( time() + 20, 'weekly', 'wpdef_firewall_clean_up_unlockout' );
}
add_action( 'wpdef_firewall_clean_up_unlockout', array( $this, 'clean_up_unlockout' ) );
// Additional hooks.
add_action( 'defender_enqueue_assets', array( $this, 'enqueue_assets' ), 11 );
add_action( 'admin_print_scripts', array( $this, 'print_emoji_script' ) );
$this->maybe_extend_mime_types();
if ( ! wp_next_scheduled( 'wpdef_firewall_fetch_trusted_proxy_preset_ips' ) ) {
wp_schedule_event( time(), 'daily', 'wpdef_firewall_fetch_trusted_proxy_preset_ips' );
}
add_action(
'wpdef_firewall_fetch_trusted_proxy_preset_ips',
array(
&
$this,
'update_trusted_proxy_preset_ips',
)
);
add_action( 'wp_ajax_' . Smart_Ip_Detection::ACTION_PING, array( $this, 'handle_detect_ip_header' ) );
add_action( 'wp_ajax_nopriv_' . Smart_Ip_Detection::ACTION_PING, array( $this, 'handle_detect_ip_header' ) );
if ( $this->service_sid->is_smart_ip_detection_enabled() ) {
if ( ! wp_next_scheduled( 'wpdef_smart_ip_detection_ping' ) ) {
wp_schedule_event( time(), 'weekly', 'wpdef_smart_ip_detection_ping' );
}
add_action( 'wpdef_smart_ip_detection_ping', array( $this, 'smart_ip_detection_ping' ) );
}
/**
* Whitelist server IP.
*/
if ( ! wp_next_scheduled( 'wpdef_firewall_whitelist_server_public_ip' ) ) {
wp_schedule_event( time() + 15, 'twicedaily', 'wpdef_firewall_whitelist_server_public_ip' );
}
add_action( 'wpdef_firewall_whitelist_server_public_ip', array( $this, 'whitelist_server_public_ip' ) );
}
/**
* Get menu title.
*
* @param string $title The original menu title.
*
* @return string
*/
protected function menu_title( string $title ): string {
return $title;
}
/**
* Clean up all the old logs from the local storage, this will happen per hourly basis.
*
* @return void
* @throws Exception On failure.
*/
public function clean_up_firewall_logs(): void {
$this->service->firewall_clean_up_logs();
}
/**
* Clean up temporary IP block list.
*
* @return void
*/
public function clean_up_temporary_ip_blocklist(): void {
$this->service->firewall_clean_up_temporary_ip_blocklist();
}
/**
* This is for handling request from dashboard.
*
* @defender_route
* @return Response
*/
public function dashboard_activation() {
$il = wd_di()->get( Login_Lockout_Model::class );
$nf = wd_di()->get( Notfound_Lockout::class );
$ua = wd_di()->get( User_Agent_Lockout::class );
$il->enabled = true;
$il->save();
$nf->enabled = true;
$nf->save();
$ua->enabled = true;
$ua->save();
return new Response( true, $this->to_array() );
}
/**
* Render the view page.
*
* @return void
*/
public function main_view(): void {
$this->render( 'main' );
}
/**
* Save settings.
*
* @param Request $request The request object containing new settings data.
*
* @return Response
* @defender_route
*/
public function save_settings( Request $request ): Response {
$data = $request->get_data_by_model( $this->model );
// 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.
$is_preset_update = false;
if (
in_array(
$data['http_ip_header'],
Firewall_Service::custom_http_headers(),
true
)
&& ! empty( $data['trusted_proxy_preset'] )
&& $data['trusted_proxy_preset'] !== $this->model->trusted_proxy_preset
) {
$is_preset_update = true;
}
$is_ip_detection_type_changed = false;
if ( 'automatic' === $data['ip_detection_type'] && $this->model->ip_detection_type !== $data['ip_detection_type'] ) {
$is_ip_detection_type_changed = true;
}
$is_http_ip_header_changed = false;
if ( $this->model->http_ip_header !== $data['http_ip_header'] ) {
$is_http_ip_header_changed = true;
}
$this->model->import( $data );
if ( $this->model->validate() ) {
$this->service->update_cron_schedule_interval( $data['ip_blocklist_cleanup_interval'] );
$this->model->save();
Config_Hub_Helper::set_clear_active_flag();
// Fetch trusted proxy ips.
if ( $is_preset_update ) {
$this->service->update_trusted_proxy_preset_ips();
}
if ( $is_ip_detection_type_changed ) {
$this->service_sid->smart_ip_detection_ping();
}
// Maybe track.
if ( ( $is_ip_detection_type_changed || $is_http_ip_header_changed )
&& ! defender_is_wp_cli()
) {
$firewall_analytics = wd_di()->get( Firewall_Analytics::class );
$detection_method = Firewall_Analytics::get_detection_method_label(
$data['ip_detection_type'],
$data['http_ip_header']
);
$firewall_analytics->track_feature(
Firewall_Analytics::EVENT_IP_DETECTION,
array( Firewall_Analytics::PROP_IP_DETECTION => $detection_method )
);
}
return new Response(
true,
array(
'message' => esc_html__( 'Your settings have been updated.', 'defender-security' ),
'auto_close' => true,
)
);
}
return new Response(
false,
array(
'message' => $this->model->get_formatted_errors(),
)
);
}
/**
* Converts the current object to an array representation.
*
* @return array The array representation of the object.
*/
public function to_array(): array {
$il = wd_di()->get( Login_Lockout_Model::class );
$nf = wd_di()->get( Notfound_Lockout::class );
$ua = wd_di()->get( User_Agent_Lockout::class );
return array_merge(
array(
'summary' => array(
'ip' => array(
'week' => Lockout_Log::count_login_lockout_last_7_days(),
),
'nf' => array(
'week' => Lockout_Log::count_404_lockout_last_7_days(),
),
'ua' => array(
'week' => Lockout_Log::count_ua_lockout_last_7_days(),
),
'lastLockout' => Lockout_Log::get_last_lockout_date(),
),
'notification' => true,
'enabled' => $nf->enabled || $il->enabled || $ua->enabled,
'enable_login' => $il->enabled,
'enable_404' => $nf->enabled,
'enable_ua' => $ua->enabled,
),
$this->dump_routes_and_nonces()
);
}
/**
* Enqueues scripts and styles for this page.
* Only enqueues assets if the page is active.
*/
public function enqueue_assets() {
if ( ! $this->is_page_active() ) {
return;
}
wp_enqueue_media();
wp_localize_script( 'def-iplockout', 'iplockout', $this->data_frontend() );
wp_enqueue_script( 'def-iplockout' );
$this->enqueue_main_assets();
do_action( 'defender_ip_lockout_action_assets' );
}
/**
* Renders the preview of lockout screen.
*
* @return void
*/
private function maybe_show_demo_lockout(): void {
$is_test = HTTP::get( 'def-lockout-demo', 0 );
if ( 1 === (int) $is_test ) {
$type = HTTP::get( 'type' );
$remaining_time = 0;
switch ( $type ) {
case 'login':
$settings = wd_di()->get( Login_Lockout_Model::class );
$message = $settings->lockout_message;
$remaining_time = 3600;
break;
case '404':
$settings = wd_di()->get( Notfound_Lockout::class );
$message = $settings->lockout_message;
$remaining_time = 3600;
break;
case 'blocklist':
$settings = wd_di()->get( Blacklist_Model::class );
$message = $settings->ip_lockout_message;
break;
case 'ua-lockout':
$settings = wd_di()->get( User_Agent_Lockout::class );
$message = $settings->message;
$remaining_time = 3600;
break;
default:
$message = esc_html__( 'Demo', 'defender-security' );
break;
}
$this->actions_for_blocked( $message, $remaining_time, 'demo', $this->get_user_ip() );
exit;
}
}
/**
* Checks the attempt counter for a blocked IP address.
*
* @param string $blocked_ip The blocked IP address to check the attempt counter for.
*
* @return bool
*/
private function check_attempt_counter_by( $blocked_ip ): bool {
$blocked_ip = $this->check_ip_by_remote_addr( $blocked_ip );
$request_count = get_transient( $blocked_ip );
$disabled = false;
if ( false === $request_count ) {
set_transient( $blocked_ip, 1, Unlock_Me::EXPIRED_COUNTER_TIME );
} elseif ( (int) $request_count >= Unlock_Me::get_attempt_limit() ) {
$disabled = true;
} else {
++$request_count;
set_transient( $blocked_ip, $request_count, Unlock_Me::EXPIRED_COUNTER_TIME );
}
return $disabled;
}
/**
* Verify if the user is blocked.
*
* @param Request $request The request object.
*
* @return Response
* @defender_route
* @is_public
* @throws InvalidDatabaseException When unexpected data is found in the database.
*/
public function verify_blocked_user( Request $request ): Response {
$data = $request->get_data(
array(
'user_data' => array(
'type' => 'string',
'sanitize' => 'sanitize_text_field',
),
)
);
$maybe_email = $data['user_data'];
if ( empty( $maybe_email ) ) {
return new Response( false, array() );
}
$ips = $this->get_user_ip();
// Check if at least one IP is blocked.
$blocked_ip = $this->service->get_blocked_ip( $ips );
// If nothing, just return.
if ( '' === $blocked_ip ) {
return new Response( false, array() );
}
// Maybe is it a user email?
$user = get_user_by( 'email', $maybe_email );
if ( ! is_object( $user ) ) {
// Maybe is it a username?
$user = get_user_by( 'login', $maybe_email );
if ( ! is_object( $user ) ) {
$this->check_attempt_counter_by( $blocked_ip );
return new Response( false, array() );
}
}
// Send email only for admins.
if ( ! $this->is_admin( $user ) ) {
// No need to count attempts for existed user but non-admin.
return new Response( false, array() );
}
// Create Unlockout records.
$arr_uids = array();
foreach ( $ips as $ip ) {
// Collect blocked IP's.
$created_id = wd_di()->get( Unlockout::class )->create( $ip, $user->user_email );
if ( $created_id ) {
$arr_uids[] = $created_id;
}
}
$this->send_unlock_email( $user->user_email, $user->user_login, $arr_uids );
return new Response( true, array() );
}
/**
* Send again if the attempt limit has not expired.
*
* @return Response
* @defender_route
* @is_public
* @throws InvalidDatabaseException When unexpected data is found in the database.
*/
public function send_again(): Response {
// Check if at least one IP is blocked.
$blocked_ip = $this->service->get_blocked_ip( $this->get_user_ip() );
if ( '' === $blocked_ip ) {
return new Response( false, array() );
}
$request_count = get_transient( $this->check_ip_by_remote_addr( $blocked_ip ) );
$is_expired = false !== $request_count && $request_count >= Unlock_Me::get_attempt_limit();
return new Response(
! $is_expired,
array()
);
}
/**
* Sends an unlock email to the user.
*
* @param string $user_email The email address of the user.
* @param string $user_login The login name of the user.
* @param array $arr_uids The array of unique IDs.
*
* @return bool True if the email is sent successfully, false otherwise.
*/
protected function send_unlock_email( $user_email, $user_login, $arr_uids ): bool {
$headers = wd_di()->get( Mail::class )->get_headers(
defender_noreply_email( 'wd_unlock_noreply_email' ),
Unlock_Me::SLUG_UNLOCK
);
$subject = esc_html__( 'Request to Unblock IP Address', 'defender-security' );
$content_body = $this->render_partial(
'email/unlockout',
array(
'subject' => $subject,
'name' => $user_login,
'unlocked_link' => Unlock_Me::create_url( $user_email, $user_login, $arr_uids ),
'generated_time' => $this->get_local_human_date( time() ),
),
false
);
$content = $this->render_partial(
'email/index',
array(
'title' => esc_html__( 'Firewall', 'defender-security' ),
'content_body' => $content_body,
'unsubscribe_link' => '',
),
false
);
// Send email.
return wp_mail( $user_email, $subject, $content, $headers );
}
/**
* Run actions for locked entities.
*
* @param string $message The message to show.
* @param int $remaining_time Remaining countdown time in seconds.
* @param string $reason Block's reason.
* @param array $ips Array of blocked IP's.
*
* @return void
*/
public function actions_for_blocked(
string $message,
int $remaining_time = 0,
string $reason = '',
array $ips = array()
): void {
$action = HTTP::get( 'action', false );
if ( defender_base_action() === $action ) {
$nonce = HTTP::get( '_def_nonce', false );
$route = HTTP::get( 'route', '' );
$route = wp_unslash( $route );
if ( wp_verify_nonce( $nonce, $route ) ) {
return;
}
}
// Maybe unblock the request?
if ( Unlock_Me::SLUG_UNLOCK === $action && wd_di()->get( Unlock_Me::class )->maybe_unlock() ) {
return;
}
// Create a Lockout cookie to avoid caching in real case.
if ( 'demo' !== $reason ) {
// We follow the default naming process to find the required cookie later.
$cookie_name = str_replace( '.', '_', $ips[0] );
$cookie_name = 'wpdef_lockout_' . $cookie_name;
if ( ! isset( $_COOKIE[ $cookie_name ] ) ) {
setcookie( $cookie_name, true, time() + HOUR_IN_SECONDS, '/' );
}
}
$global_service = wd_di()->get( Global_IP_Component::class );
if ( Global_IP_Component::REASON_SLUG === $reason ) {
$global_service->log_event( $ips[0] );
}
$antibot_service = wd_di()->get( Antibot_Global_Firewall_Component::class );
if ( Antibot_Global_Firewall_Component::REASON_SLUG === $reason ) {
$antibot_service->log_ip_message( 'Blocked IP(s): ' . implode( ', ', $ips ) );
}
ob_start();
if ( ! headers_sent() ) {
if ( ! defined( 'DONOTCACHEPAGE' ) ) {
define( 'DONOTCACHEPAGE', true );
}
header( 'HTTP/1.0 403 Forbidden' );
header( 'Cache-Control: no-cache, no-store, must-revalidate, max-age=0' ); // HTTP 1.1.
header( 'Pragma: no-cache' ); // HTTP 1.0.
header( 'Expires: ' . wp_date( 'D, d M Y H:i:s', time() - 3600 ) . ' GMT' ); // Proxies.
header( 'Clear-Site-Data: "cache"' ); // Clear cache of the current request.
$global_ip_lockout = wd_di()->get( Global_Ip_Lockout::class );
$is_displayed = Unlock_Me::is_displayed( $reason, $ips );
$is_displayed_agf = $antibot_service->is_displayed( $ips );
$allow_self_unlock = $global_ip_lockout->allow_self_unlock;
$hide_btn_agf = $is_displayed_agf && ! $allow_self_unlock;
$params = array(
'message' => ! $hide_btn_agf ? $message : '',
'remaining_time' => $remaining_time,
'is_unlock_me' => $is_displayed,
'is_unlock_me_agf' => $is_displayed_agf,
'module_name_agf' => Antibot_Global_Firewall_Setting::get_module_name(),
'hide_btn_agf' => $hide_btn_agf,
);
// For AntiBot Global Firewall "Unlock me captcha".
if ( $is_displayed_agf && $allow_self_unlock ) {
$altcha_challenge = wd_di()->get( Altcha_Handler::class )->create_challenge();
$collection = $this->dump_routes_and_nonces();
$routes = $collection['routes'];
$nonces = $collection['nonces'];
$args = array(
'action' => defender_base_action(),
'_def_nonce' => $nonces['agf_unlock_user'],
'route' => $this->check_route( $routes['agf_unlock_user'] ),
);
$params['action_agf_unlock_user'] = add_query_arg( $args, admin_url( 'admin-ajax.php' ) );
$params['button_title'] = Antibot_Global_Firewall_Component::get_button_text();
$params['altcha'] = $altcha_challenge;
} elseif ( $is_displayed ) { // Only for "Unlock me".
$collection = $this->dump_routes_and_nonces();
$routes = $collection['routes'];
$nonces = $collection['nonces'];
// Prepare data.
$args = array(
'action' => defender_base_action(),
'_def_nonce' => $nonces['verify_blocked_user'],
'route' => $this->check_route( $routes['verify_blocked_user'] ),
);
$params['action_verify_blocked_user'] = add_query_arg( $args, admin_url( 'admin-ajax.php' ) );
// Rewrite args for another action.
$args['_def_nonce'] = $nonces['send_again'];
$args['route'] = $this->check_route( $routes['send_again'] );
$params['action_send_again'] = add_query_arg( $args, admin_url( 'admin-ajax.php' ) );
$params['button_title'] = Unlock_Me::get_feature_title();
$button_disabled = false;
if ( ! empty( $ips ) ) {
// Get IP's.
$request_count = get_transient( $this->check_ip_by_remote_addr( $ips[0] ) );
$button_disabled = false !== $request_count && $request_count >= Unlock_Me::get_attempt_limit();
}
$params['button_disabled'] = $button_disabled;
}
$this->render_partial(
'ip-lockout/locked',
$params
);
}
/**
* Ignore WordPress.Security.EscapeOutput.OutputNotEscaped
* Why?
* Escaping this content would break the page.
*/
echo ob_get_clean(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
exit();
}
/**
* We will check and prevent the access if the current IP is blacklist, or get temporary banned.
*
* @param string $ip The IP to check.
*
* @return void|string
* @throws InvalidDatabaseException When unexpected data is found in the database.
*/
public function maybe_lockout( $ip ) {
do_action( 'wd_before_lockout', $ip );
if ( $this->service->skip_priority_lockout_checks( $ip ) ) {
return;
}
$is_blocklisted = $this->service->is_blocklisted_ip( $ip );
if ( $is_blocklisted['result'] ) {
// Get Blacklist_Lockout instance.
$blacklist_model = wd_di()->get( Blacklist_Model::class );
// This one is get blacklisted.
$this->actions_for_blocked(
$blacklist_model->ip_lockout_message,
0,
$is_blocklisted['reason'],
array( $ip )
);
}
// Get an instance of UA component.
$service_ua = wd_di()->get( User_Agent_Component::class );
if ( $service_ua->is_active_component() ) {
$user_agent = $service_ua->sanitize_user_agent();
if ( $service_ua->is_bad_post( $user_agent ) ) {
$service_ua->block_user_agent_or_ip( $user_agent, $ip, User_Agent_Component::REASON_BAD_POST );
return $service_ua->get_message();
}
if ( ! empty( $user_agent )
/**
* Apply additional checks for user agent before determining if it is a bad user agent.
*
* @param bool $is_bad_user_agent The result of checking if the user agent is bad.
* @param string $user_agent The user agent string to be checked.
* @param string $ip The IP address associated with the user agent.
*
* @return bool The final result after applying additional checks.
* @since 3.1.0
*/
&& apply_filters(
'wd_user_agent_additional_check',
$service_ua->is_bad_user_agent( $user_agent ),
$user_agent,
$ip
)
) {
// Todo: if we use a hook then we should extend cases with a custom reason and send it for log.
$service_ua->block_user_agent_or_ip( $user_agent, $ip, User_Agent_Component::REASON_BAD_USER_AGENT );
return $service_ua->get_message();
}
}
$notfound_lockout = wd_di()->get( Notfound_Lockout::class );
if ( $notfound_lockout->enabled && false === $notfound_lockout->detect_logged && is_user_logged_in() ) {
/**
* We don't need to check the IP if:
* the current user can logged-in and isn't from blacklisted,
* the option detect_404_logged is disabled.
*/
return;
}
// Check blacklist.
$model = Lockout_Ip::get( $ip );
if ( is_object( $model ) && $model->is_locked() ) {
$remaining_time = $model->remaining_release_time();
$this->actions_for_blocked( $model->lockout_message, $remaining_time, 'blacklist', array( $ip ) );
}
}
/**
* Remove all IP logs.
*
* @return Response
* @defender_route
*/
public function empty_logs(): Response {
if ( Lockout_Log::truncate() ) {
$this->log( 'Logs have been successfully deleted.', self::FIREWALL_LOG );
return new Response(
true,
array(
'message' => esc_html__( 'Your logs have been successfully deleted.', 'defender-security' ),
'interval' => 1,
)
);
}
return new Response(
false,
array(
'message' => esc_html__( 'Failed remove!', 'defender-security' ),
)
);
}
/**
* Return summary data.
*
* @return array
*/
public function get_summary(): array {
$summary = Lockout_Log::get_summary();
return array(
'lockout_last' => isset( $summary['lockout_last'] ) ?
$this->format_date_time( $summary['lockout_last'] ) :
esc_html__( 'Never', 'defender-security' ),
'lockout_today' => $summary['lockout_today'] ?? 0,
'lockout_this_month' => $summary['lockout_this_month'] ?? 0,
'lockout_login_today' => $summary['lockout_login_today'] ?? 0,
'lockout_login_this_week' => $summary['lockout_login_this_week'] ?? 0,
'lockout_login_this_month' => $summary['lockout_login_this_month'] ?? 0,
'lockout_404_today' => $summary['lockout_404_today'] ?? 0,
'lockout_404_this_week' => $summary['lockout_404_this_week'] ?? 0,
'lockout_404_this_month' => $summary['lockout_404_this_month'] ?? 0,
'lockout_ua_today' => $summary['lockout_ua_today'] ?? 0,
'lockout_ua_this_week' => $summary['lockout_ua_this_week'] ?? 0,
'lockout_ua_this_month' => $summary['lockout_ua_this_month'] ?? 0,
);
}
/**
* Removes settings for all submodules.
*/
public function remove_settings(): void {
( new Login_Lockout_Model() )->delete();
( new Blacklist_Model() )->delete();
( new Notfound_Lockout() )->delete();
( new Firewall_Settings() )->delete();
( new User_Agent_Lockout() )->delete();
( new Global_Ip_Lockout() )->delete();
( new Antibot_Global_Firewall_Setting() )->delete();
}
/**
* Delete all the data & the cache.
*/
public function remove_data(): void {
Lockout_Log::truncate();
// Remove cached data.
Array_Cache::remove( 'countries', 'ip_lockout' );
// Remove Global IP data.
( new Global_Ip() )->remove_data();
// Remove AntiBot Global Firewall data.
wd_di()->get( Antibot_Global_Firewall::class )->remove_data();
// Remove Bot Trap data.
wd_di()->get( Bot_Trap::class )->remove_data();
// Clear Trusted Proxy data.
$trusted_proxy_preset = wd_di()->get( Trusted_Proxy_Preset::class );
foreach ( array_keys( Firewall_Service::trusted_proxy_presets() ) as $preset ) {
$trusted_proxy_preset->set_proxy_preset( $preset );
$trusted_proxy_preset->delete_ips();
}
// Remove Unlockouts.
Unlockout::truncate();
Smart_Ip_Detection::remove_header();
delete_site_option( Firewall_Service::WHITELIST_SERVER_PUBLIC_IP_OPTION );
delete_site_option( Main_Wp::WHITELIST_DASHBOARD_PUBLIC_IP_OPTION );
}
/**
* Provides data for the frontend.
*
* @return array An array of data for the frontend.
*/
public function data_frontend(): array {
$summary_data = $this->get_summary();
$user_ip = $this->get_user_ip();
$http_ip_header_value = $this->get_user_ip_header();
$data = array(
'login' => array(
'month' => $summary_data['lockout_login_this_month'],
'week' => $summary_data['lockout_login_this_week'],
'day' => $summary_data['lockout_login_today'],
),
'nf' => array(
'month' => $summary_data['lockout_404_this_month'],
'week' => $summary_data['lockout_404_this_week'],
'day' => $summary_data['lockout_404_today'],
),
'ua' => array(
'month' => $summary_data['lockout_ua_this_month'],
'week' => $summary_data['lockout_ua_this_week'],
'day' => $summary_data['lockout_ua_today'],
),
'month' => $summary_data['lockout_this_month'],
'day' => $summary_data['lockout_today'],
'last_lockout' => $summary_data['lockout_last'],
'settings' => $this->model->export(),
'login_lockout' => wd_di()->get( Login_Lockout_Model::class )->enabled,
'nf_lockout' => wd_di()->get( Notfound_Lockout::class )->enabled,
'report' => wd_di()->get( Firewall_Report::class )->to_string(),
'notification_lockout' => 'enabled' === wd_di()->get( Firewall_Notification::class )->status,
'ua_lockout' => wd_di()->get( User_Agent_Lockout::class )->enabled,
'user_ip' => implode( ', ', $user_ip ),
'user_ip_header' => $http_ip_header_value,
'trusted_proxy_presets' => Firewall_Service::trusted_proxy_presets(),
'global_ip' => wd_di()->get( \WP_Defender\Controller\Global_Ip::class )->data_frontend(),
'hub_connector' => wd_di()->get( Hub_Connector::class )->data_frontend(),
'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
);
return array_merge( $data, $this->dump_routes_and_nonces() );
}
/**
* Provides data for the dashboard widget.
*
* @return array An array of dashboard widget data.
*/
public function dashboard_widget(): array {
return array(
'countries' => wd_di()->get( Blacklist_Lockout::class )->get_top_countries_blocked(),
);
}
/**
* Imports data into the model.
*
* @param array $data Data to be imported into the model.
*/
public function import_data( array $data ) {
$model = $this->model;
$model->import( $data );
if ( $model->validate() ) {
$model->save();
}
}
/**
* Exports strings.
*
* @return array An array of strings.
*/
public function export_strings(): array {
$strings = array();
$is_pro = ( new WPMUDEV() )->is_pro();
$firewall_report = new Firewall_Report();
// Login lockout.
$strings[] = Login_Lockout_Model::get_module_name() . ' '
. Login_Lockout_Model::get_module_state( (bool) ( new Login_Lockout_Model() )->enabled );
// Notfound lockout.
$strings[] = Notfound_Lockout::get_module_name() . ' '
. Notfound_Lockout::get_module_state( (bool) ( new Notfound_Lockout() )->enabled );
// Global IP lockout.
$strings[] = Global_Ip_Lockout::get_module_name() . ' '
. Global_Ip_Lockout::get_module_state( (bool) ( new Global_Ip_Lockout() )->enabled );
// UA lockout.
$strings[] = User_Agent_Lockout::get_module_name() . ' '
. User_Agent_Lockout::get_module_state( (bool) ( new User_Agent_Lockout() )->enabled );
// Notifications and reports.
if ( 'enabled' === ( new Firewall_Notification() )->status ) {
$strings[] = esc_html__( 'Email notifications active', 'defender-security' );
}
if ( $is_pro && 'enabled' === $firewall_report->status ) {
$strings[] = sprintf(
/* translators: %s: Frequency value. */
esc_html__( 'Email reports sending %s', 'defender-security' ),
$firewall_report->frequency
);
} elseif ( ! $is_pro ) {
$strings[] = sprintf(
/* translators: %s: Html for Pro-tag. */
esc_html__( 'Email report inactive %s', 'defender-security' ),
'<span class="sui-tag sui-tag-pro">Pro</span>'
);
}
return $strings;
}
/**
* Generates configuration strings based on the provided configuration and
* whether the product is a pro version.
*
* @param array $config Configuration data.
* @param bool $is_pro Indicates if the product is a pro version.
*
* @return array Returns an array of configuration strings.
*/
public function config_strings( array $config, bool $is_pro ): array {
$strings = array();
// Login lockout.
if ( isset( $config['login_protection'] ) ) {
$strings[] = Login_Lockout_Model::get_module_name() . ' '
. Login_Lockout_Model::get_module_state( (bool) $config['login_protection'] );
}
// NF lockout.
if ( isset( $config['detect_404'] ) ) {
$strings[] = Notfound_Lockout::get_module_name() . ' '
. Notfound_Lockout::get_module_state( (bool) $config['detect_404'] );
}
// Custom IP List.
if ( isset( $config['global_ip_list'] ) ) {
$strings[] = Global_Ip_Lockout::get_module_name() . ' '
. Global_Ip_Lockout::get_module_state( (bool) $config['global_ip_list'] );
}
// UA lockout.
if ( isset( $config['ua_banning_enabled'] ) ) {
$strings[] = User_Agent_Lockout::get_module_name() . ' '
. User_Agent_Lockout::get_module_state( (bool) $config['ua_banning_enabled'] );
}
// Notifications.
if ( isset( $config['notification'] ) && 'enabled' === $config['notification'] ) {
$strings[] = esc_html__( 'Email notifications active', 'defender-security' );
}
// Report.
if ( $is_pro && 'enabled' === $config['report'] ) {
$strings[] = sprintf(
/* translators: %s: Frequency value. */
esc_html__( 'Email reports sending %s', 'defender-security' ),
$config['report_frequency']
);
} elseif ( ! $is_pro ) {
$strings[] = sprintf(
/* translators: %s: Html for Pro-tag. */
esc_html__( 'Email report inactive %s', 'defender-security' ),
'<span class="sui-tag sui-tag-pro">Pro</span>'
);
}
return $strings;
}
/**
* Schedule cleanup blocklist ips event.
*
* @return void
*/
private function schedule_cleanup_blocklist_ips_event() {
// Sometimes multiple requests come at the same time. So we will only count the web requests.
if ( defined( 'DOING_AJAX' ) || defined( 'DOING_CRON' ) ) {
return;
}
$clear = get_site_option( 'wpdef_clear_schedule_firewall_cleanup_temp_blocklist_ips', false );
if ( $clear ) {
wp_clear_scheduled_hook( 'firewall_cleanup_temp_blocklist_ips' );
}
if ( wp_next_scheduled( 'firewall_cleanup_temp_blocklist_ips' ) ) {
return;
}
$interval = $this->model->ip_blocklist_cleanup_interval;
if ( ! $interval || 'never' === $interval ) {
return;
}
wp_schedule_event( time() + 15, $interval, 'firewall_cleanup_temp_blocklist_ips' );
}
/**
* Maybe add a filter to extend mime types.
*
* @return void
* @since 2.6.3
*/
public function maybe_extend_mime_types(): void {
if ( is_admin() ) {
$server = defender_get_data_from_request( null, 's' );
$current_url = set_url_scheme( 'http://' . $server['HTTP_HOST'] . $server['REQUEST_URI'] );
$current_query = wp_parse_url( $current_url, PHP_URL_QUERY );
$current_query = $current_query ?? '';
$referer_url = ! empty( $server['HTTP_REFERER'] ) ?
filter_var( $server['HTTP_REFERER'], FILTER_SANITIZE_URL ) :
'';
$referer_query = wp_parse_url( $referer_url, PHP_URL_QUERY );
$referer_query = $referer_query ?? '';
parse_str( $current_query, $current_queries );
parse_str( $referer_query, $referer_queries );
if (
( preg_match( '#^' . network_admin_url() . '#i', $current_url ) &&
! empty( $current_queries['page'] ) && $this->slug === $current_queries['page']
) ||
( preg_match( '#^' . network_admin_url() . '#i', $referer_url ) &&
! empty( $referer_queries['page'] ) && $this->slug === $referer_queries['page']
)
) {
// Add action hook here.
add_filter( 'upload_mimes', array( $this, 'extend_mime_types' ) );
}
}
}
/**
* Filter list of allowed mime types and file extensions.
*
* @param array $types List of mime types.
*
* @return array
*/
public function extend_mime_types( array $types ) {
if ( empty( $types['csv'] ) ) {
$types['csv'] = 'text/csv';
}
return $types;
}
/**
* Remove all lockouts.
*
* @return Response
* @defender_route
* @since 3.3.0
*/
public function empty_lockouts() {
if ( Lockout_Ip::truncate() ) {
$this->log( 'Deleted lockout records successfully.', self::FIREWALL_LOG );
return new Response(
true,
array(
'message' => esc_html__( 'Deleted lockout records successfully.', 'defender-security' ),
'interval' => 1,
)
);
}
return new Response(
false,
array(
'message' => esc_html__( 'Failed remove!', 'defender-security' ),
)
);
}
/**
* Sync IP and it's HTTP header.
*
* @param Request $request The request object.
*
* @return Response
* @defender_route
*/
public function sync_ip_header( Request $request ): Response {
$data = $request->get_data();
if ( 'automatic' === $data['ip_detection_type'] ) {
$this->service_sid->smart_ip_detection_ping( true );
$ip_detail = $this->service_sid->get_smart_ip_detection_details();
$user_ip = isset( $ip_detail[0] ) ? $ip_detail[0] : '';
$user_ip_header = isset( $ip_detail[1] ) ? $ip_detail[1] : '';
} else {
$remote_addr = wd_di()->get( Remote_Address::class );
$remote_addr->set_http_ip_header( $data['selected_http_header'] );
$user_ip = $remote_addr->get_ip_address();
$user_ip_header = $remote_addr->get_http_ip_header_value( $data['selected_http_header'] );
}
$data = array(
'user_ip' => is_array( $user_ip ) ? implode( ', ', $user_ip ) : $user_ip,
'user_ip_header' => $user_ip_header,
);
return new Response(
true,
$data
);
}
/**
* Prints inline Emoji detection script on specific admin pages only.
* The conflict happens when other plugins work with emoji flags.
*
* @return void
* @since 3.7.0
*/
public function print_emoji_script(): void {
$allowed_pages = array(
$this->slug,
wd_di()->get( Dashboard::class )->slug,
);
if ( in_array( HTTP::get( 'page' ), $allowed_pages, true ) ) {
if ( ! function_exists( 'print_emoji_detection_script' ) ) {
include_once ABSPATH . WPINC . '/formatting.php';
}
remove_filter( 'emoji_svg_url', '__return_false' );
print_emoji_detection_script();
}
}
/**
* Clean up unwanted records from lockout table.
*
* @return void
* @since 3.8.0
*/
public function clean_up_firewall_lockout(): void {
$this->service->firewall_clean_up_lockout();
}
/**
* Gather IP(s) from various headers and check if any IP is blacklisted, or temporary banned.
*
* @return void
* @since 4.4.2
*/
public function maybe_lockout_gathered_ips(): void {
$msg = '';
$ips = $this->service->get_user_ip();
if ( ! empty( $ips ) && is_array( $ips ) ) {
foreach ( $ips as $ip ) {
$result = $this->maybe_lockout( $ip );
if ( empty( $msg ) && ! empty( $result ) ) {
$msg = $result;
}
}
}
if ( ! empty( $msg ) ) {
$this->actions_for_blocked( $msg, 0, 'blacklist', $ips );
}
}
/**
* Clean up old records.
*
* @return void
* @since 4.6.0
*/
public function clean_up_unlockout(): void {
$timestamp = $this->local_to_utc( Unlock_Me::get_expired_time() );
Unlockout::remove_records( $timestamp, 100 );
}
/**
* Update trusted proxy preset IPs periodically.
*
* @return void
*/
public function update_trusted_proxy_preset_ips(): void {
$this->service->update_trusted_proxy_preset_ips();
}
/**
* Handle the request to detect the IP header.
*
* @return void
*/
public function handle_detect_ip_header(): void {
$nonce = defender_get_data_from_request( 'nonce', 'g' );
$nonce_ctx = Smart_Ip_Detection::get_nonce_context();
if ( empty( $nonce ) || get_transient( $nonce_ctx ) !== $nonce ) {
wp_send_json_error( __( 'Invalid nonce.', 'defender-security' ) );
}
delete_transient( $nonce_ctx );
$result = $this->service_sid->smart_ip_detect_header();
if ( is_wp_error( $result ) ) {
wp_send_json_error( $result->get_error_message() );
} else {
wp_send_json_success(
isset( $result['message'] )
? $result['message']
: esc_html__( 'IP detection process completed.', 'defender-security' )
);
}
}
/**
* Schedule to send request to the API for smart IP detection.
*
* @return void
*/
public function smart_ip_detection_ping(): void {
$this->service_sid->smart_ip_detection_ping();
}
/**
* Unlock user from AntiBot Global Firewall blocklist.
*
* @return Response
* @defender_route
* @is_public
*/
public function agf_unlock_user(): Response {
$user_ips = $this->get_user_ip(); // Get all IPs belonging to the user.
$attempts_key_prefix = 'wp_defender_agf_unlock_attempts_'; // Prefix for each transient key.
// Load attempts data and check limits in a single loop.
foreach ( $user_ips as $ip ) {
$key = $attempts_key_prefix . $ip;
$attempts_data = get_transient( $key );
$timestamps = is_array( $attempts_data ) ? $attempts_data : array();
// Check if the IP has reached the limit.
if ( count( $timestamps ) >= Unlock_Me::get_attempt_limit() && ( time() - end( $timestamps ) ) < DAY_IN_SECONDS ) {
$this->log( 'Verification attempt limit reached for IP: ' . $ip, Altcha_Handler::LOG_FILE_NAME );
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' ) ) );
}
}
// Retrieve captcha payload data.
$captcha_checkbox = defender_get_data_from_request( 'captcha', 'r' );
$captcha_payload = array(
'algorithm' => defender_get_data_from_request( 'algorithm', 'r' ),
'challenge' => defender_get_data_from_request( 'challenge', 'r' ),
'salt' => defender_get_data_from_request( 'salt', 'r' ),
'signature' => defender_get_data_from_request( 'signature', 'r' ),
'number' => defender_get_data_from_request( 'solution', 'r' ),
);
// Successful verification of captcha.
if ( '0' === $captcha_checkbox && wd_di()->get( Altcha_Handler::class )->verify_solution( $captcha_payload ) ) {
// Reset attempt data for all IPs in a batch.
foreach ( $user_ips as $ip ) {
delete_transient( $attempts_key_prefix . $ip );
}
$this->log( 'Captcha verified successfully. IP(s): ' . implode( ', ', $user_ips ), Altcha_Handler::LOG_FILE_NAME );
$unlock_result = wd_di()->get( Antibot_Global_Firewall_Model::class )->unlock_ips( $user_ips );
if ( false !== $unlock_result ) {
wd_di()->get( Antibot_Global_Firewall_Component::class )->log_ip_message( 'Successfully unlocked IP(s): ' . implode( ', ', $user_ips ) );
}
return new Response( true, array() );
}
// Verification failed: increment attempt count for all IPs.
$current_time = time();
foreach ( $user_ips as $ip ) {
$attempts_key = $attempts_key_prefix . $ip;
$timestamps = get_transient( $attempts_key ) ?? array();
$timestamps[] = $current_time; // Add the current timestamp.
set_transient( $attempts_key, $timestamps, DAY_IN_SECONDS );
}
$this->log( 'Captcha verification failed for IP(s): ' . implode( ', ', $user_ips ), Altcha_Handler::LOG_FILE_NAME );
return new Response( false, array( 'message' => esc_html__( 'Captcha verification failed. Please try again.', 'defender-security' ) ) );
}
/**
* Whitelist server public IP.
*
* @return void
* @since 5.0.2
*/
public function whitelist_server_public_ip(): void {
$this->service->set_whitelist_server_public_ip();
}
}