register_routes();
add_action( 'defender_enqueue_assets', array( $this, 'enqueue_assets' ) );
$this->model = $model;
$this->service = $service;
$this->wpmudev = $wpmudev;
add_action( 'admin_init', array( $this, 'maybe_hcm_connection_attempt' ), 20 );
add_action( 'wpdef_hub_connector_synced', array( $this, 'maybe_hcm_connection_attempt' ) );
add_action( 'wpmudev_hub_connector_first_sync_completed', array( $this, 'maybe_hcm_connection_attempt' ) );
/**
* Download and store Blocklist from the API.
*/
if ( $this->service->is_active_via_plugin() ) {
/**
* Network Cron Manager
*
* @var Network_Cron_Manager $network_cron_manager
*/
$network_cron_manager = wd_di()->get( Network_Cron_Manager::class );
$network_cron_manager->register_callback(
'wpdef_antibot_global_firewall_fetch_blocklist',
array( $this, 'handle_download_and_store_blocklist' ),
12 * HOUR_IN_SECONDS,
time() + 15
);
} elseif ( wp_next_scheduled( 'wpdef_antibot_global_firewall_fetch_blocklist' ) ) {
wp_clear_scheduled_hook( 'wpdef_antibot_global_firewall_fetch_blocklist' );
}
if ( $this->wpmudev->is_wpmu_hosting() ) {
add_action( 'init', array( $this, 'sync_state' ) );
}
add_action( 'init', array( $this, 'handle_expired_membership' ) );
}
/**
* Handle request to save settings.
*
* @param Request $request The request object.
*
* @defender_route
* @return Response
*/
public function save_settings( Request $request ) {
$data = $request->get_data(
array(
'enabled' => array( 'type' => 'bool' ),
'managed_by' => array( 'type' => 'string' ),
// Temporary property.
'module_title' => array(
'type' => 'string',
'sanitize' => 'sanitize_text_field',
),
'module_location' => array(
'type' => 'string',
'sanitize' => 'sanitize_text_field',
),
'redirect_to_feature_page' => array( 'type' => 'bool' ),
// End.
)
);
$old_enabled = $this->model->enabled;
$location = 'Feature Page';
// Split module's titles and locations.
if ( isset( $data['module_title'] ) && 'antibot' === $data['module_title'] ) {
if ( ! isset( $data['module_location'] ) || '' === $data['module_location'] ) {
$location = 'Dashboard';
} else {
$location = $data['module_location'];
unset( $data['module_location'] );
}
unset( $data['module_title'] );
}
$this->model->import( $data );
if ( $this->model->validate() ) {
$this->model->save();
if ( 'plugin' === $this->service->get_managed_by() ) {
if ( $old_enabled !== $this->model->enabled ) {
if ( ! $this->model->enabled ) {
$this->service->delete_blocklist();
} elseif ( $this->model->enabled && $this->service->maybe_download() ) {
$this->service->download_and_store_blocklist();
}
}
} else {
$old_enabled = $this->service->frontend_is_enabled();
$result = $this->service->toggle_on_hosting( $this->model->enabled );
if ( is_wp_error( $result ) ) {
return new Response(
false,
array( 'message' => esc_html__( 'Something wrong happened, please try again!', 'defender-security' ) )
);
}
}
Config_Hub_Helper::set_clear_active_flag();
// Maybe track.
if ( ! defender_is_wp_cli() && $old_enabled !== $data['enabled'] ) {
wd_di()->get( Antibot_Analytics::class )->track_antibot( $old_enabled, $location );
}
$update_message = $this->get_update_message( $data, $old_enabled, Antibot_Global_Firewall_Setting::get_module_name() );
$referrer = wp_get_referer();
if ( $referrer && strpos( $referrer, 'page=wp-defender' ) !== false ) {
$update_message = sprintf(
/* translators: 1: Module name. */
__( '%s is now enabled.', 'defender-security' ),
'' . Antibot_Global_Firewall_Setting::get_module_name() . ''
);
}
$data_frontend = $this->data_frontend();
if ( isset( $data['redirect_to_feature_page'] ) && $data['redirect_to_feature_page'] ) {
$data_frontend['redirect'] = network_admin_url( 'admin.php?page=wdf-ip-lockout' );
}
return new Response(
true,
array_merge(
array(
'message' => $update_message,
'auto_close' => true,
),
$data_frontend
)
);
}
return new Response(
false,
array( 'message' => $this->model->get_formatted_errors() )
);
}
/**
* Queue assets and require data.
*
* @return void
*/
public function enqueue_assets() {
if ( ! $this->is_page_active() ) {
return;
}
}
/**
* All the variables that we will show on frontend, both in the main page, or dashboard widget.
*
* @return array
*/
public function data_frontend(): array {
$model_export = $this->model->export();
$model_export['managed_by'] = $this->service->get_managed_by();
$module_name = Antibot_Global_Firewall_Setting::get_module_name();
return array_merge(
array(
'model' => $model_export,
'misc' => array(
'module_slug' => Antibot_Global_Firewall_Setting::get_module_slug(),
'module_name' => $module_name,
'show_notice' => false,
'sync_schedule' => __( 'Twice Daily', 'defender-security' ),
'ips_count' => $this->service->get_blocklisted_ip_count(),
'blocklist_stats_full' => $this->service->get_cached_blocklist_stats_data(),
'frontend_is_enabled' => $this->service->frontend_is_enabled(),
'frontend_mode' => $this->service->frontend_mode(),
'is_active' => $this->service->is_active(),
'show_stats_button' => ! $this->wpmudev->is_whitelabel_enabled(),
'show_checker' => ! $this->wpmudev->is_wpmu_hosting()
|| $this->wpmudev->is_wpmu_dev_admin()
|| ! ( $this->service->is_active_via_hosting() && $this->wpmudev->is_whitelabel_enabled() ),
'active_tooltip_text' => __( 'List of exploit attempts detected and blocked across all connected sites by AntiBot Firewall.', 'defender-security' ),
'inactive_tooltip_text' => sprintf(
/* translators: %s: Module name. */
__( '%s is Inactive.', 'defender-security' ),
$module_name
),
'current_user' => esc_html( wp_get_current_user()->display_name ?? __( 'User', 'defender-security' ) ),
'is_expired_membership' => $this->is_expired_membership_type(),
'show_antibot_options' => $this->service->get_states_of_antibot_options_for_different_hosting_types(),
),
),
$this->dump_routes_and_nonces()
);
}
/**
* Download and store blocklist.
*
* @return void
*/
public function handle_download_and_store_blocklist(): void {
if ( is_multisite() ) {
$next_run = (int) get_site_option( Antibot_Global_Firewall_Component::DOWNLOAD_SYNC_NEXT_RUN_OPTION, 0 );
if ( $next_run > 0 && $next_run > time() ) {
return;
}
$interval = (int) wd_di()->get( Scheduler::class )->get_cron_schedule_interval( Antibot_Global_Firewall_Component::DOWNLOAD_SYNC_SCHEDULE );
$next_run = time() + ( $interval > 0 ? $interval : 12 * HOUR_IN_SECONDS );
update_site_option( Antibot_Global_Firewall_Component::DOWNLOAD_SYNC_NEXT_RUN_OPTION, $next_run );
}
if ( true === $this->service->is_active_via_plugin() ) {
$is_switch_to_main_site = is_multisite() && ! is_main_site();
if ( $is_switch_to_main_site ) {
switch_to_blog( get_main_site_id() );
}
$this->service->download_and_store_blocklist();
if ( $is_switch_to_main_site ) {
restore_current_blog();
}
}
}
/**
* Handle the HCM connection attempt via Antibot page.
*
* @return void
*/
public function maybe_hcm_connection_attempt() {
$data = get_site_transient( Hub_Connector::TRANSIENT_KEY );
$module_slug = $data['module_slug'] ?? '';
if ( Antibot_Global_Firewall_Setting::get_module_slug() === $module_slug && self::get_hcm_status() ) {
delete_site_transient( Hub_Connector::TRANSIENT_KEY );
if ( 'plugin' === $this->service->get_managed_by() ) {
$this->service->managed_by_plugin_action();
} else {
$this->service->managed_by_hosting_action();
}
}
}
/**
* Export the data of this module, we will use this for export to HUB, create a preset etc.
*
* @return array
*/
public function to_array(): array {
return array();
}
/**
* Import the data from the HUB, or from the preset.
*
* @param array $data The data to be imported.
*
* @return void
*/
public function import_data( $data ) {
$model = $this->model;
if ( isset( $data['antibot'] ) ) {
$model->enabled = (bool) $data['antibot'];
} else {
$model->enabled = true;
}
$model->save();
}
/**
* Remove all settings, configs generated in this container runtime.
*/
public function remove_settings() {}
/**
* Remove all data.
*/
public function remove_data() {
$this->service->delete_blocklist();
delete_site_option( Antibot_Global_Firewall_Component::DOWNLOAD_SYNC_NEXT_RUN_OPTION );
// Don't display Antibot global notice. We can delete slugs about this notice in the future.
delete_site_option( Antibot_Global_Firewall_Component::NOTICE_SLUG );
delete_site_option( Antibot_Global_Firewall_Component::GLOBAL_NOTICE_TIME_OPTION );
// Blocklist stats data.
delete_site_transient( Antibot_Global_Firewall_Component::BLOCKLIST_STATS_KEY . '_' . Antibot_Global_Firewall_Setting::MODE_BASIC );
delete_site_transient( Antibot_Global_Firewall_Component::BLOCKLIST_STATS_KEY . '_' . Antibot_Global_Firewall_Setting::MODE_STRICT );
delete_site_transient( Antibot_Global_Firewall_Component::BLOCKLIST_STATS_FULL_KEY );
delete_site_transient( Antibot_Global_Firewall_Component::IS_SWITCHING_TO_PLUGIN_IN_PROGRESS );
}
/**
* Export strings.
*
* @return array
*/
public function export_strings() {
return array();
}
/**
* Handle request to switch managed by.
*
* @defender_route
* @return Response
*/
public function switch_managed_by(): Response {
$result = $this->service->switch_managed_by();
if ( false !== $result ) {
return new Response(
true,
array(
'message' => sprintf(
/* translators: 1. Open tag. 2. Close tag. 3. Managed by label. */
__( '%1$sAntiBot%2$s blocklist is now being managed by %3$s.', 'defender-security' ),
'',
'',
$this->service->get_managed_by_label()
),
'managed_by' => $result,
'frontend_mode' => $this->service->frontend_mode(),
'ips_count' => $this->service->get_blocklisted_ip_count(),
'auto_close' => true,
)
);
}
return new Response(
false,
array(
'message' => __( 'Failed to switch. Please try again.', 'defender-security' ),
'auto_close' => true,
)
);
}
/**
* Various actions during form submission.
*
* @param Request $request The HTTP request object.
*
* @defender_route
* @return Response
*/
public function blocklist_checker_submit_trusted_ip_form( Request $request ): Response {
$data = $request->get_data(
array(
'ip' => array(
'type' => 'string',
'sanitize' => 'sanitize_text_field',
),
'email' => array(
'type' => 'string',
'sanitize' => 'sanitize_email',
),
'service' => array(
'type' => 'string',
'sanitize' => 'sanitize_text_field',
),
'reason' => array(
'type' => 'string',
'sanitize' => 'sanitize_text_field',
),
)
);
$reporter_email = isset( $data['email'] ) ? trim( $data['email'] ) : '';
if ( '' === $reporter_email || ! is_email( $reporter_email ) ) {
return new Response(
false,
array(
'message' => esc_html__( 'A valid email address is required.', 'defender-security' ),
)
);
}
// Logging.
$message = sprintf(
'IP: %s, email: %s, service: %s, reason: %s',
$data['ip'],
$reporter_email,
$data['service'],
$data['reason']
);
$this->service->log_ip_message( $message );
// Track the request. Forced so it operates independently of tracking consent.
$this->forced_track(
'def_request_trusted_ip',
array(
'IP Address' => $data['ip'],
'User Email' => $reporter_email,
'Service' => $data['service'],
'Reason' => $data['reason'],
)
);
return new Response( true, array() );
}
/**
* Check if an IP exists in the Local, Custom and AntiBot blocklist.
*
* @param Request $request The HTTP request object.
*
* @defender_route
* @return Response
*/
public function blocklist_checker_search( Request $request ): Response {
$data = $request->get_data(
array(
'ip' => array(
'type' => 'string',
'sanitize' => 'sanitize_text_field',
),
)
);
if ( false === filter_var( $data['ip'], FILTER_VALIDATE_IP ) ) {
return new Response(
false,
array(
'success' => false,
'show_notice' => false,
'message' => __( 'Invalid IP address.', 'defender-security' ),
)
);
}
$bl_service = wd_di()->get( Blacklist_Lockout::class );
$gi_service = wd_di()->get( Global_IP::class );
$ip = $data['ip'];
$is_ip_allowed = $bl_service->is_ip_whitelisted( $ip )
|| $bl_service->is_country_whitelist( $ip )
|| ( $gi_service->is_global_ip_enabled() && $gi_service->is_ip_allowed( $ip ) );
// Fetch the latest Custom IP blocklist from the Hub so we don't rely on stale cached data.
$gi_service->fetch_global_ip_list();
return new Response(
true,
array(
'success' => true,
'show_notice' => false,
'message' => __( 'The IP address is searched successfully.', 'defender-security' ),
'local' => ! $is_ip_allowed && $bl_service->is_blacklist( $data['ip'] ),
'central' => ! $is_ip_allowed && $gi_service->is_global_ip_enabled() && $gi_service->is_ip_blocked( $data['ip'] ),
'antibot' => ! $is_ip_allowed && $this->service->is_ip_blocked( $ip ),
)
);
}
/**
* Add IP to AntiBot allowlist.
*
* @param Request $request The HTTP request object.
*
* @defender_route
* @return Response
*/
public function blocklist_checker_add_ip_to_allowlist( Request $request ): Response {
$data = $request->get_data(
array(
'ip' => array(
'type' => 'string',
'sanitize' => 'sanitize_text_field',
),
)
);
$ip = $data['ip'];
if ( false === filter_var( $ip, FILTER_VALIDATE_IP ) ) {
return new Response(
false,
array(
'message' => esc_html__( 'Invalid IP address.', 'defender-security' ),
)
);
}
$collection = 'allowlist';
$model = wd_di()->get( \WP_Defender\Model\Setting\Blacklist_Lockout::class );
$already_in_list = $model->is_ip_in_list( $ip, $collection );
// Add to Local allowlist.
if ( ! $already_in_list ) {
$model->add_to_list( $ip, $collection );
}
// Add to Custom IP allowlist.
$global_ip_service = wd_di()->get( Global_IP::class );
if ( ! $already_in_list && $global_ip_service->can_central_ip_autosync() ) {
$data = array(
'allow_list' => array( $ip ),
);
$res = $global_ip_service->add_to_global_ip_list( $data );
if ( is_wp_error( $res ) ) {
return new Response(
false,
array(
'message' => $res->get_error_message(),
)
);
}
}
$manage_link_open = '';
$manage_link_close = '';
if ( $already_in_list ) {
$message = sprintf(
/* translators: 1: IP address. 2: Opening anchor tag. 3: Closing anchor tag. */
esc_html__(
'IP %1$s is already in your Site\'s allowlist. You can manage it in %2$sIP Lockouts%3$s.',
'defender-security'
),
$ip,
$manage_link_open,
$manage_link_close
);
} else {
$message = sprintf(
/* translators: 1: IP address. 2: Opening anchor tag. 3: Closing anchor tag. */
esc_html__(
'IP %1$s has been added to your Site\'s allowlist. You can manage it in %2$sIP Lockouts%3$s.',
'defender-security'
),
$ip,
$manage_link_open,
$manage_link_close
);
}
return new Response(
true,
array(
'message' => $message,
'already_in_list' => $already_in_list,
)
);
}
/**
* Sync AntiBot status with Hosting.
* If AntiBot is enabled in Hosting, update the 'managed_by' and 'enabled' status.
* If AntiBot is disabled in Hosting, do nothing.
*
* @return void
*/
public function sync_state(): void {
$hosting_enabled = defender_get_hosting_feature_state( 'antibot' );
if (
true === $hosting_enabled
&& 'plugin' === $this->model->managed_by
&& true !== get_site_transient( Antibot_Global_Firewall_Component::IS_SWITCHING_TO_PLUGIN_IN_PROGRESS )
) {
$this->model->enabled = false;
$this->model->managed_by = 'hosting';
$this->model->save();
}
}
/**
* Handle request to switch mode.
*
* @defender_route
* @return Response
*/
public function switch_mode(): Response {
$result = $this->process_switch_mode();
if ( $result['success'] ) {
if ( $this->maybe_track() ) {
wd_di()->get( Antibot_Analytics::class )->track_antibot( false, 'Feature Page' );
}
return new Response( true, $result['data'] );
}
return new Response( false, $result['data'] );
}
/**
* Process request to switch mode.
*
* @return array
*/
public function process_switch_mode(): array {
$result = $this->service->switch_mode();
if ( false !== $result && ! is_wp_error( $result ) ) {
return array(
'success' => true,
'data' => array(
'message' => sprintf(
/* translators: 1. Open tag. 2. Close tag. 3. Mode. */
__( '%1$sAntiBot Firewall%2$s mode updated – You\'re now using %3$s Mode.', 'defender-security' ),
'',
'',
ucfirst( $result )
),
'mode' => $result,
'ips_count' => $this->service->get_blocklisted_ip_count(),
'auto_close' => true,
),
);
}
return array(
'success' => false,
'data' => array(
'message' => __( 'Failed to switch. Please try again.', 'defender-security' ),
'auto_close' => true,
),
);
}
/**
* Handle expired membership by automatically disabling the AntiBot Global Firewall module.
* Logs the action when the feature is disabled due to expired membership.
*
* @return void
*/
public function handle_expired_membership(): void {
if ( $this->model->enabled && $this->is_expired_membership_type() ) {
$this->service->managed_by_plugin_action( false );
$this->log( 'AntiBot Global Firewall automatically disabled due to expired membership.', Antibot_Global_Firewall_Component::LOG_FILE_NAME );
}
}
/**
* Handle request to send feedback.
*
* @param Request $request The request object.
*
* @defender_route
* @return Response
*/
public function send_feedback( Request $request ) {
$data = $request->get_data(
array(
'feedback' => array( 'type' => 'string' ),
)
);
if ( '' !== $data['feedback'] ) {
// Send feedback.
$this->track_feature(
'Antibot Survey',
array(
'Notice Action' => 'Share feedback',
'Feedback' => $data['feedback'],
)
);
}
$message = sprintf(
/* translators: 1: Open tag, 2: Close tag */
__( '%1$sThanks for your feedback!%2$s It helps us make Defender even better.', 'defender-security' ),
'',
''
);
return new Response(
true,
array(
'message' => $message,
'auto_close' => true,
)
);
}
}