options = get_option(self::OPTION_SETTINGS, []);
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-asn-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-fingerprint-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-geoip-manager.php';
$this->geolocation_manager = new ADVAIPBL_Geolocation_Manager();
if (version_compare(PHP_VERSION, '8.1', '>=')) {
$this->geoip_manager = new ADVAIPBL_GeoIP_Manager($this);
} else {
$this->geoip_manager = new stdClass();
}
$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->fingerprint_manager = new ADVAIPBL_Fingerprint_Manager($this);
// Carga condicional del 2FA Manager.
if ( version_compare(PHP_VERSION, '8.1', '>=') ) {
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-2fa-manager.php';
$this->tfa_manager = new ADVAIPBL_2FA_Manager($this);
} else {
// En un entorno antiguo (web o CLI), creamos un objeto vacío.
$this->tfa_manager = new stdClass();
}
add_action('plugins_loaded', [$this, 'check_database_update']);
$this->add_hooks();
if ( defined( 'WP_CLI' ) && WP_CLI ) {
require_once plugin_dir_path( __FILE__ ) . 'class-advaipbl-cli.php';
WP_CLI::add_command( 'advaipbl', 'ADVAIPBL_CLI' );
}
if (is_admin()) {
add_action('admin_notices', [$this, 'display_admin_notice']);
}
}
/**
* Inicializa los managers que solo son necesarios en contextos de backend (admin, ajax, login).
* Se engancha a un hook temprano del admin para asegurar que las instancias existan
* antes de que se registren o ejecuten otros hooks dependientes.
*/
public function initialize_backend_managers() {
static $initialized = false;
if ($initialized) {
return;
}
// Cargar los archivos de clase necesarios para el backend.
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-dashboard-manager.php';
require_once plugin_dir_path(__FILE__) . 'class-advaipbl-2fa-manager.php';
// Instanciar los managers de backend.
$this->dashboard_manager = new ADVAIPBL_Dashboard_Manager($this, $this->session_manager);
if (!(defined('WP_CLI') && WP_CLI && version_compare(PHP_VERSION, '8.1', '<'))) {
$this->tfa_manager = new ADVAIPBL_2FA_Manager($this);
} else {
$this->tfa_manager = new stdClass();
}
$initialized = true;
}
private function add_hooks() {
// --- HOOKS DE INICIALIZACIÓN Y BLOQUEO (SE EJECUTAN SIEMPRE) ---
add_action('init', [$this, 'is_visitor_asn_whitelisted'], -100);
add_action('init', [$this, 'block_xmlrpc_requests_if_disabled'], -5);
add_action('init', [$this, 'log_request_signature'], -2);
add_action('init', [$this, 'check_for_endpoint_lockdown'], -1);
add_action('init', [$this, 'check_for_malicious_signature'], -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_action('status_header', [$this, 'detect_http_error_status'], 10, 2);
// --- Inicialización Condicional de Managers de Backend ---
if (is_admin() || (isset($_GET['action']) && strpos($_GET['action'], 'advaipbl_') === 0)) {
// 'admin_init' es el hook perfecto. Se ejecuta en todas las páginas de admin,
// en las peticiones de admin-ajax.php, y en admin-post.php.
add_action('admin_init', [$this, 'initialize_backend_managers'], 0);
}
global $pagenow;
if ($pagenow === 'wp-login.php') {
$this->initialize_backend_managers();
}
// --- REGISTRO DE TODOS LOS DEMÁS HOOKS ---
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('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 );
}
// Hooks de Cron
add_action('advaipbl_purge_old_logs_event', [$this, 'purge_old_logs']);
add_action('advaipbl_send_summary_email', [$this, 'process_and_send_summary']);
add_action('advaipbl_send_telemetry_data_event', [$this, 'send_telemetry_data']);
add_action('advaipbl_threat_score_decay_event', [$this, 'execute_threat_score_decay']);
add_action('advaipbl_signature_analysis_event', [$this, 'execute_signature_analysis']);
add_action('advaipbl_update_geoip_db_event', [$this, 'execute_geoip_db_update']);
add_filter('cron_schedules', [$this, 'add_cron_intervals']);
add_action('advaipbl_clear_expired_blocks_event', [$this, 'limpiar_ips_expiradas']);
add_action('advaipbl_cleanup_expired_cache_event', [$this, 'cleanup_expired_cache_entries']);
// Hooks del Backend
if (is_admin()) {
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, 'register_settings']);
add_action('admin_init', [$this, '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']);
add_action('admin_enqueue_scripts', [$this, 'load_admin_scripts']);
add_action('admin_footer', [$this, 'print_modal_html_in_footer']);
add_action('wp_ajax_advaipbl_test_outbound_connection', [$this, 'ajax_test_outbound_connection']);
add_action('wp_ajax_advaipbl_add_ip_to_whitelist', [$this, 'ajax_add_ip_to_whitelist']);
add_action('wp_ajax_advaipbl_verify_api_key', [$this, 'ajax_verify_api_key']);
add_action('wp_ajax_advaipbl_update_geoip_db', [$this, 'ajax_update_geoip_db']);
add_action('wp_ajax_advaipbl_get_dashboard_stats', [$this, '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_handle_telemetry_notice']);
add_action('wp_ajax_advaipbl_reset_threat_score', [$this, 'ajax_reset_threat_score']);
add_action('wp_ajax_advaipbl_get_score_history', [$this, 'ajax_get_score_history']);
add_action('wp_ajax_advaipbl_delete_signature', [$this, 'ajax_delete_signature']);
add_action('wp_ajax_advaipbl_get_signature_details', [$this, 'ajax_get_signature_details']);
add_action('wp_ajax_advaipbl_whitelist_signature', [$this, 'ajax_whitelist_signature']);
add_action('wp_ajax_advaipbl_get_lockdown_details', [$this, '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('advaipbl_update_spamhaus_list_event', [$this, 'update_spamhaus_list']);
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);
}
}
/**
* Obtiene un valor de nuestra tabla de caché personalizada.
*
* @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 ) {
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_cache';
$result = $wpdb->get_row( $wpdb->prepare(
"SELECT cache_value, expires_at FROM {$table_name} WHERE cache_key = %s",
$key
) );
if ( ! $result ) {
return false;
}
if ( time() > $result->expires_at ) {
$wpdb->delete( $table_name, ['cache_key' => $key] );
return false;
}
if ($get_full_object) {
return [
'value' => maybe_unserialize( $result->cache_value ),
'expires_at' => $result->expires_at,
];
}
return maybe_unserialize( $result->cache_value );
}
/**
* Guarda un valor en nuestra 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 ) {
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_cache';
$serialized_value = maybe_serialize( $value );
$expires_at = time() + $expiration;
$wpdb->query( $wpdb->prepare(
"INSERT INTO {$table_name} (cache_key, cache_value, expires_at)
VALUES (%s, %s, %d)
ON DUPLICATE KEY UPDATE cache_value = %s, expires_at = %d",
$key,
$serialized_value,
$expires_at,
$serialized_value,
$expires_at
) );
}
/**
* 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() {
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_cache';
$current_time = time();
$deleted_rows = $wpdb->query( $wpdb->prepare(
"DELETE FROM {$table_name} WHERE expires_at < %d",
$current_time
) );
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_2fa_generate']);
add_action('wp_ajax_advaipbl_2fa_activate', [$this, 'ajax_2fa_activate']);
add_action('wp_ajax_advaipbl_2fa_deactivate', [$this, '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']);
}
public function ajax_get_dashboard_stats() {
// 1. Verificamos el nonce. El primer argumento debe coincidir con la acción del nonce que creamos.
check_ajax_referer('wp_ajax_advaipbl_get_dashboard_stats', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Permission denied.']);
}
$stats = $this->dashboard_manager->get_dashboard_stats();
if ($stats) {
wp_send_json_success($stats);
} else {
wp_send_json_error(['message' => 'Could not retrieve stats.']);
}
}
/**
* AJAX callback para resetear la puntuación de amenaza de una IP.
*/
public function ajax_reset_threat_score() {
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => __('Permission denied.', 'advanced-ip-blocker')]);
}
check_ajax_referer('advaipbl_reset_score_nonce', 'nonce');
$ip = isset($_POST['ip']) ? sanitize_text_field(wp_unslash($_POST['ip'])) : '';
if (!$ip) {
wp_send_json_error(['message' => __('Invalid IP address.', 'advanced-ip-blocker')]);
}
// 1. Primero, reseteamos la puntuación.
$success = $this->threat_score_manager->reset_score($ip);
// 2. Después, nos aseguramos de que la IP sea completamente desbloqueada de TODAS las listas.
$this->desbloquear_ip($ip);
if ($success) {
/* translators: %1$s: IP, %2$s: Username. */
$this->log_event(sprintf(__('Threat score for IP %1$s was manually reset by %2$s.', 'advanced-ip-blocker'), $ip, $this->get_current_admin_username()), 'info');
wp_send_json_success(['message' => __('Score reset and IP unblocked successfully.', 'advanced-ip-blocker')]);
} else {
wp_send_json_error(['message' => __('Failed to reset score.', 'advanced-ip-blocker')]);
}
}
/**
* 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; }
if (wp_doing_cron() || is_admin() || (defined('WP_CLI') && WP_CLI)) { return; }
if ($this->challenge_passed_this_request) { 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 desde los 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',
'fa5da9a69d6ef080920638b100bf53c535e7343125249045130f3be22f0e7f37',
'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',
// --- Otros Crawlers Legítimos (identificados genéricamente) ---
'4357e810006cbcca85538dcbf2dfbedbb402d81783942c19ef2fd2bcf36cac30',
'022d8938605fb7ace7bf0b8065024ac4b7c1a1cd0a30b102b52e1294b7d72add',
'11e72d8146bdcbf3a53517d240284f9a1d1293573f2445f01bbbf334a211fa11',
'4f8ca18d80d3d9011bd62d4e3ecd2525d2df4ac020b02c2dc9f830b138b0446d',
'7a292aeecb714d9b6b488308dbe4e7045b4648dfee6d8d8fdbd0035ee40a9f58',
'96cf4186d7adc3da7338031aad4265e9c4fe60d83c02d4d0dcbfa570ad557ecf',
'9ce22068e218c8bee0c7c91dbd4827682f60c53dc8d20a85e2f60a2b307ad239',
'a02e355c79010c8c3ac7a05a209f39b68216112b729774cfe8e7f929232832a5',
'c08feb2c6c0d36215e6e9c30fa1e6b4705473631f6549dbec3677040606f28aa',
'c5364e70a5108b71ef91dbdc2914864534116b92cf368c41cd86ebead67d0af1',
'd05bf96e94cac639887a3492b728a953da8c26d06ab980bb0ab4194d2d945ff7',
], $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; // Firma de confianza, no se registra.
}
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_request_log';
$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,
];
$wpdb->insert($table_name, $data_to_log);
}
/**
* AJAX callback para añadir una firma a la lista blanca y eliminarla de la lista de bloqueo.
*/
public function ajax_whitelist_signature() {
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => __('Permission denied.', 'advanced-ip-blocker')]);
}
check_ajax_referer('advaipbl_whitelist_signature_nonce', 'nonce');
$hash = isset($_POST['hash']) ? sanitize_text_field(wp_unslash($_POST['hash'])) : '';
if (strlen($hash) !== 64) {
wp_send_json_error(['message' => __('Invalid signature hash.', 'advanced-ip-blocker')]);
}
// 1. Obtenemos los detalles de la firma para construir los comentarios.
$details = $this->fingerprint_manager->get_signature_details($hash);
if ($details === false) {
wp_send_json_error(['message' => __('Could not retrieve signature details to create whitelist entry.', 'advanced-ip-blocker')]);
}
// 2. Construimos la entrada para la lista blanca con comentarios.
$entry_lines = ["\n# Signature Components:"];
$entry_lines[] = "# User-Agent: " . ($details['sample_user_agent'] ?? 'N/A');
if (!empty($details['sample_headers'])) {
foreach ($details['sample_headers'] as $key => $value) {
$entry_lines[] = "# " . $key . ": " . $value;
}
}
$entry_lines[] = $hash;
$entry_to_add = implode("\n", $entry_lines);
// 3. Obtenemos el array COMPLETO de settings, modificamos la clave y guardamos.
$options = get_option(self::OPTION_SETTINGS, []);
$current_whitelist = $options['trusted_signature_hashes'] ?? '';
$new_whitelist = $current_whitelist . "\n" . $entry_to_add;
// Actualizamos la clave dentro del array de opciones.
$options['trusted_signature_hashes'] = trim($new_whitelist);
// Guardamos el array de opciones completo.
update_option(self::OPTION_SETTINGS, $options);
// 4. Eliminamos la firma de la lista de maliciosos.
$this->fingerprint_manager->delete_signature($hash);
/* translators: %s: hash, %s: Username. */
$this->log_event(sprintf(__('Signature %1$s... whitelisted by %2$s.', 'advanced-ip-blocker'), substr($hash, 0, 12), $this->get_current_admin_username()), 'info');
wp_send_json_success(['message' => __('Signature whitelisted successfully.', 'advanced-ip-blocker')]);
}
/**
* 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->request_is_asn_whitelisted) { return; }
if (empty($this->options['enable_signature_blocking'])) {
return;
}
if (isset($_POST['_advaipbl_js_token'])) {
$this->verify_js_challenge();
}
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';
$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->serve_js_challenge();
}
}
/**
* Verifica la respuesta del desafío JavaScript.
* Si es correcta, establece una cookie y redirige.
*/
private function verify_js_challenge() {
$token = sanitize_key($_POST['_advaipbl_js_token'] ?? '');
$response = (int) ($_POST['_advaipbl_js_response'] ?? 0);
$transient_key = 'advaipbl_challenge_' . $token;
$correct_answer = get_transient($transient_key);
delete_transient($transient_key);
if ($correct_answer !== false && $response === (int) $correct_answer) {
// ¡Correcto! Establecemos la cookie y redirigimos.
$expiration = time() + (4 * HOUR_IN_SECONDS);
setcookie('advaipbl_js_verified', '1', $expiration, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true);
wp_redirect(esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])));
exit;
}
// Si la verificación falla, lo registramos y bloqueamos.
$this->log_event('JS challenge verification failed.', 'warning', ['ip' => $this->get_client_ip()]);
wp_die('Verification failed. Please enable JavaScript and cookies.', 'Forbidden', ['response' => 403]);
}
/**
* Muestra la página del desafío JavaScript y detiene la ejecución de WordPress.
*
*/
private function serve_js_challenge() {
if (!defined('DONOTCACHEPAGE')) define('DONOTCACHEPAGE', true);
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: -1');
$num1 = wp_rand(1, 9);
$num2 = wp_rand(1, 9);
$answer = $num1 + $num2;
$token = wp_generate_password(32, false);
set_transient('advaipbl_challenge_' . $token, $answer, 120);
header('HTTP/1.1 503 Service Temporarily Unavailable');
header('Content-Type: text/html; charset=utf-8');
header('Retry-After: 10');
$html = '
Verifying your connection...
Verifying you are human...
Your browser is being verified. This is a standard security check to protect the website from automated attacks. This page will reload automatically.
';
// Todas las variables dinámicas ($token, $num1, $num2) ya han sido saneadas o escapadas de forma segura durante su construcción.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $html;
exit;
}
/**
* AJAX callback para eliminar una firma maliciosa.
*/
public function ajax_delete_signature() {
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => __('Permission denied.', 'advanced-ip-blocker')]);
}
check_ajax_referer('advaipbl_delete_signature_nonce', 'nonce');
$signature_hash = isset($_POST['hash']) ? sanitize_text_field(wp_unslash($_POST['hash'])) : '';
// Validamos que el hash tenga la longitud correcta de un sha256.
if (strlen($signature_hash) !== 64) {
wp_send_json_error(['message' => __('Invalid signature hash format.', 'advanced-ip-blocker')]);
}
$success = $this->fingerprint_manager->delete_signature($signature_hash);
if ($success) {
/* translators: %1$s: Hash, %2$s: Username */
$this->log_event(sprintf(__('Malicious signature %1$s... was manually deleted by %2$s.', 'advanced-ip-blocker'), substr($signature_hash, 0, 12), $this->get_current_admin_username()), 'warning');
wp_send_json_success(['message' => __('Signature deleted successfully.', 'advanced-ip-blocker')]);
} else {
wp_send_json_error(['message' => __('Failed to delete signature. It might have already expired or been removed.', 'advanced-ip-blocker')]);
}
}
/**
* AJAX callback para obtener los detalles de una firma maliciosa.
*/
public function ajax_get_signature_details() {
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => __('Permission denied.', 'advanced-ip-blocker')]);
}
check_ajax_referer('advaipbl_get_signature_details_nonce', 'nonce');
$signature_hash = isset($_POST['hash']) ? sanitize_text_field(wp_unslash($_POST['hash'])) : '';
if (strlen($signature_hash) !== 64) {
wp_send_json_error(['message' => __('Invalid signature hash format.', 'advanced-ip-blocker')]);
}
$details = $this->fingerprint_manager->get_signature_details($signature_hash);
if ($details !== false) {
wp_send_json_success(['details' => $details]);
} else {
wp_send_json_error(['message' => __('Could not retrieve signature details.', 'advanced-ip-blocker')]);
}
}
/**
* AJAX callback para obtener los detalles de un Endpoint Lockdown.
*/
public function ajax_get_lockdown_details() {
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => __('Permission denied.', 'advanced-ip-blocker')]);
}
check_ajax_referer('advaipbl_get_lockdown_details_nonce', 'nonce');
$lockdown_id = isset($_POST['id']) ? absint($_POST['id']) : 0;
if (!$lockdown_id) {
wp_send_json_error(['message' => __('Invalid Lockdown ID.', 'advanced-ip-blocker')]);
}
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_endpoint_lockdowns';
$lockdown = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table_name} WHERE id = %d", $lockdown_id), ARRAY_A);
if ($lockdown) {
wp_send_json_success(['details' => $lockdown]);
} else {
wp_send_json_error(['message' => __('Could not retrieve lockdown details.', 'advanced-ip-blocker')]);
}
}
/**
* 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.
*/
public function send_signature_flagged_notification($signature_hash, $reason, $user_agent = 'N/A') {
$short_hash = substr($signature_hash, 0, 12) . '...';
$site_name = get_bloginfo('name');
// --- Notificación Push (Webhook) ---
if (!empty($this->options['enable_push_notifications'])) {
$message_lines = [
sprintf('*:dna: [%s] New Attack Signature Identified*', $site_name),
'> A new attack pattern has been automatically identified and is now being challenged.',
sprintf('> • *Signature Hash:* `%s`', $short_hash),
sprintf('> • *Reason:* %s', $reason),
sprintf('> • *Sample User-Agent:* `%s`', $user_agent),
];
$this->execute_webhook_send(implode("\n", $message_lines));
}
// --- Notificación por Email ---
if (!empty($this->options['enable_email_notifications'])) {
$to = !empty($this->options['notification_email']) ? $this->options['notification_email'] : get_option('admin_email');
/* translators: %s: Site name. */
$email_subject = sprintf(__('[%s] Security Alert: New Attack Signature Identified', 'advanced-ip-blocker'), $site_name);
$template_title = __('New Attack Signature Identified', 'advanced-ip-blocker');
$button_url = admin_url('admin.php?page=advaipbl_settings_page&tab=ip_management&sub-tab=blocked_signatures');
$content_html =
'
' . esc_html__("The plugin's analysis engine has automatically identified a new distributed attack pattern. Requests matching this signature will now be challenged.", 'advanced-ip-blocker') . '
';
$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, $email_subject, $body);
remove_filter('wp_mail_content_type', [$this, 'set_html_mail_content_type']);
}
}
/**
* AJAX callback para obtener el historial de eventos de una IP.
*/
public function ajax_get_score_history() {
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => __('Permission denied.', 'advanced-ip-blocker')]);
}
check_ajax_referer('advaipbl_get_history_nonce', 'nonce');
$ip = isset($_POST['ip']) ? sanitize_text_field(wp_unslash($_POST['ip'])) : '';
if (!$ip) {
wp_send_json_error(['message' => __('Invalid IP address.', 'advanced-ip-blocker')]);
}
$history = $this->threat_score_manager->get_log_details($ip);
if ($history !== false) {
wp_send_json_success(['history' => $history]);
} else {
wp_send_json_error(['message' => __('Could not retrieve history.', 'advanced-ip-blocker')]);
}
}
/**
* 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() {
register_rest_route('advaipbl/v1', '/live-attacks', [
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_live_attacks_for_feed'],
'permission_callback' => function( $request ) {
// El nonce puede venir como un header o como un parámetro GET/POST
$nonce = $request->get_header('x-wp-nonce') ?? $request->get_param('nonce');
return wp_verify_nonce( $nonce, 'wp_rest' );
},
'args' => [
'since' => [
'description' => 'Get attacks since this log ID.',
'type' => 'integer',
'sanitize_callback' => 'absint',
],
],
]);
}
/**
* 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) {
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_logs';
$since_id = $request->get_param('since');
// Construimos la cadena de la consulta base. Usamos %i para el nombre de la tabla.
$sql = "SELECT log_id, timestamp, ip, log_type, message, details
FROM {$table_name}
WHERE level = 'critical' AND log_type != 'general'";
// Creamos un array para los parámetros que se van a preparar.
$params = [];
if ($since_id) {
$sql .= " AND log_id > %d";
$params[] = $since_id;
}
$sql .= " ORDER BY log_id DESC LIMIT 20";
// Preparamos y ejecutamos la consulta en una sola línea.
$results = $wpdb->get_results(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->prepare($sql, $params),
ARRAY_A
);
$attacks = [];
$last_id = 0;
if (!empty($results)) {
$last_id = (int) $results[0]['log_id'];
foreach ($results as $row) {
$details = json_decode($row['details'], true) ?: [];
$location = 'Unknown Location';
if (!empty($details['city']) && !empty($details['country'])) {
$location = $details['city'] . ', ' . $details['country'];
} elseif (!empty($details['country'])) {
$location = $details['country'];
}
$attacks[] = [
'id' => (int) $row['log_id'],
'time' => human_time_diff(strtotime($row['timestamp'])) . ' ago',
'ip' => esc_html($row['ip']),
'type' => esc_html(ucwords(str_replace('_', ' ', $row['log_type']))),
'location' => esc_html($location),
'method' => esc_html($details['method'] ?? 'N/A'),
'details' => esc_html($row['message']),
'user_agent' => esc_html($details['user_agent'] ?? 'N/A'),
'uri' => esc_html($details['uri'] ?? ($details['url'] ?? '')),
];
}
}
$response_data = [
'attacks' => $attacks,
'last_id' => $last_id,
];
return new WP_REST_Response($response_data, 200);
}
/**
* 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() {
return new WP_REST_Response( [
'nonce' => wp_create_nonce( 'wp_rest' )
], 200 );
}
/**
* 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) {
wp_enqueue_script('advaipbl-live-feed-js', plugin_dir_url(dirname(__FILE__)) . 'js/advaipbl-live-feed.js', ['jquery'], ADVAIPBL_VERSION, true);
wp_localize_script('advaipbl-live-feed-js', 'advaipbl_feed_data', [
'api_url' => get_rest_url(null, 'advaipbl/v1/live-attacks'),
'nonce_url' => get_rest_url(null, 'advaipbl/v1/live-feed-nonce'),
'text' => [
'blocked_from' => __('blocked from', 'advanced-ip-blocker'),
'type' => __('Type', 'advanced-ip-blocker'),
'method' => __('Method', 'advanced-ip-blocker'),
'details' => __('Details', 'advanced-ip-blocker'),
'uri' => __('URI', 'advanced-ip-blocker'),
'user_agent' => __('User Agent', 'advanced-ip-blocker'),
]
]);
$logo_url_desktop = plugin_dir_url(dirname(__FILE__)) . 'assets/img/logo-ligth.png';
$logo_url_mobile = plugin_dir_url(dirname(__FILE__)) . 'assets/img/icon-256x256.png';
$css = '';
$html = '
⬤
Live Security Feed
Awaiting security events...
';
return $css . $html;
}
/**
* 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(),
'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)
]
);
}
}
/**
* Muestra la tabla de logs específica para las ejecuciones de WP-Cron.
* Esto es principalmente una herramienta de diagnóstico.
*/
private function display_cron_logs_tab() {
echo '
';
esc_html_e( 'This log shows which IP addresses are triggering WP-Cron tasks on your site, and which scheduled tasks (hooks) were due to run at that moment.', 'advanced-ip-blocker' );
echo '
';
foreach ( $details['due_hooks'] as $hook ) {
echo '
' . esc_html( $hook ) . '
';
}
echo '';
} else {
echo '–';
}
?>
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);
}
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($_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;
}
/**
* Muestra la pestaña para gestionar el bloqueo por ASN, ahora con estados separados.
*/
private function display_asn_blocking_tab() {
$provider = $this->options['geolocation_provider'] ?? '';
$is_compatible = in_array($provider, ['ip-api.com', 'ipinfo.io'], true);
$is_spamhaus_enabled = !empty($this->options['enable_spamhaus_asn']);
$is_manual_enabled = !empty($this->options['enable_manual_asn']);
$settings_url = admin_url('admin.php?page=advaipbl_settings_page-settings');
$spamhaus_stats = $this->dashboard_manager->get_spamhaus_stats();
?>
Warning: Your current geolocation provider (%1$s) does not support ASN lookups. This feature is fully supported by ip-api.com and ipinfo.io. Please change your provider to use ASN blocking.', 'advanced-ip-blocker'),
['strong' => [], 'a' => ['href' => []]]
),
esc_html($provider),
esc_url($settings_url)
); ?>
query($wpdb->prepare("DELETE FROM {$table_name} WHERE expires_at <= %d", time()));
$items = $wpdb->get_results("SELECT * FROM {$table_name} ORDER BY created_at DESC", ARRAY_A);
?>
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;
}
if (!isset($_POST['g-recaptcha-response']) || empty($_POST['g-recaptcha-response'])) {
return new WP_Error('recaptcha_empty', __('ERROR: Please complete the reCAPTCHA verification.', 'advanced-ip-blocker'));
}
$token = sanitize_text_field($_POST['g-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.
*/
private 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 bloqueadas.
* La primera vez que se llama, ejecuta la limpieza de IPs expiradas y cachea el resultado.
*
* @return int El número total de entradas bloqueadas.
*/
private function get_blocked_count() {
// Si ya hemos calculado el conteo en esta petición, lo devolvemos directamente.
if ( null !== $this->blocked_count ) {
return $this->blocked_count;
}
// 1. Ejecutar la limpieza de IPs expiradas PRIMERO.
$this->limpiar_ips_expiradas();
// 2. Obtener las entradas limpias y contarlas.
$entries = $this->get_all_blocked_entries();
$this->blocked_count = count( $entries );
// 3. Devolver el conteo.
return $this->blocked_count;
}
/**
* AJAX callback to test the server's outbound connection.
*/
public function ajax_test_outbound_connection() {
if ( ! current_user_can('manage_options') ) {
wp_send_json_error( [ 'message' => 'Permission denied.' ] );
}
check_ajax_referer( 'advaipbl_test_connection_nonce', 'nonce' );
$response = wp_remote_get('https://api.ipify.org?format=json', [ 'timeout' => 10 ]);
if ( is_wp_error($response) ) {
wp_send_json_error( [ 'message' => 'Error: ' . $response->get_error_message() ] );
}
$http_code = wp_remote_retrieve_response_code( $response );
if ( $http_code !== 200 ) {
wp_send_json_error( [ 'message' => sprintf( 'Error: Received HTTP status code %d.', $http_code ) ] );
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( isset( $body['ip'] ) && filter_var( $body['ip'], FILTER_VALIDATE_IP ) ) {
wp_send_json_success( [ 'message' => sprintf( 'Success! Connection established from IP: %s', $body['ip'] ) ] );
}
wp_send_json_error( [ 'message' => 'Error: The response from the test service was invalid.' ] );
}
/**
* 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' );
if ( filter_var( $entry_to_whitelist, FILTER_VALIDATE_IP ) ) {
$this->desbloquear_ip( $entry_to_whitelist );
} else {
// 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;
}
/**
* AJAX callback to add a specific IP to the whitelist.
* This is used by the interactive buttons in the admin interface.
*/
public function ajax_add_ip_to_whitelist() {
// 1. Validar permisos y nonce de seguridad (esto no cambia).
if ( ! current_user_can('manage_options') ) {
wp_send_json_error( [ 'message' => __( 'Permission denied.', 'advanced-ip-blocker' ) ] );
}
check_ajax_referer( 'advaipbl_add_whitelist_nonce', 'nonce' );
// 2. Obtener y validar la IP del POST. Esta función AJAX solo maneja IPs individuales.
if ( ! isset( $_POST['ip'] ) || ! filter_var( wp_unslash( $_POST['ip'] ), FILTER_VALIDATE_IP ) ) {
wp_send_json_error( [ 'message' => __( 'Invalid or missing IP address.', 'advanced-ip-blocker' ) ] );
}
$ip = sanitize_text_field( wp_unslash( $_POST['ip'] ) );
$detail = isset( $_POST['detail'] ) ? sanitize_text_field( wp_unslash( $_POST['detail'] ) ) : __('Added via admin action', 'advanced-ip-blocker');
// 3. Reutilizar nuestra nueva lógica centralizada.
$success = $this->add_to_whitelist_and_unblock( $ip, $detail );
if ( $success ) {
/* translators: %s: The IP address that was successfully whitelisted. */
wp_send_json_success( [ 'message' => sprintf( __( '%s successfully added to the whitelist.', 'advanced-ip-blocker' ), $ip ) ] );
} else {
/* translators: %s: The IP address that is already whitelisted. */
wp_send_json_success( [ 'message' => sprintf( __( '%s is already whitelisted.', 'advanced-ip-blocker' ), $ip ) ] );
}
}
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';
$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;
}
/**
* Envuelve el contenido de un email en una plantilla HTML básica y profesional.
*
* @param string $subject El asunto del email.
* @param string $content_html El contenido principal del email en HTML.
* @return string El cuerpo completo del email en HTML.
*/
private function get_html_email_template( $subject, $content_html ) {
$site_name = get_bloginfo( 'name' );
$site_url = home_url();
$settings_url = admin_url( 'options-general.php?page=advaipbl_settings_page' );
$logo_url = plugin_dir_url( dirname( __FILE__ ) ) . 'assets/img/logo-email.png';
// Estilos CSS en línea
$body_style = "background-color: #f6f7f8; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';";
$wrapper_style = "background-color: #ffffff; border: 1px solid #e0e0e0; border-radius: 4px; padding: 40px; max-width: 600px; margin: 0 auto;";
$logo_wrapper_style = "text-align: center; margin-bottom: 25px;";
// Estilos flexibles para el logo
$logo_style = "max-width: 220px; height: auto; border: 0; display: inline-block; vertical-align: middle;";
$header_style = "font-size: 24px; font-weight: bold; color: #2271b1; margin-top: 0; margin-bottom: 20px; text-align: center;";
$footer_style = "font-size: 12px; color: #777777; text-align: center; margin-top: 30px; line-height: 1.6; border-top: 1px solid #e0e0e0; padding-top: 20px;";
ob_start();
?>
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' );
// Asunto para la línea "Subject:" del email (largo y descriptivo).
/* translators: %s: The name of the website. */
$email_subject = sprintf( __( '[%s] Welcome & Quick Setup Guide for Advanced IP Blocker', 'advanced-ip-blocker' ), $site_name );
// Título para el
dentro del cuerpo del email (corto y limpio).
$template_title = __( 'Welcome & Quick Setup Guide', 'advanced-ip-blocker' );
// 3. Construir el contenido HTML del email
$content_html =
'
' . esc_html__( 'Thank you for using Advanced IP Blocker! This email confirms your email settings are working. Here is a quick guide to get the most out of the plugin:', 'advanced-ip-blocker' ) . '
' .
sprintf(
wp_kses( /* translators: %s: a link to the Status & Debug tab. */
__( 'This is the most important step to prevent locking yourself out. Go to the Status & Debug tab to confirm that your IP address and your server\'s IP are correctly detected. If they are not on the whitelist, add them with a single click.', 'advanced-ip-blocker' ),
[ 'a' => [ 'href' => [] ] ]
),
esc_url( add_query_arg( 'tab', 'status', $settings_url ) )
) .
'
' .
sprintf(
wp_kses( /* translators: %s: a link to the User Agents tab. */
__( 'Go to the User Agents tab and copy the suggested list of malicious bots into the "Blocked" text area.', 'advanced-ip-blocker' ),
[ 'a' => [ 'href' => [] ] ]
),
esc_url( add_query_arg( 'tab', 'user_agents', $settings_url ) )
) . ' ' .
sprintf(
wp_kses( /* translators: %s: a link to the Honeypot URLs tab. */
__( 'Then, go to the Honeypot URLs tab and do the same. This will immediately block thousands of common automated attacks.', 'advanced-ip-blocker' ),
[ 'a' => [ 'href' => [] ] ]
),
esc_url( add_query_arg( 'tab', 'honeypot', $settings_url ) )
) .
'
' .
sprintf(
wp_kses( /* translators: %s: a link to the main Settings tab. */
__( 'Finally, visit the main Settings tab to enable logging and configure notification frequency to your liking. The default blocking rules are a great starting point for most sites.', 'advanced-ip-blocker' ),
[ 'a' => [ 'href' => [] ] ]
),
esc_url( add_query_arg( 'tab', 'general_settings', $settings_url ) )
) .
'
';
// 4. Envolver en la plantilla y enviar (usando las nuevas variables)
$body = $this->get_html_email_template( $template_title, $content_html );
add_filter( 'wp_mail_content_type', [ $this, 'set_html_mail_content_type' ] );
$sent = wp_mail( $to, $email_subject, $body );
remove_filter( 'wp_mail_content_type', [ $this, 'set_html_mail_content_type' ] );
// 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 load_admin_scripts($hook) {
// Cargar scripts en nuestras páginas Y en las páginas de perfil/edición de usuario.
$allowed_hooks = ['profile.php', 'user-edit.php'];
$is_plugin_page = strpos($hook, 'advaipbl_settings_page') !== false;
if ( !in_array($hook, $allowed_hooks) && !$is_plugin_page ) {
return;
}
// --- 1. Determinar la pestaña y sub-pestaña activas ---
$active_main_tab = isset($_GET['tab']) ? sanitize_key($_GET['tab']) : 'dashboard';
$active_sub_tab = isset($_GET['sub-tab']) ? sanitize_key($_GET['sub-tab']) : null;
// Si no hay sub-pestaña en la URL, la deducimos de la pestaña principal.
if (is_null($active_sub_tab)) {
switch ($active_main_tab) {
case 'dashboard':
$active_sub_tab = 'main_dashboard';
break;
case 'settings':
$active_sub_tab = 'general_settings';
break;
case 'rules':
$active_sub_tab = 'waf';
break;
case 'ip_management':
$active_sub_tab = 'blocked_ips';
break;
case 'logs':
$active_sub_tab = 'security_log';
break;
case 'about':
$active_sub_tab = 'credits';
break;
default:
$active_sub_tab = 'main_dashboard';
}
}
// --- 2. Cargar Assets Comunes (siempre necesarios en nuestras páginas) ---
if ($is_plugin_page || in_array($hook, $allowed_hooks)) {
$this->session_manager->enqueue_scripts_styles(); // Asumiendo que es necesario en el perfil también.
wp_enqueue_style( 'advaipbl-styles-main', plugin_dir_url( dirname( __FILE__ ) ) . 'css/advaipbl-styles.css', [], ADVAIPBL_VERSION );
wp_enqueue_style( 'advaipbl-select2-css', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/css/select2.min.css', [], '4.1.0-rc.0' );
wp_enqueue_script( 'advaipbl-select2-js', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/js/select2.min.js', [ 'jquery' ], '4.1.0-rc.0', true );
wp_enqueue_script( 'advaipbl-admin-js', plugin_dir_url( dirname( __FILE__ ) ) . 'js/advaipbl-admin.js', [ 'jquery', 'advaipbl-select2-js' ], ADVAIPBL_VERSION, true );
$floating_bar_css = " #advaipbl-floating-save-bar { position: fixed; bottom: 0; left: 160px; right: 0; background-color: #fff; box-shadow: 0 -2px 5px rgba(0,0,0,0.1); padding: 15px 30px; z-index: 999; transition: transform 0.3s ease-in-out; transform: translateY(100%); } #advaipbl-floating-save-bar.advaipbl-save-bar-visible { transform: translateY(0); } .advaipbl-save-bar-content { display: flex; justify-content: space-between; align-items: center; max-width: 1200px; margin: 0 auto; } .advaipbl-save-bar-text { font-size: 1.1em; font-weight: 600; } .advaipbl-save-bar-buttons .button { margin-left: 10px; } @media screen and (max-width: 960px) { #advaipbl-floating-save-bar { left: 36px; } } @media screen and (max-width: 782px) { #advaipbl-floating-save-bar { left: 0; padding: 10px 15px; } .advaipbl-save-bar-text { display: none; } .advaipbl-save-bar-content { justify-content: flex-end; } }";
wp_add_inline_style('advaipbl-styles-main', $floating_bar_css);
}
// --- 3. Cargar Assets Específicos (solo cuando sea necesario) ---
if ($is_plugin_page) {
// Cargar assets del Dashboard Principal
if ( 'main_dashboard' === $active_sub_tab ) {
wp_enqueue_script('chartjs', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/js/chart.min.js', [], '3.9.1', true);
wp_enqueue_style('leaflet-css', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/css/leaflet.css');
wp_enqueue_script('leaflet-js', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/js/leaflet.js', [], '1.9.4', true);
wp_enqueue_style('leaflet-markercluster-css', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/css/MarkerCluster.css');
wp_enqueue_style('leaflet-markercluster-default-css', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/css/MarkerCluster.Default.css');
wp_enqueue_script('leaflet-markercluster-js', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/js/leaflet.markercluster.js', ['leaflet-js'], '1.5.3', true);
wp_enqueue_script('advaipbl-dashboard-js', plugin_dir_url( dirname( __FILE__ ) ) . 'js/advaipbl-dashboard.js', ['jquery', 'chartjs', 'leaflet-markercluster-js'], ADVAIPBL_VERSION, true);
}
// Cargar assets de la página "About"
if ('credits' === $active_sub_tab) {
wp_enqueue_script('stripe-buy-button', 'https://js.stripe.com/v3/buy-button.js', [], null, true);
}
}
// --- 4. Localizar Datos para JS (siempre necesario en nuestras páginas) ---
if ($is_plugin_page) {
// --- Procesar información del servidor ---
$server_ip = $this->get_server_ip();
$server_info = [ 'ip' => $server_ip, 'country_code' => '', 'country_name' => '', 'is_whitelisted' => false ];
if ( $server_ip ) {
$location = $this->geolocation_manager->fetch_location( $server_ip );
if ( $location && empty( $location['error'] ) ) {
$server_info['country_code'] = $location['country_code'] ?? '';
$server_info['country_name'] = $location['country'] ?? '';
}
$server_info['is_whitelisted'] = $this->is_whitelisted( $server_ip );
}
// --- Procesar información del administrador (usuario actual) ---
$admin_ip = $this->get_client_ip();
$admin_info = [ 'ip' => $admin_ip, 'country_code' => '', 'country_name' => '', 'is_whitelisted' => false ];
if ( $admin_ip && filter_var( $admin_ip, FILTER_VALIDATE_IP ) ) {
$location = $this->geolocation_manager->fetch_location( $admin_ip );
if ( $location && empty( $location['error'] ) ) {
$admin_info['country_code'] = $location['country_code'] ?? '';
$admin_info['country_name'] = $location['country'] ?? '';
}
$admin_info['is_whitelisted'] = $this->is_whitelisted( $admin_ip );
}
// --- Localizar scripts con datos para el frontend ---
wp_localize_script('advaipbl-admin-js', 'advaipbl_admin_data', [
'counts' => [
'blocked' => count( $this->get_all_blocked_entries() ),
],
'geoblock' => [
'server' => $server_info,
'admin' => $admin_info,
'blocked_countries' => $this->options['geoblock_countries'] ?? [],
],
'nonces' => [
'test_connection' => wp_create_nonce( 'advaipbl_test_connection_nonce' ),
'add_whitelist' => wp_create_nonce( 'advaipbl_add_whitelist_nonce' ),
'export' => wp_create_nonce( 'advaipbl_export_nonce' ),
'verify_api' => wp_create_nonce( 'advaipbl_verify_api_nonce' ),
'get_dashboard_stats' => wp_create_nonce( 'wp_ajax_advaipbl_get_dashboard_stats' ),
'telemetry' => wp_create_nonce('advaipbl_telemetry_nonce'),
'reset_score' => wp_create_nonce('advaipbl_reset_score_nonce'),
'get_history' => wp_create_nonce('advaipbl_get_history_nonce'),
'delete_signature' => wp_create_nonce('advaipbl_delete_signature_nonce'),
'get_signature_details' => wp_create_nonce('advaipbl_get_signature_details_nonce'),
'whitelist_signature' => wp_create_nonce('advaipbl_whitelist_signature_nonce'),
'get_lockdown_details' => wp_create_nonce('advaipbl_get_lockdown_details_nonce'),
],
'text' => [ /* translators: 1: Country, 2:IP. */
'server_whitelisted' => __( 'Info: Your server is located in %1$s and its IP (%2$s) is correctly whitelisted. You can safely block this country.', 'advanced-ip-blocker' ),
/* translators: 1: Country, 2:IP. */
'server_not_whitelisted' => __( 'CRITICAL WARNING! Your server is located in %1$s and its IP (%2$s) is NOT whitelisted. Blocking this country WILL likely break your site. Please add the server IP to the whitelist BEFORE blocking this country.', 'advanced-ip-blocker' ),
/* translators: 1: IP, 2:Country. */
'admin_whitelisted' => __( 'Info: Your current IP address (%1$s) from %2$s is whitelisted. You will not be locked out if you block this country.', 'advanced-ip-blocker' ),
/* translators: 1: IP, 2:Country. */
'admin_not_whitelisted' => __( 'Caution: Your current IP address (%1$s) from %2$s is NOT whitelisted. If you block this country, you may lose access to your admin panel.', 'advanced-ip-blocker' ),
/* translators: 1: IP, 2:Country. */
'remove_server_ip_warning' => __( 'CRITICAL WARNING: This IP (%1$s) belongs to your server, located in a blocked country (%2$s). REMOVING IT FROM THE WHITELIST WILL LIKELY BREAK YOUR SITE!', 'advanced-ip-blocker' ),
/* translators: 1: IP, 2:Country. */
'remove_admin_ip_warning' => __( 'CAUTION: This is your current IP address (%1$s) from a blocked country (%2$s). REMOVING THIS IP FROM THE WHITELIST MAY LOCK YOU OUT OF YOUR ADMIN PANEL.', 'advanced-ip-blocker' ),
'confirm_removal' => __( 'Are you absolutely sure you want to proceed?', 'advanced-ip-blocker' ),
'add_to_whitelist_btn' => __( 'Add to Whitelist', 'advanced-ip-blocker' ),
'adding_to_whitelist' => __( 'Adding...', 'advanced-ip-blocker' ),
'added_to_whitelist' => __( 'Successfully Added!', 'advanced-ip-blocker' ),
'select2_placeholder' => __( 'Search for a country to block...', 'advanced-ip-blocker' ),
'verify_api_button' => __( 'Verify', 'advanced-ip-blocker' ),
'verifying_api' => __( 'Verifying...', 'advanced-ip-blocker' ),
'enter_api_key' => __( 'Please enter an API key.', 'advanced-ip-blocker' ),
'ajax_error' => __( 'AJAX error. Check browser console.', 'advanced-ip-blocker' ),
'discard_title' => __( 'Discard Changes?', 'advanced-ip-blocker' ),
'discard_message' => __( 'You have unsaved changes. Are you sure you want to discard them?', 'advanced-ip-blocker' ),
'discard_confirm_btn' => __( 'Yes, Discard', 'advanced-ip-blocker' ),
'attacks_label' => __( 'attacks', 'advanced-ip-blocker' ),
'blocks_label' => __( 'blocks', 'advanced-ip-blocker' ),
'confirm_bulk_action_title' => __( 'Confirm Bulk Action', 'advanced-ip-blocker' ),
/* translators: %d: The number IPs that were unblocked. */
'confirm_bulk_unblock_message' => __( 'Are you sure you want to unblock the selected %d entries? This action cannot be undone.', 'advanced-ip-blocker' ),
'confirm_bulk_unblock_button' => __( 'Yes, Unblock Selected', 'advanced-ip-blocker' ),
'alert_no_action' => __( 'Please select a bulk action.', 'advanced-ip-blocker' ),
'alert_no_items' => __( 'Please select at least one item to apply the action.', 'advanced-ip-blocker' ),
],
]);
}
}
public function detect_http_error_status($status_header, $code) {
if ($this->request_is_asn_whitelisted) { return; }
if ($this->error_handled_this_request || is_admin()) {
return;
}
$excluded_urls = $this->options['excluded_error_urls'] ?? '';
if (!empty($excluded_urls)) {
$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;
}
}
}
if ($code === 404) {
$this->error_handled_this_request = true;
$ip = $this->get_client_ip();
if ($this->is_whitelisted($ip)) return;
if (!empty($this->options['enable_honeypot_blocking'])) {
$honeypot_urls = get_option(self::OPTION_HONEYPOT_URLS, []);
if (!empty($honeypot_urls)) {
$requested_url = strtolower($this->get_current_request_uri());
foreach ($honeypot_urls as $trap_url) {
if (!empty($trap_url) && stripos($requested_url, strtolower(trim($trap_url))) !== false) {
/* translators: %d: Honeypot URL accessed. */
$reason = sprintf(__('Honeypot URL accessed: %s', 'advanced-ip-blocker'), $trap_url);
// Llamamos a nuestra función central y LUEGO terminamos.
$this->handle_threat_event($ip, 'honeypot', $reason, ['url' => $requested_url]);
return;
}
}
}
}
$this->handle_error('404');
} elseif ($code === 403) {
$this->error_handled_this_request = true;
$ip = $this->get_client_ip();
// Comprobación de whitelist.
if ($this->is_whitelisted($ip)) return;
$all_block_types = ['geoblock', 'honeypot', '404', '403', 'login', 'user_agent', 'waf', 'rate_limit', 'asn', 'xmlrpc_block', 'threat_score'];
foreach ($all_block_types as $type) {
if (get_transient('advaipbl_bloqueo_' . $type . '_' . md5($ip))) {
return;
}
}
$this->handle_error('403');
}
}
/**
* Renders the checkbox for the Whitelist Login Access feature, including security checks.
*/
public function restrict_login_page_callback() {
$args = [
'name' => 'restrict_login_page',
'label' => __( 'Restrict access to wp-login.php to whitelisted IPs only', 'advanced-ip-blocker' )
];
$client_ip = $this->get_client_ip();
$is_whitelisted = $this->is_whitelisted( $client_ip );
// Llama al callback del checkbox normal
$this->checkbox_field_callback($args);
// Añadimos la advertencia de seguridad
echo '
' . esc_html__( 'WARNING: This is a highly restrictive feature. Only enable it if you have a static IP address and public user registration is disabled on your site.', 'advanced-ip-blocker' ) . '
';
// Si la IP del admin no está en la whitelist, mostramos un aviso y un botón para añadirla
if ( ! $is_whitelisted ) {
echo '
';
/* translators: %s: The admin's current IP address. */
echo '' . sprintf( esc_html__( 'Your current IP address (%s) is NOT whitelisted.', 'advanced-ip-blocker' ), esc_html( $client_ip ) ) . ' ' . esc_html__( 'You MUST add it to the whitelist before enabling this feature, or you will be locked out.', 'advanced-ip-blocker' ); echo '';
echo '
';
}
}
/**
* Bloquea de forma temprana y completa todas las peticiones a xmlrpc.php si la opción está en modo 'disabled'.
* Enganchado a un hook 'init' temprano para máxima eficacia.
*/
public function block_xmlrpc_requests_if_disabled() {
// Solo actuar si la opción está explícitamente en modo 'disabled'.
if ( empty( $this->options['xmlrpc_protection_mode'] ) || 'disabled' !== $this->options['xmlrpc_protection_mode'] ) {
return;
}
// Comprobamos si la petición actual es para xmlrpc.php.
$request_uri = $this->get_current_request_uri();
if ( strpos( $request_uri, 'xmlrpc.php' ) !== false ) {
$ip = $this->get_client_ip();
// Logueamos el intento de acceso (opcional pero recomendado para visibilidad)
$this->log_event(
sprintf( 'Access to disabled xmlrpc.php endpoint was denied for IP: %s', $ip ),
'warning',
['uri' => $request_uri]
);
// Devolvemos una respuesta 403 Forbidden y terminamos la ejecución.
wp_die(
'XML-RPC services are disabled on this site.',
'Forbidden',
[ 'response' => 403 ]
);
}
}
/**
* Handles the actual restriction of the login page.
* Hooked to 'login_init'.
*/
public function handle_login_page_restriction() {
// Solo actuar si la opción está activada
if ( ! empty( $this->options['restrict_login_page'] ) ) {
$client_ip = $this->get_client_ip();
// Si la IP del visitante no está en la whitelist...
if ( ! $this->is_whitelisted( $client_ip ) ) {
/* translators: %s: The IP address that was denied access. */
$this->log_event( sprintf( __( 'Access to wp-login.php denied for non-whitelisted IP: %s', 'advanced-ip-blocker' ), $client_ip ), 'critical', $client_ip );
// Mostramos un mensaje genérico de acceso denegado y terminamos.
wp_die(
esc_html__( 'Access to this page has been restricted by the administrator.', 'advanced-ip-blocker' ),
esc_html__( 'Access Denied', 'advanced-ip-blocker' ),
[ 'response' => 403 ]
);
}
}
}
/**
* Disables the REST API user endpoints for non-authenticated users if the option is enabled.
*
* @param array $endpoints The available REST API endpoints.
* @return array The modified endpoints.
*/
public function disable_rest_api_user_endpoints( $endpoints ) {
// Solo actuar si la opción está activada en los ajustes.
if ( ! empty( $this->options['disable_user_enumeration'] ) && ! is_user_logged_in() ) {
if ( isset( $endpoints['/wp/v2/users'] ) ) {
unset( $endpoints['/wp/v2/users'] );
}
if ( isset( $endpoints['/wp/v2/users/(?P[\d]+)'] ) ) {
unset( $endpoints['/wp/v2/users/(?P[\d]+)'] );
}
}
return $endpoints;
}
/**
* Prevents user enumeration by redirecting author archive scans at an early hook.
* Enganchado a 'init'.
*/
public function prevent_author_enumeration_redirect() {
// Solo actuar si la opción está activada y no estamos en el admin.
if ( ! empty( $this->options['prevent_author_scanning'] ) && ! is_admin() ) {
// Comprobamos directamente el parámetro GET 'author'. Es mucho más fiable.
if ( isset( $_GET['author'] ) && is_numeric( $_GET['author'] ) ) {
wp_safe_redirect( home_url(), 301 );
exit;
}
}
}
/**
* Checks if a given IP address falls within a specified range (CIDR or hyphenated).
*
* @param string $ip The IP address to check.
* @param string $range The range to check against (e.g., "192.168.1.0/24" or "192.168.1.100-192.168.1.200").
* @return bool True if the IP is in the range, false otherwise.
*/
public function is_ip_in_range( $ip, $range ) {
// 1. Comprobar si es un rango CIDR
if ( strpos( $range, '/' ) !== false ) {
list( $subnet, $bits ) = explode( '/', $range );
if ( filter_var( $subnet, FILTER_VALIDATE_IP ) && is_numeric( $bits ) ) {
$ip_long = ip2long( $ip );
$subnet_long = ip2long( $subnet );
$mask = -1 << ( 32 - (int) $bits );
$subnet_masked = $subnet_long & $mask;
return ( $ip_long & $mask ) === $subnet_masked;
}
}
// 2. Comprobar si es un rango con guion
if ( strpos( $range, '-' ) !== false ) {
list( $start_ip, $end_ip ) = explode( '-', $range, 2 );
$start_ip = trim( $start_ip );
$end_ip = trim( $end_ip );
if ( filter_var( $start_ip, FILTER_VALIDATE_IP ) && filter_var( $end_ip, FILTER_VALIDATE_IP ) ) {
$ip_long = ip2long( $ip );
$start_long = ip2long( $start_ip );
$end_long = ip2long( $end_ip );
return $ip_long >= $start_long && $ip_long <= $end_long;
}
}
// 3. Si no es un rango, es una IP individual. Hacemos una comparación directa.
return $ip === $range;
}
public function run_all_block_checks() {
if ($this->request_is_asn_whitelisted) { return; }
if ( $this->is_visitor_actively_blocked() ) {
$this->access_denied_page(__('403 - Access Denied', 'advanced-ip-blocker'), $this->get_block_message('generic'));
exit;
}
if ( defined( 'WP_CLI' ) && WP_CLI ) {
return;
}
$ip = $this->get_client_ip();
if ($this->is_whitelisted($ip)) {
return;
}
$request_uri = $this->get_current_request_uri();
$is_xmlrpc_request = (strpos($request_uri, 'xmlrpc.php') !== false);
// ENDPOINT LOCKDOWN (PRIMERA ACCIÓN)
if ($is_xmlrpc_request && !empty($this->options['enable_xmlrpc_lockdown'])) {
if (get_transient('advaipbl_lockdown_active_xmlrpc')) {
$user_agent = $this->get_user_agent();
$is_trusted_service = false;
$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' => 'xmlrpc.php', 'reason' => 'XML-RPC Lockdown Mode Active', 'uri' => $request_uri], 'warning');
$this->serve_js_challenge();
}
}
}
if ($is_xmlrpc_request && ($this->options['xmlrpc_protection_mode'] ?? 'smart') === 'smart') {
$is_trusted_request = false;
// Capa 2: Confianza en la Infraestructura (ASN de la conexión directa)
$remote_addr = $this->get_remote_addr();
if ($remote_addr) {
$location_data_remote = $this->geolocation_manager->fetch_location($remote_addr);
$remote_asn = $this->asn_manager->extract_asn_from_data($location_data_remote);
// Lista de ASNs de infraestructura de confianza.
$trusted_infra_asns = ['AS2635', 'AS13335']; // Automattic, Cloudflare
if ($remote_asn && in_array($remote_asn, $trusted_infra_asns, true)) {
$is_trusted_request = true;
}
}
// Capa 3: Confianza en el User-Agent (si la capa de infraestructura no lo confirmó)
if (!$is_trusted_request) {
$user_agent = $this->get_user_agent();
$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_request = true;
break;
}
}
}
// Capa 4: Decisión final de bloqueo
if (!$is_trusted_request) {
/* translators: %s: User-Agent. */
$reason = sprintf(__('Blocked untrusted XML-RPC request from User-Agent: %s', 'advanced-ip-blocker'), $this->get_user_agent());
$log_data = ['user_agent' => $this->get_user_agent(), 'uri' => $request_uri, 'remote_addr' => $remote_addr];
if (!empty($this->options['enable_xmlrpc_lockdown'])) {
$this->increment_lockdown_counter('xmlrpc');
}
$this->block_ip_instantly($ip, 'xmlrpc_block', $reason, $log_data);
}
// Si es una petición XML-RPC, no continuamos con las demás comprobaciones
return;
}
// Comprobación de bloqueos manuales
$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 ) ) {
$this->block_ip_instantly( $ip, 'manual', __('Access manually blocked.', 'advanced-ip-blocker') );
}
}
}
// GEOBLOCKING
if (!empty($this->options['enable_geoblocking'])) {
$blocked_countries = $this->options['geoblock_countries'] ?? [];
if ( ! empty( $blocked_countries ) ) {
$location = $this->geolocation_manager->fetch_location( $ip );
if ( $location && ! empty( $location['country_code'] ) ) {
if ( in_array( $location['country_code'], $blocked_countries, true ) ) {
/* translators: %s: Country block. */
$reason = sprintf( __( 'Country block: %s', 'advanced-ip-blocker' ), $location['country'] ?? $location['country_code'] );
$log_data = [ 'country' => $location['country'] ?? '', 'uri' => $this->get_current_request_uri() ];
$this->block_ip_instantly( $ip, 'geoblock', $reason, $log_data );
}
}
}
}
// USER-AGENT CHECK
if (!empty($this->options['enable_user_agent_blocking'])) {
$user_agent = $this->get_user_agent();
if (!empty($user_agent) && !$this->is_internal_request($user_agent)) {
$is_ua_whitelisted = false;
$whitelisted_user_agents = get_option('advaipbl_whitelisted_user_agents', []);
if (!empty($whitelisted_user_agents)) {
foreach ($whitelisted_user_agents as $whitelisted_ua_raw) {
$whitelisted_ua = trim(preg_replace('/#.*$/', '', $whitelisted_ua_raw));
if (!empty($whitelisted_ua) && stripos($user_agent, $whitelisted_ua) !== false) {
$is_ua_whitelisted = true;
break;
}
}
}
if (!$is_ua_whitelisted) {
$blocked_user_agents = get_option('advaipbl_blocked_user_agents', []);
if (!empty($blocked_user_agents)) {
foreach ($blocked_user_agents as $blocked_ua_raw) {
$blocked_ua = trim(preg_replace('/#.*$/', '', $blocked_ua_raw));
if (!empty($blocked_ua) && stripos($user_agent, $blocked_ua) !== false) {
/* translators: %s: User-Agent. */
$reason_message = sprintf(__('Blocked User-Agent pattern: %s', 'advanced-ip-blocker'), $blocked_ua);
$log_data = [
'user_agent' => $user_agent,
'detail' => $reason_message,
'uri' => $this->get_current_request_uri()
];
$this->handle_threat_event($ip, 'user_agent', $reason_message, $log_data);
return;
}
}
}
}
}
}
// WAF CHECK
if ( ! empty( $this->options['enable_waf'] ) ) {
$triggered_rule = $this->waf_manager->run_waf_scan();
if ( $triggered_rule !== false ) {
/* translators: %s: WAF Rule Triggered. */
$reason_message = sprintf(__('WAF Rule Triggered: %s', 'advanced-ip-blocker'), $triggered_rule);
$log_data = [
'rule' => $triggered_rule,
'uri' => $this->get_current_request_uri(),
'method' => $this->get_request_method()
];
$this->handle_threat_event($ip, 'waf', $reason_message, $log_data);
}
}
// ASN CHECK
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)) {
$asn_block_data = $this->asn_manager->check_asn_block($ip);
if ($asn_block_data) {
$this->handle_threat_event($ip, 'asn', $asn_block_data['reason_message'], $asn_block_data['log_data']);
}
}
}
}
public function is_whitelisted($ip) {
if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
return false; // No es una IP pública válida, no puede estar en la whitelist.
}
$whitelist = get_option( self::OPTION_WHITELIST, [] );
if ( empty( $whitelist ) ) {
return false;
}
// Comprobación rápida para IPs individuales (la mayoría de los casos)
if ( array_key_exists( $ip, $whitelist ) ) {
return true;
}
// Iterar sobre las claves de la whitelist para buscar rangos.
foreach ( array_keys( $whitelist ) as $entry ) {
// Si la entrada no es la IP exacta, podría ser un rango.
if ( $entry !== $ip && $this->is_ip_in_range( $ip, $entry ) ) {
return true;
}
}
return false;
}
/**
* Comprueba si el ASN del visitante actual está en la lista blanca.
* Si lo está, establece una bandera interna para saltar otras comprobaciones de seguridad.
* Esta función está diseñada para ejecutarse en un hook muy temprano.
*
* @return bool True si el ASN está en la lista blanca, false en caso contrario.
*/
public function is_visitor_asn_whitelisted() {
if ( ! $this->geoip_manager instanceof ADVAIPBL_GeoIP_Manager ) {
return false;
}
// Obtenemos la lista blanca. Si está vacía, no hay nada que hacer.
$whitelisted_asns = get_option(self::OPTION_WHITELISTED_ASNS, []);
if (empty($whitelisted_asns)) {
return false;
}
// Obtenemos la IP del visitante.
$ip = $this->get_client_ip();
if (!$ip) {
return false;
}
// Obtenemos el ASN de la IP.
$location_data = $this->geolocation_manager->fetch_location($ip);
$visitor_asn = $this->asn_manager->extract_asn_from_data($location_data);
if (!$visitor_asn) {
return false;
}
// Comprobamos si el ASN del visitante está en nuestra lista blanca.
if (in_array($visitor_asn, $whitelisted_asns, true)) {
// ¡Coincidencia! Establecemos la bandera y devolvemos true.
$this->request_is_asn_whitelisted = true;
return true;
}
return false;
}
private function is_internal_request($user_agent) { return strpos($user_agent, 'WordPress/') === 0; }
private function get_block_message($type) {
$messages = [
'generic' => __('Access denied by security policy.', 'advanced-ip-blocker'),
'geoblock' => __('Access from your country has been blocked.', 'advanced-ip-blocker'),
'honeypot' => __('Access blocked due to suspicious activity.', 'advanced-ip-blocker'),
'manual' => __('Access manually blocked.', 'advanced-ip-blocker'),
'404' => __('Access blocked due to repeated 404 errors.', 'advanced-ip-blocker'),
'403' => __('Access blocked due to repeated attempts on protected resources.', 'advanced-ip-blocker'),
'login' => __('Access blocked due to multiple failed login attempts.', 'advanced-ip-blocker'),
'user_agent' => __('Access blocked due to suspicious User-Agent.', 'advanced-ip-blocker'),
'waf' => __('Your request was blocked by the security firewall.', 'advanced-ip-blocker'),
'rate_limit' => __('Your connection has been temporarily suspended due to an excessive request rate.', 'advanced-ip-blocker'),
'asn' => __('Access from your network (ASN) has been blocked by the administrator.', 'advanced-ip-blocker'),
'xmlrpc_block' => __('Blocked suspicious XML-RPC request.', 'advanced-ip-blocker'),
'threat_score' => __('Your connection has been blocked due to a high threat score.', 'advanced-ip-blocker'),
];
return $messages[$type] ?? __('Access blocked.', 'advanced-ip-blocker');
}
public function block_ip_instantly($ip, $type, $reason_message, $extra_data = [], $context = 'frontend_block') {
if ( in_array( $ip, [ '127.0.0.1', '::1' ], true ) ) {
$remote_addr = $this->get_remote_addr();
if ( $remote_addr && ! in_array( $remote_addr, [ '127.0.0.1', '::1' ], true ) ) {
$original_ip = $ip;
$ip = $remote_addr;
$extra_data['_spoofed_ip'] = $original_ip;
/* translators: %s: The real attacker IP. */
$reason_message .= sprintf(' (Spoofing attempt from %s)', $ip);
} else {
return;
}
}
global $wpdb;
$lock_key = 'lock_blocking_' . md5($ip);
if (!$wpdb->query($wpdb->prepare("INSERT IGNORE INTO {$wpdb->prefix}advaipbl_cache (cache_key, cache_value, expires_at) VALUES (%s, '1', %d)", $lock_key, time() + 15))) {
if ($context === 'frontend_block') { $this->access_denied_page(__('403 - Access Denied', 'advanced-ip-blocker'), $this->get_block_message('generic')); exit; }
return;
}
$table_name = $wpdb->prefix . 'advaipbl_blocked_ips';
if ($wpdb->get_var($wpdb->prepare("SELECT id FROM {$table_name} WHERE ip_range = %s", $ip))) {
$wpdb->delete("{$wpdb->prefix}advaipbl_cache", ['cache_key' => $lock_key]);
if ($context === 'frontend_block') { $this->access_denied_page(__('403 - Access Denied', 'advanced-ip-blocker'), $this->get_block_message('generic')); exit; }
return;
}
$definitions = $this->get_all_block_type_definitions();
$def = $definitions[$type] ?? null;
$timestamp = time();
$duration_in_seconds = 10 * YEAR_IN_SECONDS;
if ($def && !empty($def['duration_key'])) {
$duration_in_minutes = (int) ($this->options[$def['duration_key']] ?? 1440);
$duration_in_seconds = ($duration_in_minutes > 0) ? $duration_in_minutes * 60 : 0;
}
$expires_at = ($duration_in_seconds > 0) ? $timestamp + $duration_in_seconds : 0;
$wpdb->insert($table_name, [ 'ip_range' => $ip, 'block_type' => $type, 'timestamp' => $timestamp, 'expires_at' => $expires_at, 'reason' => $reason_message ]);
$this->log_specific_error($type, $ip, $extra_data, 'critical');
set_transient('advaipbl_blocked_ip_' . md5($ip), true, $duration_in_seconds);
if ($def && $def['uses_transient']) {
set_transient('advaipbl_bloqueo_' . $type . '_' . md5($ip), true, $duration_in_seconds);
}
$this->send_block_notification($ip, $type, 1, $extra_data);
$this->clear_blocked_ips_cache();
$wpdb->delete("{$wpdb->prefix}advaipbl_cache", ['cache_key' => $lock_key]);
if ($context === 'frontend_block') {
$this->access_denied_page(__('403 - Access Denied', 'advanced-ip-blocker'), $this->get_block_message($type));
exit;
}
}
public function add_admin_ip_to_whitelist_on_first_run() {
if ( get_option('advaipbl_admin_ip_whitelist_trigger') ) {
if ( ! wp_doing_cron() && is_user_logged_in() && current_user_can('manage_options') ) {
$whitelist = get_option('advaipbl_ips_whitelist', []);
$ips_to_add = [];
// 1. Obtener IP del administrador.
$admin_ip = $this->get_client_ip();
if ( filter_var($admin_ip, FILTER_VALIDATE_IP) && !array_key_exists($admin_ip, $whitelist) ) {
$ips_to_add[$admin_ip] = __('Admin IP (auto-added on activation)', 'advanced-ip-blocker');
}
// 2. Obtener IP del servidor.
$server_ip = $this->get_server_ip();
if ( $server_ip && !array_key_exists($server_ip, $whitelist) ) {
// Evitar añadir la misma IP dos veces si el admin y el servidor son el mismo.
if (!isset($ips_to_add[$server_ip])) {
$ips_to_add[$server_ip] = __('Server IP (auto-added on activation)', 'advanced-ip-blocker');
}
}
if ( ! empty($ips_to_add) ) {
foreach ($ips_to_add as $ip => $detail) {
$whitelist[$ip] = [ 'timestamp' => time(), 'detail' => $detail ];
/* translators: %1$s: The IP address, %2$s: The reason/detail for whitelisting. */
$this->log_event( sprintf( __( 'IP %1$s added to whitelist. Reason: %2$s', 'advanced-ip-blocker' ), $ip, $detail ), 'info', $ip );
}
update_option('advaipbl_ips_whitelist', $whitelist);
// Forzamos la invalidación de la caché aquí también.
wp_cache_delete('advaipbl_ips_whitelist', 'options');
}
delete_option('advaipbl_admin_ip_whitelist_trigger');
}
}
}
public function schedule_cron_jobs() {
// Tarea de purga de logs (diaria)
if (!wp_next_scheduled('advaipbl_purge_old_logs_event')) {
wp_schedule_event(time(), 'daily', 'advaipbl_purge_old_logs_event');
}
// Tarea de resúmenes por email (diaria o semanal, según ajustes)
$freq = $this->options['notification_frequency'] ?? 'disabled';
if (($freq === 'daily' || $freq === 'weekly') && !wp_next_scheduled('advaipbl_send_summary_email')) {
$this->schedule_notification_cron();
}
// Tarea de telemetría (cada 3 días, si está activada)
if (!empty($this->options['allow_telemetry']) && '1' === $this->options['allow_telemetry']) {
if (!wp_next_scheduled('advaipbl_send_telemetry_data_event')) {
// Programamos la primera ejecución para dentro de 24 horas para no sobrecargar en la activación.
wp_schedule_event(time() + DAY_IN_SECONDS, '3_days', 'advaipbl_send_telemetry_data_event');
}
} else {
// Si la opción está desactivada, nos aseguramos de que no haya ninguna tarea programada.
wp_clear_scheduled_hook('advaipbl_send_telemetry_data_event');
}
// Tarea de decaimiento de la puntuación de amenaza (cada hora)
if (!wp_next_scheduled('advaipbl_threat_score_decay_event')) {
wp_schedule_event(time(), 'hourly', 'advaipbl_threat_score_decay_event');
}
// Tarea de análisis de firmas (cada 30 minutos)
if (!wp_next_scheduled('advaipbl_signature_analysis_event')) {
wp_schedule_event(time(), '10_minutes', 'advaipbl_signature_analysis_event');
}
// Tarea de actualización de la base de datos GeoIP (diaria)
if ( ! wp_next_scheduled( 'advaipbl_update_geoip_db_event' ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'advaipbl_update_geoip_db_event' );
}
// Tarea de limpieza de la caché (diaria)
if (!wp_next_scheduled('advaipbl_cleanup_expired_cache_event')) {
wp_schedule_event(time(), 'daily', 'advaipbl_cleanup_expired_cache_event');
}
// Asegura que el cron de Spamhaus esté programado si la opción está activa.
if (!empty($this->options['enable_spamhaus_asn']) && '1' === $this->options['enable_spamhaus_asn']) {
if (!wp_next_scheduled('advaipbl_update_spamhaus_list_event')) {
// Si no está programado, lo programamos ahora para que se ejecute diariamente.
wp_schedule_event(time(), 'daily', 'advaipbl_update_spamhaus_list_event');
}
} else {
// Si la opción está desactivada, nos aseguramos de que no haya ningún cron programado.
wp_clear_scheduled_hook('advaipbl_update_spamhaus_list_event');
}
}
public function purge_old_logs() {
$this->options = get_option(self::OPTION_SETTINGS, []);
$retention_days = (int) ($this->options['log_retention_days'] ?? 30);
if ($retention_days <= 0) return;
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_logs';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query($wpdb->prepare("DELETE FROM $table_name WHERE timestamp < DATE_SUB(NOW(), INTERVAL %d DAY)", $retention_days));
}
public function check_database_update() {
static $already_checked = false;
if ( $already_checked ) {
return;
}
$current_db_version = get_option('advaipbl_db_version', '1.0');
if ( version_compare($current_db_version, ADVAIPBL_DB_VERSION, '<') ) {
self::setup_database_tables();
update_option('advaipbl_db_version', ADVAIPBL_DB_VERSION);
}
// Ejecutamos nuestras migraciones de datos aquí.
$this->migrate_whitelist_format();
$this->migrate_blocked_ips_to_table();
$this->cleanup_legacy_options();
$already_checked = true;
}
/**
* Limpia las opciones de la base de datos de versiones muy antiguas del plugin.
* Se ejecuta una sola vez gracias a un sistema de versionado.
*/
private function cleanup_legacy_options() {
if (get_option('advaipbl_legacy_options_cleaned') === self::LEGACY_OPTIONS_CLEANUP_VERSION) {
return;
}
$legacy_options_to_delete = [
'advaipbl_ips_bloqueadas_manual',
'advaipbl_ips_bloqueadas_404',
'advaipbl_ips_bloqueadas_403',
'advaipbl_ips_bloqueadas_login',
'advaipbl_ips_bloqueadas_geoblock',
'advaipbl_ips_bloqueadas_honeypot',
'advaipbl_ips_bloqueadas_user_agent',
'advaipbl_ips_bloqueadas_waf',
'advaipbl_ips_bloqueadas_threat_score',
'advaipbl_ips_bloqueadas_rate_limit',
'advaipbl_ips_bloqueadas_asn',
'advaipbl_ips_bloqueadas_xmlrpc_block',
'advanced-ip-blocker_ips_bloqueadas_404',
'advanced-ip-blocker_ips_bloqueadas_403',
'advanced-ip-blocker_ips_bloqueadas_login',
'advanced-ip-blocker_ips_bloqueadas_honeypot',
'advanced-ip-blocker_ips_bloqueadas_user_agent',
'advanced-ip-blocker_settings',
'advanced-ip-blocker_blocked_user_agents',
'advanced-ip-blocker_whitelisted_user_agents',
'advanced-ip-blocker_ips_whitelist',
'advanced-ip-blocker_ips_bloqueadas_manual',
'advanced-ip-blocker_honeypot_urls',
];
foreach ($legacy_options_to_delete as $option_name) {
delete_option($option_name);
}
update_option('advaipbl_legacy_options_cleaned', self::LEGACY_OPTIONS_CLEANUP_VERSION);
}
/**
* Migrates the old whitelist format ( ip => timestamp ) to the new format ( ip => [ 'timestamp' => ..., 'detail' => ... ] ).
* Runs only if needed.
*/
private function migrate_whitelist_format() {
$whitelist = get_option('advaipbl_ips_whitelist', []);
if ( empty($whitelist) ) {
return; // Nada que migrar.
}
// Comprobamos el formato de la primera entrada para ver si necesita migración.
$first_entry = reset($whitelist);
if ( is_array($first_entry) && isset($first_entry['timestamp']) ) {
return; // Ya está en el nuevo formato.
}
$migrated_whitelist = [];
foreach ($whitelist as $ip => $timestamp) {
if ( is_string($ip) && filter_var($ip, FILTER_VALIDATE_IP) ) {
$migrated_whitelist[$ip] = [
'timestamp' => is_numeric($timestamp) ? $timestamp : time(),
'detail' => __('Migrated from old format', 'advanced-ip-blocker')
];
}
}
update_option( 'advaipbl_ips_whitelist', $migrated_whitelist );
$this->log_event('Whitelist data format successfully migrated.', 'info');
}
/**
* Migra las IPs bloqueadas desde múltiples opciones de WP a la nueva tabla dedicada.
* Se ejecuta una sola vez y limpia las opciones antiguas después de la migración.
*/
private function migrate_blocked_ips_to_table() {
// Usamos una opción para asegurarnos de que esto solo se ejecute una vez.
if (get_option('advaipbl_ip_table_migration_complete')) {
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_blocked_ips';
$definitions = $this->get_all_block_type_definitions();
$migrated_count = 0;
foreach ($definitions as $type => $def) {
// Solo migramos tipos que tenían una lista persistente.
if (empty($def['option_key'])) {
continue;
}
$list = get_option($def['option_key'], []);
if (!is_array($list) || empty($list)) {
// Borramos la opción si está vacía para limpiar la BD.
delete_option($def['option_key']);
continue;
}
foreach ($list as $ip_or_range => $block_data) {
$timestamp = is_array($block_data) && isset($block_data['timestamp']) ? (int)$block_data['timestamp'] : time();
$reason = is_array($block_data) && isset($block_data['detail']) ? $block_data['detail'] : '';
$duration_in_seconds = 10 * YEAR_IN_SECONDS; // Permanente por defecto
if (!empty($def['duration_key'])) {
$duration_in_minutes = (int) ($this->options[$def['duration_key']] ?? 1440);
if ($duration_in_minutes > 0) {
$duration_in_seconds = $duration_in_minutes * 60;
}
}
$expires_at = $timestamp + $duration_in_seconds;
// Insertamos la fila en la nueva tabla.
$wpdb->insert(
$table_name,
[
'ip_range' => $ip_or_range,
'block_type' => $type,
'timestamp' => $timestamp,
'expires_at' => $expires_at,
'reason' => $reason,
]
);
$migrated_count++;
}
// Una vez migradas todas las IPs de esta opción, la borramos.
delete_option($def['option_key']);
}
if ($migrated_count > 0) {
$this->log_event(sprintf('Successfully migrated %d blocked entries to the new database table.', $migrated_count), 'info');
}
// Marcamos la migración como completada para no volver a ejecutarla.
update_option('advaipbl_ip_table_migration_complete', true);
}
public static function setup_database_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
// Tabla de Logs (existente)
$table_name_logs = $wpdb->prefix . 'advaipbl_logs';
$sql_logs = "CREATE TABLE $table_name_logs (
log_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
timestamp DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
ip VARCHAR(100) NOT NULL,
log_type VARCHAR(20) NOT NULL,
level VARCHAR(20) NOT NULL,
message TEXT NOT NULL,
details LONGTEXT NULL,
PRIMARY KEY (log_id),
KEY ip (ip(16)),
KEY log_type (log_type),
KEY timestamp (timestamp)
) $charset_collate;";
dbDelta($sql_logs);
// Tabla de Cola de Notificaciones (existente)
$table_name_queue = $wpdb->prefix . 'advaipbl_notifications_queue';
$sql_queue = "CREATE TABLE $table_name_queue (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
timestamp DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
ip VARCHAR(100) NOT NULL,
block_type VARCHAR(20) NOT NULL,
reason TEXT NOT NULL,
PRIMARY KEY (id),
KEY timestamp (timestamp)
) $charset_collate;";
dbDelta($sql_queue);
// Tabla de Puntuación de Amenaza
$table_name_scores = $wpdb->prefix . 'advaipbl_ip_scores';
$sql_scores = "CREATE TABLE $table_name_scores (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
ip VARCHAR(100) NOT NULL,
score INT(11) NOT NULL DEFAULT 0,
last_event_timestamp INT(11) UNSIGNED NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
log_details TEXT,
PRIMARY KEY (id),
UNIQUE KEY ip (ip),
KEY score (score)
) $charset_collate;";
dbDelta($sql_scores);
// Nueva Tabla de Logs de Peticiones y Firmas
$table_name_requests = $wpdb->prefix . 'advaipbl_request_log';
$sql_requests = "CREATE TABLE $table_name_requests (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
timestamp INT(11) UNSIGNED NOT NULL,
ip_hash CHAR(64) NOT NULL,
request_uri TEXT NOT NULL,
user_agent TEXT,
request_headers TEXT,
request_method VARCHAR(10) NOT NULL,
signature_hash CHAR(64) NOT NULL,
PRIMARY KEY (id),
KEY signature_hash (signature_hash),
KEY timestamp (timestamp)
) $charset_collate;";
dbDelta($sql_requests);
// Nueva Tabla de Firmas Maliciosas Identificadas
$table_name_signatures = $wpdb->prefix . 'advaipbl_malicious_signatures';
$sql_signatures = "CREATE TABLE $table_name_signatures (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
signature_hash CHAR(64) NOT NULL,
reason VARCHAR(255) NOT NULL,
first_seen INT(11) UNSIGNED NOT NULL,
last_seen INT(11) UNSIGNED NOT NULL,
expires_at INT(11) UNSIGNED NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY signature_hash (signature_hash),
KEY expires_at (expires_at)
) $charset_collate;";
dbDelta($sql_signatures);
// Nueva Tabla de Caché
$table_name_cache = $wpdb->prefix . 'advaipbl_cache';
$sql_cache = "CREATE TABLE $table_name_cache (
cache_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
cache_key VARCHAR(191) NOT NULL,
cache_value LONGTEXT NOT NULL,
expires_at INT(11) UNSIGNED NOT NULL,
PRIMARY KEY (cache_id),
UNIQUE KEY cache_key (cache_key)
) $charset_collate;";
dbDelta($sql_cache);
// Nueva Tabla de Endpoint Lockdowns
$table_name_lockdowns = $wpdb->prefix . 'advaipbl_endpoint_lockdowns';
$sql_lockdowns = "CREATE TABLE $table_name_lockdowns (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
endpoint_key VARCHAR(50) NOT NULL,
reason VARCHAR(255) NOT NULL,
created_at INT(11) UNSIGNED NOT NULL,
expires_at INT(11) UNSIGNED NOT NULL,
details TEXT,
PRIMARY KEY (id),
KEY endpoint_key (endpoint_key),
KEY expires_at (expires_at)
) $charset_collate;";
dbDelta($sql_lockdowns);
// Nueva Tabla de IP Bloqueadas
$table_name_blocked_ips = $wpdb->prefix . 'advaipbl_blocked_ips';
$sql_blocked_ips = "CREATE TABLE $table_name_blocked_ips (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
ip_range VARCHAR(100) NOT NULL,
block_type VARCHAR(20) NOT NULL,
timestamp INT(11) UNSIGNED NOT NULL,
expires_at INT(11) UNSIGNED NOT NULL,
reason TEXT,
PRIMARY KEY (id),
UNIQUE KEY ip_range (ip_range(40)),
KEY block_type (block_type),
KEY expires_at (expires_at)
) $charset_collate;";
dbDelta($sql_blocked_ips);
update_option('advaipbl_db_version', ADVAIPBL_DB_VERSION);
}
public static function get_formatted_datetime($timestamp) { if (!is_numeric($timestamp)) { $timestamp = strtotime($timestamp); } if ($timestamp === false) { return __('Invalid date', 'advanced-ip-blocker'); } $options = get_option('advaipbl_settings', []); $timezone_string = $options['log_timezone'] ?? wp_timezone_string(); try { $date = new DateTime('@' . $timestamp); $timezone = new DateTimeZone($timezone_string); $date->setTimezone($timezone); return $date->format('Y-m-d H:i:s T'); } catch (Exception $e) { $format = get_option('date_format') . ' ' . get_option('time_format'); return date_i18n($format, $timestamp); } }
/**
* Parsea una cadena de User-Agent y devuelve una descripción legible.
*
* @param string $ua La cadena de User-Agent.
* @return string Una descripción formateada (ej. "Chrome on Windows").
*/
private static function parse_user_agent($ua) {
if (empty($ua) || $ua === __('Unknown', 'advanced-ip-blocker')) {
return __('Unknown device', 'advanced-ip-blocker');
}
$os = __('Unknown OS', 'advanced-ip-blocker');
$browser = __('Unknown Browser', 'advanced-ip-blocker');
// Mapeos para una detección más precisa
$os_map = [
'/windows nt 10/i' => 'Windows 10/11',
'/windows nt 6.3/i' => 'Windows 8.1',
'/windows nt 6.2/i' => 'Windows 8',
'/windows nt 6.1/i' => 'Windows 7',
'/windows nt 6.0/i' => 'Windows Vista',
'/windows nt 5.1/i' => 'Windows XP',
'/macintosh|mac os x/i' => 'Mac OS',
'/linux/i' => 'Linux',
'/android/i' => 'Android',
'/iphone/i' => 'iPhone',
'/ipad/i' => 'iPad',
];
$browser_map = [
'/msie/i' => 'Internet Explorer',
'/firefox/i' => 'Firefox',
'/safari/i' => 'Safari',
'/chrome/i' => 'Chrome',
'/edge/i' => 'Edge',
'/opera/i' => 'Opera',
'/netscape/i' => 'Netscape',
'/maxthon/i' => 'Maxthon',
'/konqueror/i' => 'Konqueror',
'/mobile/i' => 'Mobile Browser',
];
foreach ($os_map as $regex => $value) {
if (preg_match($regex, $ua)) {
$os = $value;
break;
}
}
foreach ($browser_map as $regex => $value) {
if (preg_match($regex, $ua)) {
$browser = $value;
break;
}
}
// Corrección para que "Chrome" no se identifique como "Safari"
if ($browser === 'Safari' && strpos(strtolower($ua), 'chrome') !== false) {
$browser = 'Chrome';
}
/* translators: 1: Browser name, 2: Operating System name. */
return sprintf(__('%1$s on %2$s', 'advanced-ip-blocker'), $browser, $os);
}
/**
* Obtiene la IP del visitante actual. Actúa como un wrapper para get_ip_intelligence()
*
* @return string La IP del visitante.
*/
public function get_client_ip() {
if ( null !== $this->client_ip ) {
return $this->client_ip;
}
$ip_data = $this->get_ip_intelligence();
$this->client_ip = $ip_data['visitor_ip'] ?? '0.0.0.0';
return $this->client_ip;
}
/**
* Obtiene un análisis completo y verificado de las direcciones IP de una petición.
*/
public function get_ip_intelligence() {
// Usamos un caché estático por petición para evitar trabajo repetido.
static $result = null;
if ( null !== $result ) {
return $result;
}
// 1. Inicializar
$result = [
'visitor_ip' => null,
'visitor_ip_source' => null,
'proxy_chain' => [],
'is_proxied' => false,
'cdn_info' => ['provider' => 'None', 'ray_id' => null, 'country' => null],
];
// 2. Obtener cabeceras
$headers = [];
if (function_exists('apache_request_headers')) {
$headers = apache_request_headers();
}
foreach ($_SERVER as $key => $value) {
if (strpos($key, 'HTTP_') === 0) {
$header_key = str_replace('_', '-', strtolower(substr($key, 5)));
$headers[$header_key] = $value;
}
}
$headers = array_change_key_case($headers, CASE_LOWER);
// 3. Jerarquía de Confianza (Detección Primaria)
$ip_found = null;
if (isset($headers['cf-connecting-ip'])) {
$ip_found = filter_var($headers['cf-connecting-ip'], FILTER_VALIDATE_IP);
$result['visitor_ip_source'] = 'Header: CF-Connecting-IP';
$result['is_proxied'] = true;
$result['cdn_info']['provider'] = 'Cloudflare';
} elseif (isset($headers['true-client-ip'])) {
$ip_found = filter_var($headers['true-client-ip'], FILTER_VALIDATE_IP);
$result['visitor_ip_source'] = 'Header: True-Client-IP';
$result['is_proxied'] = true;
} elseif (isset($headers['x-real-ip'])) {
$ip_found = filter_var($headers['x-real-ip'], FILTER_VALIDATE_IP);
$result['visitor_ip_source'] = 'Header: X-Real-IP';
$result['is_proxied'] = true;
} elseif (isset($headers['x-forwarded-for'])) {
$result['is_proxied'] = true;
}
$ip_found = $ip_found ?: $this->get_remote_addr();
$result['visitor_ip_source'] = $result['visitor_ip_source'] ?: 'Direct Connection (REMOTE_ADDR)';
// 4. Corrección de Proxy y Detección de Spoofing
if ($result['is_proxied']) {
$forwarded_headers = ['x-forwarded-for', 'x-real-ip', 'true-client-ip', 'cf-connecting-ip'];
foreach ($forwarded_headers as $header_name) {
if (isset($headers[$header_name])) {
$ips_in_header = array_map('trim', explode(',', $headers[$header_name]));
foreach ($ips_in_header as $ip_candidate) {
if (filter_var($ip_candidate, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
$ip_found = $ip_candidate;
$result['visitor_ip_source'] = 'Header: ' . ucwords(str_replace('-', ' ', $header_name)) . ' (Proxy)';
break 2;
}
}
}
}
}
$result['visitor_ip'] = $ip_found;
// 5. Poblar la cadena de proxies
$remote_addr = $this->get_remote_addr();
if ($result['is_proxied'] && $remote_addr && $remote_addr !== $result['visitor_ip']) {
if (!in_array($remote_addr, $result['proxy_chain'])) {
$result['proxy_chain'][] = $remote_addr;
}
}
return $result;
}
/**
* Gets the server's public IP address using a robust method.
* It first tries the fast $_SERVER variable, then falls back to an external API call.
*
* @return string|null The server's IP address or null if not found.
*/
private function get_server_ip() {
// Método 1: El más rápido, si está disponible y es una IP pública.
$server_ip = $_SERVER['SERVER_ADDR'] ?? null;
if ( $server_ip && filter_var( $server_ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
return $server_ip;
}
// Método 2: Fallback a un servicio externo (muy fiable).
// Hacemos una petición a un servicio que nos devuelve nuestra propia IP.
$response = wp_remote_get( 'https://api.ipify.org' );
if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) {
$ip = trim( wp_remote_retrieve_body( $response ) );
if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
return $ip;
}
}
// Si ambos métodos fallan, no podemos determinar la IP.
return null;
}
public function log_event( $message, $level = 'info', $details = [] ) {
if ( empty( $this->options['enable_logging'] ) ) {
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_logs';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
@$wpdb->insert(
$table_name,
[
'timestamp' => current_time( 'mysql', 1 ),
'ip' => $this->get_client_ip(),
'log_type' => 'general',
'level' => $level,
'message' => $message,
'details' => ! empty( $details ) ? wp_json_encode( $details ) : null,
]
);
}
public function log_specific_error($type, $ip, $extra_data = [], $level = 'warning') {
if (empty($this->options['enable_logging'])) {
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_logs';
$details = [
'url' => $this->get_current_request_uri(),
'method' => $this->get_request_method(),
'referrer' => $this->get_http_referer(),
'user_agent' => $this->get_user_agent(),
];
if (is_array($extra_data)) {
$details = array_merge($details, $extra_data);
}
if (!isset($details['lat']) || !isset($details['lon'])) {
$location_data = $this->geolocation_manager->fetch_location($ip);
if ($location_data && empty($location_data['error'])) {
$details['country'] = $location_data['country'] ?? null;
$details['country_code'] = $location_data['country_code'] ?? null;
$details['city'] = $location_data['city'] ?? null;
$details['lat'] = $location_data['lat'] ?? null;
$details['lon'] = $location_data['lon'] ?? null;
}
}
$message = '';
switch ($type) {
/* translators: %s: Username. */
case 'login': $message = sprintf(__('Failed login attempt for user: %s', 'advanced-ip-blocker'), $details['username'] ?? __('unknown', 'advanced-ip-blocker')); break;
/* translators: %s: WAF rule. */
case 'waf': $message = sprintf(__('Request blocked by WAF rule: %s', 'advanced-ip-blocker'), $details['rule'] ?? __('unknown rule', 'advanced-ip-blocker')); break;
/* translators: 1: Number of request, 2: Seconds */
case 'rate_limit': $message = sprintf(__('Rate limit exceeded: %1$d requests in %2$d seconds.', 'advanced-ip-blocker'), $details['count'] ?? 0, $details['window'] ?? 0); break;
/* translators: %s: AS number. */
case 'asn': $message = sprintf(__('Request blocked due to blacklisted ASN: %s', 'advanced-ip-blocker'), $details['asn_number'] ?? 'N/A'); break;
/* translators: %s: User-Agent. */
case 'xmlrpc_block': $message = $details['_reason'] ?? sprintf(__('Blocked untrusted XML-RPC request from User-Agent: %s', 'advanced-ip-blocker'), $details['user_agent'] ?? 'N/A'); break;
/* translators: %s: Honeypot URL. */
case 'honeypot': $message = sprintf(__('Honeypot URL accessed: %s', 'advanced-ip-blocker'), $details['url'] ?? 'N/A'); break;
/* translators: %s: Country. */
case 'geoblock': $message = sprintf(__('Blocked access from country: %s', 'advanced-ip-blocker'), $details['country'] ?? 'N/A'); break;
/* translators: %s: User-Agent. */
case 'user_agent': $message = sprintf(__('Blocked due to User-Agent match: %s', 'advanced-ip-blocker'), $details['user_agent'] ?? 'N/A'); break;
case 'threat_score': $message = $details['_reason'] ?? __('Threat score threshold exceeded', 'advanced-ip-blocker'); break;
case 'signature_flagged': $message = $details['_reason'] ?? __('New Attack Signature Identified.', 'advanced-ip-blocker'); break;
case 'signature_challenge': $message = $details['_reason'] ?? __('Signature challenge not overcome.', 'advanced-ip-blocker'); break;
case 'endpoint_challenge': $message = $details['reason'] ?? __('Endpoint challenge served.', 'advanced-ip-blocker'); break;
/* translators: %s: Default error reason. */
default: $message = sprintf(__('A %s error occurred.', 'advanced-ip-blocker'), strtoupper($type)); break;
}
// 1. Insertar el log específico del evento (comportamiento actual)
@$wpdb->insert(
$table_name,
[
'timestamp' => current_time('mysql', 1),
'ip' => $ip,
'log_type' => $type,
'level' => $level,
'message' => $message,
'details' => wp_json_encode($details)
]
);
// 2. Si el evento es un bloqueo (nivel 'critical'), añadimos una entrada de auditoría al log general.
if ('critical' === $level) {
$type_label = ucwords(str_replace('_', ' ', $type));
$general_log_message = sprintf(
/* translators: 1: IP Address, 2: Block Type Label (e.g., "WAF Block") */
__('IP %1$s was automatically blocked. Reason: %2$s.', 'advanced-ip-blocker'),
$ip,
$type_label
);
$this->log_event($general_log_message, 'critical', ['original_log_type' => $type]);
}
}
public function registrar_intento_login_fallido( $username ) {
$this->handle_error( 'login', [ 'username' => $username ] );
}
private function handle_error($type, $extra_data = []) {
if (is_user_logged_in() && current_user_can('unfiltered_html')) {
return;
}
$ip = $this->get_client_ip();
if ($this->is_whitelisted($ip)) {
return;
}
$all_block_types = ['geoblock', 'honeypot', 'manual', '404', '403', 'login', 'user_agent', 'waf', 'rate_limit', 'asn', 'xmlrpc_block', 'threat_score'];
foreach ($all_block_types as $reason_type) {
if (get_transient('advaipbl_bloqueo_' . $reason_type . '_' . md5($ip))) {
$this->log_specific_error($type, $ip, $extra_data);
return;
}
}
// Si es un error 404 o 403, nos aseguramos de que $extra_data contenga la URL.
if (in_array($type, ['404', '403']) && !isset($extra_data['url'])) {
$extra_data['url'] = $this->get_current_request_uri();
}
$this->log_specific_error($type, $ip, $extra_data);
if ('login' === $type && !empty($this->options['enable_login_lockdown'])) { // Usaremos una nueva opción 'enable_login_lockdown'
$this->increment_login_lockdown_counter();
}
if (!empty($this->options['enable_threat_scoring'])) {
$points_to_add = (int) ($this->options['score_' . $type] ?? 0);
if ($points_to_add > 0) {
// Pasamos $extra_data, que ahora contiene la URL, a increment_score.
$new_score = $this->threat_score_manager->increment_score($ip, $points_to_add, $type, $extra_data);
$threshold = (int) ($this->options['threat_score_threshold'] ?? 100);
$should_block = false;
if ($new_score >= $threshold || $points_to_add >= 100) {
$should_block = true;
}
if ($should_block) {
$trigger_detail = '';
if ($type === 'login') {
$trigger_detail = 'Login attempt for user: ' . ($extra_data['username'] ?? 'N/A');
} else {
$trigger_detail = $this->get_current_request_uri();
}
/* translators: 1: Points number, 2: An IP will be blocked when its threat score reaches or exceeds this value. */
$score_summary = sprintf(__('Score %1$d/%2$d', 'advanced-ip-blocker'), $new_score, $threshold);
$reason_message = sprintf( /* translators: 1: Score, 2: Block type, 3: Triger. (eg. Score 100/100 via User agent | Trigger: Blocked User-Agent pattern: MJ12bot.) */
__('%1$s via %2$s | Trigger: %3$s', 'advanced-ip-blocker'),
$score_summary,
ucfirst($type),
$trigger_detail
);
$locations = $this->session_manager->get_cached_locations([$ip]);
$location_data = $locations[$ip] ?? null;
$geo_details = [];
if ($location_data && empty($location_data['error'])) {
$geo_details['country'] = $location_data['country'] ?? null;
$geo_details['country_code'] = $location_data['country_code'] ?? null;
$geo_details['city'] = $location_data['city'] ?? null;
$geo_details['lat'] = $location_data['lat'] ?? null;
$geo_details['lon'] = $location_data['lon'] ?? null;
}
$log_details = array_merge([
'final_score' => $new_score,
'triggering_event' => $type,
'_reason' => $reason_message,
'block_reason_code' => $block_reason_code // Dato extra para depuración
], $geo_details);
$this->block_ip_instantly($ip, 'threat_score', $reason_message, $log_details);
}
}
} else {
// LÓGICA ANTIGUA (FALLBACK)
$s = $this->options;
$minute_in_seconds = 60;
$threshold = (int) ($s["threshold_{$type}"] ?? 5);
$window = (int) ($s["transient_expiration_{$type}"] ?? 60) * $minute_in_seconds;
$count_key = "advaipbl_errores_{$type}_" . md5($ip);
$errors = (int) get_transient($count_key) + 1;
set_transient($count_key, $errors, $window);
if ($errors >= $threshold) {
$trigger_detail = ($type === 'login')
? ($extra_data['username'] ?? 'N/A')
: $this->get_current_request_uri();
self::$block_queue = [
'ip' => $ip,
'type' => $type,
'trigger' => $trigger_detail,
'to' => !empty($this->options['notification_email']) && is_email($this->options['notification_email']) ? $this->options['notification_email'] : get_option('admin_email'),
'options' => $this->options,
'errors' => $errors,
];
if (!self::$shutdown_hook_registered) {
register_shutdown_function([$this, 'execute_shutdown_block']);
self::$shutdown_hook_registered = true;
}
delete_transient($count_key);
}
}
}
/**
* Centraliza la decisión de cómo manejar un evento de amenaza instantáneo (WAF, Honeypot, etc.).
* Si el sistema de puntuación está activo, suma puntos. Si no, bloquea directamente.
*
* @param string $ip La dirección IP del atacante.
* @param string $type El tipo de evento (ej. 'waf', 'honeypot').
* @param string $reason_message La razón detallada del evento.
* @param array $log_data Datos adicionales para el log.
*/
public function handle_threat_event($ip, $type, $reason_message, $log_data) {
$scoring_system_active = !empty($this->options['enable_threat_scoring']);
$this->log_specific_error($type, $ip, $log_data, 'warning');
if ($scoring_system_active) {
$points_to_add = (int) ($this->options['score_' . $type] ?? 0);
if ($points_to_add <= 0) { return; }
$new_score = $this->threat_score_manager->increment_score($ip, $points_to_add, $type, $log_data);
$threshold = (int) ($this->options['threat_score_threshold'] ?? 100);
if ($new_score >= $threshold) {
/* translators: 1: Points number, 2: An IP will be blocked when its threat score reaches or exceeds this value. */
$score_summary = sprintf(__('Score %1$d/%2$d', 'advanced-ip-blocker'), $new_score, $threshold);
$block_reason_detail = sprintf( /* translators: 1: Points number, 2: An IP will be blocked when its threat score reaches or exceeds this value, 3: Trigger. */
__('%1$s via %2$s | Trigger: %3$s', 'advanced-ip-blocker'),
$score_summary, ucfirst(str_replace('_', ' ', $type)), $reason_message
);
$locations = $this->session_manager->get_cached_locations([$ip]);
$location_data = $locations[$ip] ?? null;
$geo_details = [];
if ($location_data && empty($location_data['error'])) {
$geo_details = ['country' => $location_data['country'] ?? null, 'country_code' => $location_data['country_code'] ?? null, 'city' => $location_data['city'] ?? null];
}
$block_log_details = array_merge($log_data, $geo_details, ['_reason' => $block_reason_detail]);
$this->block_ip_instantly($ip, 'threat_score', $block_reason_detail, $block_log_details);
}
} else {
$this->block_ip_instantly($ip, $type, $reason_message, $log_data);
}
}
public function execute_shutdown_block() {
if (empty(self::$block_queue)) {
return;
}
$q = self::$block_queue;
$ip = $q['ip'];
$original_ip_for_log = $ip; // Guardamos la IP original para los logs
$extra_data_for_log = ['trigger' => $q['trigger'], 'count' => ($q['errors'] ?? 1)];
if ( in_array( $ip, [ '127.0.0.1', '::1' ], true ) ) {
$remote_addr = $this->get_remote_addr();
if ( $remote_addr && ! in_array( $remote_addr, [ '127.0.0.1', '::1' ], true ) ) {
$ip = $remote_addr; // Cambiamos el objetivo del bloqueo
$extra_data_for_log['_spoofed_ip'] = $original_ip_for_log;
} else {
return; // Petición interna genuina, no bloquear.
}
}
global $wpdb;
$lock_key = 'lock_blocking_shutdown_' . md5($ip);
$lock_acquired = $wpdb->query($wpdb->prepare(
"INSERT IGNORE INTO {$wpdb->prefix}advaipbl_cache (cache_key, cache_value, expires_at) VALUES (%s, %s, %d)",
$lock_key, '1', time() + 15
));
if ( ! $lock_acquired ) {
return;
}
$table_name_blocked = $wpdb->prefix . 'advaipbl_blocked_ips';
$existing_block = $wpdb->get_var($wpdb->prepare("SELECT id FROM {$table_name_blocked} WHERE ip_range = %s", $ip));
if ($existing_block) {
$wpdb->delete("{$wpdb->prefix}advaipbl_cache", ['cache_key' => $lock_key]);
return;
}
$type = $q['type'];
$trigger = $q['trigger'];
$to = $q['to'];
$options = $q['options'];
$errors = (isset($q['errors']) && is_numeric($q['errors'])) ? $q['errors'] : 1;
// 1. Mapear el tipo de bloqueo a su constante de opción correspondiente.
$option_key_map = [
'404' => self::OPTION_BLOCKED_404,
'403' => self::OPTION_BLOCKED_403,
'login' => self::OPTION_BLOCKED_LOGIN,
];
$option_key = $option_key_map[$type] ?? null;
if ( ! $option_key ) {
$wpdb->delete("{$wpdb->prefix}advaipbl_cache", ['cache_key' => $lock_key]); // Liberar cerrojo antes de salir
return;
}
// 2. Acciones de bloqueo (transient y opción persistente)
$duration = (int) ($options["duration_{$type}"] ?? 120) * 60;
$block_key = "advaipbl_bloqueo_{$type}_" . md5($ip);
set_transient($block_key, true, $duration);
$list = get_option($option_key, []);
if (!is_array($list)) $list = [];
$list[$ip] = ['timestamp' => time(), 'detail' => $trigger];
update_option($option_key, $list);
// 3. Obtener datos de geolocalización COMPLETOS
$location_data = $this->geolocation_manager->fetch_location($ip);
$geo_details = [];
if ($location_data && empty($location_data['error'])) {
$geo_details['country'] = $location_data['country'] ?? null;
$geo_details['country_code'] = $location_data['country_code'] ?? null;
$geo_details['city'] = $location_data['city'] ?? null;
$geo_details['lat'] = $location_data['lat'] ?? null;
$geo_details['lon'] = $location_data['lon'] ?? null;
}
// 4. Lógica de email y logs
$notification_enabled = !empty($options['enable_email_notifications']) && '1' === $options['enable_email_notifications'];
$frequency = $options['notification_frequency'] ?? 'disabled';
$email_sent = null;
$reason = '';
switch ($type) {
case '404':
case '403':
$reason = sprintf( /* translators: 1: %1\$d errors number, 2: %2\$s errors number, 3: Triggering URL. */ __("Generated %1\$d %2\$s errors. Triggering URL: %3\$s", 'advanced-ip-blocker'), $errors, strtoupper($type), $trigger);
break;
case 'login':
$reason = sprintf( /* translators: 1: %1$d failed login attempts, 2: %2$s Username. */ __('Made %1$d failed login attempts for user: %2$s', 'advanced-ip-blocker'), $errors, $trigger);
break;
}
if ($notification_enabled && $frequency === 'instant') {
$site_name = get_bloginfo( 'name' );
/* translators: %s: Site name. */
$email_subject = sprintf(__('[%s] Security Alert: IP Automatically Blocked', 'advanced-ip-blocker'), $site_name);
$template_title = __('Security Alert: IP Blocked', 'advanced-ip-blocker');
/* translators: %d: Minutes. */
$duration_text = sprintf(__('%d minutes', 'advanced-ip-blocker'), (int) ($options["duration_{$type}"] ?? 120));
$button_url = admin_url('admin.php?page=advaipbl_ip_management&sub-tab=blocked_ips');
$button_style = "background-color: #2271b1; color: #ffffff; padding: 12px 25px; text-decoration: none; border-radius: 4px; display: inline-block; font-weight: bold;";
$content_html =
'
' . esc_html__( "An IP address has been automatically blocked on your website.", 'advanced-ip-blocker' ) . '
display_dashboard_tab(); break;
case 'status': $this->display_status_tab(); break;
case 'general_settings': $this->display_general_settings_tab(); break;
case '2fa_management': $this->display_2fa_management_tab(); break;
case 'import_export': $this->render_import_export_controls_callback(); break;
case 'waf': $this->display_waf_tab(); break;
case 'user_agents': $this->display_user_agents_tab(); break;
case 'honeypot': $this->display_honeypot_tab(); break;
case 'asn_blocking': $this->display_asn_blocking_tab(); break;
case 'blocked_ips': $this->display_blocked_ips_tab(); break;
case 'blocked_signatures': $this->display_blocked_signatures_tab(); break;
case 'blocked_endpoints': $this->display_blocked_endpoints_tab(); break;
case 'whitelist': $this->display_whitelist_tab(); break;
case 'security_log': $this->display_security_log_tab(); break;
case 'general_log': $this->display_general_log_tab(); break;
case 'ip_trust_log': $this->display_ip_trust_log_tab(); break;
case 'user_sessions': $this->session_manager->display_admin_page(); break;
case 'cron_logs': $this->display_cron_logs_tab(); break;
case 'credits': $this->display_credits_tab(); break;
default: echo '
%s',
$is_enabled ? 'enabled' : 'disabled',
$is_enabled ? esc_html__('Active', 'advanced-ip-blocker') : esc_html__('Inactive', 'advanced-ip-blocker')
);
printf(
wp_kses(
/* translators: %1$s: Status tag (Active/Inactive), %2$s: Link to Settings, %3$s: Link to Profile. */
__('Two-Factor Authentication (2FA) is currently %1$s. You can change this in the Settings. When enabled, users will see the 2FA setup section in their Profile.', 'advanced-ip-blocker'),
[
'span' => ['class' => true],
'a' => ['href' => true],
]
),
$status_tag,
esc_url($settings_url),
esc_url($profile_url)
);
?>
';
}
/**
* 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 '
display_log_table_generic($security_log_types, ['critical', 'warning']);
}
/**
* Muestra la pestaña de logs generales (auditoría), restaurada a su formato original.
*/
private function display_general_log_tab() {
if (empty($this->options['enable_logging'])) {
echo '
' . esc_html__('General event logging is disabled in the "Settings" tab.', 'advanced-ip-blocker') . '
%s',
$is_enabled ? 'enabled' : 'disabled',
$is_enabled ? esc_html__('Active', 'advanced-ip-blocker') : esc_html__('Inactive', 'advanced-ip-blocker')
);
printf(
wp_kses(
/* translators: 1: Status tag (Active/Inactive), 2: Link to settings page. */
__('The Threat Scoring System is currently %1$s. You can change this in the Settings.', 'advanced-ip-blocker'),
[ 'span' => ['class' => true], 'a' => ['href' => []] ]
),
$status_tag,
esc_url($settings_url)
);
?>
0"];
if (!empty($search_term)) {
$where_clauses[] = $wpdb->prepare("ip LIKE %s", '%' . $wpdb->esc_like($search_term) . '%');
}
$where_sql = implode(' AND ', $where_clauses);
$total_items = $wpdb->get_var("SELECT COUNT(id) FROM {$table_name} WHERE {$where_sql}");
$total_pages = ceil($total_items / $per_page);
$offset = ($current_page - 1) * $per_page;
$items = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$table_name} WHERE {$where_sql} ORDER BY " . esc_sql($orderby) . " " . esc_sql($order) . " LIMIT %d OFFSET %d",
$per_page, $offset
), ARRAY_A);
$ips_on_page = wp_list_pluck($items, 'ip');
$locations = $this->session_manager->get_cached_locations($ips_on_page);
$threshold = (int) ($this->options['threat_score_threshold'] ?? 100);
?>