options = get_option(self::OPTION_SETTINGS, []);
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-admin-pages.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-action-handler.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-ajax-handler.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-settings-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-geolocation-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-geoip-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-session-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-asn-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-waf-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-rate-limiting-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-dashboard-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-threat-score-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-bot-verifier.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-rules-engine.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-fingerprint-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-abuseipdb-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-htaccess-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-cloudflare-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-reporter-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-community-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-community-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-site-scanner.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-site-scanner.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-security-headers.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-audit-logger.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-file-verifier.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-js-challenge.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-cache-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-live-feed-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-cron-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-notification-manager.php';
$this->site_scanner = new ADVAIPBL_Site_Scanner($this);
$this->community_manager = new ADVAIPBL_Community_Manager($this);
$this->admin_pages = new ADVAIPBL_Admin_Pages($this);
$this->action_handler = new ADVAIPBL_Action_Handler($this);
$this->ajax_handler = new ADVAIPBL_Ajax_Handler($this);
$this->settings_manager = new ADVAIPBL_Settings_Manager($this, $this->admin_pages);
$this->geolocation_manager = new ADVAIPBL_Geolocation_Manager($this);
$this->session_manager = new ADVAIPBL_User_Session_Manager($this, $this->geolocation_manager);
$this->asn_manager = new ADVAIPBL_Asn_Manager($this, $this->geolocation_manager);
$this->waf_manager = new ADVAIPBL_Waf_Manager($this);
$this->rate_limit_manager = new ADVAIPBL_Rate_Limiting_Manager($this);
$this->dashboard_manager = new ADVAIPBL_Dashboard_Manager($this, $this->session_manager);
$this->threat_score_manager = new ADVAIPBL_Threat_Score_Manager($this);
$this->bot_verifier = new ADVAIPBL_Bot_Verifier($this);
$this->rules_engine = new ADVAIPBL_Rules_Engine($this);
$this->fingerprint_manager = new ADVAIPBL_Fingerprint_Manager($this);
$this->abuseipdb_manager = new ADVAIPBL_AbuseIPDB_Manager($this);
$this->htaccess_manager = new ADVAIPBL_Htaccess_Manager($this);
$this->cloudflare_manager = new ADVAIPBL_Cloudflare_Manager($this);
$this->reporter_manager = new ADVAIPBL_Reporter_Manager($this);
$this->security_headers_manager = new ADVAIPBL_Security_Headers($this);
$this->audit_logger = new ADVAIPBL_Audit_Logger($this);
$this->file_verifier = new ADVAIPBL_File_Verifier($this);
$this->js_challenge_manager = new ADVAIPBL_JS_Challenge($this);
$this->cache_manager = new ADVAIPBL_Cache_Manager();
$this->live_feed_manager = new ADVAIPBL_Live_Feed_Manager($this);
$this->cron_manager = new ADVAIPBL_Cron_Manager($this);
$this->notification_manager = new ADVAIPBL_Notification_Manager($this);
if (version_compare(PHP_VERSION, '8.1', '>=')) {
// Initialize GeoIP Manager only if needed
if (($this->options['geolocation_method'] ?? 'api') === 'local_db') {
$this->geoip_manager = new ADVAIPBL_GeoIP_Manager($this);
} else {
$this->geoip_manager = new stdClass();
}
// Carga perezosa del 2FA Manager solo en backend y si es seguro
if (is_admin() || (isset($GLOBALS['pagenow']) && $GLOBALS['pagenow'] === 'wp-login.php')) {
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-2fa-manager.php';
// Verificación de seguridad: ¿Están las librerías cargadas?
// Esto evita el error fatal si la instalación falló y faltan archivos.
if ( class_exists('BaconQrCode\Renderer\ImageRenderer') ) {
$this->tfa_manager = new ADVAIPBL_2FA_Manager($this);
} else {
// Fallback silencioso para no romper el sitio
$this->tfa_manager = new stdClass();
// error_log('Advanced IP Blocker: 2FA libraries missing. Feature disabled to prevent crash.');
}
} else {
$this->tfa_manager = new stdClass();
}
} else {
$this->geoip_manager = new stdClass();
$this->tfa_manager = new stdClass();
}
// --- Hook para reglas de usuario (Username Block) ---
add_filter('authenticate', [$this, 'check_login_rules'], 20, 3);
add_action('admin_init', [$this, 'check_database_update']);
$this->add_hooks();
if (is_admin()) {
add_action('admin_notices', [$this, 'display_admin_notice']);
}
}
public function initialize_backend_managers() {
static $initialized = false;
if ($initialized) {
return;
}
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-dashboard-manager.php';
$this->dashboard_manager = new ADVAIPBL_Dashboard_Manager($this, $this->session_manager);
// Secure logic for 2FA
if (version_compare(PHP_VERSION, '8.1', '>=')) {
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-2fa-manager.php';
if (class_exists('BaconQrCode\Renderer\ImageRenderer')) {
$this->tfa_manager = new ADVAIPBL_2FA_Manager($this);
} else {
$this->tfa_manager = new stdClass();
// error_log('Advanced IP Blocker: 2FA libraries missing in backend init.');
}
} else {
$this->tfa_manager = new stdClass();
}
$initialized = true;
}
private function add_hooks() {
// Ejecutar chequeo de base de datos muy temprano en init
add_action('init', [$this, 'check_database_version'], -9999);
// Intercepción global y obligatoria para todos los JS Challenges (evita orfandad en módulos de bloqueo avanzados)
add_action('init', [$this->js_challenge_manager, 'verify_submission'], -999);
add_action('init', [$this, 'is_visitor_asn_whitelisted'], -100);
add_action('init', [$this, 'verify_known_bots'], -99);
add_action('init', [$this, 'check_ip_with_abuseipdb'], 10);
add_action('init', [$this, 'block_xmlrpc_requests_if_disabled'], -5);
add_action('init', [$this, 'log_request_signature'], -2);
add_action('plugins_loaded', [$this, 'maybe_set_donotcachepage_constant'], 0);
add_action('init', [$this, 'check_for_endpoint_lockdown'], -1);
add_action('init', [$this, 'check_for_malicious_signature'], -1);
add_action('init', [$this, 'check_for_geo_challenge'], -1);
add_action('init', [$this, 'run_all_block_checks'], 0);
add_action('init', [$this, 'scanner_ping_endpoint'], 1);
add_action('init', [$this, 'log_wp_cron_execution'], 1);
add_action('init', [$this->rate_limit_manager, 'check_request_rate'], -1);
add_action('init', [$this, 'add_admin_ip_to_whitelist_on_first_run']);
add_filter('status_header', [$this, 'detect_http_error_status'], 10, 2);
add_action('advaipbl_community_report_event_v2', [$this, 'execute_community_report']);
add_action('advaipbl_update_community_list_event', [$this->community_manager, 'update_list']);
add_action('wp_ajax_advaipbl_run_deep_scan', [$this->ajax_handler, 'ajax_run_deep_scan']);
add_action('wp_ajax_advaipbl_check_server_reputation', [$this->ajax_handler, 'ajax_check_server_reputation']);
add_action('wp_ajax_advaipbl_clear_audit_log', [$this->ajax_handler, 'ajax_clear_audit_logs']);
add_action('wp_ajax_advaipbl_run_fim_scan', [$this->ajax_handler, 'ajax_run_fim_scan']);
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$action = isset($_GET['action']) ? sanitize_text_field(wp_unslash($_GET['action'])) : '';
if (is_admin() || (strpos($action, 'advaipbl_') === 0)) {
add_action('admin_init', [$this, 'initialize_backend_managers'], 0);
}
global $pagenow;
if ($pagenow === 'wp-login.php') {
$this->initialize_backend_managers();
}
add_action('init', [$this, 'prevent_author_enumeration_redirect'], 9);
add_filter('the_author_login', [$this, 'prevent_user_enumeration_via_feeds']);
add_filter('get_the_author_login', [$this, 'prevent_user_enumeration_via_feeds']);
add_action('wp_login_failed', [$this, 'registrar_intento_login_fallido']);
add_action('login_init', [ $this, 'handle_login_page_restriction' ], 1 );
add_action('login_init', [ $this, 'handle_login_geo_restriction' ], 2 );
add_action('wp_login', [$this, 'auto_whitelist_admin_on_login'], 10, 2);
add_filter('rest_endpoints', [ $this, 'disable_rest_api_user_endpoints' ] );
add_filter('oembed_response_data', [$this, 'prevent_user_enumeration_via_oembed'], 99, 4);
add_filter('authenticate', [$this, 'prevent_login_hinting'], 99, 1);
add_action('rest_api_init', [$this, 'register_live_feed_api_endpoint']);
add_action('rest_api_init', function() {
register_rest_route('advaipbl/v1', '/live-feed-nonce', [
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_live_feed_nonce'],
'permission_callback' => '__return_true',
]);
});
add_shortcode('advaipbl_live_feed', [$this, 'render_live_feed_shortcode']);
$this->add_2fa_hooks();
if ( ! empty( $this->options['show_admin_bar_menu'] ) && '1' === $this->options['show_admin_bar_menu'] ) {
add_action( 'admin_bar_menu', [ $this, 'add_admin_bar_menu' ], 999 );
}
add_action('advaipbl_purge_old_logs_event', [$this, 'purge_old_logs']);
add_action('advaipbl_cloudflare_cleanup_event', [$this->cloudflare_manager, 'clear_all_aib_rules']);
// add_action('advaipbl_send_summary_email', [$this, 'process_and_send_summary']); // Moved to Notification Manager
add_action('advaipbl_send_telemetry_data_event', [$this, 'send_telemetry_data']);
add_action('advaipbl_update_geoip_db_event', [$this, 'execute_geoip_db_update']);
add_action('advaipbl_clear_expired_blocks_event', [$this, 'limpiar_ips_expiradas']);
add_action('advaipbl_cleanup_expired_cache_event', [$this, 'cleanup_expired_cache_entries']);
add_action('advaipbl_daily_fim_scan', [$this->file_verifier, 'scan_files']);
if (is_admin()) {
add_action('admin_notices', [$this, 'display_setup_wizard_notice']);
add_action('admin_init', [$this, 'maybe_redirect_to_wizard']);
add_action('admin_init', [$this, 'schedule_cron_jobs']);
add_action('admin_init', [$this, 'conditionally_remove_admin_notices']);
add_action('admin_init', [$this, 'update_option_autoload_states']);
add_action('admin_menu', [$this, 'admin_menu']);
add_action('admin_init', [$this->settings_manager, 'register_settings']);
add_action('admin_init', [$this->action_handler, 'handle_admin_actions']);
add_action('update_option_' . self::OPTION_SETTINGS, [$this, 'on_settings_update'], 10, 2);
add_action('update_option_' . self::OPTION_WAF_RULES, [$this, 'on_waf_rules_update'], 10, 2);
add_action('admin_post_advaipbl_refresh_spamhaus', [$this, 'handle_spamhaus_refresh_action']);
add_action('admin_notices', [$this, 'display_force_2fa_setup_notice']);
add_action('admin_notices', [$this, 'display_admin_notice']);
// CSS Hiding Strategy:
// Ocultar visualmente el enlace del Setup Wizard del menú lateral
add_action('admin_head', function() {
?>
ajax_handler, 'ajax_test_outbound_connection']);
add_action('wp_ajax_advaipbl_add_ip_to_whitelist', [$this->ajax_handler, 'ajax_add_ip_to_whitelist']);
add_action('wp_ajax_advaipbl_verify_api_key', [$this->ajax_handler, 'ajax_verify_api_key']);
add_action('wp_ajax_advaipbl_get_free_api_key', [$this->ajax_handler, 'ajax_get_free_api_key']);
add_action('wp_ajax_advaipbl_update_geoip_db', [$this->ajax_handler, 'ajax_update_geoip_db']);
add_action('wp_ajax_advaipbl_get_dashboard_stats', [$this->ajax_handler, 'ajax_get_dashboard_stats']);
add_action('wp_ajax_advaipbl_export_settings_ajax', [ $this, 'handle_export_settings_ajax' ] );
add_action('wp_ajax_advaipbl_handle_telemetry_notice', [$this->ajax_handler, 'ajax_handle_telemetry_notice']);
add_action('wp_ajax_advaipbl_reset_threat_score', [$this->ajax_handler, 'ajax_reset_threat_score']);
add_action('wp_ajax_advaipbl_get_score_history', [$this->ajax_handler, 'ajax_get_score_history']);
add_action('wp_ajax_advaipbl_delete_signature', [$this->ajax_handler, 'ajax_delete_signature']);
add_action('wp_ajax_advaipbl_get_signature_details', [$this->ajax_handler, 'ajax_get_signature_details']);
add_action('wp_ajax_advaipbl_whitelist_signature', [$this->ajax_handler, 'ajax_whitelist_signature']);
add_action('wp_ajax_advaipbl_get_lockdown_details', [$this->ajax_handler, 'ajax_get_lockdown_details']);
add_action('admin_post_advaipbl_import_settings', [ $this, 'handle_import_settings' ] );
add_action('admin_post_advaipbl_clear_location_cache_action', [$this, 'handle_clear_cache_action']);
add_action('admin_post_advaipbl_send_test_email', [ $this, 'handle_send_test_email' ] );
add_action('admin_post_advaipbl_send_test_push', [ $this, 'handle_send_test_push' ] );
add_action('admin_post_advaipbl_run_manual_scan', [ $this, 'handle_run_manual_scan' ] );
// Hooks para el asistente de configuración
add_action( 'admin_post_advaipbl_wizard_step_1', [ $this->action_handler, 'handle_wizard_step_1' ] );
add_action( 'admin_post_advaipbl_wizard_step_2', [ $this->action_handler, 'handle_wizard_step_2' ] );
add_action( 'admin_post_advaipbl_wizard_step_3', [ $this->action_handler, 'handle_wizard_step_3' ] );
add_action( 'admin_post_advaipbl_wizard_step_4', [ $this->action_handler, 'handle_wizard_step_4' ] );
}
add_action('wp_ajax_advaipbl_get_advanced_rules', [$this->ajax_handler, 'ajax_get_advanced_rules']);
add_action('wp_ajax_advaipbl_save_advanced_rule', [$this->ajax_handler, 'ajax_save_advanced_rule']);
add_action('wp_ajax_advaipbl_delete_advanced_rule', [$this->ajax_handler, 'ajax_delete_advanced_rule']);
add_action('wp_ajax_advaipbl_reorder_rules', [$this->ajax_handler, 'ajax_reorder_advanced_rules']);
add_action('wp_ajax_advaipbl_bulk_delete_advanced_rules', [$this->ajax_handler, 'ajax_bulk_delete_advanced_rules']);
add_action('wp_ajax_advaipbl_verify_abuseipdb_key', [$this->ajax_handler, 'ajax_verify_abuseipdb_key']);
add_action('wp_ajax_advaipbl_bulk_import_whitelist', [$this->ajax_handler, 'ajax_bulk_import_whitelist']);
add_action('wp_ajax_advaipbl_bulk_export_whitelist', [$this->ajax_handler, 'ajax_bulk_export_whitelist']);
add_action('wp_ajax_advaipbl_bulk_import_blocked_ips', [$this->ajax_handler, 'ajax_bulk_import_blocked_ips']);
add_action('wp_ajax_advaipbl_bulk_export_blocked_ips', [$this->ajax_handler, 'ajax_bulk_export_blocked_ips']);
if ( ! empty( $this->options['xmlrpc_protection_mode'] ) && 'disabled' === $this->options['xmlrpc_protection_mode'] ) {
add_filter( 'xmlrpc_enabled', '__return_false' );
remove_action( 'wp_head', 'rsd_link' );
remove_action( 'wp_head', 'wlwmanifest_link' );
}
if ( !empty($this->options['recaptcha_enable']) && '1' === $this->options['recaptcha_enable'] && !empty($this->options['recaptcha_site_key']) && !empty($this->options['recaptcha_secret_key']) ) {
add_action('login_enqueue_scripts', array($this, 'enqueue_recaptcha_script'));
add_action('login_form', array($this, 'display_recaptcha_field'));
add_filter('authenticate', array($this, 'validate_recaptcha_response'), 20, 3);
}
}
/**
* Comprueba si la versión de la base de datos coincide con la versión definida en el plugin.
* Si no coincide, o si es forzada, ejecuta la configuración de tablas.
*/
public function check_database_version() {
$installed_ver = get_option('advaipbl_db_version');
if ( version_compare( $installed_ver, ADVAIPBL_DB_VERSION, '<' ) ) {
self::setup_database_tables();
}
}
/**
* Helper routine to automatically generate V3 API Tokens for users who
* already had the AIB Network activated in older versions.
*/
private function auto_migrate_v3_token() {
// Ensure options are freshly loaded since this runs early in init during updates
$this->options = get_option(self::OPTION_SETTINGS, []);
// If the user hasn't opted-in to the community network, do nothing (privacy first)
if (empty($this->options['enable_community_network'])) {
return;
}
// If they already have a V3 token, do nothing
if (!empty($this->options['api_token_v3'])) {
return;
}
// Trigger the internal site registration to generate keys and fetch the V3 token
if (isset($this->community_manager)) {
$this->community_manager->register_site();
// Re-load options into memory as register_site() modifies the DB directly sometimes
$this->options = get_option(self::OPTION_SETTINGS, []);
}
}
/**
* @param string $key La clave de la caché.
* @return mixed El valor de la caché, o false si no existe o ha caducado.
*/
public function get_from_custom_cache( $key, $get_full_object = false ) {
if ( ! isset($this->cache_manager) ) {
$this->cache_manager = new ADVAIPBL_Cache_Manager();
}
return $this->cache_manager->get( $key, $get_full_object );
}
/**
* Guarda un valor en tabla de caché personalizada.
*
* @param string $key La clave de la caché.
* @param mixed $value El valor a guardar (será serializado).
* @param int $expiration Duración de la caché en segundos.
*/
public function set_in_custom_cache( $key, $value, $expiration ) {
if ( ! isset($this->cache_manager) ) {
$this->cache_manager = new ADVAIPBL_Cache_Manager();
}
return $this->cache_manager->set( $key, $value, $expiration );
}
/**
* Limpia todas las entradas caducadas de la tabla de caché personalizada.
* Diseñado para ser llamado por un WP-Cron job para el mantenimiento de la base de datos.
*/
public function cleanup_expired_cache_entries() {
if ( ! isset($this->cache_manager) ) {
$this->cache_manager = new ADVAIPBL_Cache_Manager();
}
$deleted_rows = $this->cache_manager->cleanup_expired();
if ( is_numeric($deleted_rows) && $deleted_rows > 0 ) {
/* translators: %d: The number of IPs entries that were deleted. */
$this->log_event( sprintf( __( 'Cache cleanup task ran. Removed %d stale entries.', 'advanced-ip-blocker' ), $deleted_rows ), 'info' );
}
}
/**
* Registra todos los hooks relacionados con 2FA.
*/
public function add_2fa_hooks() {
//if ( ! $this->tfa_manager ) { return; }
// Hooks del Perfil
add_action('show_user_profile', [$this, 'display_2fa_section_in_profile']);
add_action('edit_user_profile', [$this, 'display_2fa_section_in_profile']);
add_action('personal_options_update', [$this, 'save_2fa_section_in_profile']);
add_action('edit_user_profile_update', [$this, 'save_2fa_section_in_profile']);
// Hooks de AJAX
add_action('wp_ajax_advaipbl_2fa_generate', [$this->ajax_handler, 'ajax_2fa_generate']);
add_action('wp_ajax_advaipbl_2fa_activate', [$this->ajax_handler, 'ajax_2fa_activate']);
add_action('wp_ajax_advaipbl_2fa_deactivate', [$this->ajax_handler, 'ajax_2fa_deactivate']);
// Hooks del Proceso de Login
add_filter('authenticate', [$this, 'intercept_login_step_1'], 20, 3); // Prioridad 20
add_action('login_form_advaipbl_validate_2fa', [$this, 'display_2fa_login_form_step_2']);
add_action('login_form_advaipbl_validate_2fa_backup', [$this, 'display_2fa_backup_code_form']);
add_action('login_form_login', [$this, 'handle_login_action']);
}
/**
* Registra la firma de la petición actual si la opción está activada.
*/
public function log_request_signature() {
if (empty($this->options['enable_signature_engine'])) { return; }
$request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
// No loguear las peticiones del Live Feed interno
if (strpos($request_uri, '/advaipbl/v1/live-attacks') !== false) {
return;
}
// Excluir AJAX, JSON, Cron y procesos internos (ej: Elementor, WP Dashboard), PERO monitorear nuestras APIs JSON
if (wp_doing_ajax() || is_admin() || wp_doing_cron() || (defined('WP_CLI') && WP_CLI)) {
return;
}
if (wp_is_json_request() && strpos($request_uri, '/telemetry/') === false && strpos($request_uri, '/aib-network/') === false && strpos($request_uri, '/aib-scanner/') === false && strpos($request_uri, '/aib-api/') === false) {
return;
}
if ($this->challenge_passed_this_request) { return; }
// Skip if whitelisted ASN (e.g. Google, Bing) to avoid false positives.
if ($this->request_is_asn_whitelisted) { return; }
$ip = $this->get_client_ip();
if ($this->is_whitelisted($ip)) { return; }
$signature_hash = $this->fingerprint_manager->generate_signature();
// Obtenemos la lista de confianza del usuario settings.
$raw_user_whitelist = $this->options['trusted_signature_hashes'] ?? '';
$user_whitelisted_hashes = [];
if (!empty($raw_user_whitelist)) {
$lines = explode("\n", $raw_user_whitelist);
foreach ($lines as $line) {
$trimmed_line = trim($line);
if (!empty($trimmed_line) && strpos($trimmed_line, '#') !== 0) {
$user_whitelisted_hashes[] = $trimmed_line;
}
}
}
// Combinamos la lista interna (hardcoded) con la del usuario.
$trusted_signature_hashes = array_merge([
// --- Jetpack / WordPress.com ---
'd455b0a2f262bd4daa891c5109feafce12e1307e2c759ffaae7da211463e8446',
// --- Google Services (Googlebot, AdsBot, etc.) ---
'9706d557e15672071207bb8a9281929cf8ebae2effc969efaa4c02b6ecd25827',
'c2d7bb6e8ffaa95be56128bbb44628bd870fb64a86259225b0ec9548b4a95e43',
'c81b74779fe9a063ad540c69ba17f62208bd7f030f1bce0ac6d8037f51888420',
'60f72d589709b1cd58f984ceffd68898edea65d331a517b52cba3d333da61732',
'dac6b4a4bb17c443e168393e1063cbc8f8366b09c7b75f0b32dd49726743cd85',
'6da42ede39fb3d22b9b80bc190f22340a81b3f8e6dc4a2a454d954646131cb71',
'76682a743fcec2df6aae7ce7d278d9a234e7827f1f8b0f1bdab5618592bb0414',
'd11373aa8cb33763f7d14f52d4495c0b6fbe4ba1f78604867b1545c092a43f87',
'fa5da9a69d6ef080920638100bf53c535e7343125249045130f3be22f0e7f37',
'175b7f0266cc41089a497ad33dc60a0498ed47d41025c3a1dd3a51491535c1be',
'6b7b1ee32414fd118f3be0888f0b46b228ff02fa2dbf08e83855aeb990d75e18',
'fb9c076e52d4afa4511c481a090108b5bb4510d0e675c08da7bcfdc89a796fa0',
// --- Facebook / Meta Crawler ---
'831c290f9b8e5047055e08320c196a9e70555f6555a7f382b1befaae68b13059',
'caa9f66174999f31250b227609717cc785530ffde6a325180ad2acaabb441722',
'f28c08c87db78fc72feed997490034a94adfa7048062213013c0f9504fc4f23a',
'0af2ea30ffd71fe4ad6cb65a27dc2785efbbcedbbf5695749868920b416508b2',
// --- BingBot / Microsoft ---
'eb596d705f64c69d53d48abebd43bac8d9fdf5d0d2baa5ba6fbf75b87510c8be',
'c9f80e720084bba5a49d312a3575363d270fa3cd5a236bf97b23d85459c34478',
'29f52bf75f09d138232d626a8c73fb9888fafdd50ab3c0c44c0652d7f7260e01',
// --- Applebot ---
'52b0598a19b0426786f053928e92d576e7835f22ce3bbd3ef63372f844c75980',
'93048d042e680c7add19592884caeb684e57195acbe5fbe93d82d369b4e1e3a6',
// --- AhrefsBot ---
'df88e81ab810ad131aeb9a2217dab49328ce84ab3fcf410118dea55058552571',
'1d3a3b731b9c7607c0d6eb0e3cd9798ee8dc42682530062ae981f795170bfcaf',
'5fa6ae3a3cc89216d8731c9be1009b6cdb08923098e0a9632d6d004e5dd90afd',
'e5df7bf69927bf260b85ee76a6a6e6d8a53a9f92a6ddabffce15c5e224730716',
// --- Babbar Crawler ---
'93b9822843c48732de0743dc76818d5f154b013db126d2fcf1b992f9255ef0a9',
'322e854629560b8cde97321f0ebf36a743e191343de0bc3452da60782d198ea7',
// --- PetalBot Crawler ---
'ca6e21ce33e83043585425efcbab5301b9883823a10398870a0c99376f9ace24',
'7d106ba8946c804cdd085f101a4624958b02ebd97e729d61c008d4cdee969cc7',
// --- HubSpot Crawler ---
'29eb3a1ba269b9e25f8e17bfbb435a17d713728cb9517aa5460717c2b5fce622',
'19c2029fc8a0f5570cd7e76cb753f59754cc030d3f0809703639184e737cfb5e',
// --- YandexBot Crawler ---
'06305ec342fcb037a73ca0745e33542547384618f128e3cfb1cb760a76ee4325',
'b884864ffbd62435980ce3e79d22d2f06fa6d6ae1f714016d179a2ef4ef3085c',
// --- AmazonProductDiscovery Crawler ---
'58bc871b23551ec8e03c5cfd743accb5add82d0d414960e67f12a38fe73c4c32',
'15c81a56f544780993d34ffd7c615640ff27e0f5bf0011675c838f7e9a18a075',
// --- OpenAI ---
'13d8b8022f757f9d9b2879cf0aaa8a7e68ead1a221c9c51dac3c5f5c937a1277',
// --- Otros Crawlers Legítimos (identificados genéricamente) ---
'4357e810006cbcca85538dcbf2dfbedbb402d81783942c19ef2fd2bcf36cac30',
'022d8938605fb7ace7bf0b8065024ac4b7c1a1cd0a30b102b52e1294b7d72add',
'11e72d8146bdcbf3a53517d240284f9a1d1293573f2445f01bbbf334a211fa11',
'4f8ca18d80d3d9011bd62d4e3ecd2525d2df4ac020b02c2dc9f830b138b0446d',
'7a292aeecb714d9b6b488308dbe4e7045b4648dfee6d8d8fdbd0035ee40a9f58',
'96cf4186d7adc3da7338031aad4265e9c4fe60d83c02d4d0dcbfa570ad557ecf',
'9ce22068e218c8bee0c7c91dbd4827682f60c53dc8d20a85e2f60a2b307ad239',
'a02e355c79010c8c3ac7a05a209f39b68216112b729774cfe8e7f929232832a5',
'c08feb2c6c0d36215e6e9c30fa1e6b4705473631f6549dbec3677040606f28aa',
'c5364e70a5108b71ef91dbdc2914864534116b92cf368c41cd86ebead67d0af1',
'd05bf96e94cac639887a3492b728a953da8c26d06ab980bb0ab4194d2d945ff7',
// --- CookieYesbot/ ---
'65372b51ff23921d541d9df2a93c2e503497b53f01dc2574649ddfd62ef7e234',
'6c4778800f4ee0f037147bf27108d100a23e7e085635c10c08b5be7edadafebc',
], $user_whitelisted_hashes);
$trusted_signature_hashes = apply_filters('advaipbl_trusted_signature_hashes', $trusted_signature_hashes);
if (in_array($signature_hash, $trusted_signature_hashes, true)) {
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_request_log';
$is_fake_bot = 0;
// Check if the User-Agent claims to be a known bot (Google, Bing, etc.)
if ( isset($this->bot_verifier) && $this->bot_verifier->is_known_bot_impersonator($ip, $this->get_user_agent()) ) {
// 1. Check transient cache first (Optimization)
if ( get_transient('advaipbl_verified_bot_' . md5($ip)) ) {
$is_fake_bot = 0; // It's verified and legitimate
} else {
// 2. Perform DNS verification
// is_verified_bot performs the DNS lookup if not cached statically
if ( ! $this->bot_verifier->is_verified_bot($ip, $this->get_user_agent()) ) {
$is_fake_bot = 1;
}
}
}
$data_to_log = [
'timestamp' => time(),
'ip_hash' => hash('sha256', $ip),
'request_uri' => $this->get_current_request_uri(),
'user_agent' => $this->get_user_agent(),
'request_headers' => $this->fingerprint_manager->get_request_headers_for_log(),
'request_method' => $this->get_request_method(),
'signature_hash' => $signature_hash,
'is_fake_bot' => $is_fake_bot,
];
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
$wpdb->insert($table_name, $data_to_log);
}
/**
* Purges all cache from popular caching plugins.
* Called when security settings or blocklists change to ensure immediate effect.
*/
public function purge_all_page_caches() {
// LiteSpeed Cache
if (has_action('litespeed_purge_all')) {
do_action('litespeed_purge_all');
} elseif (defined('LSCWP_V')) {
do_action('litespeed_purge_all_hook');
}
// WP Rocket
if (function_exists('rocket_clean_domain')) {
rocket_clean_domain();
}
// W3 Total Cache
if (function_exists('w3tc_flush_all')) {
w3tc_flush_all();
}
// WP Super Cache
if (function_exists('wp_cache_clear_cache')) {
wp_cache_clear_cache();
}
// WP Fastest Cache
if (isset($GLOBALS['wp_fastest_cache']) && method_exists($GLOBALS['wp_fastest_cache'], 'deleteCache')) {
$GLOBALS['wp_fastest_cache']->deleteCache(true);
}
// Autoptimize
if (class_exists('autoptimizeCache')) {
autoptimizeCache::clearall();
}
// SG Optimizer (SiteGround)
if (function_exists('sg_cachepress_purge_cache')) {
sg_cachepress_purge_cache();
}
// Kinsta Cache
if (class_exists('Kinsta\Cache')) {
do_action('kinsta_purge_edge_cache');
}
/* translators: %s: Type of action triggered. */
$this->log_event(__('System page caches successfully flushed after a security update.', 'advanced-ip-blocker'), 'info', 'localhost');
}
/**
* Se ejecuta temprano para verificar bots conocidos y tomar acciones.
*/
public function verify_known_bots() {
// 1. Salir si la función está desactivada o no aplica a esta petición.
if (empty($this->options['enable_bot_verification']) || is_admin() || wp_doing_cron() || (defined('WP_CLI') && WP_CLI)) {
return;
}
$ip = $this->get_client_ip();
$user_agent = $this->get_user_agent();
// 2. Comprobar si la IP ya ha sido verificada en una petición anterior (cacheado en transient).
if (get_transient('advaipbl_verified_bot_' . md5($ip))) {
$this->request_is_asn_whitelisted = true; // Reutilizamos esta bandera para saltar otras comprobaciones.
return;
}
// 3. Realizar la verificación de DNS (solo si el User-Agent parece de un bot conocido).
$is_verified = $this->bot_verifier->is_verified_bot($ip, $user_agent);
if ($is_verified) {
// -- ÉXITO: Es un bot legítimo --
// Verificar si el usuario quiere bloquearlo explícitamente
$blocked_uas = get_option('advaipbl_blocked_user_agents', []);
foreach ($blocked_uas as $blocked_ua) {
// Eliminar comentarios
$blocked_ua = trim(preg_replace('/#.*$/', '', $blocked_ua));
if (!empty($blocked_ua) && stripos($user_agent, $blocked_ua) !== false) {
// El usuario lo ha bloqueado explícitamente. NO damos inmunidad.
// Dejamos que siga el flujo normal, donde será bloqueado por la regla de User-Agent.
return;
}
}
// Si no está bloqueado explícitamente, damos inmunidad y cacheamos
set_transient('advaipbl_verified_bot_' . md5($ip), true, DAY_IN_SECONDS);
$this->request_is_asn_whitelisted = true;
} elseif ($this->bot_verifier->is_known_bot_impersonator($ip, $user_agent)) {
// -- FALLO: Es un impostor conocido --
// Instant Block (Zero Tolerance)
// Skip scoring system as impersonation is a confirmed threat.
$duration_minutes = 1440; // Default fallback
// 1. Si el sistema de Puntuación (Threat Scoring) está activo:
// a) Registramos los puntos para que conste en el Log de Confianza.
// b) Usamos su duración configurada.
if (!empty($this->options['enable_threat_scoring'])) {
// Registrar Puntos
$points_impersonation = (int) ($this->options['score_impersonation'] ?? 100);
$this->threat_score_manager->increment_score(
$ip,
$points_impersonation,
'impersonation',
['user_agent' => $user_agent]
);
// Usar Duración
$duration_minutes = (int) ($this->options['duration_threat_score'] ?? 1440);
} else {
// 2. Si no, usamos la duración configurada para User-Agents "malos"
$duration_minutes = (int) ($this->options['duration_user_agent'] ?? 1440);
}
$duration_seconds = ($duration_minutes > 0) ? $duration_minutes * 60 : 0; // 0 = Permanent
$this->block_ip_instantly(
$ip,
'impersonation',
__('Blocked for impersonating a known crawler.', 'advanced-ip-blocker'),
[
'impersonated_user_agent' => $user_agent,
'uri' => $this->get_current_request_uri()
],
$context = 'frontend_block',
$duration_seconds // Pass explicit duration
);
}
}
/**
* Se ejecuta en un hook muy temprano para comprobar la firma de la petición.
* Si la firma es maliciosa, sirve un desafío JavaScript.
*/
public function check_for_malicious_signature() {
if ($this->is_request_uri_excluded()) { return; }
if ($this->request_is_asn_whitelisted) { return; }
// Excluir AJAX, JSON, Cron y procesos internos (ej: Elementor, WP Dashboard)
if (wp_doing_ajax() || wp_is_json_request() || is_admin() || wp_doing_cron() || (defined('WP_CLI') && WP_CLI)) {
return;
}
// Si el usuario acaba de pasar un desafío, le damos un pase de gracia de 15s.
if (get_transient('advaipbl_grace_pass_' . md5($this->get_client_ip()))) {
return;
}
if (empty($this->options['enable_signature_blocking'])) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing
if (isset($_POST['_advaipbl_js_token'])) {
// Parámetros para el desafío de firmas: cookie 'advaipbl_js_verified', duración 4 horas.
$this->js_challenge_manager->verify_challenge('advaipbl_js_verified', 4 * HOUR_IN_SECONDS);
}
if (isset($_COOKIE['advaipbl_js_verified']) && $_COOKIE['advaipbl_js_verified'] === '1') {
return;
}
$signature_hash = $this->fingerprint_manager->generate_signature();
global $wpdb;
$signatures_table = $wpdb->prefix . 'advaipbl_malicious_signatures';
// Fix: Check if table exists to avoid fatal error on fresh install
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
if ($wpdb->get_var("SHOW TABLES LIKE '$signatures_table'") != $signatures_table) {
return;
}
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
$is_malicious = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$signatures_table} WHERE signature_hash = %s AND expires_at > %d",
$signature_hash,
time()
));
if ($is_malicious) {
// 1. Registramos el evento aquí, en el punto de decisión.
$this->log_specific_error(
'signature_challenge',
$this->get_client_ip(),
[
'signature_hash' => $signature_hash,
'uri' => $this->get_current_request_uri()
],
'warning'
);
// 2. Luego, servimos el desafío.
$this->js_challenge_manager->serve_challenge('signature');
}
}
/**
* Comprueba si el visitante proviene de un país que requiere un desafío geográfico.
* Se ejecuta en un hook 'init' temprano.
*/
public function check_for_geo_challenge() {
if ($this->is_request_uri_excluded()) { return; }
if (empty($this->options['enable_geo_challenge'])) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing
if (isset($_POST['_advaipbl_challenge_type']) && $_POST['_advaipbl_challenge_type'] === 'geo_challenge') {
$duration_hours = (int)($this->options['geo_challenge_cookie_duration'] ?? 24);
$duration_seconds = ($duration_hours > 0) ? $duration_hours * HOUR_IN_SECONDS : 0;
$this->js_challenge_manager->verify_challenge('advaipbl_js_verified', $duration_seconds);
}
if (isset($_COOKIE['advaipbl_js_verified']) && $_COOKIE['advaipbl_js_verified'] === '1') {
return;
}
$ip = $this->get_client_ip();
if (get_transient('advaipbl_grace_pass_' . md5($ip))) {
return;
}
if ($this->request_is_asn_whitelisted || wp_doing_cron() || is_admin() || (defined('WP_CLI') && WP_CLI) || $this->is_whitelisted($ip)) {
return;
}
$challenged_countries = $this->options['geo_challenge_countries'] ?? [];
if (empty($challenged_countries)) {
return;
}
$location = $this->geolocation_manager->fetch_location($ip);
if ($location && !empty($location['country_code'])) {
if (in_array($location['country_code'], $challenged_countries, true)) {
$this->log_specific_error(
'geo_challenge',
$ip,
[
'country' => $location['country'] ?? $location['country_code'],
'uri' => $this->get_current_request_uri()
],
'warning'
);
$this->js_challenge_manager->serve_challenge('geo_challenge');
}
}
}
/**
* Envía notificaciones (Email/Push) cuando una nueva firma maliciosa es identificada.
*
* @param string $signature_hash El hash de la firma.
* @param string $reason La razón por la que fue marcada.
* @param string $user_agent El User-Agent de ejemplo asociado a la firma.
*/
/**
* Envía notificaciones (Email/Push) cuando una nueva firma maliciosa es identificada.
*/
public function send_signature_flagged_notification($signature_hash, $reason, $user_agent = 'N/A') {
if ( isset($this->notification_manager) ) {
$this->notification_manager->send_signature_flagged_notification($signature_hash, $reason, $user_agent);
}
}
/**
* Callback para el evento de cron que ejecuta el decaimiento de la puntuación de amenaza.
*/
public function execute_threat_score_decay() {
// Solo se ejecuta si el sistema de puntuación está activado.
if (empty($this->options['enable_threat_scoring'])) {
return;
}
$decay_points = (int) ($this->options['score_decay_points'] ?? 1);
$decay_frequency_hours = (int) ($this->options['score_decay_frequency'] ?? 1);
$inactive_for_seconds = $decay_frequency_hours * HOUR_IN_SECONDS;
if ($decay_points > 0 && $decay_frequency_hours > 0) {
$result = $this->threat_score_manager->decay_scores($decay_points, $inactive_for_seconds);
if (($result['updated'] ?? 0) > 0 || ($result['deleted'] ?? 0) > 0) {
$log_message = sprintf( /* translators: 1: Number of scores reduced, 2: Number of scores reset to zero. */
__('Threat score decay process ran. Reduced: %1$d IPs, Reset: %2$d IPs.', 'advanced-ip-blocker'),
$result['updated'],
$result['deleted']
);
$this->log_event($log_message, 'info');
}
}
}
/**
* Callback para el evento de cron que ejecuta el análisis de firmas de ataque.
*/
public function execute_signature_analysis() {
if (empty($this->options['enable_signature_analysis'])) {
return;
}
$ip_threshold = (int) ($this->options['signature_ip_threshold'] ?? 5);
$analysis_window_hours = (int) ($this->options['signature_analysis_window'] ?? 1);
$rule_ttl_hours = (int) ($this->options['signature_rule_ttl'] ?? 24);
$this->fingerprint_manager->analyze_and_flag_signatures(
$ip_threshold,
$analysis_window_hours * HOUR_IN_SECONDS,
$rule_ttl_hours * HOUR_IN_SECONDS
);
}
/**
* Registra el endpoint de la API REST para el feed de ataques en vivo.
*/
public function register_live_feed_api_endpoint() {
if ( ! isset($this->live_feed_manager) ) {
$this->live_feed_manager = new ADVAIPBL_Live_Feed_Manager($this);
}
$this->live_feed_manager->register_api_endpoint();
}
/**
* Callback para el endpoint de la API. Devuelve los últimos ataques.
*
* @param WP_REST_Request $request
* @return WP_REST_Response
*/
public function get_live_attacks_for_feed(WP_REST_Request $request) {
if ( ! isset($this->live_feed_manager) ) {
$this->live_feed_manager = new ADVAIPBL_Live_Feed_Manager($this);
}
return $this->live_feed_manager->get_live_attacks($request);
}
/**
* Endpoint de la API REST para obtener un nonce fresco para el feed en vivo.
* Esto evita problemas de caché de página.
*/
public function get_live_feed_nonce() {
if ( ! isset($this->live_feed_manager) ) {
$this->live_feed_manager = new ADVAIPBL_Live_Feed_Manager($this);
}
return $this->live_feed_manager->get_nonce();
}
/**
* Función del shortcode [advaipbl_live_feed].
* Genera el HTML y el CSS necesario para el feed, y encola el script JS.
*
* @param array $atts Atributos del shortcode.
* @return string El HTML y CSS para el feed.
*/
public function render_live_feed_shortcode($atts) {
if ( ! isset($this->live_feed_manager) ) {
$this->live_feed_manager = new ADVAIPBL_Live_Feed_Manager($this);
}
return $this->live_feed_manager->render_shortcode($atts);
}
/**
* Detecta y registra las ejecuciones de WP-Cron.
*/
public function log_wp_cron_execution() {
$is_doing_cron = ( defined( 'DOING_CRON' ) && DOING_CRON );
$is_cron_url = ( strpos($this->get_current_request_uri(), 'wp-cron.php') !== false );
if ( $is_doing_cron || $is_cron_url ) {
$ip = $this->get_client_ip();
$transient_key = 'advaipbl_cron_log_lock_' . md5($ip);
if ( false !== get_transient($transient_key) ) {
return;
}
set_transient($transient_key, true, 60);
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_logs';
$cron_array = _get_cron_array();
$due_hooks = [];
if ( is_array( $cron_array ) ) {
$current_time = time();
foreach ( $cron_array as $timestamp => $hooks ) {
if ( $timestamp <= $current_time ) {
foreach ( $hooks as $hook_name => $events ) {
$due_hooks[] = $hook_name;
}
}
}
}
$due_hooks = array_unique( $due_hooks );
$server_ip = $this->get_server_ip();
$source = 'External'; // Por defecto, asumimos que es externa.
if ( $ip === $server_ip || $ip === '127.0.0.1' || $ip === '::1' ) {
$source = 'Server'; // Si coincide con la IP del servidor o localhost, es interna.
}
$details = [
'url' => $this->get_current_request_uri(),
'uri' => $this->get_current_request_uri(),
'method' => $this->get_request_method(),
'user_agent' => $this->get_user_agent(),
'due_hooks' => $due_hooks,
'source' => $source, // Añadimos la nueva información
];
@$wpdb->insert(
$table_name,
[
'timestamp' => current_time('mysql', 1),
'ip' => $ip,
'log_type' => 'wp_cron',
'level' => 'info',
'message' => __('WP-Cron execution triggered.', 'advanced-ip-blocker'),
'details' => wp_json_encode($details)
]
);
}
}
/**
* Enqueues the Google reCAPTCHA API script on the login page.
*/
public function enqueue_recaptcha_script() {
$version = $this->options['recaptcha_version'] ?? 'v3';
$site_key = $this->options['recaptcha_site_key'] ?? '';
// La comprobación de si la clave está vacía ya se hace en add_hooks,
// pero una doble comprobación no hace daño.
if (empty($site_key)) {
return;
}
$script_url = 'https://www.google.com/recaptcha/api.js';
if ('v3' === $version) {
$script_url = add_query_arg('render', $site_key, $script_url);
}
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
wp_enqueue_script(
'google-recaptcha',
$script_url,
array(),
null,
true
);
}
/**
* Se dispara cuando las reglas del WAF son actualizadas desde la página de ajustes.
* Registra el evento en el log general.
*
* @param mixed $old_value El valor antiguo de la opción.
* @param mixed $new_value El nuevo valor de la opción.
*/
public function on_waf_rules_update($old_value, $new_value) {
// Solo registramos si el valor ha cambiado realmente.
if ($old_value !== $new_value) {
$message = sprintf(
/* translators: %s: El nombre de usuario del administrador que realizó el cambio. */
__('WAF rules list updated by %s.', 'advanced-ip-blocker'),
$this->get_current_admin_username()
);
$this->log_event($message, 'info');
}
}
/**
* Descarga y procesa la lista Spamhaus ASN DROP y la guarda en la base de datos.
* Es llamada por el WP-Cron y por la acción de refresco manual.
*/
public function update_spamhaus_list() {
$url = 'https://www.spamhaus.org/drop/asndrop.json';
$response = wp_remote_get($url, ['timeout' => 15]);
if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
$error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code($response);
$this->log_event(sprintf('Failed to download Spamhaus ASN list. Error: %s', $error_message), 'error');
return;
}
$body_string = wp_remote_retrieve_body($response);
// El archivo de Spamhaus es una secuencia de objetos JSON, no un único array JSON.
// Lo dividimos en objetos individuales.
// Usamos una expresión regular para encontrar cada objeto JSON { ... }
preg_match_all('/\{.*?\}/', $body_string, $matches);
if (empty($matches[0])) {
$this->log_event('Failed to parse Spamhaus ASN list: No valid JSON objects found in the response.', 'error');
return;
}
$json_objects = $matches[0];
$asns = [];
foreach ($json_objects as $json_string) {
$entry = json_decode($json_string, true);
if (json_last_error() === JSON_ERROR_NONE && isset($entry['asn'])) {
// El formato es correcto y tiene la clave 'asn'.
$asns[] = 'AS' . $entry['asn'];
}
}
if (empty($asns)) {
$this->log_event('Spamhaus ASN list was parsed but no ASN entries were extracted.', 'warning');
return;
}
update_option('advaipbl_spamhaus_asn_list', array_unique($asns));
update_option('advaipbl_spamhaus_last_update', time());
$this->log_event(sprintf('Successfully updated Spamhaus ASN list with %d entries.', count($asns)), 'info');
}
public function handle_spamhaus_refresh_action() {
if (!current_user_can('manage_options') || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'] ?? '')), 'advaipbl-refresh-spamhaus')) {
wp_die('Security check failed.');
}
$this->update_spamhaus_list();
set_transient(self::TRANSIENT_ADMIN_NOTICE, ['message' => __('Spamhaus ASN list has been updated.', 'advanced-ip-blocker'), 'type' => 'success'], 30);
wp_safe_redirect(wp_get_referer());
exit;
}
/**
* Displays the reCAPTCHA field on the login form.
*/
public function display_recaptcha_field() {
$site_key = $this->options['recaptcha_site_key'] ?? '';
$version = $this->options['recaptcha_version'] ?? 'v3';
if ('v2' === $version) {
echo '
';
} else {
echo '';
?>
options['recaptcha_secret_key'] ?? '';
// La comprobación principal ya se hace en add_hooks,
// pero si alguien modificara la opción mientras tanto, esto es un seguro.
if (empty($secret_key)) {
return $user;
}
// FIX: XML-RPC requests cannot perform reCAPTCHA validation (no JS).
// Allow them to bypass this check to prevent infinite authentication loops.
if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
return $user;
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$recaptcha_response = isset($_POST['g-recaptcha-response']) ? sanitize_text_field(wp_unslash($_POST['g-recaptcha-response'])) : '';
if (empty($recaptcha_response)) {
return new WP_Error('recaptcha_empty', __('ERROR: Please complete the reCAPTCHA verification.', 'advanced-ip-blocker'));
}
$token = $recaptcha_response;
$visitor_ip = $this->get_client_ip();
$response = wp_remote_post('https://www.google.com/recaptcha/api/siteverify', array(
'body' => array(
'secret' => $secret_key,
'response' => $token,
'remoteip' => $visitor_ip,
),
));
if (is_wp_error($response)) {
return new WP_Error('recaptcha_api_error', __('ERROR: Could not connect to the reCAPTCHA service.', 'advanced-ip-blocker'));
}
$result = json_decode(wp_remote_retrieve_body($response), true);
if (!isset($result['success']) || true !== $result['success']) {
return new WP_Error('recaptcha_failed', __('ERROR: reCAPTCHA verification failed. Please try again.', 'advanced-ip-blocker'));
}
$version = $this->options['recaptcha_version'] ?? 'v3';
if ('v3' === $version) {
$threshold = (float) ($this->options['recaptcha_score_threshold'] ?? 0.5);
if (!isset($result['score']) || $result['score'] < $threshold) {
return new WP_Error('recaptcha_low_score', __('ERROR: Your action was blocked as it was flagged as automated.', 'advanced-ip-blocker'));
}
}
return $user;
}
/**
* Detecta plugins activos que probablemente usan la interfaz XML-RPC.
*
* Utiliza una combinación de una lista de plugins conocidos y un análisis de los hooks de XML-RPC.
*
* @return array Un array con los nombres de los plugins detectados. Vacío si no se encuentra ninguno.
*/
public function get_xmlrpc_dependent_plugins() {
$dependent_plugins = [];
$active_plugins = get_option('active_plugins');
// 1. Lista de plugins conocidos que dependen de XML-RPC.
$known_slugs = [
'jetpack/jetpack.php',
// Si encuentras más en el futuro, como la app de WooCommerce si usa un plugin puente.
'xmlrpc-debugger/xmlrpc-debugger.php'
];
foreach ($known_slugs as $slug) {
if (in_array($slug, $active_plugins, true)) {
$plugin_path = WP_PLUGIN_DIR . '/' . $slug;
if (file_exists($plugin_path)) {
$plugin_data = get_plugin_data($plugin_path);
if (!empty($plugin_data['Name'])) {
$dependent_plugins[$slug] = $plugin_data['Name'];
}
}
}
}
// 2. Análisis de Hooks (Heurística).
// Buscamos plugins que se enganchen a los eventos de XML-RPC.
global $wp_filter;
$xmlrpc_hooks = [
'xmlrpc_methods',
'xmlrpc_call',
'xmlrpc_call_success',
'xmlrpc_call_failure',
'xmlrpc_before_insert_post',
];
foreach ($xmlrpc_hooks as $hook_name) {
if (isset($wp_filter[$hook_name])) {
foreach ($wp_filter[$hook_name]->callbacks as $priority => $callbacks) {
foreach ($callbacks as $callback) {
$function = $callback['function'];
$reflection = null;
try {
if (is_array($function) && is_object($function[0])) {
$reflection = new ReflectionClass($function[0]);
} elseif (is_string($function) && function_exists($function)) {
$reflection = new ReflectionFunction($function);
}
} catch (ReflectionException $e) {
// Ignorar funciones/clases que no se pueden reflejar (ej. closures)
continue;
}
if ($reflection) {
$file_path = $reflection->getFileName();
// Comprobamos si la ruta del archivo está dentro del directorio de plugins.
if ($file_path && strpos($file_path, WP_PLUGIN_DIR) !== false) {
$plugin_dir_path = str_replace('\\', '/', WP_PLUGIN_DIR);
$file_path = str_replace('\\', '/', $file_path);
$relative_path = ltrim(str_replace($plugin_dir_path, '', $file_path), '/');
$plugin_slug_parts = explode('/', $relative_path);
$plugin_folder = $plugin_slug_parts[0];
// Buscamos el nombre real del plugin para no mostrar solo el slug.
foreach ($active_plugins as $active_plugin_slug) {
if (strpos($active_plugin_slug, $plugin_folder) === 0) {
if (!isset($dependent_plugins[$active_plugin_slug])) {
$plugin_data = get_plugin_data(WP_PLUGIN_DIR . '/' . $active_plugin_slug);
if (!empty($plugin_data['Name'])) {
$dependent_plugins[$active_plugin_slug] = $plugin_data['Name'];
}
}
break;
}
}
}
}
}
}
}
}
return array_values($dependent_plugins);
}
/**
* Obtiene el número de IPs/rangos bloqueados.
* Cachea el resultado para mejorar el rendimiento.
*
* @return int El número total de entradas de IP bloqueadas.
*/
public function get_blocked_count() {
if ( null !== $this->blocked_count ) {
return $this->blocked_count;
}
// Usamos la caché de objetos de WP para un mejor rendimiento
$count = wp_cache_get('blocked_ips_count', 'advaipbl');
if (false === $count) {
$this->limpiar_ips_expiradas(); // La limpieza es crucial antes de contar
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_blocked_ips';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
$count = (int) $wpdb->get_var("SELECT COUNT(id) FROM {$table_name}");
wp_cache_set('blocked_ips_count', $count, 'advaipbl', 300); // Cache por 5 minutos
}
$this->blocked_count = $count;
return $this->blocked_count;
}
/**
* Obtiene el número de firmas de ataque activas.
* @return int
*/
public function get_blocked_signatures_count() {
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_malicious_signatures';
// Usamos cache para evitar consultas repetidas en la misma petición
$count = wp_cache_get('blocked_signatures_count', 'advaipbl');
if (false === $count) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
$count = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(id) FROM {$table_name} WHERE expires_at > %d", time()));
wp_cache_set('blocked_signatures_count', $count, 'advaipbl', 300); // Cache por 5 minutos
}
return $count;
}
/**
* Obtiene el número de endpoints actualmente en lockdown.
* @return int
*/
public function get_blocked_endpoints_count() {
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_endpoint_lockdowns';
$count = wp_cache_get('blocked_endpoints_count', 'advaipbl');
if (false === $count) {
// Primero, limpiamos los expirados para un conteo preciso
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
$wpdb->query($wpdb->prepare("DELETE FROM {$table_name} WHERE expires_at <= %d", time()));
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
$count = (int) $wpdb->get_var("SELECT COUNT(id) FROM {$table_name}");
wp_cache_set('blocked_endpoints_count', $count, 'advaipbl', 300); // Cache por 5 minutos
}
return $count;
}
/**
* Gets the username of the current logged-in administrator.
*
* @return string The username or a default string if not available.
*/
public function get_current_admin_username() {
$user = wp_get_current_user();
// Si el usuario existe y tiene un ID, devuelve su login. Si no, devuelve un texto genérico.
return ( $user && $user->ID ) ? $user->user_login : __( 'an unknown user', 'advanced-ip-blocker' );
}
/**
* Añade una entrada (IP o rango) a la whitelist y se asegura de que sea eliminada
* de todas las listas de bloqueo. Este es el método centralizado para esta acción.
*
* @param string $entry_to_whitelist La IP o rango a añadir a la whitelist.
* @param string $detail Una descripción de por qué se añadió.
* @return bool True si la entrada se añadió con éxito, false si ya existía.
*/
public function add_to_whitelist_and_unblock( $entry_to_whitelist, $detail ) {
if ( ! $this->is_valid_ip_or_range( $entry_to_whitelist ) ) {
return false;
}
$whitelist = get_option( self::OPTION_WHITELIST, [] );
if ( array_key_exists( $entry_to_whitelist, $whitelist ) ) {
return false;
}
$whitelist[ $entry_to_whitelist ] = [ 'timestamp' => time(), 'detail' => $detail ];
update_option( self::OPTION_WHITELIST, $whitelist );
wp_cache_delete( self::OPTION_WHITELIST, 'options' );
// FIX: Always unblock from DB/Htaccess/Cloudflare (IPs AND Ranges)
$this->desbloquear_ip( $entry_to_whitelist );
// Legacy Cleanup: Only for ranges (to clean old wp_options arrays if they exist)
if ( ! filter_var( $entry_to_whitelist, FILTER_VALIDATE_IP ) ) {
// Usar el mapa de constantes para limpiar el rango de TODAS las listas.
$option_key_map = [
'geoblock' => self::OPTION_BLOCKED_GEO,
'honeypot' => self::OPTION_BLOCKED_HONEYPOT,
'user_agent' => self::OPTION_BLOCKED_USER_AGENT,
'manual' => self::OPTION_BLOCKED_MANUAL,
'404' => self::OPTION_BLOCKED_404,
'403' => self::OPTION_BLOCKED_403,
'login' => self::OPTION_BLOCKED_LOGIN,
'waf' => self::OPTION_BLOCKED_WAF,
'rate_limit' => self::OPTION_BLOCKED_RATE_LIMIT,
'asn' => self::OPTION_BLOCKED_ASN,
'xmlrpc_block' => self::OPTION_BLOCKED_XMLRPC,
'threat_score' => self::OPTION_BLOCKED_THREAT_SCORE,
];
foreach ( $option_key_map as $type => $option_key ) {
$list = get_option( $option_key, [] );
if ( is_array($list) && array_key_exists( $entry_to_whitelist, $list ) ) {
unset( $list[ $entry_to_whitelist ] );
update_option( $option_key, $list );
}
}
}/* translators: 1: The whitelisted IP/range, 2: The admin username. */
$this->log_event( sprintf( __( 'Entry %1$s added to whitelist by %2$s.', 'advanced-ip-blocker' ), $entry_to_whitelist, $this->get_current_admin_username() ), 'info', is_array($entry_to_whitelist) ? wp_json_encode($entry_to_whitelist) : $entry_to_whitelist );
return true;
}
public function handle_clear_cache_action() {
if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'advaipbl_clear_location_cache_nonce')) {
wp_die('Invalid nonce.');
}
if (!current_user_can('manage_options')) {
wp_die('Permission denied.');
}
// Vaciamos la tabla de caché.
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_cache';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
$wpdb->query("TRUNCATE TABLE `{$table_name}`");
/* translators: %s: Admin username. */
$this->log_event( sprintf( __( 'User session location cache manually cleared by %s.', 'advanced-ip-blocker' ), $this->get_current_admin_username() ), 'info' );
set_transient('advaipbl_admin_notice', ['message' => __('Location cache has been cleared.', 'advanced-ip-blocker'), 'type' => 'success'], 30);
wp_safe_redirect(wp_get_referer());
exit;
}
/**
* Handles the request to send a test email from the settings page.
*/
public function handle_send_test_email() {
// 1. Security check: Nonce and user permissions.
if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'advaipbl_send_test_email_nonce' ) ) {
wp_die( 'Invalid nonce.' );
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Permission denied.' );
}
// 2. Preparar variables
$to = ! empty( $this->options['notification_email'] ) && is_email( $this->options['notification_email'] ) ? $this->options['notification_email'] : get_option( 'admin_email' );
$site_name = get_bloginfo( 'name' );
$settings_url = admin_url( 'options-general.php?page=advaipbl_settings_page' );
if ( isset($this->notification_manager) ) {
$sent = $this->notification_manager->send_test_email($to);
} else {
$sent = false;
}
// 5. Lógica de notificación y redirección
if ( $sent ) {
$message = __( 'The setup guide has been sent successfully to your configured email address.', 'advanced-ip-blocker' );
$type = 'success';
/* translators: 1: Recipient's email address, 2: Admin username */
$this->log_event( sprintf( __( 'Setup guide sent to %1$s by %2$s.', 'advanced-ip-blocker' ), $to, $this->get_current_admin_username() ), 'info' );
} else {
$message = __( 'Failed to send the setup guide email. Please check your site\'s email configuration.', 'advanced-ip-blocker' );
$type = 'error';
/* translators: 1: Recipient's email address, 2: Admin username */
$this->log_event( sprintf( __( 'FAILED to send setup guide to %1$s. Action by %2$s.', 'advanced-ip-blocker' ), $to, $this->get_current_admin_username() ), 'error' );
}
set_transient( 'advaipbl_admin_notice', [ 'message' => $message, 'type' => $type ], 30 );
wp_safe_redirect( wp_get_referer() );
exit;
}
public function handle_send_test_push() {
check_admin_referer('advaipbl_send_test_push_nonce');
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Cheatin’ uh?', 'advanced-ip-blocker' ) );
}
$webhook_urls_str = $this->options['push_webhook_urls'] ?? '';
$webhook_urls = array_filter(array_map('trim', explode("\n", $webhook_urls_str)));
if (empty($webhook_urls)) {
wp_die(
'
' . esc_html__('Your request was blocked by the security firewall.', 'advanced-ip-blocker'),
'excluded_error_urls' => implode( "\n", $default_exclusions_404_403 ),
'waf_excluded_urls' => implode("\n", $default_waf_exclusions),
'rows_per_page' => 20, 'delete_data_on_uninstall' => '0', 'show_admin_bar_menu' => '1',
'trusted_proxies' => "# Cloudflare\nAS13335\nAS209242\n# Local Nginx/Varnish Proxy\n127.0.0.1\n::1",
// Notificaciones
'enable_email_notifications' => '0', 'notification_frequency' => 'disabled', 'notification_email' => '',
'enable_push_notifications' => '0',
'push_webhook_urls' => '',
'push_critical_only' => '0',
'push_mentions' => '',
// Smart 404/403 Distributed Lockdown
'enable_404_lockdown' => '0',
'lockdown_404_event_threshold' => 50,
'lockdown_404_ip_threshold' => 5,
'lockdown_404_window' => 10,
'lockdown_404_duration' => 60,
'enable_403_lockdown' => '0',
'lockdown_403_event_threshold' => 50,
'lockdown_403_ip_threshold' => 5,
'lockdown_403_window' => 10,
'lockdown_403_duration' => 60,
// APIs Externas
'geolocation_provider' => 'ip-api.com', 'api_key_ipapicom' => '', 'api_key_ipstackcom' => '',
'api_key_ipinfocom' => '', 'api_key_ip_apicom' => '', 'geolocation_method' => 'api',
'maxmind_license_key' => '',
'enable_community_network' => '0', // Por defecto 0 hasta que el usuario acepte o complete el Wizard
// AIB Community Network
'enable_community_blocking' => '1',
'community_blocking_action' => 'block',
'duration_aib_network' => 1440,
// AbuseIPDB Integration
'enable_abuseipdb' => '0',
'abuseipdb_api_key' => '',
'abuseipdb_threshold' => 90,
'duration_abuseipdb' => 1440,
'abuseipdb_action' => 'block',
// Htaccess Firewall
'enable_htaccess_write' => '0',
'enable_htaccess_ip_blocking' => '0',
'enable_htaccess_all_ips' => '0',
'htaccess_protect_system_files' => '0',
'htaccess_protect_wp_config' => '0',
'htaccess_protect_readme' => '0',
// Geobloqueo
'geoblock_countries' => [],
// Ajustes de reCAPTCHA
'recaptcha_enable' => '0', 'recaptcha_version' => 'v3', 'recaptcha_site_key' => '',
'recaptcha_secret_key' => '', 'recaptcha_score_threshold' => 0.5,
// CLAVES 2FA
'enable_2fa' => '0',
'tfa_force_roles' => [],
'prevent_login_hinting' => '1',
// Telemetría
'allow_telemetry' => '0',
// Sistema de Puntuación de Amenaza (IP Trust System)
'enable_threat_scoring' => '0',
'threat_score_threshold' => 100,
'duration_threat_score' => 1440,
'score_404' => 5,
'score_403' => 10,
'score_login' => 15,
'score_user_agent' => 100,
'score_waf' => 100,
'score_honeypot' => 100,
'score_asn' => 100,
'score_impersonation' => 100,
'score_decay_points' => 1,
'score_decay_frequency' => 1,
'enable_signature_engine' => '0',
'enable_signature_analysis' => '0',
'signature_ip_threshold' => 5,
'signature_analysis_window' => 1,
'signature_rule_ttl' => 24,
'enable_signature_blocking' => '0',
'trusted_signature_hashes' => '',
// Cloudflare Integration
'enable_cloudflare' => '0',
'cf_api_token' => '',
'cf_zone_id' => '',
'cf_sync_manual' => '0', // Sincronizar bloqueos manuales
'cf_sync_temporary' => '0', // Sincronizar bloqueos temporales (WAF, Login, etc)
// Site Scanner Automation
'enable_scheduled_scans' => '0',
'scan_frequency' => 'weekly',
'scan_notification_email' => '',
// Granular Scanner Checks
'scan_check_ssl' => '1',
'scan_check_updates' => '1',
'scan_check_php' => '1',
'scan_check_wp' => '1',
'scan_check_debug' => '1',
// Audit Log
'enable_audit_log' => '1',
// File Integrity Monitor (FIM)
'enable_fim' => '0',
];
}
/**
* Devuelve la lista por defecto de User-Agents maliciosos para el asistente.
* @return array
*/
public function get_default_user_agents() {
return [
'# === Vulnerability Scanners & Pentesting Tools ===',
'Acunetix', 'Arachni', 'Burp', 'Dirb', 'DirBuster', 'Feroxbuster', 'Go-http-client', 'Havij', 'Nessus', 'Nikto', 'Nmap', 'Netsparker', 'OpenVAS', 'Photon/1.0', 'sqlmap', 'Vega', 'Wfuzz', 'WhatWeb', 'WPScan', 'WPSec', 'ZAP/', 'masscan', 'ScanNG', 'PressVuln', 'PostmanRuntime', 'CensysInspect', 'Expanse', 'internet-measurement', 'JSScanner/',
'# === Generic Bots & Scripting Libraries ===',
'#curl', 'HTTrack', '#Java/', 'okhttp', 'perl', 'php/', 'Python', 'python-requests', 'Scrapy', 'wget', 'libwww', 'ruby',
'# === Aggressive Scrapers & Black Hat SEO Bots ===',
'#AhrefsBot', 'Bytespider', 'contabot', 'dataprovider', 'DigExt', '#DotBot', 'EmailCollector', 'ExtractorPro', 'MegaIndex', '#MJ12bot', 'SemrushBot', 'WebCollector', 'WebCopier', 'AliyunSecBot', 'AwarioBot', 'BW/', '#GoogleOther', 'IonCrawl', 'ISSCyberRiskCrawler',
'# === Spam, Low-Quality AI & Comment Bots ===',
'#Applebot-Extended', 'ClaudeBot', 'Diffbot', '#FacebookBot', 'FriendlyCrawler', '#Google-Extended', 'ImagesiftBot', 'Image2dataset', '#Meta-ExternalAgent', 'omgili', 'Timpibot', 'omgilibot', 'AcoonBot/', 'anthropic-ai', 'BoardReader', 'CCBot', '#ChatGPT-User', 'Claude-Web', 'DataForSeoBot', '#GPTBot', 'PerplexityBot', '#petalbot', '#YandexBot', 'ZmEu',
'# === Aggressive Regional Crawlers (optional) ===',
'Baiduspider', 'Baiduspider-image', 'Baiduspider-news', 'Barkrowler', 'msnbot-media', 'SeznamBot', 'Sogou', 'YisouSpider', 'BLEXBot', 'news-please', 'Orbbot', 'peer39_crawler', 'VelenPublicWebCrawler', '#wp_is_mobile', 'Zoominfobot',
'# === Suspicious or Malformed User-Agents ===',
'Dalvik/', 'morfeus', 'ShellBot', 'zgrab', 'Chrome/45', 'Mozilla/4.0', 'Empty', 'Mozlila', 'GRequests/'
];
}
/**
* Devuelve la lista por defecto de URLs Honeypot para el asistente.
* @return array
*/
public function get_default_honeypot_urls() {
return [
'/.env',
'/wp-config.php',
'/.git/',
'/phpinfo.php',
'/shell.php',
'/wso.php',
'/wordpress.zip',
'/phpunit',
'/eval-stdin.php'
];
}
/**
* Devuelve la lista por defecto de reglas WAF para el asistente.
* @return array
*/
public function get_default_waf_rules() {
return [
'# === SQL Injection (SQLi) ===',
'union\s+select',
'information_schema\.',
'# === Cross-Site Scripting (XSS) ===',
'<\s*script',
'on(error|load|click|mouseover)\s*=',
'javascript:', // Añadido: Protocolo peligroso en inputs
'# === Path Traversal & LFI ===',
'\.\.\/',
'/etc/passwd',
'php://input',
'# === Sensitive Files & Backups ===',
'/(wp-config\.php|\.env)',
'\.sql$',
'\.log$'
];
}
public static function activate_plugin() {
// 1. Obtener la instancia del plugin SIEMPRE al principio.
// Esto asegura que $instance siempre exista y que las propiedades (como $options) estén cargadas.
$instance = self::get_instance();
// 2. Lógica de la base de datos.
self::setup_database_tables();
if ( ! wp_next_scheduled( 'advaipbl_purge_old_logs_event' ) ) {
wp_schedule_event( time(), 'daily', 'advaipbl_purge_old_logs_event' );
}
if ( ! wp_next_scheduled( 'advaipbl_update_geoip_db_event' ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'advaipbl_update_geoip_db_event' );
}
if ( ! wp_next_scheduled( 'advaipbl_cleanup_expired_cache_event' ) ) {
wp_schedule_event( time(), 'daily', 'advaipbl_cleanup_expired_cache_event' );
}
// 3. Crear los ajustes por defecto SOLO si no existen.
if ( false === get_option( self::OPTION_SETTINGS ) ) {
$defaults = $instance->get_default_settings();
update_option( self::OPTION_SETTINGS, $defaults );
}
// 4. Programar los crons de notificación (usando la instancia).
$notification_frequency = $instance->options['notification_frequency'] ?? 'disabled';
if ( 'disabled' !== $notification_frequency && 'instant' !== $notification_frequency ) {
if ( ! wp_next_scheduled( 'advaipbl_send_summary_email' ) ) {
$instance->schedule_notification_cron();
}
}
// 5. Re-sincronizar transients (ahora $instance siempre existe).
$instance->resync_block_transients();
// 6. Añadir las demás opciones si no existen.
add_option( self::OPTION_BLOCKED_UAS, [], '', 'no' );
add_option( self::OPTION_WHITELISTED_UAS, [], '', 'no' );
add_option( self::OPTION_WHITELIST, [], '', 'no' );
add_option( self::OPTION_BLOCKED_MANUAL, [], '', 'no' );
add_option( self::OPTION_BLOCKED_HONEYPOT, [], '', 'no' );
add_option( self::OPTION_BLOCKED_USER_AGENT, [], '', 'no' );
add_option( self::OPTION_BLOCKED_GEO, [], '', 'no' );
add_option( ADVAIPBL_USM_OPTION_PER_PAGE, ADVAIPBL_USM_DEFAULT_PER_PAGE, '', 'no' );
add_option( self::OPTION_HONEYPOT_URLS, [], '', 'no' );
// --- Safe ASN List ---
$default_safe_asns = [
'AS15169', // Google LLC
'AS8075', // Microsoft Corporation
'AS2635', // Automattic Inc.
'AS32934', // Facebook, Inc.
'AS5091', // Stripe, Inc.
'AS394562', // Stripe, Inc.
'AS17012' // PayPal, Inc.
];
add_option( self::OPTION_WHITELISTED_ASNS, $default_safe_asns, '', 'no' );
// 7. Inicializar los triggers de activación.
add_option( self::OPTION_ADMIN_IP_TRIGGER, 'yes', '', 'no' );
}
/**
* Comprueba si estamos en una de las páginas de nuestro plugin y, si es así,
* elimina todos los demás avisos de administrador para una interfaz limpia.
*/
public function conditionally_remove_admin_notices() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$current_page_slug = isset($_GET['page']) ? sanitize_key($_GET['page']) : '';
$plugin_pages = [
'advaipbl_settings_page',
'advaipbl-setup-wizard'
];
// Comprobamos si el slug de la página actual es o empieza por uno de nuestros slugs.
foreach ($plugin_pages as $page_slug) {
if (strpos($current_page_slug, $page_slug) === 0) {
remove_all_actions('admin_notices');
remove_all_actions('all_admin_notices');
// Una vez que encontramos una coincidencia y limpiamos, no necesitamos seguir.
return;
}
}
}
public function admin_menu() {
$blocked_count = $this->get_blocked_count();
$bubble_html = $blocked_count > 0 ? ' ' . number_format_i18n($blocked_count) . '' : '';
// Slug base para la página principal. Es buena práctica tenerlo en una variable.
$main_page_slug = 'advaipbl_settings_page';
// 1. Menú Principal de Nivel Superior
add_menu_page(
__('Advanced IP Blocker', 'advanced-ip-blocker'),
__('Security', 'advanced-ip-blocker') . $bubble_html,
'manage_options',
$main_page_slug,
[$this, 'settings_page_content'],
'dashicons-shield',
'80.123'
);
// 2. Submenús
// El primer submenú (Dashboard) usa el mismo slug que el padre para ser la página por defecto.
// Al hacer clic en "Security", se cargará esta página.
add_submenu_page(
$main_page_slug,
__('Dashboard', 'advanced-ip-blocker'),
__('Dashboard', 'advanced-ip-blocker'),
'manage_options',
$main_page_slug, // Slug = padre. Correcto.
[$this, 'settings_page_content']
);
// Para los demás submenús, usamos slugs únicos y simples.
// WordPress generará automáticamente los enlaces correctos, por ejemplo:
// admin.php?page=advaipbl_settings_page-settings
add_submenu_page(
$main_page_slug,
__('Settings', 'advanced-ip-blocker'),
__('Settings', 'advanced-ip-blocker'),
'manage_options',
$main_page_slug . '-settings', // Slug único
[$this, 'settings_page_content']
);
add_submenu_page(
$main_page_slug,
__('Security Headers', 'advanced-ip-blocker'),
__('Security Headers', 'advanced-ip-blocker'),
'manage_options',
$main_page_slug . '-security-headers', // Slug único
[$this, 'settings_page_content']
);
add_submenu_page(
$main_page_slug,
__('Blocking Rules', 'advanced-ip-blocker'),
__('Blocking Rules', 'advanced-ip-blocker'),
'manage_options',
$main_page_slug . '-rules', // Slug único
[$this, 'settings_page_content']
);
add_submenu_page(
$main_page_slug,
__('IP Management', 'advanced-ip-blocker'),
__('IP Management', 'advanced-ip-blocker') . $bubble_html,
'manage_options',
$main_page_slug . '-ip-management', // Slug único
[$this, 'settings_page_content']
);
add_submenu_page(
$main_page_slug,
__('Site Scanner', 'advanced-ip-blocker'),
__('Site Scanner', 'advanced-ip-blocker'),
'manage_options',
$main_page_slug . '-scanner', // Slug único
[$this, 'settings_page_content']
);
add_submenu_page(
$main_page_slug,
__('Logs & Sessions', 'advanced-ip-blocker'),
__('Logs & Sessions', 'advanced-ip-blocker'),
'manage_options',
$main_page_slug . '-logs', // Slug único
[$this, 'settings_page_content']
);
add_submenu_page(
$main_page_slug,
__('About', 'advanced-ip-blocker'),
__('About', 'advanced-ip-blocker'),
'manage_options',
$main_page_slug . '-about', // Slug único
[$this, 'settings_page_content']
);
// Registrar la página oculta para el asistente de configuración.
// 1. Registramos la página COMO HIJA DEL MENÚ PRINCIPAL (No null)
// Esto hace que WordPress sepa su título.
add_submenu_page(
$main_page_slug, // Usamos la variable $main_page_slug que definiste arriba
__( 'Setup Wizard', 'advanced-ip-blocker' ),
__( 'Setup Wizard', 'advanced-ip-blocker' ),
'manage_options',
'advaipbl-setup-wizard',
[ $this->admin_pages, 'render_setup_wizard_page' ]
);
// 2. IMPORTANTE: NO usamos remove_submenu_page aquí. Lo ocultaremos con CSS.
// Esto asegura que la página siga existiendo en el sistema de menús de WP.
}
/**
* Removes all admin notices from other plugins and WordPress core
* ONLY on this plugin's settings page.
*/
public function remove_all_other_admin_notices() {
// Esta es la técnica que sugeriste, es perfecta.
remove_all_actions( 'admin_notices' );
remove_all_actions( 'all_admin_notices' );
}
public function display_admin_notice() {
// --- DECAY STRATEGY (v8.9.4+) ---
// Notice about AIB Network falling back to degraded tier
if (get_option('advaipbl_network_degraded')) {
echo '
';
printf(
wp_kses(
/* translators: %s: URL to the plugin settings page. */
__('Advanced IP Blocker: You are receiving a limited community threat feed (50,000 IPs). To increase your protection level to 100,000+ IPs, please go to the plugin settings and Register the AIB Network Integration.', 'advanced-ip-blocker'),
array('strong' => array(), 'a' => array('href' => array()))
),
esc_url(admin_url('admin.php?page=advaipbl_settings_page-settings&sub-tab=threat_intelligence#advaipbl-community-network-card'))
);
echo '
';
}
// Primero, llamamos a la función que decide si mostrar el aviso de telemetría.
$this->display_telemetry_notice();
// Después, mostramos cualquier otro aviso de éxito/error que esté en el transient.
$notice = get_transient('advaipbl_admin_notice');
if ( $notice ) {
$type = ( 'success' === $notice['type'] ) ? 'success' : 'error';
printf(
'
%2$s
',
esc_attr( $type ),
esc_html( $notice['message'] )
);
delete_transient('advaipbl_admin_notice');
}
}
/**
* Validates if a string is a valid IP, CIDR range, or hyphenated range.
*
* @param string $input The string to validate.
* @return bool
*/
public function is_valid_ip_or_range($input) {
$input = trim($input);
// Check for single IP
if (filter_var($input, FILTER_VALIDATE_IP)) {
return true;
}
// Check for CIDR
if (strpos($input, '/') !== false) {
list($subnet, $bits) = explode('/', $input, 2);
if (!filter_var($subnet, FILTER_VALIDATE_IP)) {
return false;
}
if (!is_numeric($bits) || $bits < 0) {
return false;
}
// Validate bits based on IP version
$max_bits = (filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) ? 128 : 32;
return $bits <= $max_bits;
}
// Check for hyphenated range
if (strpos($input, '-') !== false) {
list($start_ip, $end_ip) = explode('-', $input, 2);
return filter_var(trim($start_ip), FILTER_VALIDATE_IP) && filter_var(trim($end_ip), FILTER_VALIDATE_IP);
}
return false;
}
/**
* Checks if a given IP address is from a trusted proxy source.
* This is the core function for the Trusted Proxies feature. It checks against
* a user-defined list of IPs, CIDR ranges, and ASNs.
*
* @param string $ip_to_check The IP address to verify (usually REMOTE_ADDR).
* @return bool True if the IP is from a trusted source, false otherwise.
*/
private function is_source_trusted($ip_to_check) {
// Si no hay IP para comprobar, no puede ser de confianza.
if (empty($ip_to_check)) {
return false;
}
$raw_trusted_list = $this->options['trusted_proxies'] ?? '';
if (empty($raw_trusted_list)) {
return false;
}
$trusted_list = array_filter(array_map('trim', explode("\n", $raw_trusted_list)));
if (empty($trusted_list)) {
return false;
}
// Dividimos la lista en IPs/CIDRs y ASNs para un procesamiento eficiente.
$trusted_ips_cidrs = [];
$trusted_asns = [];
foreach ($trusted_list as $entry) {
if (strpos(strtoupper($entry), 'AS') === 0) {
$trusted_asns[] = strtoupper($entry);
} else {
$trusted_ips_cidrs[] = $entry;
}
}
// 1. Comprobación directa de IP/CIDR (la más rápida).
foreach ($trusted_ips_cidrs as $trusted_entry) {
if ($this->is_ip_in_range($ip_to_check, $trusted_entry)) {
return true;
}
}
// 2. Si no hubo coincidencia de IP y hay ASNs para comprobar, procedemos con la búsqueda de ASN.
if (!empty($trusted_asns)) {
// Aprovechamos nuestro gestor de geolocalización y su caché.
$location_data = $this->geolocation_manager->fetch_location($ip_to_check);
$source_asn = $this->asn_manager->extract_asn_from_data($location_data);
if ($source_asn && in_array(strtoupper($source_asn), $trusted_asns, true)) {
return true;
}
}
return false;
}
/**
* Finds the first valid, public IP address from a comma-separated string of IPs.
*
* @param string $ip_string The string from a proxy header (e.g., X-Forwarded-For).
* @return string|null The first valid public IP, or null if none are found.
*/
private function get_first_public_ip_from_string($ip_string) {
$ips = array_map('trim', explode(',', $ip_string));
foreach ($ips as $ip) {
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip;
}
}
return null;
}
//public function handle_admin_actions() {}
public function settings_page_content() {
$this->admin_pages->settings_page_content();
}
/**
* Imprime una cabecera de columna de tabla HTML que permite ordenar.
*/
public function print_sortable_header($label, $column_key, $orderby, $order) {
$next_order = ($orderby === $column_key && 'asc' === $order) ? 'desc' : 'asc';
$arrow_class = ($orderby === $column_key) ? 'sorted ' . $order : 'sortable desc';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$filter_type_sanitized = isset($_GET['filter_type']) ? sanitize_key(wp_unslash($_GET['filter_type'])) : 'all';
$url = add_query_arg([
'orderby' => $column_key,
'order' => $next_order,
'filter_type' => $filter_type_sanitized,
]);
echo '
';
}
/**
* Renders a dropdown to select the number of items per page.
*
* @param int $current_per_page The currently selected number of items per page.
*/
public function render_per_page_selector( $current_per_page ) {
$per_page_options = [ 20, 50, 100, 200 ];
?>
$column_key,
'order' => $next_order,
]);
$url = add_query_arg($url_params, admin_url('admin.php'));
echo '
';
// Borramos la cookie para que no se muestre de nuevo
unset( $_COOKIE['advaipbl_login_error'] );
setcookie( 'advaipbl_login_error', '', time() - 3600, COOKIEPATH, COOKIE_DOMAIN );
} else {
$message = '
' . esc_html__( 'Enter the code generated by your authenticator app.', 'advanced-ip-blocker' ) . '
tfa_manager ) {
return;
}
$user_id = get_current_user_id();
// Solo continuamos si la marca existe.
if ( get_user_meta( $user_id, '_advaipbl_2fa_setup_required', true ) ) {
$user = wp_get_current_user();
$global_2fa_enabled = ! empty( $this->options['enable_2fa'] ) && '1' === $this->options['enable_2fa'];
$user_is_forced = $this->tfa_manager->is_2fa_forced_for_user( $user );
$user_has_2fa_setup = $this->tfa_manager->is_2fa_enabled_for_user( $user_id );
// CONDICIONES PARA MOSTRAR EL AVISO:
// 1. 2FA debe estar activado globalmente.
// 2. El rol del usuario debe estar en la lista de forzados.
// 3. El usuario NO debe tener 2FA configurado todavía.
if ( $global_2fa_enabled && $user_is_forced && ! $user_has_2fa_setup ) {
$profile_url = get_edit_profile_url( $user_id );
?>
go to your profile to configure it now.', 'advanced-ip-blocker' ),
[ 'a' => [ 'href' => [] ] ]
),
esc_url( $profile_url )
);
?>
' . esc_html__( 'This is a confirmation that two-factor authentication has been successfully activated on your account. Your account is now more secure.', 'advanced-ip-blocker' ) . '
'
. '
' . esc_html__( 'If you did not perform this action, please contact the site administrator immediately.', 'advanced-ip-blocker' ) . '
' . esc_html__( 'This is a notification that two-factor authentication has been deactivated on your account. Your account is now less secure.', 'advanced-ip-blocker' ) . '
'
. '
' . esc_html__( 'If you did not perform this action, please reset your password and contact the site administrator immediately.', 'advanced-ip-blocker' ) . '
';
break;
case 'reset':
/* translators: $s: Site name. */
$subject = sprintf( __( '[%s] Your Two-Factor Authentication has been Reset', 'advanced-ip-blocker' ), $site_name );
$template_title = __( '2FA Reset by Administrator', 'advanced-ip-blocker' );
$content_html = '
' . esc_html__( 'A site administrator has reset the two-factor authentication configuration for your account. You can now log in using only your username and password.', 'advanced-ip-blocker' ) . '
'
. '
' . esc_html__( 'We strongly recommend you set up 2FA again from your profile page as soon as possible.', 'advanced-ip-blocker' ) . '
' . esc_html__( 'A recovery code was used to access your account.', 'advanced-ip-blocker' ) . '
'
. '
' . sprintf(/* translators: %d: Number of recovery codes. */ esc_html__( 'You have %d recovery codes remaining.', 'advanced-ip-blocker' ), $remaining_codes ) . '
'
. '
' . esc_html__( 'If you did not perform this action, please reset your password immediately.', 'advanced-ip-blocker' ) . '
';
break;
}
if ( ! empty( $content_html ) ) {
$body = $this->get_html_email_template( $template_title, $content_html );
add_filter( 'wp_mail_content_type', [$this, 'set_html_mail_content_type'] );
wp_mail( $to, $subject, $body );
remove_filter( 'wp_mail_content_type', [$this, 'set_html_mail_content_type'] );
}
}
/**
* Previene la enumeración de usuarios a través del endpoint oEmbed de la API REST.
* Elimina los datos del autor de la respuesta si la protección está activada.
*
* @param array $data The response data.
* @param WP_Post $post The post object.
* @param int $width The requested width.
* @param int $height The requested height.
* @return array The modified response data.
*/
public function prevent_user_enumeration_via_oembed( $data, $post, $width, $height ) {
// Solo actuar si la opción de protección está activada.
if ( ! empty( $this->options['disable_user_enumeration'] ) ) {
// Eliminamos las claves que podrían revelar información del autor.
unset( $data['author_name'] );
unset( $data['author_url'] );
}
return $data;
}
/**
* Previene la enumeración de usuarios a través de los feeds RSS.
* Si la protección de escaneo de autor está activada, reemplaza el login del autor
* con su nombre público (display name) en los feeds.
*
* @param string $author_login El login del autor.
* @return string El login del autor o su nombre público.
*/
public function prevent_user_enumeration_via_feeds( $author_login ) {
// Solo actuar si la opción está activada y estamos en un feed.
if ( ! empty( $this->options['prevent_author_scanning'] ) && is_feed() ) {
$author = get_user_by( 'login', $author_login );
if ( $author ) {
// Devolvemos el nombre público en lugar del login.
return $author->display_name;
}
}
return $author_login;
}
/**
* Previene el "login hinting" interceptando los errores de autenticación
* y reemplazándolos por un mensaje genérico.
*
* @param WP_User|WP_Error|null $user El objeto de usuario o error.
* @return WP_User|WP_Error
*/
public function prevent_login_hinting( $user ) {
// Solo actuar si la opción está activada.
if ( empty( $this->options['prevent_login_hinting'] ) || '1' !== $this->options['prevent_login_hinting'] ) {
return $user;
}
// Solo nos interesa modificar los errores, no los inicios de sesión correctos.
if ( ! is_wp_error( $user ) ) {
return $user;
}
// Lista de códigos de error que queremos ocultar.
$error_codes_to_hide = [
'invalid_username',
'invalid_email',
'incorrect_password',
];
// Comprobamos si el error actual es uno de los que dan pistas.
$has_hinting_error = false;
foreach ( $error_codes_to_hide as $code ) {
if ( $user->get_error_code() === $code ) {
$has_hinting_error = true;
break;
}
}
if ( $has_hinting_error ) {
return new WP_Error(
'advaipbl_generic_login_error',
__( 'ERROR: Your login details are incorrect. Please try again.', 'advanced-ip-blocker' )
);
}
// Si no es un error de los que queremos ocultar (ej. el de 2FA), lo dejamos pasar.
return $user;
}
/**
* Guarda los cambios de la sección 2FA en el perfil de usuario.
* Este hook es necesario para WordPress. Nuestra lógica de activación/desactivación
* se maneja principalmente vía AJAX para una mejor experiencia de usuario.
*
* @param int $user_id El ID del usuario que se está actualizando.
*/
public function save_2fa_section_in_profile( $user_id ) {
// No se requiere ninguna acción aquí, ya que el JS maneja las acciones de 2FA.
// La función debe existir para que el hook 'personal_options_update' no cause un error fatal.
if ( ! current_user_can( 'edit_user', $user_id ) ) {
return;
}
// No hay nada que hacer, pero la función está aquí para evitar el error.
}
/**
* Tarea programada para actualizar las bases de datos de GeoIP.
*/
public function execute_geoip_db_update() {
// Solo se ejecuta si el método es 'local_db' y hay una clave de licencia.
if (
isset($this->options['geolocation_method']) && $this->options['geolocation_method'] === 'local_db' &&
!empty($this->options['maxmind_license_key']) && $this->geoip_manager
) {
// Increase limits for large file download/extraction
// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
if (function_exists('set_time_limit')) { @set_time_limit(300); }
// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
@ini_set('memory_limit', '256M');
$this->log_event('Starting scheduled GeoIP database update.', 'info');
$result = $this->geoip_manager->download_and_unpack_databases();
if (!$result['success']) {
$this->log_event('Scheduled GeoIP database update failed: ' . $result['message'], 'error');
} else {
$this->log_event('Scheduled GeoIP database update completed successfully.', 'info');
}
}
}
/**
* Comprueba si una IP está actualmente bajo un bloqueo activo de cualquier tipo.
* Esta función es una comprobación de alto rendimiento que prioriza los transients.
*
* @param string $ip La dirección IP a comprobar.
* @return bool True si la IP está activamente bloqueada, false en caso contrario.
*/
public function is_ip_actively_blocked($ip) {
$definitions = $this->get_all_block_type_definitions();
// 1. Comprobación de transients para todos los tipos que los usan.
foreach ($definitions as $type => $def) {
if ($def['uses_transient'] && get_transient('advaipbl_bloqueo_' . $type . '_' . md5($ip))) {
return true;
}
}
// 2. Comprobación específica para bloqueos manuales (que son permanentes).
$manual_blocks = get_option(self::OPTION_BLOCKED_MANUAL, []);
if (!empty($manual_blocks)) {
foreach (array_keys($manual_blocks) as $entry) {
if ($this->is_ip_in_range($ip, $entry)) {
return true;
}
}
}
return false;
}
/**
* Comprueba si la IP del visitante actual está activamente bloqueada.
* Utiliza un caché estático por petición para un rendimiento máximo,
* ya que esta función puede ser llamada varias veces durante una misma ejecución.
*
* @return bool True si la IP está bloqueada, false en caso contrario.
*/
private function is_visitor_actively_blocked() {
static $is_blocked = null;
if ( null !== $is_blocked ) {
return $is_blocked;
}
$ip = $this->get_client_ip();
if ( get_transient( 'advaipbl_blocked_ip_' . md5( $ip ) ) ) {
$is_blocked = true;
return true;
}
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_blocked_ips';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
$is_blocked_in_db = $wpdb->get_var( $wpdb->prepare(
"SELECT id FROM {$table_name} WHERE ip_range = %s AND (expires_at = 0 OR expires_at > %d)",
$ip,
time()
) );
if ( $is_blocked_in_db ) {
$is_blocked = true;
return true;
}
$is_blocked = false;
return false;
}
/**
* Revisa y actualiza el estado de 'autoload' para las opciones del plugin que pueden
* crecer mucho en tamaño, mejorando el rendimiento general del sitio.
* Se ejecuta automáticamente una vez gracias a un sistema de versionado.
*/
public function update_option_autoload_states() {
if ( get_option('advaipbl_autoload_version') === self::AUTOLOAD_OPTIMIZATION_VERSION ) {
return;
}
global $wpdb;
$options_to_optimize = [
self::OPTION_WHITELIST,
self::OPTION_BLOCKED_MANUAL,
self::OPTION_BLOCKED_404,
self::OPTION_BLOCKED_403,
self::OPTION_BLOCKED_LOGIN,
self::OPTION_BLOCKED_GEO,
self::OPTION_BLOCKED_HONEYPOT,
self::OPTION_BLOCKED_USER_AGENT,
self::OPTION_BLOCKED_WAF,
self::OPTION_BLOCKED_THREAT_SCORE,
self::OPTION_BLOCKED_RATE_LIMIT,
self::OPTION_BLOCKED_ASN,
self::OPTION_BLOCKED_XMLRPC,
self::OPTION_HONEYPOT_URLS,
self::OPTION_BLOCKED_UAS,
self::OPTION_WHITELISTED_UAS,
self::OPTION_WAF_RULES,
self::OPTION_BLOCKED_ASNS,
self::OPTION_WHITELISTED_ASNS,
'advaipbl_spamhaus_asn_list',
self::OPTION_ADMIN_IP_TRIGGER,
'advaipbl_spamhaus_last_update',
];
foreach ($options_to_optimize as $option_name) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query( $wpdb->prepare(
"UPDATE {$wpdb->options} SET `autoload` = 'no' WHERE `option_name` = %s",
$option_name
) );
}
update_option('advaipbl_autoload_version', self::AUTOLOAD_OPTIMIZATION_VERSION);
$this->log_event('Successfully optimized database option autoloading for performance.', 'info');
}
/**
* Obtiene y sanea la URI de la petición actual.
* Cachea el resultado para evitar trabajo repetido en una misma petición.
*
* @return string La URI de la petición saneada.
*/
public function get_current_request_uri() {
static $request_uri = null;
if ( is_null($request_uri) ) {
$request_uri = esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'] ?? ''));
}
return $request_uri;
}
/**
* Checks if the current request URI is in the global exclusion list.
*
* @return bool True if the URI should be excluded, false otherwise.
*/
public function is_request_uri_excluded() {
$excluded_urls = $this->options['excluded_error_urls'] ?? '';
if (empty($excluded_urls)) {
return false;
}
$excluded_list = array_filter(array_map('trim', explode("\n", $excluded_urls)));
$current_url = $this->get_current_request_uri();
foreach ($excluded_list as $excluded_item) {
if (!empty($excluded_item) && stripos($current_url, $excluded_item) !== false) {
return true;
}
}
return false;
}
/**
* Obtiene y sanea el User Agent de la petición actual.
* @return string El User Agent de la petición, saneado.
*/
public function get_user_agent() {
static $user_agent = null;
if (is_null($user_agent)) {
$user_agent = sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'] ?? ''));
}
return $user_agent;
}
/**
* Obtiene y sanea el método de la petición actual.
* @return string
*/
public function get_request_method() {
static $request_method = null;
if (is_null($request_method)) {
$request_method_raw = isset($_SERVER['REQUEST_METHOD']) ? wp_unslash($_SERVER['REQUEST_METHOD']) : 'GET';
$request_method = sanitize_text_field($request_method_raw);
}
return $request_method;
}
/**
* Obtiene y sanea el Referer de la petición actual.
* @return string
*/
public function get_http_referer() {
static $http_referer = null;
if (is_null($http_referer)) {
$http_referer = sanitize_text_field(wp_unslash($_SERVER['HTTP_REFERER'] ?? ''));
}
return $http_referer;
}
/**
* Obtiene y sanea la IP de la conexión directa (REMOTE_ADDR).
* @return string
*/
public function get_remote_addr() {
static $remote_addr = null;
if (is_null($remote_addr)) {
$remote_addr_raw = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
$remote_addr = filter_var($remote_addr_raw, FILTER_VALIDATE_IP) ?: '';
}
return $remote_addr;
}
/**
* Incrementa el contador de un endpoint para el sistema de Lockdown.
* Si el contador supera el umbral, activa el modo Lockdown.
*
* @param string $endpoint_key La clave del endpoint (ej. 'xmlrpc').
*/
private function increment_lockdown_counter($endpoint_key) {
$threshold = (int) ($this->options[$endpoint_key . '_lockdown_threshold'] ?? 10);
$window_minutes = (int) ($this->options[$endpoint_key . '_lockdown_window'] ?? 15);
$duration_minutes = (int) ($this->options[$endpoint_key . '_lockdown_duration'] ?? 60);
if ($threshold <= 0 || $window_minutes <= 0) {
return;
}
$cache_key = 'advaipbl_lockdown_trigger_' . $endpoint_key;
// Obtenemos los datos actuales de la caché, que ahora serán un array.
$trigger_data = $this->get_from_custom_cache($cache_key);
if (!is_array($trigger_data)) {
$trigger_data = ['count' => 0, 'ips' => []];
}
$trigger_data['count']++;
$ip = $this->get_client_ip();
// Guardamos las últimas ~15 IPs para tener contexto.
$trigger_data['ips'][] = $ip;
if (count($trigger_data['ips']) > 15) {
$trigger_data['ips'] = array_slice($trigger_data['ips'], -15);
}
$trigger_data['ips'] = array_unique($trigger_data['ips']);
// Volvemos a guardar en la caché.
$ttl = $this->get_from_custom_cache($cache_key, true)['expires_at'] ?? (time() + $window_minutes * MINUTE_IN_SECONDS);
$this->set_in_custom_cache($cache_key, $trigger_data, $ttl - time());
if ($trigger_data['count'] >= $threshold) {
global $wpdb;
$lockdowns_table = $wpdb->prefix . 'advaipbl_endpoint_lockdowns';
$now = time();
$reason = sprintf(
/* translators: 1: The number of blocks. 2: The number of minutes in the detection window. */
__('Exceeded threshold: %1$d blocks in %2$d minutes.', 'advanced-ip-blocker'),
$trigger_data['count'],
$window_minutes
);
$details = wp_json_encode(['triggering_ip_hashes' => $trigger_data['ips']]);
// Insertamos el lockdown en nuestra nueva tabla.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
$wpdb->insert(
$lockdowns_table,
[
'endpoint_key' => $endpoint_key,
'reason' => $reason,
'created_at' => $now,
'expires_at' => $now + ($duration_minutes * MINUTE_IN_SECONDS),
'details' => $details,
]
);
// Limpiamos el contador de la caché, ya que el lockdown está activo.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->delete($wpdb->prefix . 'advaipbl_cache', ['cache_key' => $cache_key]);
$this->log_event(sprintf('Endpoint Lockdown activated for "%s" for %d minutes due to %d suspicious blocks.', $endpoint_key, $duration_minutes, $trigger_data['count']), 'critical');
$this->send_lockdown_notification($endpoint_key, $duration_minutes, $threshold);
}
}
/**
* Incrementa el contador de fallos de login para el sistema de Lockdown de wp-login.php.
* Activa el lockdown si se superan los umbrales de eventos y de IPs únicas.
*/
private function increment_login_lockdown_counter() {
// Obtenemos los umbrales desde los ajustes (los añadiremos más tarde).
// Por ahora, usamos valores por defecto razonables.
$event_threshold = (int) ($this->options['login_lockdown_event_threshold'] ?? 50);
$ip_threshold = (int) ($this->options['login_lockdown_ip_threshold'] ?? 10);
$window_minutes = (int) ($this->options['login_lockdown_window'] ?? 5);
$duration_minutes = (int) ($this->options['login_lockdown_duration'] ?? 60);
if ($event_threshold <= 0 || $ip_threshold <= 0 || $window_minutes <= 0) {
return;
}
$endpoint_key = 'login';
$cache_key = 'advaipbl_lockdown_trigger_' . $endpoint_key;
// Obtenemos los datos actuales de la caché.
$trigger_data = $this->get_from_custom_cache($cache_key);
if (!is_array($trigger_data) || !isset($trigger_data['event_count'])) {
$trigger_data = ['event_count' => 0, 'ip_hashes' => []];
}
$trigger_data['event_count']++;
$ip_hash = hash('sha256', $this->get_client_ip());
if (!in_array($ip_hash, $trigger_data['ip_hashes'])) {
$trigger_data['ip_hashes'][] = $ip_hash;
}
// Guardamos los datos actualizados en la caché.
$ttl_info = $this->get_from_custom_cache($cache_key, true);
$ttl = $ttl_info ? $ttl_info['expires_at'] - time() : $window_minutes * MINUTE_IN_SECONDS;
if ($ttl > 0) {
$this->set_in_custom_cache($cache_key, $trigger_data, $ttl);
}
// Comprobamos si se han superado ambos umbrales.
if ($trigger_data['event_count'] >= $event_threshold && count($trigger_data['ip_hashes']) >= $ip_threshold) {
global $wpdb;
$lockdowns_table = $wpdb->prefix . 'advaipbl_endpoint_lockdowns';
// Verificamos que no haya ya un lockdown activo para este endpoint para evitar duplicados.
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
$is_already_active = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$lockdowns_table} WHERE endpoint_key = %s AND expires_at > %d",
$endpoint_key, time()
));
if (!$is_already_active) {
$now = time();
$reason = sprintf(
/* translators: 1: The number of failed logins. 2: The number of unique IPs. 3: The number of minutes in the detection window. */
__('Exceeded threshold: %1$d failed logins from %2$d unique IPs in %3$d minutes.', 'advanced-ip-blocker'),
$trigger_data['event_count'],
count($trigger_data['ip_hashes']),
$window_minutes
);
$details = wp_json_encode(['triggering_ip_hashes' => $trigger_data['ip_hashes']]);
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
$wpdb->insert(
$lockdowns_table,
[
'endpoint_key' => $endpoint_key,
'reason' => $reason,
'created_at' => $now,
'expires_at' => $now + ($duration_minutes * MINUTE_IN_SECONDS),
'details' => $details,
]
);
$this->log_event(sprintf('Endpoint Lockdown activated for "%s" for %d minutes.', $endpoint_key, $duration_minutes), 'critical');
$this->send_lockdown_notification($endpoint_key, $duration_minutes, $event_threshold);
}
// Limpiamos el contador de la caché, ya que el lockdown está activo.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->delete($wpdb->prefix . 'advaipbl_cache', ['cache_key' => $cache_key]);
}
}
/**
* Comprueba si un endpoint crítico está bajo "Lockdown" y sirve un desafío si es necesario.
* Se ejecuta en un hook temprano para interceptar el tráfico antes que otras comprobaciones.
*/
public function check_for_endpoint_lockdown() {
if ($this->is_request_uri_excluded()) { return; }
if (empty($this->options['enable_xmlrpc_lockdown']) && empty($this->options['enable_login_lockdown'])) {
return;
}
// Si el usuario acaba de pasar un desafío, le damos un pase de gracia de 15s.
if (get_transient('advaipbl_grace_pass_' . md5($this->get_client_ip()))) {
return;
}
if ($this->request_is_asn_whitelisted) { return; }
// Procesa la respuesta del desafío si es para este tipo.
// phpcs:ignore WordPress.Security.NonceVerification.Missing
if (isset($_POST['_advaipbl_challenge_type']) && $_POST['_advaipbl_challenge_type'] === 'endpoint') {
// Un pase de 1 hora es suficiente para un endpoint crítico.
$this->js_challenge_manager->verify_challenge('advaipbl_js_verified', 1 * HOUR_IN_SECONDS);
}
// Si ya está verificado, no hacer nada.
if (isset($_COOKIE['advaipbl_js_verified']) && $_COOKIE['advaipbl_js_verified'] === '1') {
return;
}
$request_uri = $this->get_current_request_uri();
$endpoint_key = '';
$is_xmlrpc_request = strpos($request_uri, 'xmlrpc.php') !== false;
$is_login_request = strpos($request_uri, 'wp-login.php') !== false;
if ($is_xmlrpc_request && !empty($this->options['enable_xmlrpc_lockdown'])) {
$endpoint_key = 'xmlrpc';
} elseif ($is_login_request && !empty($this->options['enable_login_lockdown'])) {
$endpoint_key = 'login';
}
if (empty($endpoint_key)) {
return;
}
global $wpdb;
$lockdowns_table = $wpdb->prefix . 'advaipbl_endpoint_lockdowns';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
$is_lockdown_active = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$lockdowns_table} WHERE endpoint_key = %s AND expires_at > %d",
$endpoint_key,
time()
));
if ($is_lockdown_active) {
$ip = $this->get_client_ip();
if ($this->is_whitelisted($ip)) {
return;
}
$user_agent = $this->get_user_agent();
$is_trusted_service = false;
if ('xmlrpc' === $endpoint_key) {
$automattic_ua_patterns = [ '/^WordPress\/\d+\.\d+/i', '/jetpack by wordpress\.com/i', '/woocommerce/i' ];
foreach ($automattic_ua_patterns as $pattern) {
if (preg_match($pattern, $user_agent)) {
$is_trusted_service = true;
break;
}
}
}
if (!$is_trusted_service) {
$this->log_specific_error('endpoint_challenge', $ip, ['endpoint' => $endpoint_key, 'reason' => ucfirst($endpoint_key) . ' Lockdown Mode Active', 'uri' => $request_uri], 'warning');
// AIB Community Network Reporting
if ( ! empty( $this->options['enable_community_network'] ) ) {
$report_type = ($endpoint_key === 'xmlrpc') ? 'xmlrpc_block' : 'login_lockdown';
$this->reporter_manager->queue_report( $ip, $report_type, ['uri' => $request_uri] );
}
$this->js_challenge_manager->serve_challenge('endpoint');
}
}
}
/**
* Envía notificaciones (Email/Push) cuando el modo Lockdown se activa para un endpoint.
*
* @param string $endpoint_key La clave del endpoint (ej. 'xmlrpc').
* @param int $duration_minutes La duración del lockdown.
* @param int $threshold El umbral de bloqueos que lo activó.
*/
public function send_lockdown_notification($endpoint_key, $duration_minutes, $threshold) {
if ( isset($this->notification_manager) ) {
$this->notification_manager->send_lockdown_notification($endpoint_key, $duration_minutes, $threshold);
}
}
/**
* Comprueba la bandera de activación y redirige al asistente si es necesario.
* Se ejecuta en admin_init.
*/
public function maybe_redirect_to_wizard() {
if ( get_option( 'advaipbl_run_setup_wizard' ) ) {
// Evitar bucles infinitos en servidores con Object Caches muy lentos (ej. LiteSpeed)
// comprobando si YA estamos en la ruta de destino.
$is_already_on_wizard = ( isset( $_GET['page'] ) && $_GET['page'] === 'advaipbl-setup-wizard' );
delete_option( 'advaipbl_run_setup_wizard' );
if ( $is_already_on_wizard || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || isset( $_GET['activate-multi'] ) ) {
// Si es activación masiva, volvemos a poner para mostrar mensaje.
if ( isset( $_GET['activate-multi'] ) ) {
add_option( 'advaipbl_run_setup_wizard', true );
}
return;
}
wp_safe_redirect( admin_url( 'admin.php?page=advaipbl-setup-wizard' ) );
exit;
}
}
/**
* Muestra un aviso en el panel de administración si el asistente de configuración no se ha completado.
*/
public function display_setup_wizard_notice() {
// Solo mostrar el aviso si la bandera existe, el usuario puede gestionar opciones, y no estamos ya en el asistente.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( get_option( 'advaipbl_run_setup_wizard' ) && current_user_can( 'manage_options' ) && ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'advaipbl-setup-wizard' ) ) {
$wizard_url = admin_url( 'admin.php?page=advaipbl-setup-wizard' );
?>