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);
// Ejecutamos las validaciones de inmunidad (Bots y ASN) ANTES que cualquier otra cosa
add_action('init', [$this, 'is_visitor_asn_whitelisted'], -1000);
add_action('init', [$this, 'verify_known_bots'], -999);
add_action('init', [$this->rules_engine, 'evaluate'], -998); // Reglas avanzadas (ALLOW/BLOCK) evalúan antes de las validaciones de challenge
// Intercepción global y obligatoria para todos los JS Challenges
add_action('init', [$this->js_challenge_manager, 'verify_submission'], -997);
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_bot_impersonator_block'], -1);
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, '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('lostpassword_post', [$this, 'prevent_lostpassword_hinting']);
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('wp_ajax_advaipbl_inspect_ip', [$this->ajax_handler, 'ajax_inspect_ip']);
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_revoke_vip_passes_action', [$this, 'handle_revoke_vip_passes_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_export_advanced_rules', [$this->ajax_handler, 'ajax_export_advanced_rules']);
add_action('wp_ajax_advaipbl_import_advanced_rules', [$this->ajax_handler, 'ajax_import_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']) ) {
// Frontend & Backend Script Registration
add_action('login_enqueue_scripts', array($this, 'enqueue_recaptcha_script'));
add_action('wp_enqueue_scripts', array($this, 'enqueue_recaptcha_script'));
// Standard WordPress Forms
add_action('login_form', array($this, 'display_recaptcha_field'));
add_action('register_form', array($this, 'display_recaptcha_field'));
add_action('lostpassword_form', array($this, 'display_recaptcha_field'));
// Third-Party Custom Forms
add_action('woocommerce_login_form', array($this, 'display_recaptcha_field'));
add_action('woocommerce_register_form', array($this, 'display_recaptcha_field'));
add_action('woocommerce_lostpassword_form', array($this, 'display_recaptcha_field'));
add_action('um_after_login_fields', array($this, 'display_recaptcha_field'));
add_action('bp_sidebar_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;
}
}
}
// Ya no mantenemos firmas hardcoded debido a la fragilidad y rotación continua (Crawler Spoofing y cambios de versión).
// Los bots buenos legítimos (Google, Bing, etc.) ya han sido excluidos más arriba mediante ASN y rDNS.
// Aquí solo aplicamos las firmas "custom" añadidas bajo la responsabilidad del usuario.
$trusted_signature_hashes = apply_filters('advaipbl_trusted_signature_hashes', $user_whitelisted_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')) {
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
do_action('litespeed_purge_all');
} elseif (defined('LSCWP_V')) {
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
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')) {
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
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 por IP
if ( $this->is_visitor_actively_blocked() ) {
return; // Bloqueado por IP manual/activa
}
// Verificar si el usuario quiere bloquearlo explícitamente por ASN
if (!empty($this->options['enable_spamhaus_asn']) || !empty($this->options['enable_manual_asn'])) {
$provider = $this->options['geolocation_provider'] ?? '';
if (in_array($provider, ['ip-api.com', 'ipinfo.io'], true)) {
if ($this->asn_manager->check_asn_block($ip)) {
return; // Bloqueado por ASN manual/Spamhaus
}
}
}
// Verificar si el usuario quiere bloquearlo explícitamente por User-Agent
$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 --
// Guardamos el estado para bloquearlo en el hook -1 (después de evaluar Inmunidad Global)
$this->is_bot_impersonator = true;
return;
}
}
public function check_for_bot_impersonator_block() {
if (empty($this->is_bot_impersonator)) { return; }
if (!empty($this->request_is_asn_whitelisted) || $this->is_whitelisted($this->get_client_ip()) || !empty($this->is_advanced_rule_allowed)) { return; }
$ip = $this->get_client_ip();
$user_agent = $this->get_user_agent();
$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',
[
'impersonated_user_agent' => $user_agent,
'uri' => $this->get_current_request_uri()
]
);
// 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 (!empty($this->request_is_asn_whitelisted) || $this->is_whitelisted($this->get_client_ip()) || !empty($this->is_advanced_rule_allowed)) { 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 global o 4 horas.
$global_duration_hours = (int)($this->options['global_challenge_cookie_duration'] ?? 4);
$duration_seconds = ($global_duration_hours > 0) ? $global_duration_hours * HOUR_IN_SECONDS : 0;
$this->js_challenge_manager->verify_challenge('advaipbl_js_verified', $duration_seconds);
}
if ($this->js_challenge_manager->is_vip_pass_valid()) {
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) {
$mode = $this->options['signature_challenge_mode'] ?? 'managed';
// 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(),
'mode' => $mode
],
'warning'
);
// 2. Luego, servimos el desafío.
$this->js_challenge_manager->serve_challenge('signature', $mode);
}
}
/**
* 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['global_challenge_cookie_duration'] ?? 4);
$duration_seconds = ($duration_hours > 0) ? $duration_hours * HOUR_IN_SECONDS : 0;
$this->js_challenge_manager->verify_challenge('advaipbl_js_verified', $duration_seconds);
}
if ($this->js_challenge_manager->is_vip_pass_valid()) {
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) || !empty($this->is_advanced_rule_allowed)) {
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)) {
$mode = $this->options['geo_challenge_mode'] ?? 'managed';
$this->log_specific_error(
'geo_challenge',
$ip,
[
'country' => $location['country'] ?? $location['country_code'],
'action' => 'Geolocation Challenge',
'trigger' => 'Country Match',
'mode' => $mode
],
'warning'
);
$this->js_challenge_manager->serve_challenge('geo_challenge', $mode);
}
}
}
/**
* 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();
$last_cron_ip = get_option('advaipbl_last_cron_ip');
if ( $last_cron_ip !== $ip ) {
update_option('advaipbl_last_cron_ip', $ip, false);
}
$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() {
// No encolar si el usuario ya está conectado
if ( function_exists('is_user_logged_in') && is_user_logged_in() ) {
return;
}
$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, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
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';
// Hidden marker to indicate our hook fired and rendered the field.
// This allows intelligent bypassing of non-standard frontend forms.
echo '';
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
$is_wp_login = ( isset( $GLOBALS['pagenow'] ) && $GLOBALS['pagenow'] === 'wp-login.php' ) ||
( isset( $_SERVER['PHP_SELF'] ) && strpos( sanitize_text_field( wp_unslash( $_SERVER['PHP_SELF'] ) ), 'wp-login.php' ) !== false );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$is_recaptcha_rendered = isset( $_POST['advaipbl_recaptcha_rendered'] );
// If we are not on the native login page, and our rendering hooks didn't fire for this custom form,
// we bypass validation to prevent locking out legitimate users using unhookable custom themes.
if ( ! $is_wp_login && ! $is_recaptcha_rendered ) {
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;
}
public function handle_revoke_vip_passes_action() {
if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'advaipbl_revoke_vip_passes_nonce')) {
wp_die('Invalid nonce.');
}
if (!current_user_can('manage_options')) {
wp_die('Permission denied.');
}
// Generate new salt modifier
update_option('advaipbl_vip_salt_modifier', wp_generate_password(24, true, true));
/* translators: %s: Admin username. */
$this->log_event( sprintf( __( 'All JS Challenge VIP passes were manually revoked globally by %s.', 'advanced-ip-blocker' ), $this->get_current_admin_username() ), 'warning' );
set_transient('advaipbl_admin_notice', ['message' => __('All VIP passes have been successfully revoked. Everyone will be required to pass the JS Challenge again.', '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' );
}
$options = get_option( self::OPTION_SETTINGS, [] );
if ( ! empty( $options['maxmind_license_key'] ) ) {
if ( ! wp_next_scheduled( 'advaipbl_update_geoip_db_event' ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'advaipbl_3_days', 'advaipbl_update_geoip_db_event' );
}
} else {
wp_clear_scheduled_hook( '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() {
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 '