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') . '

' . '' . '' . '' . '' . '
' . esc_html__('Signature Hash', 'advanced-ip-blocker') . '' . esc_html($short_hash) . '
' . esc_html__('Reason', 'advanced-ip-blocker') . '' . esc_html($reason) . '
' . esc_html__('Sample User-Agent', 'advanced-ip-blocker') . '' . esc_html($user_agent) . '
' . '

' . esc_html__('You can view and manage all active signatures from the dashboard.', 'advanced-ip-blocker') . '

'. '
' . '' . esc_html__( 'Manage Signatures', '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

'; 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 '

'; $search_term = isset($_GET['s']) ? sanitize_text_field(wp_unslash($_GET['s'])) : ''; $orderby = isset($_GET['orderby']) && in_array($_GET['orderby'], ['timestamp', 'ip'], true) ? sanitize_key($_GET['orderby']) : 'timestamp'; $order = isset($_GET['order']) && in_array(strtolower($_GET['order']), ['asc', 'desc'], true) ? strtolower(sanitize_key($_GET['order'])) : 'desc'; $current_page = isset($_GET['paged']) ? absint($_GET['paged']) : 1; $per_page = isset($_GET['advaipbl_per_page']) ? absint($_GET['advaipbl_per_page']) : 25; global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_logs'; $where_clauses = [$wpdb->prepare("log_type = %s", 'wp_cron')]; if (!empty($search_term)) { $search_like = '%' . $wpdb->esc_like($search_term) . '%'; $where_clauses[] = $wpdb->prepare("(ip LIKE %s OR details LIKE %s)", $search_like, $search_like); } $where_sql = implode(' AND ', $where_clauses); $total_items = $wpdb->get_var("SELECT COUNT(log_id) FROM $table_name WHERE $where_sql"); $total_pages = ceil($total_items / $per_page); $offset = ($current_page - 1) * $per_page; $logs = $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); ?>
render_per_page_selector( $per_page ); ?>
add_query_arg([ 'paged' => '%#%', 's' => $search_term, 'advaipbl_per_page' => $per_page, ]), 'format' => '', 'total' => $total_pages, 'current' => $current_page ]); if ($page_links) echo wp_kses_post($page_links); ?>

print_log_sortable_header(__('Date/Time', 'advanced-ip-blocker'), 'timestamp', $orderby, $order); ?> print_log_sortable_header(__('IP Address', 'advanced-ip-blocker'), 'ip', $orderby, $order); ?>
'; 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) ); ?>




    prefix . 'advaipbl_malicious_signatures'; $is_enabled = !empty($this->options['enable_signature_blocking']); $settings_url = admin_url('admin.php?page=advaipbl_settings_page-settings&sub-tab=general_settings#section-signature_engine'); ?>

    %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 (Enabled/Disabled), 2: Link to settings page. */ __('The Attack Signature Blocking 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) ); ?>

    get_var("SELECT COUNT(id) FROM {$table_name}"); $total_pages = ceil($total_items / $per_page); $offset = ($current_page - 1) * $per_page; $items = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$table_name} ORDER BY " . esc_sql($orderby) . " " . esc_sql($order) . " LIMIT %d OFFSET %d", $per_page, $offset ), ARRAY_A); ?>

    render_per_page_selector($per_page); ?>
    add_query_arg('paged', '%#%'), 'format' => '', 'total' => $total_pages, 'current' => $current_page]); if ($page_links) echo wp_kses_post($page_links); ?>

    print_log_sortable_header(__('Signature Hash', 'advanced-ip-blocker'), 'signature_hash', $orderby, $order); ?> print_log_sortable_header(__('Reason', 'advanced-ip-blocker'), 'reason', $orderby, $order); ?> print_log_sortable_header(__('Last Seen', 'advanced-ip-blocker'), 'last_seen', $orderby, $order); ?> print_log_sortable_header(__('Expires In', 'advanced-ip-blocker'), 'expires_at', $orderby, $order); ?>

    prefix . 'advaipbl_endpoint_lockdowns'; // Array para definir los endpoints que soportan lockdown $lockdown_endpoints = [ 'xmlrpc' => [ 'name' => 'XML-RPC', 'option' => 'enable_xmlrpc_lockdown', 'anchor' => '#sub-section-xmlrpc-lockdown' ], 'login' => [ 'name' => 'Login Page (wp-login.php)', 'option' => 'enable_login_lockdown', 'anchor' => '#sub-section-login-lockdown' ], ]; $settings_base_url = admin_url('admin.php?page=advaipbl_settings_page-settings&sub-tab=general_settings'); $status_parts = []; foreach ($lockdown_endpoints as $key => $details) { $is_enabled = !empty($this->options[$details['option']]); $status_tag = sprintf( '%s', $is_enabled ? 'enabled' : 'disabled', $is_enabled ? esc_html__('Active', 'advanced-ip-blocker') : esc_html__('Inactive', 'advanced-ip-blocker') ); $settings_link = sprintf( '%s', esc_url($settings_base_url . $details['anchor']), esc_html__('Configure', 'advanced-ip-blocker') ); /* translators: 1: Endpoint name (e.g., XML-RPC), 2: Status tag (Active/Inactive), 3: Link to configure */ $status_parts[] = sprintf('%1$s: %2$s (%3$s)', esc_html($details['name']), $status_tag, $settings_link); } ?>

    [], 'span' => ['class' => true], 'a' => ['href' => true], ]); ?>

    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); ?>


    'advaipbl_delete_lockdown', 'lockdown_id' => $item['id'] ]), 'advaipbl_delete_lockdown_' . $item['id'] ); ?>
    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(); ?>
    <?php echo esc_attr__( 'Advanced IP Blocker Logo', 'advanced-ip-blocker' ); ?>


    settings page.', 'advanced-ip-blocker' ), [ 'a' => [ 'href' => [], 'style' => [] ] ] ), esc_url( $settings_url ) ); echo '
    '; /* translators: 1: Plugin name, 2: Plugin version. */ printf( esc_html__( 'Sent by %1$s v%2$s', 'advanced-ip-blocker' ), 'Advanced IP Blocker', esc_html( ADVAIPBL_VERSION ) ); ?>

    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' ) . '

    ' . '

    ' . esc_html__( 'Step 1: Verify Whitelisted IPs', '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 ) ) ) . '

    ' . '

    ' . esc_html__( 'Step 2: Activate Core Defenses', 'advanced-ip-blocker' ) . '

    ' . '

    ' . 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 ) ) ) . '

    ' . '

    ' . esc_html__( 'Step 3: Review Settings', 'advanced-ip-blocker' ) . '

    ' . '

    ' . 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' ) . '

    ' . '' . '' . '' . '' . '' . '
    ' . esc_html__( 'Blocked IP', 'advanced-ip-blocker' ) . '' . esc_html( $ip ) . '
    ' . esc_html__( 'Date and Time', 'advanced-ip-blocker' ) . '' . esc_html( self::get_formatted_datetime(time()) ) . '
    ' . esc_html__( 'Reason', 'advanced-ip-blocker' ) . '' . esc_html( $reason ) . '
    ' . esc_html__( 'Block Duration', 'advanced-ip-blocker' ) . '' . esc_html( $duration_text ) . '
    ' . '

    ' . esc_html__( 'No action is required on your part. This is just a notification.', 'advanced-ip-blocker' ) . '

    ' . '
    ' . '' . esc_html__( 'View Blocked IPs', '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']); $email_sent = wp_mail($to, $email_subject, $body); remove_filter('wp_mail_content_type', [$this, 'set_html_mail_content_type']); } elseif ($notification_enabled && in_array($frequency, ['daily', 'weekly'])) { $table_name_queue = $wpdb->prefix . 'advaipbl_notifications_queue'; @$wpdb->insert($table_name_queue, ['timestamp' => current_time('mysql', 1), 'ip' => $ip, 'block_type' => $type, 'reason' => $reason]); } // 5. Loguear los eventos de forma segura if (!empty($options['enable_logging'])) { $log_details = array_merge(['trigger' => $trigger], $geo_details); // Log específico del evento (404, 403, etc.) con nivel 'critical' porque es un bloqueo. $this->log_specific_error($type, $ip, $log_details, 'critical'); // Log general de auditoría $log_message_critical = sprintf('IP %1$s blocked via shutdown: %2$d %3$s errors.', $ip, $errors, strtoupper($type)); $this->log_event($log_message_critical, 'critical', $log_details); } $wpdb->delete("{$wpdb->prefix}advaipbl_cache", ['cache_key' => $lock_key]); } public function add_cron_intervals($schedules) { if (!isset($schedules['10_minutes'])) { $schedules['10_minutes'] = [ 'interval' => 10 * MINUTE_IN_SECONDS, 'display' => esc_html__('Every 10 Minutes', 'advanced-ip-blocker') ]; } if (!isset($schedules['30_minutes'])) { $schedules['30_minutes'] = [ 'interval' => 30 * MINUTE_IN_SECONDS, 'display' => esc_html__('Every 30 Minutes', 'advanced-ip-blocker') ]; } if (!isset($schedules['hourly'])) { $schedules['hourly'] = [ 'interval' => 60 * MINUTE_IN_SECONDS, 'display' => esc_html__('Once hourly', 'advanced-ip-blocker') ]; } if (!isset($schedules['3_days'])) { $schedules['3_days'] = [ 'interval' => 3 * DAY_IN_SECONDS, 'display' => esc_html__('Every 3 Days', 'advanced-ip-blocker') ]; } if (!isset($schedules['5_days'])) { $schedules['5_days'] = [ 'interval' => 5 * DAY_IN_SECONDS, 'display' => esc_html__('Every 5 Days', 'advanced-ip-blocker') ]; } if (!isset($schedules['weekly'])) { $schedules['weekly'] = [ 'interval' => 7 * DAY_IN_SECONDS, 'display' => esc_html__('Once Weekly', 'advanced-ip-blocker') ]; } return $schedules; } public function schedule_notification_cron() { $frequency = $this->options['notification_frequency'] ?? 'disabled'; $this->clear_notification_cron(); if ($frequency === 'daily' || $frequency === 'weekly') { $timestamp = strtotime('tomorrow 2:00 am'); wp_schedule_event($timestamp, $frequency, 'advaipbl_send_summary_email'); } } public function clear_notification_cron() { wp_clear_scheduled_hook('advaipbl_send_summary_email'); } public function process_and_send_summary() { global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_notifications_queue'; $notifications = $wpdb->get_results("SELECT * FROM $table_name ORDER BY timestamp ASC"); if (empty($notifications)) { return; } $to = !empty($this->options['notification_email']) && is_email($this->options['notification_email']) ? $this->options['notification_email'] : get_option('admin_email'); $site_name = get_bloginfo('name'); $total_blocks = count($notifications); /* translators: 1: Site name, 2: Total number of blocked IPs. */ $email_subject = sprintf(__('[%1$s] Security Summary: %2$d New Blocks', 'advanced-ip-blocker'), $site_name, $total_blocks); /* translators: %d: Total number of blocked IPs. */ $template_title = sprintf( _n( '%d New Block', '%d New Blocks', $total_blocks, 'advanced-ip-blocker' ), $total_blocks ); // 1. Construir el contenido HTML del email $blocks_by_type = []; foreach ($notifications as $notification) { $blocks_by_type[$notification->block_type][] = $notification; } $summary_html = '

    ' . esc_html__( 'Here is your security summary for the last period.', 'advanced-ip-blocker' ) . '

    '; $summary_html .= '

    ' . esc_html__( 'Summary by Block Type', 'advanced-ip-blocker' ) . '

    '; $summary_html .= '

    ' . esc_html__( 'Recent Block Details', 'advanced-ip-blocker' ) . '

    '; $summary_html .= '' . '' . '' . '' . ''; $details_limit = 20; $limited_notifications = array_slice($notifications, -$details_limit); foreach ( $limited_notifications as $notification ) { $summary_html .= '' . '' . '' . '' . ''; } $summary_html .= '
    ' . esc_html__('Time', 'advanced-ip-blocker') . '' . esc_html__('Blocked IP', 'advanced-ip-blocker') . '' . esc_html__('Reason', 'advanced-ip-blocker') . '
    ' . esc_html( self::get_formatted_datetime( $notification->timestamp ) ) . '' . esc_html( $notification->ip ) . '' . esc_html( $notification->reason ) . '
    '; $button_url = admin_url( 'options-general.php?page=advaipbl_settings_page&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;"; $summary_html .= '
    ' . '' . esc_html__( 'Manage All Blocked IPs', 'advanced-ip-blocker' ) . '' . '
    '; // 2. Envolver en la plantilla y enviar $body = $this->get_html_email_template( $template_title, $summary_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' ] ); if ($sent) { $wpdb->query("TRUNCATE TABLE $table_name"); /* translators: %s: The recipient's email address. */ $this->log_event( sprintf( __( 'Security summary sent to %s and queue cleared.', 'advanced-ip-blocker' ), $to ), 'info' ); } else { /* translators: %s: The recipient's email address. */ $this->log_event( sprintf( __( 'FAILED to send security summary to %s. Queue NOT cleared.', 'advanced-ip-blocker' ), $to ), 'error' ); } } private function send_block_notification($ip, $type, $count, $extra_data) { // Obtenemos los datos de geolocalización una sola vez para usarlos en ambas notificaciones. $location_data = $this->geolocation_manager->fetch_location($ip); // --- PASO 1: Construimos la razón detallada y la etiqueta una sola vez --- $reason = __('Unknown reason', 'advanced-ip-blocker'); $reason_label = ucwords(str_replace('_', ' ', $type)); switch ($type) { case '404': case '403': $reason = sprintf(/* translators: 1: The number of errors. 2: The error type (e.g., "404"). 3: The URL that triggered the error. */ __('Generated %1$d %2$s errors. Triggering URL: %3$s', 'advanced-ip-blocker'), $count, strtoupper($type), $extra_data['trigger'] ?? 'N/A' ); break; case 'login': /* translators: %1$d: The number of failed login attempts for user, %2$s: Username*/ $reason = sprintf(__('Made %1$d failed login attempts for user: %2$s', 'advanced-ip-blocker'), $count, isset($extra_data['trigger']) ? sanitize_text_field($extra_data['trigger']) : __('unknown', 'advanced-ip-blocker')); break; case 'manual': $reason = __('Manually blocked by an administrator.', 'advanced-ip-blocker'); break; case 'honeypot': /* translators: %s: Accessed Honeypot URL. */ $reason = sprintf(__('Accessed Honeypot URL: %s', 'advanced-ip-blocker'), $extra_data['url'] ?? 'N/A'); break; case 'user_agent': /* translators: %s: Used a blocked User-Agent. */ $reason = sprintf(__('Used a blocked User-Agent: %s', 'advanced-ip-blocker'), $extra_data['user_agent'] ?? 'N/A'); break; case 'geoblock': /* translators: %s: Blocked due to country policy reason. */ $reason = sprintf( __( 'Blocked due to country policy: %s', 'advanced-ip-blocker' ), $extra_data['country'] ?? 'Unknown Country' ); break; case 'waf': /* translators: %s: WAF Rule Triggered. */ $reason = $extra_data['detail'] ?? sprintf(__('WAF Rule Triggered: %s', 'advanced-ip-blocker'), $extra_data['rule'] ?? 'Unknown'); break; case 'rate_limit': /* translators: 1: Number of request, 2: Seconds */ $reason = sprintf(__('Rate limit exceeded: %1$d requests in %2$d seconds', 'advanced-ip-blocker'), $extra_data['count'] ?? 0, $extra_data['window'] ?? 0); break; case 'asn': /* translators: %1$d: AS number, %2$s: ASN Provider, 3$s: List (Spamhaus or Manula List). */ $reason = sprintf(__('Blocked ASN: %1$s (%2$s) - Source: %3$s', 'advanced-ip-blocker'), $extra_data['asn_number'] ?? 'N/A', $extra_data['asn_name'] ?? 'Unknown', $extra_data['source'] ?? 'N/A'); break; case 'xmlrpc_block': /* translators: %s: User-Agent. */ $reason = sprintf(__('Blocked untrusted XML-RPC request from User-Agent: %s', 'advanced-ip-blocker'), $extra_data['user_agent'] ?? 'N/A'); break; case 'threat_score': $reason = $extra_data['_reason'] ?? __('Threat score threshold exceeded', 'advanced-ip-blocker'); break; } // --- PASO 2: Lógica de Notificaciones Push (si está activada) --- if (!empty($this->options['enable_push_notifications'])) { $is_critical_block = !in_array($type, ['404', '403', 'login'], true); $send_this_push = empty($this->options['push_critical_only']) || ($this->options['push_critical_only'] === '1' && $is_critical_block); if ($send_this_push) { $site_name = get_bloginfo('name'); $message_lines = [ sprintf('*:shield: [%s] Security Alert*', $site_name) ]; $message_lines[] = sprintf('> • *Blocked Entry:* `%s`', $ip); // Cambiado de IP a Entry if ($location_data && empty($location_data['error'])) { $location_str = ($location_data['city'] ?? '') . ', ' . ($location_data['country'] ?? ''); $message_lines[] = sprintf('> • *Location:* %s (%s)', trim($location_str, ', '), $location_data['country_code'] ?? 'N/A'); } $message_lines[] = sprintf('> • *Reason:* %s', $reason_label); $message_lines[] = sprintf('> • *Details:* %s', $reason); $duration_minutes = ($type === 'manual') ? 0 : (int) ($this->options['duration_' . $type] ?? 1440); /* translators: %d: Duration minutes. */ $duration_text = ($duration_minutes <= 0) ? __('Permanent', 'advanced-ip-blocker') : sprintf(__('%d min', 'advanced-ip-blocker'), $duration_minutes); $message_lines[] = sprintf('> • *Duration:* %s', $duration_text); $request_uri = $extra_data['uri'] ?? $this->get_current_request_uri(); if (!empty($request_uri)) { $message_lines[] = sprintf('> • *URI:* `%s`', $request_uri); } if ($this->get_user_agent()) { $message_lines[] = sprintf('> • *User Agent:* `%s`', $this->get_user_agent()); } $this->execute_webhook_send(implode("\n", $message_lines)); } } // --- PASO 3: Lógica de Email --- $frequency = $this->options['notification_frequency'] ?? 'disabled'; if ( empty($this->options['enable_email_notifications']) || '1' !== $this->options['enable_email_notifications'] || 'disabled' === $frequency) { return; } if ($frequency === 'instant') { $to = !empty($this->options['notification_email']) && is_email($this->options['notification_email']) ? $this->options['notification_email'] : get_option('admin_email'); $site_name = get_bloginfo('name'); /* translators: %s: Site name. */ $email_subject = sprintf(__('[%s] Security Alert: Entry Automatically Blocked', 'advanced-ip-blocker'), $site_name); $template_title = __('Security Alert: Entry Blocked', 'advanced-ip-blocker'); $duration_minutes = ($type === 'manual') ? 0 : (int) ($this->options['duration_' . $type] ?? 1440); /* translators: %d: Duration minutes. */ $duration_text = ($duration_minutes <= 0) ? __('Permanent', 'advanced-ip-blocker') : sprintf(__('%d minutes', 'advanced-ip-blocker'), $duration_minutes); $button_url = admin_url( 'admin.php?page=advaipbl_settings_page-ip-management&sub-tab=blocked_ips' ); $content_html = '

    ' . esc_html__( "An entry has been automatically blocked on your website.", 'advanced-ip-blocker' ) . '

    ' . '' . '' . '' . '' . '' . '
    ' . esc_html__( 'Blocked Entry', 'advanced-ip-blocker' ) . '' . esc_html( $ip ) . '
    ' . esc_html__( 'Date and Time', 'advanced-ip-blocker' ) . '' . esc_html( self::get_formatted_datetime(time()) ) . '
    ' . esc_html__( 'Reason', 'advanced-ip-blocker' ) . '' . esc_html( $reason ) . '
    ' . esc_html__( 'Block Duration', 'advanced-ip-blocker' ) . '' . esc_html( $duration_text ) . '
    ' . '

    ' . esc_html__( 'No action is required on your part. This is just a notification.', 'advanced-ip-blocker' ) . '

    '. '
    ' . '' . esc_html__( 'View Blocked IPs', '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' ] ); } else { global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_notifications_queue'; @$wpdb->insert($table_name, [ 'timestamp' => current_time('mysql', 1), 'ip' => $ip, 'block_type' => $type, 'reason' => $reason ]); } } /** * Envía una notificación a todas las URLs de webhook configuradas. * * @param string $message El mensaje de texto plano a enviar. * @return bool True si al menos una notificación se envió con éxito, false en caso contrario. */ private function execute_webhook_send($message) { if (empty($this->options['enable_push_notifications'])) { return false; } $raw_urls = $this->options['push_webhook_urls'] ?? ''; if (empty(trim($raw_urls))) { return false; } $urls = array_filter(array_map('trim', explode("\n", $raw_urls))); if (empty($urls)) { return false; } // Creamos un payload compatible con Slack ("text") y Discord ("content"). $payload = [ 'content' => $message, 'text' => $message, ]; $args = [ 'body' => wp_json_encode($payload), 'headers' => [ 'Content-Type' => 'application/json', ], 'timeout' => 10, // 10 segundos de tiempo de espera ]; $success = false; foreach ($urls as $url) { $response = wp_remote_post($url, $args); if (is_wp_error($response)) { $this->log_event('Failed to send push notification: ' . $response->get_error_message(), 'error', ['webhook_url' => $url]); } elseif (wp_remote_retrieve_response_code($response) >= 300) { $this->log_event('Push notification sent, but received an error response code: ' . wp_remote_retrieve_response_code($response), 'warning', ['webhook_url' => $url]); } else { $success = true; } } return $success; } public function on_settings_update($old_value, $new_value) { $this->log_settings_change($old_value, $new_value); // Lógica para el cron de notificaciones por email $old_freq = $old_value['notification_frequency'] ?? 'disabled'; $new_freq = $new_value['notification_frequency'] ?? 'disabled'; if ($old_freq !== $new_freq) { wp_clear_scheduled_hook('advaipbl_send_summary_email'); if ($new_freq === 'daily' || $new_freq === 'weekly') { $this->schedule_notification_cron(); } } // Lógica para el cron de Spamhaus $old_spamhaus = $old_value['enable_spamhaus_asn'] ?? '0'; $new_spamhaus = $new_value['enable_spamhaus_asn'] ?? '0'; if ($new_spamhaus !== $old_spamhaus) { if ('1' === $new_spamhaus) { // Si se acaba de activar, forzamos una actualización inmediata para el usuario. // schedule_cron_jobs() se encargará de la programación recurrente. $this->update_spamhaus_list(); } else { // Si se acaba de desactivar, limpiamos el cron inmediatamente. wp_clear_scheduled_hook('advaipbl_update_spamhaus_list_event'); } } $old_forced_roles = $old_value['tfa_force_roles'] ?? []; $new_forced_roles = $new_value['tfa_force_roles'] ?? []; // Comparamos los roles antiguos con los nuevos para ver qué ha cambiado. $roles_added = array_diff($new_forced_roles, $old_forced_roles); $roles_removed = array_diff($old_forced_roles, $new_forced_roles); // Si se han añadido nuevos roles a la lista de forzados... if ( ! empty($roles_added) ) { $users_to_notify = get_users(['role__in' => $roles_added]); foreach ( $users_to_notify as $user ) { // Les ponemos la "marca" para que vean el aviso, solo si no tienen 2FA ya. if ( $this->tfa_manager && ! $this->tfa_manager->is_2fa_enabled_for_user( $user->ID ) ) { update_user_meta( $user->ID, '_advaipbl_2fa_setup_required', true ); } } } // Si se han quitado roles de la lista de forzados... if ( ! empty($roles_removed) ) { $users_to_unflag = get_users(['role__in' => $roles_removed]); foreach ( $users_to_unflag as $user ) { // Les quitamos la "marca". delete_user_meta( $user->ID, '_advaipbl_2fa_setup_required' ); } } } /** * Añade el menú del plugin a la barra de administración de WordPress con submenús anidados. * * @param WP_Admin_Bar $wp_admin_bar La instancia de la barra de administración. */ public function add_admin_bar_menu( $wp_admin_bar ) { ?> get_blocked_count(); $base_admin_url = admin_url( 'admin.php?page=advaipbl_settings_page' ); // --- 1. Nodo Principal --- $main_title = '' . esc_html__( 'Security', 'advanced-ip-blocker' ) . ''; if ( $blocked_count > 0 ) { $main_title .= ' ' . number_format_i18n( $blocked_count ) . ''; } $wp_admin_bar->add_node( [ 'id' => 'advaipbl_menu', 'title' => $main_title, 'href' => add_query_arg( ['tab' => 'dashboard', 'sub-tab' => 'main_dashboard'], $base_admin_url ), ] ); // --- 2. Grupo "Settings" --- $wp_admin_bar->add_node(['id' => 'advaipbl_settings_group', 'parent' => 'advaipbl_menu', 'title' => __('Settings', 'advanced-ip-blocker'), 'href' => false]); $wp_admin_bar->add_node(['id' => 'advaipbl_settings_config', 'parent' => 'advaipbl_settings_group', 'title' => __('Configuration', 'advanced-ip-blocker'), 'href' => add_query_arg(['tab' => 'settings', 'sub-tab' => 'general_settings'], $base_admin_url)]); $wp_admin_bar->add_node(['id' => 'advaipbl_settings_import_export', 'parent' => 'advaipbl_settings_group', 'title' => __('Import / Export', 'advanced-ip-blocker'), 'href' => add_query_arg(['tab' => 'settings', 'sub-tab' => 'import_export'], $base_admin_url)]); // --- 3. Grupo "Blocking Rules" --- $wp_admin_bar->add_node(['id' => 'advaipbl_rules_group', 'parent' => 'advaipbl_menu', 'title' => __('Blocking Rules', 'advanced-ip-blocker'), 'href' => false]); $wp_admin_bar->add_node(['id' => 'advaipbl_rules_waf', 'parent' => 'advaipbl_rules_group', 'title' => __('Firewall (WAF) Rules', 'advanced-ip-blocker'), 'href' => add_query_arg(['tab' => 'rules', 'sub-tab' => 'waf'], $base_admin_url)]); $wp_admin_bar->add_node(['id' => 'advaipbl_rules_useragent', 'parent' => 'advaipbl_rules_group', 'title' => __('User-Agent Blocking', 'advanced-ip-blocker'), 'href' => add_query_arg(['tab' => 'rules', 'sub-tab' => 'user_agents'], $base_admin_url)]); $wp_admin_bar->add_node(['id' => 'advaipbl_rules_honeypot', 'parent' => 'advaipbl_rules_group', 'title' => __('Honeypot URLs', 'advanced-ip-blocker'), 'href' => add_query_arg(['tab' => 'rules', 'sub-tab' => 'honeypot'], $base_admin_url)]); $wp_admin_bar->add_node(['id' => 'advaipbl_rules_asn', 'parent' => 'advaipbl_rules_group', 'title' => __('ASN Blocking', 'advanced-ip-blocker'), 'href' => add_query_arg(['tab' => 'rules', 'sub-tab' => 'asn_blocking'], $base_admin_url)]); // --- 4. Enlaces Directos de IP Management --- $blocked_ips_title = __('Blocked IPs', 'advanced-ip-blocker'); if ($blocked_count > 0) { $blocked_ips_title .= ' ' . number_format_i18n($blocked_count) . ''; } $wp_admin_bar->add_node(['id' => 'advaipbl_blocked_ips', 'parent' => 'advaipbl_menu', 'title' => $blocked_ips_title, 'href' => add_query_arg(['tab' => 'ip_management', 'sub-tab' => 'blocked_ips'], $base_admin_url)]); $wp_admin_bar->add_node(['id' => 'advaipbl_whitelist', 'parent' => 'advaipbl_menu', 'title' => __('Whitelist', 'advanced-ip-blocker'), 'href' => add_query_arg(['tab' => 'ip_management', 'sub-tab' => 'whitelist'], $base_admin_url)]); // --- 5. Grupo "Logs & Sessions" --- $wp_admin_bar->add_node(['id' => 'advaipbl_logs_group', 'parent' => 'advaipbl_menu', 'title' => __('Logs & Sessions', 'advanced-ip-blocker'), 'href' => false]); $wp_admin_bar->add_node(['id' => 'advaipbl_log_security', 'parent' => 'advaipbl_logs_group', 'title' => __('Security Log', 'advanced-ip-blocker'), 'href' => add_query_arg(['tab' => 'logs', 'sub-tab' => 'security_log'], $base_admin_url)]); $wp_admin_bar->add_node(['id' => 'advaipbl_log_ip_trust', 'parent' => 'advaipbl_logs_group', 'title' => __('IP Trust Log', 'advanced-ip-blocker'), 'href' => add_query_arg(['tab' => 'logs', 'sub-tab' => 'ip_trust_log'], $base_admin_url)]); $wp_admin_bar->add_node(['id' => 'advaipbl_log_general', 'parent' => 'advaipbl_logs_group', 'title' => __('General Log', 'advanced-ip-blocker'), 'href' => add_query_arg(['tab' => 'logs', 'sub-tab' => 'general_log'], $base_admin_url)]); $wp_admin_bar->add_node(['id' => 'advaipbl_user_sessions', 'parent' => 'advaipbl_logs_group', 'title' => __('User Sessions', 'advanced-ip-blocker'), 'href' => add_query_arg(['tab' => 'logs', 'sub-tab' => 'user_sessions'], $base_admin_url)]); $wp_admin_bar->add_node(['id' => 'advaipbl_log_cron', 'parent' => 'advaipbl_logs_group', 'title' => __('WP-Cron Log', 'advanced-ip-blocker'), 'href' => add_query_arg(['tab' => 'logs', 'sub-tab' => 'cron_logs'], $base_admin_url)]); // --- 6. Enlaces de Nivel Superior Restantes --- $wp_admin_bar->add_node(['id' => 'advaipbl_status', 'parent' => 'advaipbl_menu', 'title' => __('System Status', 'advanced-ip-blocker'), 'href' => add_query_arg(['tab' => 'dashboard', 'sub-tab' => 'status'], $base_admin_url)]); $wp_admin_bar->add_node(['id' => 'advaipbl_credits', 'parent' => 'advaipbl_menu', 'title' => __('About', 'advanced-ip-blocker'), 'href' => add_query_arg(['tab' => 'about', 'sub-tab' => 'credits'], $base_admin_url)]); } public function log_settings_change($old_value, $new_value) { if ($old_value === $new_value) { return; } $user = wp_get_current_user(); $username = ($user && $user->ID) ? $user->user_login : __('an unknown user', 'advanced-ip-blocker'); // Mapeo completo de claves a nombres legibles. $friendly_names = [ // General 'enable_logging' => __('Logging', 'advanced-ip-blocker'), 'log_retention_days' => __('Log Retention', 'advanced-ip-blocker'), 'log_timezone' => __('Log Timezone', 'advanced-ip-blocker'), 'custom_block_message' => __('Custom Block Message', 'advanced-ip-blocker'), 'excluded_error_urls' => __('Excluded URLs', 'advanced-ip-blocker'), 'show_admin_bar_menu' => __('Admin Bar Menu', 'advanced-ip-blocker'), // --- NUEVAS CLAVES DEL WAF, ASN(spamhaus) Y RATE LIMITING --- 'enable_waf' => __('Web Application Firewall (WAF)', 'advanced-ip-blocker'), 'duration_waf' => __('WAF Block Duration', 'advanced-ip-blocker'), 'duration_rate_limit' => __('Rate Limiting Block Duration', 'advanced-ip-blocker'), 'rate_limiting_enable' => __('Rate Limiting', 'advanced-ip-blocker'), 'rate_limiting_limit' => __('Rate Limiting Request Limit', 'advanced-ip-blocker'), 'rate_limiting_window' => __('Rate Limiting Time Window', 'advanced-ip-blocker'), 'duration_rate_limit' => __('Rate Limiting Block Duration', 'advanced-ip-blocker'), 'enable_geoblocking' => __('Geoblocking Protection', 'advanced-ip-blocker'), 'enable_honeypot_blocking' => __('Honeypot Protection', 'advanced-ip-blocker'), 'enable_user_agent_blocking' => __('User-Agent Protection', 'advanced-ip-blocker'), 'enable_spamhaus_blocking' => __('Spamhaus ASN Protection', 'advanced-ip-blocker'), // Notificaciones 'enable_email_notifications' => __('Email Notifications', 'advanced-ip-blocker'), 'notification_frequency' => __('Notification Frequency', 'advanced-ip-blocker'), 'notification_email' => __('Notification Email', 'advanced-ip-blocker'), // APIs 'geolocation_method' => __('Geolocation Method', 'advanced-ip-blocker'), 'maxmind_license_key' => __('MaxMind License Key', 'advanced-ip-blocker'), 'geolocation_provider' => __('Geolocation Provider', 'advanced-ip-blocker'), 'api_key_ip_apicom' => __('ip-api.com API Key', 'advanced-ip-blocker'), 'api_key_ipinfocom' => __('ipinfo.io API Key', 'advanced-ip-blocker'), 'api_key_ipapicom' => __('ipapi.com API Key', 'advanced-ip-blocker'), 'api_key_ipstackcom' => __('ipstack.com API Key', 'advanced-ip-blocker'), // Geobloqueo 'geoblock_countries' => __('Blocked Countries', 'advanced-ip-blocker'), 'duration_geoblock' => __('Geoblock Duration', 'advanced-ip-blocker'), // Reglas de bloqueo 'duration_honeypot' => __('Honeypot Duration', 'advanced-ip-blocker'), 'duration_user_agent' => __('User-Agent Duration', 'advanced-ip-blocker'), 'threshold_404' => __('404 Threshold', 'advanced-ip-blocker'), 'duration_404' => __('404 Duration', 'advanced-ip-blocker'), 'transient_expiration_404' => __('404 Time Window', 'advanced-ip-blocker'), 'threshold_403' => __('403 Threshold', 'advanced-ip-blocker'), 'duration_403' => __('403 Duration', 'advanced-ip-blocker'), 'transient_expiration_403' => __('403 Time Window', 'advanced-ip-blocker'), 'threshold_login' => __('Login Threshold', 'advanced-ip-blocker'), 'duration_login' => __('Login Duration', 'advanced-ip-blocker'), 'transient_expiration_login' => __('Login Time Window', 'advanced-ip-blocker'), // Protección de Login 'disable_user_enumeration' => __('REST API Protection', 'advanced-ip-blocker'), 'prevent_author_scanning' => __('Author Scan Protection', 'advanced-ip-blocker'), 'restrict_login_page' => __('Whitelist Login Access', 'advanced-ip-blocker'), 'xmlrpc_protection_mode' => __('XML-RPC Protection Mode', 'advanced-ip-blocker'), 'enable_xmlrpc_lockdown' => __('XML-RPC Lockdown Mode', 'advanced-ip-blocker'), //2FA 'enable_2fa' => __('Two-Factor Authentication (Global)', 'advanced-ip-blocker'), 'tfa_force_roles' => __('Force 2FA for Roles', 'advanced-ip-blocker'), 'prevent_login_hinting' => __('Prevent Login Hinting', 'advanced-ip-blocker'), // reCAPTCHA 'recaptcha_enable' => __('reCAPTCHA', 'advanced-ip-blocker'), 'recaptcha_version' => __('reCAPTCHA Version', 'advanced-ip-blocker'), 'recaptcha_site_key' => __('reCAPTCHA Site Key', 'advanced-ip-blocker'), 'recaptcha_secret_key' => __('reCAPTCHA Secret Key', 'advanced-ip-blocker'), 'recaptcha_score_threshold' => __('reCAPTCHA Score', 'advanced-ip-blocker'), // Desinstalación 'delete_data_on_uninstall' => __('Delete Data on Uninstall', 'advanced-ip-blocker'), ]; $changed_fields_log = []; // Array de todas nuestras opciones de tipo checkbox. $checkbox_keys = [ 'enable_logging', 'enable_email_notifications', 'disable_user_enumeration', 'enable_2fa', 'tfa_force_roles', 'prevent_author_scanning', 'restrict_login_page', 'recaptcha_enable', 'enable_waf', 'rate_limiting_enable', 'delete_data_on_uninstall', 'disable_xmlrpc', 'show_admin_bar_menu', 'enable_xmlrpc_lockdown' ]; foreach ($friendly_names as $key => $name) { $old = $old_value[$key] ?? null; $new = $new_value[$key] ?? null; if (is_array($old) || is_array($new)) { if ($old !== $new) { /* translators: %s: Changed fields log. */ $changed_fields_log[] = sprintf(__('%s: updated', 'advanced-ip-blocker'), $name); } continue; } if ($old !== $new) { if (strpos($key, '_key') !== false && !empty($new)) { /* translators: %s: Changed fields log. */ $changed_fields_log[] = sprintf(__('%s: updated', 'advanced-ip-blocker'), $name); } elseif (in_array($key, $checkbox_keys, true)) { // Usamos el nuevo array de checkboxes $status = ('1' === $new) ? __('enabled', 'advanced-ip-blocker') : __('disabled', 'advanced-ip-blocker'); /* translators: 1: Changed fields log, 2: Status */ $changed_fields_log[] = sprintf(__('%1$s: %2$s', 'advanced-ip-blocker'), $name, $status); } else { /* translators: 1: Changed fields log, 2: Status */ $changed_fields_log[] = sprintf(__('%1$s: set to "%2$s"', 'advanced-ip-blocker'), $name, esc_html($new)); } } } if (!empty($changed_fields_log)) { $message = sprintf( /* translators: 1: Status (enabled or disabled), 2: Changed fields log */ __('Plugin settings updated by %1$s. Changed fields: %2$s', 'advanced-ip-blocker'), $username, implode('; ', $changed_fields_log) ); $this->log_event($message, 'info'); } } private function access_denied_page($title, $message) { if (!defined('DONOTCACHEPAGE')) { define('DONOTCACHEPAGE', true); } if (isset($this->block_response_initiated) && $this->block_response_initiated) { exit; } $this->block_response_initiated = true; $is_xmlrpc_request = (isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], 'xmlrpc.php') !== false); if ($is_xmlrpc_request) { // Para XML-RPC, construimos y enviamos nuestra propia página de error HTML. if (!headers_sent()) { header('HTTP/1.1 403 Forbidden'); header('Content-Type: text/html; charset=utf-8'); } $custom_message = $this->options['custom_block_message'] ?? ''; $display_message = !empty($custom_message) ? nl2br(esc_html($custom_message)) : esc_html($message); // Este HTML imita la estructura de la página de wp_die() $html_output = sprintf( ' %1$s
    %2$s
    ', esc_html($title), $display_message ); echo $html_output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped exit(); } else { // Para todas las demás peticiones, usamos el método estándar. if (!headers_sent()) { header('Cache-Control: no-cache, must-revalidate, max-age=0'); header('Pragma: no-cache'); header('Expires: Wed, 11 Jan 1984 05:00:00 GMT'); status_header(403); } $custom_message = $this->options['custom_block_message'] ?? ''; $display_message = !empty($custom_message) ? nl2br(esc_html($custom_message)) : esc_html($message); wp_die(wp_kses_post($display_message), esc_html($title), ['response' => 403]); } } public function desbloquear_ip( $entry_to_unblock ) { if ( ! $this->is_valid_ip_or_range( $entry_to_unblock ) ) { return; } global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_blocked_ips'; $is_single_ip = filter_var($entry_to_unblock, FILTER_VALIDATE_IP); // Borrar de la nueva tabla $wpdb->delete($table_name, ['ip_range' => $entry_to_unblock]); $this->clear_blocked_ips_cache(); // Limpiar todos los transients asociados if ($is_single_ip) { delete_transient('advaipbl_blocked_ip_' . md5($entry_to_unblock)); $definitions = $this->get_all_block_type_definitions(); foreach ($definitions as $type => $def) { if ($def['uses_transient']) { delete_transient("advaipbl_bloqueo_{$type}_" . md5($entry_to_unblock)); } if (in_array($type, ['404', '403', 'login'])) { delete_transient("advaipbl_errores_{$type}_" . md5($entry_to_unblock)); } } } // Limpiar caché de ubicación y resetear puntuación if ($is_single_ip) { $location_cache = $this->get_from_custom_cache(ADVAIPBL_USM_LOCATION_CACHE_KEY); if (is_array($location_cache) && isset($location_cache[$entry_to_unblock])) { unset($location_cache[$entry_to_unblock]); $this->set_in_custom_cache(ADVAIPBL_USM_LOCATION_CACHE_KEY, $location_cache, ADVAIPBL_USM_LOCATION_CACHE_TTL); } if (!empty($this->options['enable_threat_scoring'])) { $this->threat_score_manager->reset_score($entry_to_unblock); } } /* translators: %1$s: IP, %2$s: Username */ $this->log_event( sprintf( __( 'Entry %1$s manually unblocked by %2$s.', 'advanced-ip-blocker' ), $entry_to_unblock, $this->get_current_admin_username() ) ); } /** * Desbloquea TODAS las IPs de la tabla de bloqueo y limpia todos los transients relacionados. * Es una acción masiva y destructiva. * * @param string $source La fuente de la acción (e.g., 'WP-CLI', 'Admin Action'). */ public function unblock_all_ips($source = 'Unknown Action') { global $wpdb; $table_name_blocked = $wpdb->prefix . 'advaipbl_blocked_ips'; // 1. Vaciar la tabla de IPs bloqueadas. $wpdb->query("TRUNCATE TABLE `{$table_name_blocked}`"); // 2. Limpiar la caché de objetos para el contador. $this->clear_blocked_ips_cache(); // 3. Limpiar todos los transients de bloqueo de la base de datos. // Esto incluye los contadores de errores (404, login, etc.) y los flags de bloqueo. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE `option_name` LIKE %s OR `option_name` LIKE %s OR `option_name` LIKE %s OR `option_name` LIKE %s", $wpdb->esc_like('_transient_advaipbl_bloqueo_') . '%', $wpdb->esc_like('_transient_timeout_advaipbl_bloqueo_') . '%', $wpdb->esc_like('_transient_advaipbl_errores_') . '%', $wpdb->esc_like('_transient_timeout_advaipbl_errores_') . '%' ) ); // 4. Registrar el evento. $this->log_event(sprintf('All blocked IPs have been unblocked. Action via %s.', $source), 'critical'); } public function limpiar_ips_expiradas() { global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_blocked_ips'; $now = time(); // 1. Obtener todas las IPs que van a expirar ANTES de borrarlas $expiring_ips = $wpdb->get_results($wpdb->prepare( "SELECT ip_range, block_type FROM {$table_name} WHERE expires_at < %d AND expires_at > 0", $now )); if (empty($expiring_ips)) { return; } // Borrar todas las entradas caducadas $deleted_rows = $wpdb->query($wpdb->prepare( "DELETE FROM {$table_name} WHERE expires_at < %d AND expires_at > 0", $now )); if ($deleted_rows > 0) { $this->clear_blocked_ips_cache(); } // 3. Limpiar transients y registrar los eventos foreach ($expiring_ips as $entry) { $ip = $entry->ip_range; $type = $entry->block_type; // Loguear el desbloqueo automático /* translators: %1$s: IP, %2$s: Status */ $this->log_event( sprintf( __('IP %1$s automatically unblocked (expired %2$s).', 'advanced-ip-blocker'), $ip, $type ), 'info' ); // Limpiar transients delete_transient('advaipbl_blocked_ip_' . md5($ip)); delete_transient("advaipbl_bloqueo_{$type}_" . md5($ip)); // Si el bloqueo que expira es de tipo 'threat_score', reseteamos su puntuación. if ($type === 'threat_score' && isset($this->threat_score_manager)) { $this->threat_score_manager->reset_score($ip); } } } /** * Re-sincroniza los transients de bloqueo con las listas persistentes. * Se ejecuta al activar el plugin para prevenir desbloqueos accidentales tras una actualización. */ public function resync_block_transients() { $definitions = $this->get_all_block_type_definitions(); $now = time(); foreach ($definitions as $type => $def) { if (empty($def['option_key']) || empty($def['duration_key']) || !$def['uses_transient']) { continue; } $duration_minutes = (int) ($this->options[$def['duration_key']] ?? 1440); if ($duration_minutes <= 0) continue; $list = get_option($def['option_key'], []); if (!is_array($list) || empty($list)) continue; $duration_seconds = $duration_minutes * 60; foreach ($list as $ip_or_range => $block_data) { if (!filter_var($ip_or_range, FILTER_VALIDATE_IP)) { continue; } $timestamp = is_array($block_data) && isset($block_data['timestamp']) ? (int)$block_data['timestamp'] : 0; if ($timestamp === 0) continue; $time_left = ($timestamp + $duration_seconds) - $now; if ($time_left > 0) { // Resincroniza el transient específico if (false === get_transient("advaipbl_bloqueo_{$type}_" . md5($ip_or_range))) { set_transient("advaipbl_bloqueo_{$type}_" . md5($ip_or_range), true, $time_left); } // AÑADIR TRANSIENT MAESTRO --- if (false === get_transient('advaipbl_blocked_ip_' . md5($ip_or_range))) { set_transient('advaipbl_blocked_ip_' . md5($ip_or_range), true, $time_left); } } } } } /** * Se engancha a wp_login para añadir automáticamente la IP de un administrador a la lista blanca. * * @param string $user_login El login del usuario. * @param WP_User $user El objeto del usuario. */ public function auto_whitelist_admin_on_login($user_login, $user) { if (empty($this->options['auto_whitelist_admin']) || '1' !== $this->options['auto_whitelist_admin']) { return; } // Asegurarnos de que es un administrador if (is_a($user, 'WP_User') && in_array('administrator', (array) $user->roles)) { $ip = $this->get_client_ip(); // Solo actuar si la IP es válida y no está ya en la lista blanca if (filter_var($ip, FILTER_VALIDATE_IP) && !$this->is_whitelisted($ip)) { $detail = sprintf( /* translators: %1$s: Username, %2$s: Email */ __('Auto-whitelisted admin: %1$s (%2$s)', 'advanced-ip-blocker'), $user->user_login, $user->user_email ); $this->add_to_whitelist_and_unblock($ip, $detail); } } } public function get_default_settings() { $default_exclusions_404_403 = [ 'wc-ajax=get_refreshed_fragments', '?wc-ajax=', 'undefinedjetpack', '/wp-cron.php', '.js.map', '.css.map', '/favicon.ico', '/.well-known/traffic-advice', ]; $default_waf_exclusions = [ '# WooCommerce Payment Gateway Webhooks & AJAX Calls', 'wc-ajax=wc_stripe_save_appearance', // Stripe Appearance Saver 'wc-ajax=wc_stripe_get_cart_details', // Stripe Cart Details 'wc-ajax=ppc-create-order', // PayPal Create Order 'wc-ajax=ppc-capture-order', // PayPal Capture Order '?wc-api=wc_gateway_stripe', // Stripe Webhook Endpoint '?wc-api=wc_gateway_paypal', // PayPal IPN/Webhook Endpoint ]; return [ // Reglas de bloqueo 'threshold_404' => 10, 'duration_404' => 120, 'transient_expiration_404' => 60, 'threshold_403' => 5, 'duration_403' => 60, 'transient_expiration_403' => 30, 'threshold_login' => 5, 'duration_login' => 120, 'transient_expiration_login' => 60, 'duration_geoblock' => 1440, 'duration_honeypot' => 1440, 'duration_user_agent' => 1440, 'duration_waf' => 1440, 'duration_rate_limit' => 30, 'duration_xmlrpc_block' => 1440, 'duration_asn' => 1440, // Endpoint Lockdown 'enable_xmlrpc_lockdown' => '0', 'xmlrpc_lockdown_threshold' => 10, 'xmlrpc_lockdown_window' => 15, 'xmlrpc_lockdown_duration' => 60, 'enable_login_lockdown' => '0', 'login_lockdown_event_threshold' => 50, 'login_lockdown_ip_threshold' => 10, 'login_lockdown_window' => 5, 'login_lockdown_duration' => 60, // Protección de Login y Módulos 'disable_user_enumeration' => '1', 'prevent_author_scanning' => '1', 'restrict_login_page' => '0', 'auto_whitelist_admin' => '0', 'enable_waf' => '0', 'rate_limiting_enable' => '0', 'rate_limiting_limit' => 120, 'rate_limiting_window' => 60, 'xmlrpc_protection_mode' => 'smart', 'enable_geoblocking' => '1', 'enable_honeypot_blocking' => '1', 'enable_user_agent_blocking' => '1', 'enable_spamhaus_asn' => '0', 'enable_manual_asn' => '1', // Ajustes generales 'enable_logging' => '1', 'log_retention_days' => 30, 'log_timezone' => wp_timezone_string(), 'custom_block_message' => '', 'excluded_error_urls' => implode( "\n", $default_exclusions_404_403 ), 'waf_excluded_urls' => implode("\n", $default_waf_exclusions), 'rows_per_page' => 20, 'delete_data_on_uninstall' => '0', 'show_admin_bar_menu' => '1', // Notificaciones 'enable_email_notifications' => '0', 'notification_frequency' => 'disabled', 'notification_email' => '', 'enable_push_notifications' => '0', 'push_webhook_urls' => '', 'push_critical_only' => '0', 'push_mentions' => '', // APIs Externas 'geolocation_provider' => 'ip-api.com', 'api_key_ipapicom' => '', 'api_key_ipstackcom' => '', 'api_key_ipinfocom' => '', 'api_key_ip_apicom' => '', 'geolocation_method' => 'api', 'maxmind_license_key' => '', // Geobloqueo 'geoblock_countries' => [], // Ajustes de reCAPTCHA 'recaptcha_enable' => '0', 'recaptcha_version' => 'v3', 'recaptcha_site_key' => '', 'recaptcha_secret_key' => '', 'recaptcha_score_threshold' => 0.5, // CLAVES 2FA 'enable_2fa' => '0', 'tfa_force_roles' => [], 'prevent_login_hinting' => '1', // Telemetría 'allow_telemetry' => '0', // Sistema de Puntuación de Amenaza (IP Trust System) 'enable_threat_scoring' => '0', 'threat_score_threshold' => 100, 'duration_threat_score' => 360, // 6 horas 'score_404' => 5, 'score_403' => 10, 'score_login' => 15, 'score_user_agent' => 25, 'score_waf' => 50, 'score_honeypot' => 100, 'score_asn' => 100, 'score_decay_points' => 1, 'score_decay_frequency' => 1, 'enable_signature_engine' => '0', 'enable_signature_analysis' => '0', 'signature_ip_threshold' => 5, 'signature_analysis_window' => 1, 'signature_rule_ttl' => 24, 'enable_signature_blocking' => '0', 'trusted_signature_hashes' => '', ]; } public static function activate_plugin() { // 1. Obtener la instancia del plugin SIEMPRE al principio. // Esto asegura que $instance siempre exista y que las propiedades (como $options) estén cargadas. $instance = self::get_instance(); // 2. Lógica de la base de datos. self::setup_database_tables(); if ( ! wp_next_scheduled( 'advaipbl_purge_old_logs_event' ) ) { wp_schedule_event( time(), 'daily', 'advaipbl_purge_old_logs_event' ); } if ( ! wp_next_scheduled( 'advaipbl_update_geoip_db_event' ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'advaipbl_update_geoip_db_event' ); } if ( ! wp_next_scheduled( 'advaipbl_cleanup_expired_cache_event' ) ) { wp_schedule_event( time(), 'daily', 'advaipbl_cleanup_expired_cache_event' ); } // 3. Crear los ajustes por defecto SOLO si no existen. if ( false === get_option( self::OPTION_SETTINGS ) ) { $defaults = $instance->get_default_settings(); update_option( self::OPTION_SETTINGS, $defaults ); } // 4. Programar los crons de notificación (usando la instancia). $notification_frequency = $instance->options['notification_frequency'] ?? 'disabled'; if ( 'disabled' !== $notification_frequency && 'instant' !== $notification_frequency ) { if ( ! wp_next_scheduled( 'advaipbl_send_summary_email' ) ) { $instance->schedule_notification_cron(); } } // 5. Re-sincronizar transients (ahora $instance siempre existe). $instance->resync_block_transients(); // 6. Añadir las demás opciones si no existen. add_option( self::OPTION_BLOCKED_UAS, [], '', 'no' ); add_option( self::OPTION_WHITELISTED_UAS, [], '', 'no' ); add_option( self::OPTION_WHITELIST, [], '', 'no' ); add_option( self::OPTION_BLOCKED_MANUAL, [], '', 'no' ); add_option( self::OPTION_BLOCKED_HONEYPOT, [], '', 'no' ); add_option( self::OPTION_BLOCKED_USER_AGENT, [], '', 'no' ); add_option( self::OPTION_BLOCKED_GEO, [], '', 'no' ); add_option( ADVAIPBL_USM_OPTION_PER_PAGE, ADVAIPBL_USM_DEFAULT_PER_PAGE, '', 'no' ); add_option( self::OPTION_HONEYPOT_URLS, [], '', 'no' ); // 7. Inicializar los triggers de activación. add_option( self::OPTION_ADMIN_IP_TRIGGER, 'yes', '', 'no' ); } public static function deactivate_plugin() { wp_clear_scheduled_hook('advaipbl_purge_old_logs_event'); wp_clear_scheduled_hook('advaipbl_update_spamhaus_list_event'); wp_clear_scheduled_hook('advaipbl_send_telemetry_data_event'); wp_clear_scheduled_hook('advaipbl_threat_score_decay_event'); wp_clear_scheduled_hook('advaipbl_signature_analysis_event'); wp_clear_scheduled_hook('advaipbl_update_geoip_db_event'); wp_clear_scheduled_hook('advaipbl_cleanup_expired_cache_event'); $instance = self::get_instance(); $instance->clear_notification_cron(); } /** * Comprueba si estamos en una de las páginas de nuestro plugin y, si es así, * elimina todos los demás avisos de administrador para una interfaz limpia. */ public function conditionally_remove_admin_notices() { // Simplemente borra los avisos. Nada más. $current_page_slug = isset($_GET['page']) ? sanitize_key($_GET['page']) : ''; if (strpos($current_page_slug, 'advaipbl_settings_page') === 0) { remove_all_actions('admin_notices'); remove_all_actions('all_admin_notices'); } } public function admin_menu() { $blocked_count = $this->get_blocked_count(); $bubble_html = $blocked_count > 0 ? ' ' . number_format_i18n($blocked_count) . '' : ''; // Slug base para la página principal. Es buena práctica tenerlo en una variable. $main_page_slug = 'advaipbl_settings_page'; // 1. Menú Principal de Nivel Superior add_menu_page( __('Advanced IP Blocker', 'advanced-ip-blocker'), __('Security', 'advanced-ip-blocker') . $bubble_html, 'manage_options', $main_page_slug, [$this, 'settings_page_content'], 'dashicons-shield', '80.123' ); // 2. Submenús // El primer submenú (Dashboard) usa el mismo slug que el padre para ser la página por defecto. // Al hacer clic en "Security", se cargará esta página. add_submenu_page( $main_page_slug, __('Dashboard', 'advanced-ip-blocker'), __('Dashboard', 'advanced-ip-blocker'), 'manage_options', $main_page_slug, // Slug = padre. Correcto. [$this, 'settings_page_content'] ); // Para los demás submenús, usamos slugs únicos y simples. // WordPress generará automáticamente los enlaces correctos, por ejemplo: // admin.php?page=advaipbl_settings_page-settings add_submenu_page( $main_page_slug, __('Settings', 'advanced-ip-blocker'), __('Settings', 'advanced-ip-blocker'), 'manage_options', $main_page_slug . '-settings', // Slug único [$this, 'settings_page_content'] ); add_submenu_page( $main_page_slug, __('Blocking Rules', 'advanced-ip-blocker'), __('Blocking Rules', 'advanced-ip-blocker'), 'manage_options', $main_page_slug . '-rules', // Slug único [$this, 'settings_page_content'] ); add_submenu_page( $main_page_slug, __('IP Management', 'advanced-ip-blocker'), __('IP Management', 'advanced-ip-blocker') . $bubble_html, 'manage_options', $main_page_slug . '-ip-management', // Slug único [$this, 'settings_page_content'] ); add_submenu_page( $main_page_slug, __('Logs & Sessions', 'advanced-ip-blocker'), __('Logs & Sessions', 'advanced-ip-blocker'), 'manage_options', $main_page_slug . '-logs', // Slug único [$this, 'settings_page_content'] ); add_submenu_page( $main_page_slug, __('About', 'advanced-ip-blocker'), __('About', 'advanced-ip-blocker'), 'manage_options', $main_page_slug . '-about', // Slug único [$this, 'settings_page_content'] ); } /** * Removes all admin notices from other plugins and WordPress core * ONLY on this plugin's settings page. */ public function remove_all_other_admin_notices() { // Esta es la técnica que sugeriste, es perfecta. remove_all_actions( 'admin_notices' ); remove_all_actions( 'all_admin_notices' ); } public function display_admin_notice() { // Primero, llamamos a la función que decide si mostrar el aviso de telemetría. $this->display_telemetry_notice(); // Después, mostramos cualquier otro aviso de éxito/error que esté en el transient. $notice = get_transient('advaipbl_admin_notice'); if ( $notice ) { $type = ( 'success' === $notice['type'] ) ? 'success' : 'error'; printf( '

    %2$s

    ', esc_attr( $type ), esc_html( $notice['message'] ) ); delete_transient('advaipbl_admin_notice'); } } /** * Muestra la pestaña de configuración del Firewall (WAF). */ private function display_waf_tab() { $is_enabled = !empty($this->options['enable_waf']); $settings_url = admin_url('admin.php?page=advaipbl_settings_page-settings&sub-tab=general_settings#sub-section-waf'); ?>

    %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. */ __('Web Application Firewall (WAF) 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 && $bits <= 32; } // Check for hyphenated range if (strpos($input, '-') !== false) { list($start_ip, $end_ip) = explode('-', $input, 2); return filter_var(trim($start_ip), FILTER_VALIDATE_IP) && filter_var(trim($end_ip), FILTER_VALIDATE_IP); } return false; } public function handle_admin_actions() { if ( isset( $_GET['action'] ) && $_GET['action'] === 'advaipbl_reset_2fa' && isset( $_GET['user_id'] ) ) { $user_id_to_reset = absint( $_GET['user_id'] ); if ( isset( $_GET['advaipbl_2fa_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['advaipbl_2fa_nonce'] ) ), 'advaipbl_reset_2fa_' . $user_id_to_reset ) ) { if ( current_user_can( 'edit_users' ) ) { if ( $this->tfa_manager ) { $this->tfa_manager->admin_reset_for_user( $user_id_to_reset ); $user_info = get_userdata($user_id_to_reset); /* translators: %s: Username. */ $message = sprintf( __( 'Two-Factor Authentication has been reset for user %s.', 'advanced-ip-blocker' ), $user_info->user_login ); set_transient( self::TRANSIENT_ADMIN_NOTICE, ['message' => $message, 'type' => 'success'], 30 ); } } } wp_safe_redirect( remove_query_arg( ['action', 'user_id', 'advaipbl_2fa_nonce'] ) ); exit; } elseif ( isset( $_GET['action'] ) && $_GET['action'] === 'advaipbl_delete_lockdown' && isset( $_GET['lockdown_id'] ) ) { $lockdown_id = absint( $_GET['lockdown_id'] ); $nonce = $_GET['_wpnonce'] ?? ''; if ( wp_verify_nonce( $nonce, 'advaipbl_delete_lockdown_' . $lockdown_id ) && current_user_can('manage_options') ) { global $wpdb; $lockdowns_table = $wpdb->prefix . 'advaipbl_endpoint_lockdowns'; $wpdb->delete($lockdowns_table, ['id' => $lockdown_id], ['%d']); $this->log_event(sprintf('Endpoint lockdown #%d was manually cancelled by %s.', $lockdown_id, $this->get_current_admin_username()), 'warning'); set_transient( self::TRANSIENT_ADMIN_NOTICE, ['message' => __('The endpoint lockdown has been cancelled.', 'advanced-ip-blocker'), 'type' => 'success'], 30 ); } wp_safe_redirect( remove_query_arg( ['action', 'lockdown_id', '_wpnonce'] ) ); exit; } $nonce = $_REQUEST['_wpnonce'] ?? ''; $action = $_REQUEST['action'] ?? ''; $action_type = $_POST['action_type'] ?? ''; $nonce_is_valid = false; if (!empty($action) && strpos($action, 'advaipbl_') === 0 && wp_verify_nonce($nonce, $action)) { $action_type = $action; $nonce_is_valid = true; } elseif (isset($_POST['advaipbl_admin_nonce_action']) && wp_verify_nonce($_POST['advaipbl_admin_nonce_action'], 'advaipbl_admin_nonce_action')) { $nonce_is_valid = true; } if (!$nonce_is_valid || !current_user_can('manage_options')) { return; } $bulk_action = $_POST['bulk_action'] ?? ($_POST['bulk_action2'] ?? '-1'); $message = ''; $type = 'success'; if ($bulk_action !== '-1') { // Caso especial: 'unblock_all' no necesita 'ips_to_process' if ($bulk_action === 'unblock_all') { $this->unblock_all_ips('Admin Action (Bulk)'); $message = __('All blocked IPs have been successfully unblocked.', 'advanced-ip-blocker'); } // Para 'unblock' (seleccionados), sí necesitamos 'ips_to_process' elseif ($bulk_action === 'unblock' && isset($_POST['ips_to_process'])) { $ips_to_process = array_map('sanitize_text_field', (array) wp_unslash($_POST['ips_to_process'])); $processed_count = 0; foreach ($ips_to_process as $ip_or_range) { if ($this->is_valid_ip_or_range($ip_or_range)) { $this->desbloquear_ip($ip_or_range); $processed_count++; } } if ($processed_count > 0) { /* translators: %d: Number of entries. */ $message = sprintf(_n('%d entry has been unblocked.', '%d entries have been unblocked.', $processed_count, 'advanced-ip-blocker'), $processed_count); } else { $message = __('No valid entries were selected for unblocking.', 'advanced-ip-blocker'); $type = 'warning'; } } } elseif (!empty($action_type)) { $current_user_login = $this->get_current_admin_username(); switch ($action_type) { case 'advaipbl_unblock_ip': $ip_to_unblock = isset($_GET['ip']) ? sanitize_text_field($_GET['ip']) : ''; if ($this->is_valid_ip_or_range($ip_to_unblock)) { $this->desbloquear_ip($ip_to_unblock); /* translators: $s: IP */ $message = sprintf(__('Entry %s has been unblocked.', 'advanced-ip-blocker'), $ip_to_unblock); } else { $message = __('Invalid entry provided for unblocking.', 'advanced-ip-blocker'); $type = 'error'; } break; case 'unblock_all': $this->unblock_all_ips('Admin Action'); $message = __('All blocked IPs have been successfully unblocked.', 'advanced-ip-blocker'); break; case 'save_user_agents': $blocked_raw = isset($_POST['blocked_user_agents']) ? sanitize_textarea_field(wp_unslash($_POST['blocked_user_agents'])) : ''; update_option(self::OPTION_BLOCKED_UAS, array_filter(array_map('trim', explode("\n", $blocked_raw)))); $whitelisted_raw = isset($_POST['whitelisted_user_agents']) ? sanitize_textarea_field(wp_unslash($_POST['whitelisted_user_agents'])) : ''; update_option(self::OPTION_WHITELISTED_UAS, array_filter(array_map('trim', explode("\n", $whitelisted_raw)))); /* translators: $s: Username. */ $this->log_event(sprintf(__('User-Agent lists updated by %s.', 'advanced-ip-blocker'), $current_user_login), 'info'); $message = __('User-Agent lists saved successfully.', 'advanced-ip-blocker'); break; case 'save_honeypot_urls': $urls_raw = isset($_POST['honeypot_urls']) ? sanitize_textarea_field(wp_unslash($_POST['honeypot_urls'])) : ''; update_option(self::OPTION_HONEYPOT_URLS, array_filter(array_map('trim', explode("\n", $urls_raw)))); /* translators: $s: Username. */ $this->log_event(sprintf(__('Honeypot URLs list updated by %s.', 'advanced-ip-blocker'), $current_user_login), 'info'); $message = __('Honeypot URLs list saved successfully.', 'advanced-ip-blocker'); break; case 'save_asn_lists': // Guardar la Blocklist $blocked_raw = isset($_POST['blocked_asns']) ? sanitize_textarea_field(wp_unslash($_POST['blocked_asns'])) : ''; $blocked_lines = array_filter(array_map('trim', explode("\n", $blocked_raw))); $valid_blocked_asns = []; foreach ($blocked_lines as $line) { if (preg_match('/^AS\d+$/i', $line)) { $valid_blocked_asns[] = strtoupper($line); } } update_option(self::OPTION_BLOCKED_ASNS, array_unique($valid_blocked_asns)); // Guardar la Whitelist $whitelisted_raw = isset($_POST['whitelisted_asns']) ? sanitize_textarea_field(wp_unslash($_POST['whitelisted_asns'])) : ''; $whitelisted_lines = array_filter(array_map('trim', explode("\n", $whitelisted_raw))); $valid_whitelisted_asns = []; foreach ($whitelisted_lines as $line) { if (preg_match('/^AS\d+$/i', $line)) { $valid_whitelisted_asns[] = strtoupper($line); } } update_option(self::OPTION_WHITELISTED_ASNS, array_unique($valid_whitelisted_asns)); /* translators: %s: Username. */ $this->log_event(sprintf(__('ASN lists updated by %s.', 'advanced-ip-blocker'), $current_user_login), 'info'); $message = __('ASN lists saved successfully.', 'advanced-ip-blocker'); break; case 'clear_all_threat_scores': global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_ip_scores'; $wpdb->query("TRUNCATE TABLE `{$table_name}`"); /* translators: %s: Username. */ $this->log_event(sprintf(__('All threat scores have been manually reset to 0 by %s.', 'advanced-ip-blocker'), $current_user_login), 'critical'); $message = __('All active threat scores have been reset to 0.', 'advanced-ip-blocker'); break; case 'clear_all_signatures': global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_malicious_signatures'; $wpdb->query("TRUNCATE TABLE `{$table_name}`"); /* translators: %s: Username. */ $this->log_event(sprintf(__('All malicious signatures have been manually deleted by %s.', 'advanced-ip-blocker'), $current_user_login), 'critical'); $message = __('All active malicious signatures have been deleted.', 'advanced-ip-blocker'); break; case 'clear_specific_logs': global $wpdb; $log_types_to_clear = isset($_POST['log_types_to_clear']) && is_array($_POST['log_types_to_clear']) ? array_map('sanitize_key', $_POST['log_types_to_clear']) : []; if (!empty($log_types_to_clear)) { $placeholders = implode(', ', array_fill(0, count($log_types_to_clear), '%s')); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}advaipbl_logs WHERE log_type IN ({$placeholders})", $log_types_to_clear)); $types_string = implode(', ', $log_types_to_clear); /* translators: 1: The log type(s). 2: The admin username. */ $this->log_event(sprintf(__('Logs of type(s) [%1$s] manually deleted by %2$s.', 'advanced-ip-blocker'), $types_string, $current_user_login), 'warning'); /* translators: %s: The log type(s) that were deleted. */ $message = sprintf(__('Selected logs (%s) have been deleted.', 'advanced-ip-blocker'), $types_string); } else { $message = __('No log types were selected to be deleted.', 'advanced-ip-blocker'); $type = 'warning'; } break; case 'clear_all_logs': global $wpdb; $wpdb->query("TRUNCATE TABLE `{$wpdb->prefix}advaipbl_logs`"); $wpdb->query("TRUNCATE TABLE `{$wpdb->prefix}advaipbl_notifications_queue`"); /* translators: %s: Username. */ $this->log_event(sprintf(__('All log records have been manually deleted by %s.', 'advanced-ip-blocker'), $current_user_login), 'critical'); $message = __('All logs have been deleted.', 'advanced-ip-blocker'); break; default: $ip_or_range = isset($_POST['ip_address']) ? sanitize_text_field(trim(wp_unslash($_POST['ip_address']))) : null; if ($ip_or_range && !$this->is_valid_ip_or_range($ip_or_range)) { $message = __('The entered value is not a valid IP, CIDR range, or IP range.', 'advanced-ip-blocker'); $type = 'error'; } elseif ($ip_or_range) { switch ($action_type) { case 'add_manual_block': // Esta es la nueva lógica centralizada global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_blocked_ips'; $exists = $wpdb->get_var($wpdb->prepare("SELECT id FROM {$table_name} WHERE ip_range = %s", $ip_or_range)); if ($exists) { /* translators: %s: IP. */ $message = sprintf( __( 'Entry %s was already blocked.', 'advanced-ip-blocker' ), $ip_or_range ); $type = 'info'; } else { $this->block_ip_instantly( $ip_or_range, 'manual', __('Manual Block', 'advanced-ip-blocker'), [], 'admin_action' ); /* translators: %s: IP. */ $message = sprintf( __( 'Entry %s has been blocked.', 'advanced-ip-blocker' ), $ip_or_range ); } break; case 'remove_block': $this->desbloquear_ip($ip_or_range); /* translators: %s: IP. */ $message = sprintf( __( 'IP %s unblocked from all lists.', 'advanced-ip-blocker' ), $ip_or_range ); break; case 'add_whitelist': $success = $this->add_to_whitelist_and_unblock( $ip_or_range, __('Manually added via admin panel', 'advanced-ip-blocker') ); if ( $success ) { /* translators: %s: IP. */ $message = sprintf( __( 'Entry %s added to whitelist and unblocked.', 'advanced-ip-blocker' ), $ip_or_range ); } else { /* translators: %s: IP. */ $message = sprintf( __( 'Entry %s was already in the whitelist or is invalid.', 'advanced-ip-blocker' ), $ip_or_range ); $type = 'info'; } break; case 'remove_whitelist': $list = get_option( self::OPTION_WHITELIST, [] ); if ( array_key_exists( $ip_or_range, $list ) ) { unset( $list[ $ip_or_range ] ); update_option( self::OPTION_WHITELIST, $list ); wp_cache_delete( self::OPTION_WHITELIST, 'options' ); /* translators: 1: IP, 2: Username */ $this->log_event( sprintf( __( 'IP %1$s removed from whitelist by %2$s.', 'advanced-ip-blocker' ), $ip_or_range, $current_user_login ), 'info', $ip_or_range ); /* translators: %s: IP. */ $message = sprintf( __( 'IP %s removed from whitelist.', 'advanced-ip-blocker' ), $ip_or_range ); } break; } } break; } } if ($message) { set_transient(self::TRANSIENT_ADMIN_NOTICE, ['message' => $message, 'type' => $type], 30); } wp_safe_redirect(remove_query_arg(['settings-updated', 's', 'action', 'ip', '_wpnonce'], wp_get_referer())); exit; } /** * Muestra el dashboard de seguridad con el layout y los estilos de alta especificidad. */ private function display_dashboard_tab() { ?> [ 'title' => __('Dashboard', 'advanced-ip-blocker'), 'icon' => 'dashicons-dashboard', 'sub_tabs' => [ 'main_dashboard' => __('Security Dashboard', 'advanced-ip-blocker'), 'status' => __('System Status', 'advanced-ip-blocker') ] ], 'settings' => [ 'title' => __('Settings', 'advanced-ip-blocker'), 'icon' => 'dashicons-admin-settings', 'sub_tabs' => [ 'general_settings' => __('General', 'advanced-ip-blocker'), '2fa_management' => __('2FA Management', 'advanced-ip-blocker'), 'import_export' => __('Import / Export', 'advanced-ip-blocker') ] ], 'rules' => [ 'title' => __('Blocking Rules', 'advanced-ip-blocker'), 'icon' => 'dashicons-shield-alt', 'sub_tabs' => [ 'waf' => __('Firewall (WAF)', 'advanced-ip-blocker'), 'user_agents' => __('User Agents', 'advanced-ip-blocker'), 'honeypot' => __('Honeypot URLs', 'advanced-ip-blocker'), 'asn_blocking' => __('ASN Blocking', 'advanced-ip-blocker') ] ], 'ip_management' => [ 'title' => __('IP Management', 'advanced-ip-blocker'), 'icon' => 'dashicons-location', 'sub_tabs' => [ 'blocked_ips' => __('Blocked IPs', 'advanced-ip-blocker'), 'blocked_signatures' => __('Blocked Signatures', 'advanced-ip-blocker'), 'blocked_endpoints' => __('Blocked Endpoints', 'advanced-ip-blocker'), 'whitelist' => __('Whitelist', 'advanced-ip-blocker') ] ], 'logs' => [ 'title' => __('Logs & Sessions', 'advanced-ip-blocker'), 'icon' => 'dashicons-list-view', 'sub_tabs' => [ 'security_log' => __('Security Log', 'advanced-ip-blocker'), 'general_log' => __('General Log', 'advanced-ip-blocker'), 'ip_trust_log' => __('IP Trust Log', 'advanced-ip-blocker'), 'user_sessions' => __('User Sessions', 'advanced-ip-blocker'), 'cron_logs' => __('WP-Cron Log', 'advanced-ip-blocker') ] ], 'about' => [ 'title' => __('About', 'advanced-ip-blocker'), 'icon' => 'dashicons-info', 'sub_tabs' => [ 'credits' => __('Credits & Support', 'advanced-ip-blocker') ] ] ]; $current_page_slug = isset($_GET['page']) ? sanitize_key($_GET['page']) : 'advaipbl_settings_page'; // Primero, verificamos si la navegación es interna (clic en una pestaña/sub-pestaña) if (isset($_GET['tab'])) { $active_main_tab = sanitize_key($_GET['tab']); $active_sub_tab = isset($_GET['sub-tab']) ? sanitize_key($_GET['sub-tab']) : null; } else { // Si no, es una navegación desde el menú de la izquierda. // Mapeamos el slug de la página a la pestaña correcta. $page_slug_to_tab_map = [ 'advaipbl_settings_page' => ['dashboard', 'main_dashboard'], 'advaipbl_settings_page-settings' => ['settings', 'general_settings'], 'advaipbl_settings_page-rules' => ['rules', 'waf'], 'advaipbl_settings_page-ip-management' => ['ip_management', 'blocked_ips'], 'advaipbl_settings_page-logs' => ['logs', 'security_log'], 'advaipbl_settings_page-about' => ['about', 'credits'], ]; if (isset($page_slug_to_tab_map[$current_page_slug])) { list($active_main_tab, $active_sub_tab) = $page_slug_to_tab_map[$current_page_slug]; } else { // Fallback por si acaso $active_main_tab = 'dashboard'; $active_sub_tab = 'main_dashboard'; } } // Validación final para asegurarse de que las pestañas existen if (!isset($menu_structure[$active_main_tab])) { $active_main_tab = 'dashboard'; } if (empty($active_sub_tab) || !isset($menu_structure[$active_main_tab]['sub_tabs'][$active_sub_tab])) { // Asigna la primera sub-pestaña de la pestaña principal activa $active_sub_tab = key($menu_structure[$active_main_tab]['sub_tabs']); } $blocked_count = $this->get_blocked_count(); ?>

    display_admin_notice(); ?>
    $sub_tab_title) : $is_active = ($sub_tab_key === $active_sub_tab); $url = add_query_arg( [ 'page' => 'advaipbl_settings_page', 'tab' => $active_main_tab, 'sub-tab' => $sub_tab_key ], 'admin.php' ); ?>
    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 '

    Error: Content for this tab is not available.

    '; break; } ?>
    options = get_option(self::OPTION_SETTINGS); // Definimos la estructura del menú lateral y las secciones. $sections = [ 'general' => __('General', 'advanced-ip-blocker'), 'notifications' => __('Notifications', 'advanced-ip-blocker'), 'geolocation' => __('Geolocation', 'advanced-ip-blocker'), 'protections' => __('Core Protections', 'advanced-ip-blocker'), 'blocking_rules' => __('Threshold Blocking', 'advanced-ip-blocker'), 'login_protection' => __('Login Protection', 'advanced-ip-blocker'), 'signature_engine' => __('Signature Engine', 'advanced-ip-blocker'), 'uninstall' => __('Uninstallation', 'advanced-ip-blocker'), ]; ?>
      $title) : ?>

    options['allow_telemetry'])) : $telemetry_setting_id = 'advaipbl_allow_telemetry'; ?>

    Enable anonymous usage tracking.', 'advanced-ip-blocker'), ['a' => ['href' => []]]), '#' . esc_attr($telemetry_setting_id)); ?>

    options['enable_2fa']); $settings_url = admin_url('admin.php?page=advaipbl_settings_page-settings&sub-tab=general_settings#sub-section-2FA'); $profile_url = admin_url('profile.php'); ?>

    %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) ); ?>

    process_bulk_action(); $users_list_table->prepare_items(); ?>


    views(); ?>
    search_box( __( 'Search Users', 'advanced-ip-blocker' ), 'user' ); $users_list_table->display(); ?>
    options['enable_user_agent_blocking']); $settings_url = admin_url('admin.php?page=advaipbl_settings_page-settings&sub-tab=general_settings#sub-section-user-agent'); ?>

    %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. */ __('User-Agent Protection 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) ); ?>


    options['enable_honeypot_blocking']); $settings_url = admin_url('admin.php?page=advaipbl_settings_page-settings&sub-tab=general_settings#sub-section-honeypot'); ?>

    %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. */ __('Honeypot Protection 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) ); ?>

    /blog/ or /contact/). Use attack-specific patterns.', 'advanced-ip-blocker') ); ?>


    prefix . 'advaipbl_blocked_ips'; $filter_type = isset($_GET['filter_type']) ? sanitize_key($_GET['filter_type']) : 'all'; $orderby = isset($_GET['orderby']) && in_array($_GET['orderby'], ['ip_range', 'block_type', 'timestamp'], true) ? sanitize_key($_GET['orderby']) : 'timestamp'; $order = isset($_GET['order']) && in_array(strtolower($_GET['order']), ['asc', 'desc'], true) ? strtolower(sanitize_key($_GET['order'])) : 'desc'; $current_page = isset($_GET['paged']) ? absint($_GET['paged']) : 1; $per_page = isset($_GET['advaipbl_per_page']) ? absint($_GET['advaipbl_per_page']) : 20; $offset = ($current_page - 1) * $per_page; $where_clauses = []; if ('all' !== $filter_type) { $where_clauses[] = $wpdb->prepare("block_type = %s", $filter_type); } $where_sql = !empty($where_clauses) ? 'WHERE ' . implode(' AND ', $where_clauses) : ''; $total_items = $wpdb->get_var("SELECT COUNT(id) FROM {$table_name} {$where_sql}"); $total_pages = ceil($total_items / $per_page); $query_string = sprintf( "SELECT * FROM %s %s ORDER BY %s %s LIMIT %%d OFFSET %%d", $table_name, $where_sql, esc_sql($orderby), esc_sql($order) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $items_for_page = $wpdb->get_results($wpdb->prepare($query_string, $per_page, $offset), ARRAY_A); $ips_and_ranges_on_page = wp_list_pluck($items_for_page, 'ip_range'); $single_ips_on_page = array_filter($ips_and_ranges_on_page, function($entry) { return filter_var($entry, FILTER_VALIDATE_IP); }); $locations = !empty($single_ips_on_page) ? $this->session_manager->get_cached_locations($single_ips_on_page) : []; $definitions = $this->get_all_block_type_definitions(); ?>

    render_per_page_selector($per_page); ?> '; ?>
    add_query_arg(['paged' => '%#%', 'filter_type' => $filter_type, 'advaipbl_per_page' => $per_page]), 'format' => '', 'total' => $total_pages, 'current' => $current_page ]); if ($page_links) echo wp_kses_post($page_links); ?>


    print_sortable_header(__('IP', 'advanced-ip-blocker'), 'ip_range', $orderby, $order); ?> print_sortable_header(__('Block Type', 'advanced-ip-blocker'), 'block_type', $orderby, $order); ?> print_sortable_header(__('Since', 'advanced-ip-blocker'), 'timestamp', $orderby, $order); ?>
    options[$duration_key] ?? 1440); if ($duration_minutes <= 0) { esc_html_e('Permanent', 'advanced-ip-blocker'); } else { printf( /* translators: %d: The number of minutes for a block duration. */ esc_html__('%d minutes', 'advanced-ip-blocker'), absint($duration_minutes) ); } } else { esc_html_e('Permanent', 'advanced-ip-blocker'); } ?> '; } } else { esc_html_e( 'Not available', 'advanced-ip-blocker' ); } ?> 'advaipbl_unblock_ip', 'ip' => $block['ip_range']]), 'advaipbl_unblock_ip' ); ?>

    $column_key, 'order' => $next_order, 'filter_type' => $filter_type_sanitized, ]); echo '' . esc_html($label) . ''; } /** * 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 '' . esc_html($label) . ''; } private function display_whitelist_tab() { $raw_whitelist_ips = get_option('advaipbl_ips_whitelist', []); $table_data = []; if ( is_array($raw_whitelist_ips) ) { foreach ($raw_whitelist_ips as $ip => $data) { if ( is_array($data) && isset($data['timestamp']) ) { $table_data[] = [ 'ip' => $ip, 'timestamp' => $data['timestamp'], 'detail' => $data['detail'] ?? '' ]; } elseif ( is_numeric($data) ) { $table_data[] = [ 'ip' => $ip, 'timestamp' => $data, 'detail' => __('Migrated from old format', 'advanced-ip-blocker') ]; } } } $sortable_columns = ['ip', 'timestamp', 'detail']; $orderby = isset($_GET['orderby']) && in_array($_GET['orderby'], $sortable_columns, true) ? sanitize_key($_GET['orderby']) : 'timestamp'; $order = isset($_GET['order']) && in_array(strtolower($_GET['order']), ['asc', 'desc'], true) ? strtolower(sanitize_key($_GET['order'])) : 'desc'; usort($table_data, function($a, $b) use ($orderby, $order) { $a_val = $a[$orderby] ?? ''; $b_val = $b[$orderby] ?? ''; if ($a_val == $b_val) return 0; return ($order === 'asc') ? strnatcasecmp((string)$a_val, (string)$b_val) : strnatcasecmp((string)$b_val, (string)$a_val); }); $current_page = isset($_GET['paged']) ? absint($_GET['paged']) : 1; $per_page = isset($_GET['advaipbl_per_page']) ? absint($_GET['advaipbl_per_page']) : 20; $total_items = count($table_data); $total_pages = ceil($total_items / $per_page); $items_for_page = array_slice($table_data, ($current_page - 1) * $per_page, $per_page); // Obtenemos ubicaciones para las IPs de la página actual --- $ips_on_page = wp_list_pluck($items_for_page, 'ip'); $locations = $this->session_manager->get_cached_locations($ips_on_page); ?>

    render_per_page_selector( $per_page ); ?>
    add_query_arg('paged', '%#%'), 'format' => '', 'total' => $total_pages, 'current' => $current_page]); if ($page_links) echo wp_kses_post($page_links); ?>

    print_log_sortable_header(__('IP / Location', 'advanced-ip-blocker'), 'ip', $orderby, $order); ?> print_log_sortable_header(__('User', 'advanced-ip-blocker'), 'user', $orderby, $order); ?> print_log_sortable_header(__('Details', 'advanced-ip-blocker'), 'detail', $orderby, $order); ?> print_log_sortable_header(__('Added Since', 'advanced-ip-blocker'), 'timestamp', $orderby, $order); ?>
    ' . esc_html(implode(', ', $location_parts)) . ''; } ?> ' . esc_html($matches[1]) . ''; echo '
    ' . esc_html($matches[2]) . ''; } ?>
    0 ? esc_html(self::get_formatted_datetime($item['timestamp'])) : '...'; ?>
    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') . '

    '; return; } $search_term = isset($_GET['s']) ? sanitize_text_field(wp_unslash($_GET['s'])) : ''; $orderby = isset($_GET['orderby']) && in_array($orderby, ['timestamp', 'level', 'ip', 'message'], true) ? sanitize_key($_GET['orderby']) : 'timestamp'; $order = isset($_GET['order']) && in_array(strtolower($_GET['order']), ['asc', 'desc'], true) ? strtolower(sanitize_key($_GET['order'])) : 'desc'; $current_page = isset($_GET['paged']) ? absint($_GET['paged']) : 1; $per_page = isset($_GET['advaipbl_per_page']) ? absint($_GET['advaipbl_per_page']) : 25; global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_logs'; $where_clauses = [$wpdb->prepare("log_type = %s", 'general')]; if (!empty($search_term)) { $search_like = '%' . $wpdb->esc_like($search_term) . '%'; $where_clauses[] = $wpdb->prepare("(ip LIKE %s OR message LIKE %s)", $search_like, $search_like); } $where_sql = implode(' AND ', $where_clauses); $total_items = $wpdb->get_var("SELECT COUNT(log_id) FROM $table_name WHERE $where_sql"); $total_pages = ceil($total_items / $per_page); $offset = ($current_page - 1) * $per_page; $logs = $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); ?>

    render_per_page_selector( $per_page ); ?>
    add_query_arg('paged', '%#%'), 'format' => '', 'total' => $total_pages, 'current' => $current_page]); if ($page_links) echo wp_kses_post($page_links); ?>

    print_log_sortable_header(__('Date/Time', 'advanced-ip-blocker'), 'timestamp', $orderby, $order); ?> print_log_sortable_header(__('Level', 'advanced-ip-blocker'), 'level', $orderby, $order); ?> print_log_sortable_header(__('IP', 'advanced-ip-blocker'), 'ip', $orderby, $order); ?> print_log_sortable_header(__('Message', 'advanced-ip-blocker'), 'message', $orderby, $order); ?>
    prefix . 'advaipbl_ip_scores'; $is_enabled = !empty($this->options['enable_threat_scoring']); $settings_url = admin_url('admin.php?page=advaipbl_settings_page-settings&sub-tab=general_settings#section-ip_trust'); ?>

    %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); ?>

    render_per_page_selector($per_page); ?>
    add_query_arg('paged', '%#%'), 'format' => '', 'total' => $total_pages, 'current' => $current_page]); if ($page_links) echo wp_kses_post($page_links); ?>

    print_log_sortable_header(__('IP / Location', 'advanced-ip-blocker'), 'ip', $orderby, $order); ?> print_log_sortable_header(__('Current Score', 'advanced-ip-blocker'), 'score', $orderby, $order); ?> print_log_sortable_header(__('Last Activity', 'advanced-ip-blocker'), 'last_event_timestamp', $orderby, $order); ?>
    ' . esc_html(trim("$city, $country", ', ')) . ''; } ?> 0 ? min(100, ($item['score'] / $threshold) * 100) : 100; $progress_color = $score_percentage > 80 ? '#dc3232' : ($score_percentage > 50 ? '#f59e0b' : '#0073aa'); ?>

    ' . esc_html(ucfirst(str_replace('_', ' ', $last_event['event']))) . ''; echo ' (+ ' . esc_html($last_event['points']) . ' ' . esc_html__('points', 'advanced-ip-blocker') . ')'; } ?>
    __('General System Event Log', 'advanced-ip-blocker'), 'wp_cron' => __('WP-Cron Execution Log', 'advanced-ip-blocker'), ]; $title = $titles[$log_types] ?? ucfirst($log_types) . ' Log'; $current_sub_tab = ($log_types === 'general') ? 'general_log' : $log_types . '_logs'; } $search_term = isset($_GET['s']) ? sanitize_text_field(wp_unslash($_GET['s'])) : ''; $orderby = isset($_GET['orderby']) && in_array($orderby, ['timestamp', 'ip', 'log_type', 'message'], true) ? sanitize_key($_GET['orderby']) : 'timestamp'; $order = isset($_GET['order']) && in_array(strtolower($_GET['order']), ['asc', 'desc'], true) ? strtolower(sanitize_key($_GET['order'])) : 'desc'; $current_page = isset($_GET['paged']) ? absint($_GET['paged']) : 1; $per_page = isset($_GET['advaipbl_per_page']) ? absint($_GET['advaipbl_per_page']) : 20; global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_logs'; $where_clauses = []; $level_placeholders = implode(', ', array_fill(0, count($levels_to_show), '%s')); $where_clauses[] = $wpdb->prepare("level IN ({$level_placeholders})", $levels_to_show); if ($is_unified_log) { $types_to_query = ($current_filter !== 'all' && in_array($current_filter, $log_types)) ? [$current_filter] : $log_types; if (!empty($types_to_query)) { $placeholders = implode(', ', array_fill(0, count($types_to_query), '%s')); $where_clauses[] = $wpdb->prepare("log_type IN ({$placeholders})", $types_to_query); } } else { $where_clauses[] = $wpdb->prepare("log_type = %s", $log_types); } if (!empty($search_term)) { $search_like = '%' . $wpdb->esc_like($search_term) . '%'; $where_clauses[] = $wpdb->prepare("(ip LIKE %s OR details LIKE %s OR message LIKE %s)", $search_like, $search_like, $search_like); } $where_sql = implode(' AND ', $where_clauses); $total_items = $wpdb->get_var("SELECT COUNT(log_id) FROM $table_name WHERE $where_sql"); $total_pages = ceil($total_items / $per_page); $offset = ($current_page - 1) * $per_page; $logs = $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); ?>

    render_per_page_selector( $per_page ); ?>
    add_query_arg('paged', '%#%'), 'format' => '', 'total' => $total_pages, 'current' => $current_page]); if ($page_links) echo wp_kses_post($page_links); ?>

    print_log_sortable_header(__('Date/Time', 'advanced-ip-blocker'), 'timestamp', $orderby, $order); ?> print_log_sortable_header(__('IP', 'advanced-ip-blocker'), 'ip', $orderby, $order); ?> print_log_sortable_header(__('Type', 'advanced-ip-blocker'), 'log_type', $orderby, $order); ?>
    options['duration_' . $log_type])) { $duration_minutes = (int) $this->options['duration_' . $log_type]; } $duration_text = ''; if ($log_type === 'manual' || $duration_minutes <= 0) { $duration_text = __('Permanent', 'advanced-ip-blocker'); } else { /* translators: %d: The number of minutes for a block duration. */ $duration_text = sprintf(esc_html__('%d min', 'advanced-ip-blocker'), $duration_minutes); } echo '
    ' . esc_html__('Blocked:', 'advanced-ip-blocker') . ' ' . esc_html($duration_text); } ?>
    ' . implode(', ', $location_parts) . ''; } ?> get_all_block_type_definitions()[$log_type]['label'] ?? ucwords(str_replace('_', ' ', $log_type))); ?> ' . esc_html__('User:', 'advanced-ip-blocker') . ' ' . esc_html($details['username'] ?? 'N/A'); break; case 'waf': $detail_display = '' . esc_html__('Rule:', 'advanced-ip-blocker') . ' ' . esc_html($details['rule'] ?? 'N/A'); if (!empty($uri)) { $detail_display .= '
    ' . $uri . ''; } break; case 'asn': $detail_display = esc_html($details['asn_number'] ?? 'N/A') . ' (' . esc_html($details['asn_name'] ?? '') . ')'; if (!empty($details['source'])) { $detail_display .= ' - ' . __('Source:', 'advanced-ip-blocker') . ' ' . esc_html($details['source']); } break; case 'threat_score': $detail_display = esc_html($entry['message']); break; case 'signature_challenge': $detail_display = esc_html($entry['message']); break; case 'signature_flagged': $hash = $details['signature_hash'] ?? 'N/A'; $short_hash = substr($hash, 0, 12) . '...'; $detail_display = '' . esc_html__('Signature:', 'advanced-ip-blocker') . ' ' . esc_html($short_hash) . ''; $detail_display .= '
    ' . esc_html($entry['message']) . ''; break; case '404': case '403': case 'honeypot': default: $detail_display = !empty($uri) ? $uri : esc_html($entry['message']); break; } echo wp_kses_post($detail_display); ?>
    [ 'label' => __('Geoblock', 'advanced-ip-blocker'), 'option_key' => self::OPTION_BLOCKED_GEO, 'duration_key' => 'duration_geoblock', 'uses_transient'=> true ], 'honeypot' => [ 'label' => __('Honeypot', 'advanced-ip-blocker'), 'option_key' => self::OPTION_BLOCKED_HONEYPOT, 'duration_key' => 'duration_honeypot', 'uses_transient'=> true ], 'user_agent' => [ 'label' => __('User-Agent', 'advanced-ip-blocker'), 'option_key' => self::OPTION_BLOCKED_USER_AGENT, 'duration_key' => 'duration_user_agent', 'uses_transient'=> true ], 'manual' => [ 'label' => __('Manual Block', 'advanced-ip-blocker'), 'option_key' => self::OPTION_BLOCKED_MANUAL, 'duration_key' => null, // Permanente 'uses_transient'=> false // No necesita transient, se comprueba siempre ], '404' => [ 'label' => __('404 Error', 'advanced-ip-blocker'), 'option_key' => self::OPTION_BLOCKED_404, 'duration_key' => 'duration_404', 'uses_transient'=> true ], '403' => [ 'label' => __('403 Error', 'advanced-ip-blocker'), 'option_key' => self::OPTION_BLOCKED_403, 'duration_key' => 'duration_403', 'uses_transient'=> true ], 'login' => [ 'label' => __('Login Failure', 'advanced-ip-blocker'), 'option_key' => self::OPTION_BLOCKED_LOGIN, 'duration_key' => 'duration_login', 'uses_transient'=> true ], 'waf' => [ 'label' => __('WAF Block', 'advanced-ip-blocker'), 'option_key' => self::OPTION_BLOCKED_WAF, 'duration_key' => 'duration_waf', 'uses_transient'=> true ], 'rate_limit' => [ 'label' => __('Rate Limit', 'advanced-ip-blocker'), 'option_key' => self::OPTION_BLOCKED_RATE_LIMIT, 'duration_key' => 'duration_rate_limit', 'uses_transient'=> true ], 'asn' => [ 'label' => __('ASN Block', 'advanced-ip-blocker'), 'option_key' => self::OPTION_BLOCKED_ASN, 'duration_key' => 'duration_asn', 'uses_transient'=> true ], 'xmlrpc_block' => [ 'label' => __('XML-RPC Block', 'advanced-ip-blocker'), 'option_key' => self::OPTION_BLOCKED_XMLRPC, 'duration_key' => 'duration_xmlrpc_block', 'uses_transient'=> true ], 'threat_score' => [ 'label' => __('Threat Score', 'advanced-ip-blocker'), 'option_key' => self::OPTION_BLOCKED_THREAT_SCORE, 'duration_key' => 'duration_threat_score', 'uses_transient'=> true ], // Tipos de log que no son bloqueos persistentes 'signature_challenge' => [ 'label' => __('Signature Challenge', 'advanced-ip-blocker'), 'option_key' => null, 'duration_key' => null, 'uses_transient' => false ], 'signature_flagged' => [ 'label' => __('Signature Flagged', 'advanced-ip-blocker'), 'option_key' => null, 'duration_key' => null, 'uses_transient' => false ], 'endpoint_challenge' => [ 'label' => __('Endpoint Challenge', 'advanced-ip-blocker'), 'option_key' => null, 'duration_key' => null, 'uses_transient' => false ], ]; } /** * Limpia la entrada de la caché de objetos para la lista de IPs bloqueadas. * Debe ser llamada cada vez que la tabla _advaipbl_blocked_ips es modificada. */ private function clear_blocked_ips_cache() { wp_cache_delete('advaipbl_all_blocked_entries', 'advaipbl'); } public function get_all_blocked_entries() { // Clave única para la caché de esta petición. $cache_key = 'advaipbl_all_blocked_entries'; // Grupo para nuestra caché, una buena práctica. $cache_group = 'advaipbl'; // 1. Intentar obtener los datos de la caché de memoria RAM primero. $cached_entries = wp_cache_get($cache_key, $cache_group); if (false !== $cached_entries) { // ¡Éxito! Los datos ya estaban en la caché. Los devolvemos directamente. return $cached_entries; } // 2. Si no estaban en la caché, los generamos desde la base de datos. global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_blocked_ips'; $definitions = $this->get_all_block_type_definitions(); $results = $wpdb->get_results("SELECT * FROM {$table_name}", ARRAY_A); $all_blocked = []; if (!empty($results)) { foreach ($results as $row) { $all_blocked[] = [ 'ip' => $row['ip_range'], 'type' => $row['block_type'], 'type_label' => $definitions[$row['block_type']]['label'] ?? ucwords(str_replace('_', ' ', $row['block_type'])), 'timestamp' => $row['timestamp'], 'detail' => $row['reason'], ]; } } // 3. Antes de devolver los datos, los guardamos en la caché de memoria wp_cache_set($cache_key, $all_blocked, $cache_group); return $all_blocked; } private function display_credits_tab() { $plugin_data = get_plugin_data( ADVAIPBL_PLUGIN_FILE ); $plugin_name = $plugin_data['Name'] ?? 'Advanced IP Blocker'; ?>

    • Stripe
    • bc1qxfsmpqk0q7an82ckm7380pzpsea2sa294824lg
    • 0xAF8CC1B71EAbF5dffDa2aF58AEF43b2559163284

    
    # General Help
    wp help advaipbl
    
    # IP Management
    wp advaipbl block <ip> [--reason=<reason>]
    wp advaipbl unblock <ip>
    wp advaipbl whitelist <add|remove|list> [<ip>]
    
    # Threat Scoring & Signatures
    wp advaipbl score <list|get|reset|decay-run> [--ip=<ip>]
    wp advaipbl signature <list|delete|whitelist> [--hash=<hash>]
    wp advaipbl signature-analyze
    
    # Geolocation, ASN & WAF
    wp advaipbl geoblock <add|remove|list> <country_code>
    wp advaipbl asn <add|remove|list> <asn>
    wp advaipbl asn-whitelist <add|remove|list> <asn>
    wp advaipbl waf <add|remove|list> [<rule>] [--from-file=<file>]
    
    # User & Session Management
    wp advaipbl session <list|terminate> [--user-id=<id>]
    wp advaipbl 2fa <status|reset> <user>
    
    # System & Config
    wp advaipbl log <list|clear> [--type=<type>] [--count=<number>]
    wp advaipbl config <get|set> [<key>] [<value>]
    wp advaipbl provider <list|set|set_key|remove_key>
    wp advaipbl spamhaus-update
    

    'sanitize_textarea_field']); add_settings_section('advaipbl_general_settings_section', null, null, $page); add_settings_field('advaipbl_enable_logging', __('Enable Logging', 'advanced-ip-blocker'), [$this, 'switch_field_callback'], $page, 'advaipbl_general_settings_section', ['name' => 'enable_logging', 'label' => __('Enable logging of events to the database.', 'advanced-ip-blocker')]); add_settings_field('advaipbl_log_retention_days', __('Log Retention (days)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_general_settings_section', ['name' => 'log_retention_days', 'default' => 30, 'description' => __('Number of days to keep logs in the DB. 0 to disable automatic purging.', 'advanced-ip-blocker')]); add_settings_field('advaipbl_log_timezone', __('Timezone for Logs', 'advanced-ip-blocker'), [$this, 'timezone_select_callback'], $page, 'advaipbl_general_settings_section'); add_settings_field('advaipbl_custom_block_message', __('Block Message', 'advanced-ip-blocker'), [$this, 'textarea_field_callback'], $page, 'advaipbl_general_settings_section', ['name' => 'custom_block_message', 'label' => __('Message for blocked users. Leave blank for default messages.', 'advanced-ip-blocker')]); add_settings_field('advaipbl_excluded_error_urls', __('Exclude URLs from Error Logging', 'advanced-ip-blocker'), [$this, 'textarea_field_callback'], $page, 'advaipbl_general_settings_section', ['name' => 'excluded_error_urls', 'label' => __('Add one URL or fragment per line. If a 403 or 404 error URL contains one of these strings, it will not be logged as an error.', 'advanced-ip-blocker')]); add_settings_field('advaipbl_show_admin_bar_menu', __( 'Admin Bar Menu', 'advanced-ip-blocker' ), [$this, 'switch_field_callback'], $page, 'advaipbl_general_settings_section', ['name' => 'show_admin_bar_menu', 'label' => __( 'Show security menu in the WordPress admin bar', 'advanced-ip-blocker' )]); add_settings_section('advaipbl_general_settings_section', null, null, $page); add_settings_field( 'advaipbl_allow_telemetry', __('Usage Tracking', 'advanced-ip-blocker'), [$this, 'switch_field_callback'], $page, 'advaipbl_general_settings_section', [ 'name' => 'allow_telemetry', 'id' => 'advaipbl_allow_telemetry', 'label' => __('Share anonymous usage data to help us improve the plugin.', 'advanced-ip-blocker'), 'description' => __('Learn More about what we collect.', 'advanced-ip-blocker') ] ); // Renombramos la sección antigua para que no haya conflictos add_settings_section('advaipbl_email_notifications_section', null, null, $page); add_settings_field('advaipbl_enable_email_notifications', __('Enable Email Notifications', 'advanced-ip-blocker'), [$this, 'switch_field_callback'], $page, 'advaipbl_email_notifications_section', ['name' => 'enable_email_notifications', 'label' => __('Send an email when an IP is blocked.', 'advanced-ip-blocker')]); add_settings_field('advaipbl_notification_frequency', __('Notification Frequency', 'advanced-ip-blocker'), [$this, 'notification_frequency_callback'], $page, 'advaipbl_email_notifications_section'); add_settings_field('advaipbl_notification_email', __('Notification Email', 'advanced-ip-blocker'), [$this, 'email_field_callback'], $page, 'advaipbl_email_notifications_section', ['name' => 'notification_email', 'label' => __('Leave empty to use the admin email.', 'advanced-ip-blocker')]); add_settings_field( 'advaipbl_send_test_email_button', __( 'Send Test Email', 'advanced-ip-blocker' ), [ $this, 'send_test_email_button_callback' ], $page, 'advaipbl_email_notifications_section' ); // Sub-sección para Push add_settings_section('advaipbl_push_notifications_section', null, null, $page); add_settings_field('advaipbl_enable_push_notifications', __('Enable Push Notifications', 'advanced-ip-blocker'), [$this, 'switch_field_callback'], $page, 'advaipbl_push_notifications_section', ['name' => 'enable_push_notifications', 'label' => __('Send real-time alerts via webhooks.', 'advanced-ip-blocker')]); // Este usará el 'large-text' por defecto, que es correcto para URLs. add_settings_field('advaipbl_push_webhook_urls', __('Push Webhook URLs', 'advanced-ip-blocker'), [$this, 'textarea_field_callback'], $page, 'advaipbl_push_notifications_section', ['name' => 'push_webhook_urls', 'description' => __('One URL per line.', 'advanced-ip-blocker')]); // A este le especificamos que use 'regular-text'. add_settings_field('advaipbl_push_mentions', __('Mentions to Trigger Push', 'advanced-ip-blocker'), [$this, 'textarea_field_callback'], $page, 'advaipbl_push_notifications_section', ['name' => 'push_mentions', 'rows' => 3, 'class' => 'regular-text', 'description' => __('Optional. To force a push notification on Slack/Discord, add a mention like @here, @channel, or a specific @username. Add one per line if needed.', 'advanced-ip-blocker')]); add_settings_field('advaipbl_push_critical_only', __('Send Critical Alerts Only', 'advanced-ip-blocker'), [$this, 'switch_field_callback'], $page, 'advaipbl_push_notifications_section', ['name' => 'push_critical_only', 'label' => __('Only send alerts for instant blocks (WAF, Honeypot, etc.), not for threshold-based blocks (404, Login).', 'advanced-ip-blocker')]); add_settings_field( 'advaipbl_send_test_push_button', __( 'Send Test Notification', 'advanced-ip-blocker' ), [ $this, 'send_test_push_button_callback' ], $page, 'advaipbl_push_notifications_section' ); // Sección Geolocation. add_settings_section( 'advaipbl_geolocation_section', null, null, $page ); add_settings_field( 'advaipbl_geolocation_method', __( 'Geolocation Method', 'advanced-ip-blocker' ), [$this, 'geolocation_method_callback'], $page, 'advaipbl_geolocation_section' ); // API PROVIDER (solo para el método 'api') add_settings_field( 'advaipbl_geolocation_provider', __( 'API Provider', 'advanced-ip-blocker' ), [$this, 'geolocation_provider_callback'], $page, 'advaipbl_geolocation_section', ['class' => 'advaipbl-geolocation-api-option'] // Clase para JS ); // API KEYS (solo para el método 'api') add_settings_field('advaipbl_api_key_ip_apicom', 'ip-api.com ' . __('API Key', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_geolocation_section', ['name' => 'api_key_ip_apicom', 'description' => __('Optional key for HTTPS access.', 'advanced-ip-blocker'), 'class' => 'api-key-field advaipbl-geolocation-api-option', 'data_provider' => 'ip-api.com']); add_settings_field('advaipbl_api_key_ipinfocom', 'ipinfo.io ' . __('API Key', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_geolocation_section', ['name' => 'api_key_ipinfocom', 'description' => __('API key is recommended for higher limits.', 'advanced-ip-blocker'), 'class' => 'api-key-field advaipbl-geolocation-api-option', 'data_provider' => 'ipinfo.io']); add_settings_field('advaipbl_api_key_ipapicom', 'ipapi.com ' . __('API Key', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_geolocation_section', ['name' => 'api_key_ipapicom', 'description' => __('API Key is required.', 'advanced-ip-blocker'), 'class' => 'api-key-field advaipbl-geolocation-api-option', 'data_provider' => 'ipapi.com']); add_settings_field('advaipbl_api_key_ipstackcom', 'ipstack.com ' . __('API Key', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_geolocation_section', ['name' => 'api_key_ipstackcom', 'description' => __('API Key is required.', 'advanced-ip-blocker'), 'class' => 'api-key-field advaipbl-geolocation-api-option', 'data_provider' => 'ipstack.com']); // MAXMIND (solo para el método 'local_db') add_settings_field('advaipbl_maxmind_license_key', __( 'MaxMind License Key', 'advanced-ip-blocker' ), [$this, 'text_field_callback'], $page, 'advaipbl_geolocation_section', ['name' => 'maxmind_license_key', 'class' => 'advaipbl-geolocation-db-option', 'description' => sprintf( wp_kses( /* translators: $s: Maxmind website URL */ __( 'Required for the Local Database method. Get a free key from the MaxMind website.', 'advanced-ip-blocker' ), ['a' => ['href' => [], 'target' => []]] ), 'https://www.maxmind.com/en/geolite2/signup' )]); add_settings_field('advaipbl_geoip_db_status', __( 'Local Database Status', 'advanced-ip-blocker' ), [$this, 'geoip_db_status_callback'], $page, 'advaipbl_geolocation_section', ['class' => 'advaipbl-geolocation-db-option']); // CACHE (siempre visible) add_settings_field('advaipbl_clear_location_cache_button', __('Geolocation Cache', 'advanced-ip-blocker'), [$this, 'clear_cache_button_callback'], $page, 'advaipbl_geolocation_section'); add_settings_section('advaipbl_geoblocking_settings_section', null, null, $page ); add_settings_field('advaipbl_enable_geoblocking', __('Enable Geoblocking', 'advanced-ip-blocker'), [$this, 'switch_field_callback'], $page, 'advaipbl_geoblocking_settings_section', ['name' => 'enable_geoblocking', 'label' => __('Activate country-based blocking.', 'advanced-ip-blocker'), 'description' => sprintf(__('Block access from entire countries. This feature requires a working Geolocation Provider to be configured above.', 'advanced-ip-blocker'))]); add_settings_field('advaipbl_geoblock_countries', __( 'Blocked Countries', 'advanced-ip-blocker' ), [ $this, 'geoblock_countries_callback' ], $page, 'advaipbl_geoblocking_settings_section' ); add_settings_field('advaipbl_duration_geoblock', __( 'Geoblock Duration (min)', 'advanced-ip-blocker' ), [ $this, 'text_field_callback' ], $page, 'advaipbl_geoblocking_settings_section', ['name' => 'duration_geoblock', 'default' => 1440, 'description' => __( 'How long an IP from a blocked country will be blocked. Set to 0 for a permanent block.', 'advanced-ip-blocker' )]); add_settings_section('advaipbl_honeypot_settings_section', null, null, $page); add_settings_field('advaipbl_enable_honeypot_blocking', __('Enable Honeypot Blocking', 'advanced-ip-blocker'), [$this, 'switch_field_callback'], $page, 'advaipbl_honeypot_settings_section', ['name' => 'enable_honeypot_blocking', 'label' => __('Activate Honeypot trap protection.', 'advanced-ip-blocker'), 'description' => sprintf(/* translators: $s: Honeypot blocking rules URL */__('Manage Honeypot URLs in the Blocking Rules tab.', 'advanced-ip-blocker'), admin_url('admin.php?page=advaipbl_settings_page&tab=rules&sub-tab=honeypot'))]); add_settings_field('advaipbl_duration_honeypot', __('Honeypot Duration (min)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_honeypot_settings_section', ['name' => 'duration_honeypot', 'default' => 1440, 'description' => __('Block duration for IPs that fall into a honeypot URL. Set to 0 for a permanent block.', 'advanced-ip-blocker')]); add_settings_section('advaipbl_user_agent_settings_section', null, null, $page); add_settings_field('advaipbl_enable_user_agent_blocking', __('Enable User-Agent Blocking', 'advanced-ip-blocker'), [$this, 'switch_field_callback'], $page, 'advaipbl_user_agent_settings_section', ['name' => 'enable_user_agent_blocking', 'label' => __('Activate User-Agent based blocking.', 'advanced-ip-blocker'), 'description' => sprintf(/* translators: $s: User-Agent blocking rules URL */__('Manage User-Agent lists in the Blocking Rules tab.', 'advanced-ip-blocker'), admin_url('admin.php?page=advaipbl_settings_page&tab=rules&sub-tab=user_agents'))]); add_settings_field('advaipbl_duration_user_agent', __('User-Agent Duration (min)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_user_agent_settings_section', ['name' => 'duration_user_agent', 'default' => 1440, 'description' => __('Block duration for IPs with a malicious User-Agent. Set to 0 for a permanent block.', 'advanced-ip-blocker')]); add_settings_section('advaipbl_asn_protection_section', __('ASN Protection', 'advanced-ip-blocker'), null, $page); add_settings_field('advaipbl_enable_spamhaus_asn', __('Spamhaus ASN Protection', 'advanced-ip-blocker'), [$this, 'switch_field_callback'], $page, 'advaipbl_asn_protection_section', ['name' => 'enable_spamhaus_asn', 'label' => __('Enable automatic blocking using the Spamhaus ASN DROP list.', 'advanced-ip-blocker'), 'description' => __('Provides a powerful, auto-updating layer of protection against the worst networks on the internet. The list is refreshed daily.', 'advanced-ip-blocker')]); add_settings_field('advaipbl_enable_manual_asn', __('Manual ASN Protection', 'advanced-ip-blocker'), [$this, 'switch_field_callback'], $page, 'advaipbl_asn_protection_section', ['name' => 'enable_manual_asn', 'label' => __('Enable blocking based on your custom Manual ASN Blocklist.', 'advanced-ip-blocker'), 'description' => sprintf(/* translators: $s: ASN blocklist rules URL */__('Manage your custom list in the Blocking Rules tab.', 'advanced-ip-blocker'), admin_url('admin.php?page=advaipbl_settings_page&tab=rules&sub-tab=asn_blocking'))]); add_settings_field('advaipbl_duration_asn', __('ASN Block Duration (min)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_asn_protection_section', ['name' => 'duration_asn', 'default' => 1440, 'description' => __('Duration to block IPs from both manual and automated ASN lists. Set to 0 for a permanent block.', 'advanced-ip-blocker')]); add_settings_section('advaipbl_waf_settings_section', null, null, $page); add_settings_field('advaipbl_enable_waf', __('Enable WAF', 'advanced-ip-blocker'), [$this, 'switch_field_callback'], $page, 'advaipbl_waf_settings_section', ['name' => 'enable_waf', 'label' => __('Activate the WAF to scan requests against your custom rules.', 'advanced-ip-blocker'), 'description' => sprintf(/* translators: $s: WAF blocking rules URL */__('Manage WAF rules in the Blocking Rules tab.', 'advanced-ip-blocker'), admin_url('admin.php?page=advaipbl_settings_page&tab=rules&sub-tab=waf'))]); add_settings_field('advaipbl_duration_waf', __('WAF Block Duration (min)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_waf_settings_section', ['name' => 'duration_waf', 'default' => 1440, 'description' => __('Duration to block IPs that trigger a WAF rule. Set to 0 for a permanent block.', 'advanced-ip-blocker')]); add_settings_field('advaipbl_waf_excluded_urls', __('Excluded URLs for WAF', 'advanced-ip-blocker'), [$this, 'textarea_field_callback'], $page, 'advaipbl_waf_settings_section', ['name' => 'waf_excluded_urls', 'label' => __('Add one URL fragment per line. Requests to URLs containing these strings will not be scanned by the WAF. Use this for payment gateway webhooks or problematic AJAX actions.', 'advanced-ip-blocker')]); add_settings_section('advaipbl_404_settings_section', null, null, $page); add_settings_field('advaipbl_threshold_404', __('404 Threshold', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_404_settings_section', ['name' => 'threshold_404', 'default' => 10, 'description' => __('Number of 404 errors to trigger a block.', 'advanced-ip-blocker')]); add_settings_field('advaipbl_transient_expiration_404', __('404 Time Window (min)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_404_settings_section', ['name' => 'transient_expiration_404', 'default' => 60, 'description' => __('Time period in which the error count must be reached.', 'advanced-ip-blocker')]); add_settings_field('advaipbl_duration_404', __('404 Duration (min)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_404_settings_section', ['name' => 'duration_404', 'default' => 120, 'description' => __('How long the IP will be blocked.', 'advanced-ip-blocker')]); add_settings_section('advaipbl_403_settings_section', null, null, $page); add_settings_field('advaipbl_threshold_403', __('403 Threshold', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_403_settings_section', ['name' => 'threshold_403', 'default' => 5, 'description' => __('Number of 403 errors to trigger a block.', 'advanced-ip-blocker')]); add_settings_field('advaipbl_transient_expiration_403', __('403 Time Window (min)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_403_settings_section', ['name' => 'transient_expiration_403', 'default' => 30, 'description' => __('Time period in which the error count must be reached.', 'advanced-ip-blocker')]); add_settings_field('advaipbl_duration_403', __('403 Duration (min)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_403_settings_section', ['name' => 'duration_403', 'default' => 60, 'description' => __('How long the IP will be blocked.', 'advanced-ip-blocker')]); add_settings_section('advaipbl_rate_limiting_section', null, null, $page); add_settings_field('advaipbl_rate_limiting_enable', __('Enable Rate Limiting', 'advanced-ip-blocker'), [$this, 'switch_field_callback'], $page, 'advaipbl_rate_limiting_section', ['name' => 'rate_limiting_enable', 'label' => __('Temporarily block IPs that make too many requests.', 'advanced-ip-blocker')]); add_settings_field('advaipbl_rate_limiting_limit', __('Request Limit', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_rate_limiting_section', ['name' => 'rate_limiting_limit', 'default' => 120, 'description' => __('Number of requests to allow within the time window.', 'advanced-ip-blocker')]); add_settings_field('advaipbl_rate_limiting_window', __('Time Window (seconds)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_rate_limiting_section', ['name' => 'rate_limiting_window', 'default' => 60, 'description' => __('The time period in which the request count is measured.', 'advanced-ip-blocker')]); add_settings_field('advaipbl_duration_rate_limit', __('Block Duration (minutes)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_rate_limiting_section', ['name' => 'duration_rate_limit', 'default' => 5, 'description' => __('How long the IP will be blocked after reaching the limit.', 'advanced-ip-blocker')]); add_settings_section('advaipbl_login_settings_section', null, null, $page); add_settings_field('advaipbl_threshold_login', __('Login Failure Threshold', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_login_settings_section', ['name' => 'threshold_login', 'default' => 5, 'description' => __('Number of failed logins to trigger a block.', 'advanced-ip-blocker')]); add_settings_field('advaipbl_transient_expiration_login', __('Login Time Window (min)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_login_settings_section', ['name' => 'transient_expiration_login', 'default' => 60, 'description' => __('Time period in which the failure count must be reached.', 'advanced-ip-blocker')]); add_settings_field('advaipbl_duration_login', __('Login Duration (min)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_login_settings_section', ['name' => 'duration_login', 'default' => 120, 'description' => __('How long the IP will be blocked.', 'advanced-ip-blocker')]); add_settings_section('advaipbl_advanced_login_section', null, null, $page); add_settings_field('advaipbl_auto_whitelist_admin', __('Auto-Whitelist Admins', 'advanced-ip-blocker'), [$this, 'switch_field_callback'], $page, 'advaipbl_advanced_login_section', ['name' => 'auto_whitelist_admin', 'label' => __('Automatically add administrator IPs to the whitelist on successful login.', 'advanced-ip-blocker'), 'description' => __('Ideal for admins who travel or have dynamic IP addresses.', 'advanced-ip-blocker')]); add_settings_field('advaipbl_disable_user_enumeration', __( 'REST API User Protection', 'advanced-ip-blocker' ), [$this, 'switch_field_callback'], $page, 'advaipbl_advanced_login_section', ['name' => 'disable_user_enumeration', 'label' => __( 'Disable User Enumeration via REST API', 'advanced-ip-blocker' )]); add_settings_field('advaipbl_prevent_author_scanning', __( 'Author Scan Protection', 'advanced-ip-blocker' ), [$this, 'switch_field_callback'], $page, 'advaipbl_advanced_login_section', ['name' => 'prevent_author_scanning', 'label' => __( 'Prevent user enumeration via author scans', 'advanced-ip-blocker' )]); add_settings_field( 'advaipbl_prevent_login_hinting', __( 'Prevent Login Hinting', 'advanced-ip-blocker' ), [$this, 'switch_field_callback'], $page, 'advaipbl_advanced_login_section', [ 'name' => 'prevent_login_hinting', 'label' => __( 'Prevent WordPress from revealing whether a username or email exists on login failure.', 'advanced-ip-blocker' ), 'description' => __( 'Shows a generic error message for all login and "lost password" failures to prevent user enumeration.', 'advanced-ip-blocker' ) ] ); add_settings_field('advaipbl_restrict_login_page', __( 'Whitelist Login Access', 'advanced-ip-blocker' ), [ $this, 'restrict_login_page_callback' ], $page, 'advaipbl_advanced_login_section'); add_settings_section('advaipbl_advanced_xmlrpc_section', null, null, $page); add_settings_field('advaipbl_xmlrpc_protection_mode', __('XML-RPC Protection Mode', 'advanced-ip-blocker'), [$this, 'xmlrpc_protection_mode_callback'], $page, 'advaipbl_advanced_xmlrpc_section'); add_settings_field('advaipbl_duration_xmlrpc_block', __('XML-RPC Block Duration (min)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_advanced_xmlrpc_section', ['name' => 'duration_xmlrpc_block', 'default' => 1440, 'description' => __('How long to block IPs that make suspicious XML-RPC requests. Set to 0 for a permanent block.', 'advanced-ip-blocker')]); // --- Campos para el nuevo Endpoint Lockdown ---advaipbl_advanced_xmlrpc_protection_section add_settings_section('advaipbl_advanced_xmlrpc_protection_section', null, null, $page); add_settings_field( 'advaipbl_enable_xmlrpc_lockdown', __( 'XML-RPC Lockdown Mode (Beta)', 'advanced-ip-blocker' ), [$this, 'switch_field_callback'], $page, 'advaipbl_advanced_xmlrpc_protection_section', [ 'name' => 'enable_xmlrpc_lockdown', 'label' => __( 'Enable automated protection against distributed XML-RPC attacks.', 'advanced-ip-blocker' ), 'description' => sprintf(/* translators: $s: Blocked Endpoints tab link. */__( 'When a sustained attack is detected, this mode will temporarily challenge all non-whitelisted traffic to xmlrpc.php with a JavaScript verifier, stopping the attack without flooding your block list. Manage endpoints that are currently in "Lockdown Mode" in the Blocked Endpoints tab.', 'advanced-ip-blocker' ), admin_url('admin.php?page=advaipbl_settings_page&tab=ip_management&sub-tab=blocked_endpoints')) ] ); add_settings_field( 'advaipbl_xmlrpc_lockdown_threshold', __( 'Lockdown Threshold', 'advanced-ip-blocker' ), [$this, 'text_field_callback'], $page, 'advaipbl_advanced_xmlrpc_protection_section', [ 'name' => 'xmlrpc_lockdown_threshold', 'default' => 10, 'description' => __( 'Number of unique IP blocks on xmlrpc.php needed to trigger the lockdown.', 'advanced-ip-blocker' ) ] ); add_settings_field( 'advaipbl_xmlrpc_lockdown_window', __( 'Lockdown Detection Window (min)', 'advanced-ip-blocker' ), [$this, 'text_field_callback'], $page, 'advaipbl_advanced_xmlrpc_protection_section', [ 'name' => 'xmlrpc_lockdown_window', 'default' => 15, 'description' => __( 'The time frame (in minutes) in which the threshold must be reached.', 'advanced-ip-blocker' ) ] ); add_settings_field( 'advaipbl_xmlrpc_lockdown_duration', __( 'Lockdown Duration (min)', 'advanced-ip-blocker' ), [$this, 'text_field_callback'], $page, 'advaipbl_advanced_xmlrpc_protection_section', [ 'name' => 'xmlrpc_lockdown_duration', 'default' => 60, 'description' => __( 'How long the lockdown mode will remain active (in minutes).', 'advanced-ip-blocker' ) ] ); add_settings_field( 'advaipbl_enable_login_lockdown', __( 'Login Page Lockdown Mode (Beta)', 'advanced-ip-blocker' ), [$this, 'switch_field_callback'], $page, 'advaipbl_advanced_login_section', [ 'name' => 'enable_login_lockdown', 'label' => __( 'Enable automated protection against distributed brute-force attacks.', 'advanced-ip-blocker' ), 'description' => sprintf(/* translators: $s: Blocked Endpoints tab link. */__( 'When a sustained brute-force attack is detected, this mode will temporarily challenge all non-whitelisted traffic to wp-login.php with a JavaScript verifier.Manage endpoints that are currently in "Lockdown Mode" in the Blocked Endpoints tab.', 'advanced-ip-blocker' ), admin_url('admin.php?page=advaipbl_settings_page&tab=ip_management&sub-tab=blocked_endpoints')) ] ); add_settings_field( 'advaipbl_login_lockdown_event_threshold', __( 'Lockdown Event Threshold', 'advanced-ip-blocker' ), [$this, 'text_field_callback'], $page, 'advaipbl_advanced_login_section', [ 'name' => 'login_lockdown_event_threshold', 'default' => 50, 'description' => __( 'Total number of failed login attempts needed to trigger the lockdown.', 'advanced-ip-blocker' ) ] ); add_settings_field( 'advaipbl_login_lockdown_ip_threshold', __( 'Lockdown Unique IP Threshold', 'advanced-ip-blocker' ), [$this, 'text_field_callback'], $page, 'advaipbl_advanced_login_section', [ 'name' => 'login_lockdown_ip_threshold', 'default' => 10, 'description' => __( 'Minimum number of unique IPs that must contribute to the failed logins.', 'advanced-ip-blocker' ) ] ); add_settings_field( 'advaipbl_login_lockdown_window', __( 'Lockdown Detection Window (min)', 'advanced-ip-blocker' ), [$this, 'text_field_callback'], $page, 'advaipbl_advanced_login_section', [ 'name' => 'login_lockdown_window', 'default' => 5, 'description' => __( 'The time frame (in minutes) in which the thresholds must be reached.', 'advanced-ip-blocker' ) ] ); add_settings_field( 'advaipbl_login_lockdown_duration', __( 'Lockdown Duration (min)', 'advanced-ip-blocker' ), [$this, 'text_field_callback'], $page, 'advaipbl_advanced_login_section', [ 'name' => 'login_lockdown_duration', 'default' => 60, 'description' => __( 'How long the lockdown mode will remain active (in minutes).', 'advanced-ip-blocker' ) ] ); add_settings_section('advaipbl_recaptcha_section', null, null, $page); add_settings_field('advaipbl_recaptcha_enable', __('Enable reCAPTCHA', 'advanced-ip-blocker'), [$this, 'recaptcha_enable_callback'], $page, 'advaipbl_recaptcha_section'); add_settings_field('advaipbl_recaptcha_version', __('reCAPTCHA Version', 'advanced-ip-blocker'), [$this, 'recaptcha_version_callback'], $page, 'advaipbl_recaptcha_section'); add_settings_field('advaipbl_recaptcha_site_key', __('Site Key', 'advanced-ip-blocker'), [$this, 'recaptcha_site_key_callback'], $page, 'advaipbl_recaptcha_section'); add_settings_field('advaipbl_recaptcha_secret_key', __('Secret Key', 'advanced-ip-blocker'), [$this, 'recaptcha_secret_key_callback'], $page, 'advaipbl_recaptcha_section'); add_settings_field('advaipbl_recaptcha_score_threshold', __('v3 Score Threshold', 'advanced-ip-blocker'), [$this, 'recaptcha_score_callback'], $page, 'advaipbl_recaptcha_section'); // SECCIÓN DE 2FA add_settings_section( 'advaipbl_2fa_settings_section', null, // El título ya está en el HTML de la pestaña null, $page ); add_settings_field( 'advaipbl_enable_2fa', __( 'Enable Two-Factor Authentication', 'advanced-ip-blocker' ), [$this, 'switch_field_callback'], $page, 'advaipbl_2fa_settings_section', [ 'name' => 'enable_2fa', 'label' => __( 'Enable the 2FA feature for all users on this site.', 'advanced-ip-blocker' ), 'description' => sprintf(/* translators: $s: User profile URL */__('When enabled, users will see the 2FA setup section in their Profile.', 'advanced-ip-blocker'), admin_url('profile.php')) ] ); add_settings_field( 'advaipbl_tfa_force_roles', __( 'Force 2FA for Roles', 'advanced-ip-blocker' ), [$this, 'tfa_force_roles_callback'], $page, 'advaipbl_2fa_settings_section' ); add_settings_section('advaipbl_uninstall_section', null, null, $page); add_settings_field('advaipbl_delete_data_on_uninstall', __('Delete Data on Uninstall', 'advanced-ip-blocker'), [$this, 'switch_field_callback'], $page, 'advaipbl_uninstall_section', ['name' => 'delete_data_on_uninstall', 'label' => __('Check to delete all data from this plugin when it is deleted.', 'advanced-ip-blocker')]); add_settings_section('advaipbl_threat_scoring_section', null, null, $page); add_settings_field('advaipbl_enable_threat_scoring', __('Enable Threat Scoring System', 'advanced-ip-blocker'), [$this, 'switch_field_callback'], $page, 'advaipbl_threat_scoring_section', [ 'name' => 'enable_threat_scoring', 'label' => __('Activate the dynamic IP threat scoring system.', 'advanced-ip-blocker'), 'description' => __('When enabled, the plugin will assign threat points to IPs for malicious actions instead of using simple thresholds. An IP will be blocked only when its total score reaches the configured threshold.', 'advanced-ip-blocker') ]); add_settings_field('advaipbl_threat_score_threshold', __('Blocking Threshold', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_threat_scoring_section', [ 'name' => 'threat_score_threshold', 'default' => 100, 'description' => __('An IP will be blocked when its threat score reaches or exceeds this value.', 'advanced-ip-blocker') ]); add_settings_field('advaipbl_duration_threat_score', __('Block Duration (minutes)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_threat_scoring_section', [ 'name' => 'duration_threat_score', 'default' => 360, 'description' => __('How long an IP will be blocked after reaching the threshold. Set to 0 for a permanent block.', 'advanced-ip-blocker') ]); add_settings_field('advaipbl_threat_scores', __('Threat Event Points', 'advanced-ip-blocker'), [$this, 'threat_scores_callback'], $page, 'advaipbl_threat_scoring_section'); add_settings_field('advaipbl_score_decay_points', __('Score Decay Rate', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_threat_scoring_section', [ 'name' => 'score_decay_points', 'default' => 1, 'description' => __('Number of points to subtract from an inactive IP\'s score during each decay cycle.', 'advanced-ip-blocker') ]); add_settings_field('advaipbl_score_decay_frequency', __('Score Decay Frequency (Hours)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_threat_scoring_section', [ 'name' => 'score_decay_frequency', 'default' => 1, 'description' => __('How often (in hours) to run the decay process. The score of an IP will only decay if it has been inactive for at least this period.', 'advanced-ip-blocker') ]); add_settings_section('advaipbl_signature_engine_section', null, null, $page); add_settings_field('advaipbl_enable_signature_engine', __('Enable Signature Logging', 'advanced-ip-blocker'), [$this, 'switch_field_callback'], $page, 'advaipbl_signature_engine_section', [ 'name' => 'enable_signature_engine', 'label' => __('Activate the experimental request fingerprinting engine.', 'advanced-ip-blocker'), 'description' => __('Warning: This is an advanced feature for data collection. Activating it will log details of incoming requests to a new database table. Use only if you are aware of the potential performance impact on high-traffic sites.', 'advanced-ip-blocker') ]); add_settings_field('advaipbl_enable_signature_analysis', __('Enable Signature Analysis', 'advanced-ip-blocker'), [$this, 'switch_field_callback'], $page, 'advaipbl_signature_engine_section', [ 'name' => 'enable_signature_analysis', 'label' => __('Activate automatic analysis of request signatures.', 'advanced-ip-blocker'), 'description' => __('When enabled, a background process will analyze the collected logs to find patterns of a distributed attack.', 'advanced-ip-blocker') ]); add_settings_field('advaipbl_enable_signature_blocking', __('Enable Signature Blocking', 'advanced-ip-blocker'), [$this, 'switch_field_callback'], $page, 'advaipbl_signature_engine_section', [ 'name' => 'enable_signature_blocking', 'label' => __('Activate blocking based on malicious signatures.', 'advanced-ip-blocker'), 'description' => __('Enable this to activate protection. When a request matches a known malicious signature, the plugin will present a JavaScript challenge to filter out bots.', 'advanced-ip-blocker') ]); add_settings_field('advaipbl_signature_ip_threshold', __('Signature IP Threshold', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_signature_engine_section', [ 'name' => 'signature_ip_threshold', 'default' => 5, 'description' => __('The minimum number of different IPs that must use the same attack signature to be flagged as malicious.', 'advanced-ip-blocker') ]); add_settings_field('advaipbl_signature_analysis_window', __('Analysis Window (Hours)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_signature_engine_section', [ 'name' => 'signature_analysis_window', 'default' => 1, 'description' => __('How far back (in hours) the analysis process should look for patterns.', 'advanced-ip-blocker') ]); add_settings_field('advaipbl_signature_rule_ttl', __('Signature Rule Lifetime (Hours)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_signature_engine_section', [ 'name' => 'signature_rule_ttl', 'default' => 24, 'description' => __('How long a malicious signature will remain active before it expires.', 'advanced-ip-blocker') ]); add_settings_section('advaipbl_trusted_signatures_section', __('Trusted Signatures (Whitelist)', 'advanced-ip-blocker'), [$this, 'trusted_signatures_section_callback'], $page); add_settings_field('advaipbl_trusted_signature_hashes', __('Trusted Signature Hashes', 'advanced-ip-blocker'), [$this, 'textarea_field_callback'], $page, 'advaipbl_trusted_signatures_section', [ 'name' => 'trusted_signature_hashes', 'label' => __('Add known legitimate signature hashes here (one per line). Requests matching these exact fingerprints will always be allowed and never logged by the Signature Engine. Use this for services like Googlebot, Facebook Crawler, etc., to prevent false positives. Find hashes in the "Blocked Signatures" page by clicking "View Details".', 'advanced-ip-blocker') ]); } /** * Muestra el campo $label) : ?>


    -
    -

    1.0) { return 1.0; } return $score; } public function recaptcha_enable_callback() { $value = $this->options['recaptcha_enable'] ?? '0'; ?> >

    options['recaptcha_version'] ?? 'v3'; ?> options['recaptcha_site_key'] ?? ''; ?> options['recaptcha_secret_key'] ?? ''; ?> options['recaptcha_score_threshold'] ?? '0.5'; ?>

    ' . esc_html__( 'Block access from entire countries. This feature requires a working Geolocation Provider to be configured above.', 'advanced-ip-blocker' ) . '

    '; echo '

    ' . esc_html__( 'Warning:', 'advanced-ip-blocker' ) . ' ' . esc_html__( 'Blocking countries can prevent legitimate users and services (like payment gateways) from accessing your site. Use with caution.', 'advanced-ip-blocker' ) . '

    '; } /** * Muestra el campo de selección múltiple para los países. */ public function geoblock_countries_callback() { $blocked_countries = $this->options['geoblock_countries'] ?? []; $all_countries = $this->get_country_list(); ?>

    Nombre). * @return array */ private function get_country_list() { // Esta lista se puede expandir o mover a un archivo separado si se desea. return [ 'AF' => 'Afghanistan', 'AX' => 'Aland Islands', 'AL' => 'Albania', 'DZ' => 'Algeria', 'AS' => 'American Samoa', 'AD' => 'Andorra', 'AO' => 'Angola', 'AI' => 'Anguilla', 'AQ' => 'Antarctica', 'AG' => 'Antigua And Barbuda', 'AR' => 'Argentina', 'AM' => 'Armenia', 'AW' => 'Aruba', 'AU' => 'Australia', 'AT' => 'Austria', 'AZ' => 'Azerbaijan', 'BS' => 'Bahamas', 'BH' => 'Bahrain', 'BD' => 'Bangladesh', 'BB' => 'Barbados', 'BY' => 'Belarus', 'BE' => 'Belgium', 'BZ' => 'Belize', 'BJ' => 'Benin', 'BM' => 'Bermuda', 'BT' => 'Bhutan', 'BO' => 'Bolivia', 'BA' => 'Bosnia And Herzegovina', 'BW' => 'Botswana', 'BV' => 'Bouvet Island', 'BR' => 'Brazil', 'IO' => 'British Indian Ocean Territory', 'BN' => 'Brunei Darussalam', 'BG' => 'Bulgaria', 'BF' => 'Burkina Faso', 'BI' => 'Burundi', 'KH' => 'Cambodia', 'CM' => 'Cameroon', 'CA' => 'Canada', 'CV' => 'Cape Verde', 'KY' => 'Cayman Islands', 'CF' => 'Central African Republic', 'TD' => 'Chad', 'CL' => 'Chile', 'CN' => 'China', 'CX' => 'Christmas Island', 'CC' => 'Cocos (Keeling) Islands', 'CO' => 'Colombia', 'KM' => 'Comoros', 'CG' => 'Congo', 'CD' => 'Congo, Democratic Republic', 'CK' => 'Cook Islands', 'CR' => 'Costa Rica', 'CI' => 'Cote D\'Ivoire', 'HR' => 'Croatia', 'CU' => 'Cuba', 'CY' => 'Cyprus', 'CZ' => 'Czech Republic', 'DK' => 'Denmark', 'DJ' => 'Djibouti', 'DM' => 'Dominica', 'DO' => 'Dominican Republic', 'EC' => 'Ecuador', 'EG' => 'Egypt', 'SV' => 'El Salvador', 'GQ' => 'Equatorial Guinea', 'ER' => 'Eritrea', 'EE' => 'Estonia', 'ET' => 'Ethiopia', 'FK' => 'Falkland Islands (Malvinas)', 'FO' => 'Faroe Islands', 'FJ' => 'Fiji', 'FI' => 'Finland', 'FR' => 'France', 'GF' => 'French Guiana', 'PF' => 'French Polynesia', 'TF' => 'French Southern Territories', 'GA' => 'Gabon', 'GM' => 'Gambia', 'GE' => 'Georgia', 'DE' => 'Germany', 'GH' => 'Ghana', 'GI' => 'Gibraltar', 'GR' => 'Greece', 'GL' => 'Greenland', 'GD' => 'Grenada', 'GP' => 'Guadeloupe', 'GU' => 'Guam', 'GT' => 'Guatemala', 'GG' => 'Guernsey', 'GN' => 'Guinea', 'GW' => 'Guinea-Bissau', 'GY' => 'Guyana', 'HT' => 'Haiti', 'HM' => 'Heard Island & Mcdonald Islands', 'VA' => 'Holy See (Vatican City State)', 'HN' => 'Honduras', 'HK' => 'Hong Kong', 'HU' => 'Hungary', 'IS' => 'Iceland', 'IN' => 'India', 'ID' => 'Indonesia', 'IR' => 'Iran, Islamic Republic Of', 'IQ' => 'Iraq', 'IE' => 'Ireland', 'IM' => 'Isle Of Man', 'IL' => 'Israel', 'IT' => 'Italy', 'JM' => 'Jamaica', 'JP' => 'Japan', 'JE' => 'Jersey', 'JO' => 'Jordan', 'KZ' => 'Kazakhstan', 'KE' => 'Kenya', 'KI' => 'Kiribati', 'KR' => 'Korea', 'KW' => 'Kuwait', 'KG' => 'Kyrgyzstan', 'LA' => 'Lao People\'s Democratic Republic', 'LV' => 'Latvia', 'LB' => 'Lebanon', 'LS' => 'Lesotho', 'LR' => 'Liberia', 'LY' => 'Libyan Arab Jamahiriya', 'LI' => 'Liechtenstein', 'LT' => 'Lithuania', 'LU' => 'Luxembourg', 'MO' => 'Macao', 'MK' => 'Macedonia', 'MG' => 'Madagascar', 'MW' => 'Malawi', 'MY' => 'Malaysia', 'MV' => 'Maldives', 'ML' => 'Mali', 'MT' => 'Malta', 'MH' => 'Marshall Islands', 'MQ' => 'Martinique', 'MR' => 'Mauritania', 'MU' => 'Mauritius', 'YT' => 'Mayotte', 'MX' => 'Mexico', 'FM' => 'Micronesia, Federated States Of', 'MD' => 'Moldova', 'MC' => 'Monaco', 'MN' => 'Mongolia', 'ME' => 'Montenegro', 'MS' => 'Montserrat', 'MA' => 'Morocco', 'MZ' => 'Mozambique', 'MM' => 'Myanmar', 'NA' => 'Namibia', 'NR' => 'Nauru', 'NP' => 'Nepal', 'NL' => 'Netherlands', 'AN' => 'Netherlands Antilles', 'NC' => 'New Caledonia', 'NZ' => 'New Zealand', 'NI' => 'Nicaragua', 'NE' => 'Niger', 'NG' => 'Nigeria', 'NU' => 'Niue', 'NF' => 'Norfolk Island', 'MP' => 'Northern Mariana Islands', 'NO' => 'Norway', 'OM' => 'Oman', 'PK' => 'Pakistan', 'PW' => 'Palau', 'PS' => 'Palestinian Territory, Occupied', 'PA' => 'Panama', 'PG' => 'Papua New Guinea', 'PY' => 'Paraguay', 'PE' => 'Peru', 'PH' => 'Philippines', 'PN' => 'Pitcairn', 'PL' => 'Poland', 'PT' => 'Portugal', 'PR' => 'Puerto Rico', 'QA' => 'Qatar', 'RE' => 'Reunion', 'RO' => 'Romania', 'RU' => 'Russian Federation', 'RW' => 'Rwanda', 'BL' => 'Saint Barthelemy', 'SH' => 'Saint Helena', 'KN' => 'Saint Kitts And Nevis', 'LC' => 'Saint Lucia', 'MF' => 'Saint Martin', 'PM' => 'Saint Pierre And Miquelon', 'VC' => 'Saint Vincent And Grenadines', 'WS' => 'Samoa', 'SM' => 'San Marino', 'ST' => 'Sao Tome And Principe', 'SA' => 'Saudi Arabia', 'SN' => 'Senegal', 'RS' => 'Serbia', 'SC' => 'Seychelles', 'SL' => 'Sierra Leone', 'SG' => 'Singapore', 'SK' => 'Slovakia', 'SI' => 'Slovenia', 'SB' => 'Solomon Islands', 'SO' => 'Somalia', 'ZA' => 'South Africa', 'GS' => 'South Georgia And Sandwich Isl.', 'ES' => 'Spain', 'LK' => 'Sri Lanka', 'SD' => 'Sudan', 'SR' => 'Suriname', 'SJ' => 'Svalbard And Jan Mayen', 'SZ' => 'Swaziland', 'SE' => 'Sweden', 'CH' => 'Switzerland', 'SY' => 'Syrian Arab Republic', 'TW' => 'Taiwan', 'TJ' => 'Tajikistan', 'TZ' => 'Tanzania', 'TH' => 'Thailand', 'TL' => 'Timor-Leste', 'TG' => 'Togo', 'TK' => 'Tokelau', 'TO' => 'Tonga', 'TT' => 'Trinidad And Tobago', 'TN' => 'Tunisia', 'TR' => 'Turkey', 'TM' => 'Turkmenistan', 'TC' => 'Turks And Caicos Islands', 'TV' => 'Tuvalu', 'UG' => 'Uganda', 'UA' => 'Ukraine', 'AE' => 'United Arab Emirates', 'GB' => 'United Kingdom', 'US' => 'United States', 'UM' => 'United States Outlying Islands', 'UY' => 'Uruguay', 'UZ' => 'Uzbekistan', 'VU' => 'Vanuatu', 'VE' => 'Venezuela', 'VN' => 'Viet Nam', 'VG' => 'Virgin Islands, British', 'VI' => 'Virgin Islands, U.S.', 'WF' => 'Wallis And Futuna', 'EH' => 'Western Sahara', 'YE' => 'Yemen', 'ZM' => 'Zambia', 'ZW' => 'Zimbabwe', ]; } public function clear_cache_button_callback() { $nonce_url = wp_nonce_url( admin_url( 'admin-post.php?action=advaipbl_clear_location_cache_action' ), // Usamos un admin-post para la acción 'advaipbl_clear_location_cache_nonce' ); ?>

    This is a test push notification from Advanced IP Blocker.', '> Your webhooks are configured correctly!' ]; $test_message = implode("\n", $message_lines); $mentions = $this->options['push_mentions'] ?? ''; $sent = $this->execute_webhook_send($test_message, $mentions); if ($sent) { $message = __('Test push notification sent successfully!', 'advanced-ip-blocker'); $type = 'success'; $this->log_event(sprintf('Test push notification sent by %s.', $this->get_current_admin_username()), 'info'); } else { $message = __('Failed to send test push notification. Please check your settings and webhook URLs.', 'advanced-ip-blocker'); $type = 'error'; $this->log_event(sprintf('FAILED to send test push notification. Action by %s.', $this->get_current_admin_username()), 'error'); } set_transient(self::TRANSIENT_ADMIN_NOTICE, ['message' => $message, 'type' => $type], 30); wp_safe_redirect(wp_get_referer()); exit; } /** * AJAX callback para verificar una API key de geolocalización. */ public function ajax_verify_api_key() { if ( ! current_user_can('manage_options') ) { wp_send_json_error( ['message' => __('Permission denied.', 'advanced-ip-blocker')] ); } check_ajax_referer( 'advaipbl_verify_api_nonce', 'nonce' ); $provider = isset($_POST['provider']) ? sanitize_text_field($_POST['provider']) : ''; $api_key = isset($_POST['api_key']) ? sanitize_text_field($_POST['api_key']) : ''; if (empty($provider)) { // La clave puede estar vacía para algunos proveedores wp_send_json_error( ['message' => __('Provider is missing.', 'advanced-ip-blocker')] ); } $this->geolocation_manager->set_transient_api_key($provider, $api_key); $result = $this->geolocation_manager->fetch_location('8.8.8.8'); $this->geolocation_manager->clear_transient_api_key($provider); if ( $result && !isset($result['error']) ) { wp_send_json_success(['message' => __('API Key is valid!', 'advanced-ip-blocker')]); } else { // La clave está aquí: usamos el error_message que nos devuelve el geolocation_manager. $error_message = $result['error_message'] ?? __('Invalid API Key or connection error.', 'advanced-ip-blocker'); wp_send_json_error(['message' => $error_message]); } } /** * Muestra el aviso para solicitar el consentimiento de telemetría. */ public function display_telemetry_notice() { // Condición 1: Solo mostrar en las páginas de nuestro plugin. if (!isset($_GET['page']) || strpos($_GET['page'], 'advaipbl_settings_page') === false) { return; } // Condición 2: Si el usuario ya ha activado la telemetría, no mostrar. if (!empty($this->options['allow_telemetry']) && '1' === $this->options['allow_telemetry']) { return; } // Condición 3: Si el usuario ya ha descartado el aviso, no mostrar. // get_option devolverá 'false' si no existe, lo cual es correcto. if (get_option('advaipbl_telemetry_notice_dismissed')) { return; } ?>


    options['allow_telemetry']) || '1' !== $this->options['allow_telemetry']) { return; } global $wpdb; $is_woocommerce_active = class_exists('WooCommerce'); $telemetry_data = [ 'site_url' => home_url(), 'plugin_version' => ADVAIPBL_VERSION, 'wp_version' => get_bloginfo('version'), 'php_version' => PHP_VERSION, 'is_multisite' => is_multisite(), 'site_locale' => get_locale(), 'server_country' => 'N/A', 'store_country' => 'N/A', 'user_count' => count_users()['total_users'] ?? 0, 'is_woo_active' => $is_woocommerce_active, 'geo_provider' => $this->options['geolocation_provider'] ?? 'N/A', 'geolocation_method' => $this->options['geolocation_method'] ?? 'api', ]; if ($is_woocommerce_active) { $woo_country = get_option('woocommerce_default_country'); if ($woo_country) { $country_parts = explode(':', $woo_country); $telemetry_data['store_country'] = $country_parts[0]; } } // Array de settings completo que refleja todos los módulos principales. // Las claves coinciden con las opciones reales para mayor claridad. $telemetry_data['settings'] = [ 'enable_waf' => !empty($this->options['enable_waf']), 'rate_limiting_enable' => !empty($this->options['rate_limiting_enable']), 'enable_geoblocking' => !empty($this->options['enable_geoblocking']), 'enable_honeypot_blocking' => !empty($this->options['enable_honeypot_blocking']), 'enable_user_agent_blocking' => !empty($this->options['enable_user_agent_blocking']), 'enable_spamhaus_asn' => !empty($this->options['enable_spamhaus_asn']), 'enable_manual_asn' => !empty($this->options['enable_manual_asn']), 'xmlrpc_protection_mode' => $this->options['xmlrpc_protection_mode'] ?? 'smart', 'recaptcha_enable' => !empty($this->options['recaptcha_enable']), 'enable_push_notifications' => !empty($this->options['enable_push_notifications']), 'enable_threat_scoring' => !empty($this->options['enable_threat_scoring']), 'auto_whitelist_admin' => !empty($this->options['auto_whitelist_admin']), 'disable_user_enumeration' => !empty($this->options['disable_user_enumeration']), 'prevent_author_scanning' => !empty($this->options['prevent_author_scanning']), 'restrict_login_page' => !empty($this->options['restrict_login_page']), 'prevent_login_hinting' => !empty($this->options['prevent_login_hinting']), 'enable_email_notifications' => !empty($this->options['enable_email_notifications']), 'enable_signature_engine' => !empty($this->options['enable_signature_engine']), 'enable_signature_analysis' => !empty($this->options['enable_signature_analysis']), 'enable_signature_blocking' => !empty($this->options['enable_signature_blocking']), 'enable_2fa' => !empty($this->options['enable_2fa']), 'enable_xmlrpc_lockdown' => !empty($this->options['enable_xmlrpc_lockdown']), 'enable_login_lockdown' => !empty($this->options['enable_login_lockdown']), ]; $seven_days_ago = gmdate('Y-m-d H:i:s', strtotime('-7 days')); $blocks_by_type_results = $wpdb->get_results( $wpdb->prepare( "SELECT log_type, COUNT(log_id) as count FROM {$wpdb->prefix}advaipbl_logs WHERE level = 'critical' AND timestamp >= %s GROUP BY log_type", $seven_days_ago ), ARRAY_A); $blocks_by_type_7d = []; foreach ($blocks_by_type_results as $row) { $blocks_by_type_7d[$row['log_type']] = (int) $row['count']; } $telemetry_data['stats'] = [ 'whitelist_count' => count(get_option(self::OPTION_WHITELIST, [])), 'manual_block_count' => count(get_option(self::OPTION_BLOCKED_MANUAL, [])), 'waf_rules_count' => count(array_filter(explode("\n", get_option(self::OPTION_WAF_RULES, '')))), 'honeypot_urls_count' => count(get_option(self::OPTION_HONEYPOT_URLS, [])), 'blocked_user_agents_count' => count(get_option(self::OPTION_BLOCKED_UAS, [])), 'manual_asn_count' => count(get_option(self::OPTION_BLOCKED_ASNS, [])), 'total_blocks_7d' => (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(log_id) FROM {$wpdb->prefix}advaipbl_logs WHERE level = 'critical' AND timestamp >= %s", $seven_days_ago)), 'blocks_by_type_7d' => $blocks_by_type_7d, 'geoblock_country_count' => count($this->options['geoblock_countries'] ?? []), 'active_blocks_count' => $this->get_blocked_count(), 'ips_with_active_score' => (int) $wpdb->get_var("SELECT COUNT(id) FROM {$wpdb->prefix}advaipbl_ip_scores"), 'active_malicious_signatures' => (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(id) FROM {$wpdb->prefix}advaipbl_malicious_signatures WHERE expires_at > %d", time())), ]; $server_ip = $this->get_server_ip(); if ($server_ip) { $location = $this->geolocation_manager->fetch_location($server_ip); if ($location && empty($location['error']) && !empty($location['country_code'])) { $telemetry_data['server_country'] = $location['country_code']; } } $endpoint_url = 'https://advaipbl.com/wp-json/telemetry/v1/submit'; $secret_key = 'yV.vZRp|g6E{zJ,DI7WcMIiGDejmH($$~<0-I$$Bd7Y) D5Z65M/*P:h>w:/E 15, 'blocking' => false, 'headers' => [ 'Content-Type' => 'application/json', 'X-Telemetry-Key' => $secret_key ], 'body' => wp_json_encode($telemetry_data), ]); } /** * Callback de AJAX para gestionar la respuesta al aviso de telemetría. */ public function ajax_handle_telemetry_notice() { if (!current_user_can('manage_options')) { wp_send_json_error(); } check_ajax_referer('advaipbl_telemetry_nonce', 'nonce'); $action = isset($_POST['telemetry_action']) ? sanitize_key($_POST['telemetry_action']) : ''; if ('allow' === $action) { $options = get_option(self::OPTION_SETTINGS, []); $options['allow_telemetry'] = '1'; update_option(self::OPTION_SETTINGS, $options); update_option('advaipbl_telemetry_notice_dismissed', '1'); // Programamos el cron job inmediatamente después de obtener el consentimiento. if (!wp_next_scheduled('advaipbl_send_telemetry_data_event')) { // Programamos la primera ejecución para dentro de 24 horas para dar tiempo // a que se acumulen algunos datos iniciales y no sobrecargar en la activación. wp_schedule_event(time() + DAY_IN_SECONDS, 'weekly', 'advaipbl_send_telemetry_data_event'); $this->log_event('Telemetry cron job scheduled after user consent.', 'info'); } wp_send_json_success(); } elseif ('dismiss' === $action) { update_option('advaipbl_telemetry_notice_dismissed', '1'); // Como medida de seguridad, nos aseguramos de que si el usuario rechaza, // cualquier cron job programado previamente se elimine. wp_clear_scheduled_hook('advaipbl_send_telemetry_data_event'); wp_send_json_success(); } wp_send_json_error(); } public function sanitize_settings($input) { $new_input = []; $defaults = $this->get_default_settings(); $numeric_fields = [ 'duration_geoblock', 'duration_waf', 'duration_rate_limit', 'rate_limiting_limit', 'rate_limiting_window', 'duration_xmlrpc_block', 'duration_asn', 'log_retention_days', 'threshold_404', 'duration_404', 'transient_expiration_404', 'threshold_403', 'duration_403', 'transient_expiration_403', 'threshold_login', 'duration_login', 'transient_expiration_login', 'duration_honeypot', 'duration_user_agent', 'threat_score_threshold', 'duration_threat_score', 'score_404', 'score_403', 'score_login', 'score_user_agent', 'score_waf', 'score_honeypot', 'score_asn', 'score_decay_points', 'score_decay_frequency', 'signature_ip_threshold', 'signature_analysis_window', 'signature_rule_ttl', 'xmlrpc_lockdown_threshold', 'xmlrpc_lockdown_window', 'xmlrpc_lockdown_duration', 'login_lockdown_event_threshold', 'login_lockdown_ip_threshold', 'login_lockdown_window', 'login_lockdown_duration' ]; foreach ($numeric_fields as $field) { $new_input[$field] = isset($input[$field]) ? absint($input[$field]) : $defaults[$field]; } $checkbox_fields = [ 'prevent_author_scanning', 'disable_user_enumeration', 'restrict_login_page', 'enable_logging', 'delete_data_on_uninstall', 'enable_email_notifications', 'enable_waf', 'enable_geoblocking', 'enable_honeypot_blocking', 'enable_user_agent_blocking', 'rate_limiting_enable', 'show_admin_bar_menu', 'enable_spamhaus_asn', 'enable_manual_asn', 'enable_push_notifications', 'push_critical_only', 'auto_whitelist_admin', 'allow_telemetry', 'enable_threat_scoring', 'enable_signature_engine', 'enable_signature_blocking', 'enable_signature_analysis', 'enable_2fa', 'recaptcha_enable', 'prevent_login_hinting', 'enable_xmlrpc_lockdown', 'enable_login_lockdown' ]; foreach ($checkbox_fields as $field) { $new_input[$field] = (isset($input[$field]) && $input[$field] === '1') ? '1' : '0'; } // Saneamiento específico para el array de roles de 2FA // Solo cargamos la librería si NO estamos en un CLI con PHP antiguo. if ( isset( $input['tfa_force_roles'] ) && is_array( $input['tfa_force_roles'] ) ) { $new_input['tfa_force_roles'] = array_map( 'sanitize_key', $input['tfa_force_roles'] ); } else { // Si no se envía nada (ningún rol marcado), guardamos un array vacío. $new_input['tfa_force_roles'] = []; } $new_input['notification_frequency'] = isset($input['notification_frequency']) && in_array($input['notification_frequency'], ['disabled', 'instant', 'daily', 'weekly']) ? $input['notification_frequency'] : $defaults['notification_frequency']; $new_input['custom_block_message'] = isset($input['custom_block_message']) ? sanitize_textarea_field($input['custom_block_message']) : $defaults['custom_block_message']; $new_input['excluded_error_urls'] = isset($input['excluded_error_urls']) ? sanitize_textarea_field($input['excluded_error_urls']) : $defaults['excluded_error_urls']; $new_input['waf_excluded_urls'] = isset($input['waf_excluded_urls']) ? sanitize_textarea_field($input['waf_excluded_urls']) : $defaults['waf_excluded_urls']; $raw_urls = isset($input['push_webhook_urls']) ? $input['push_webhook_urls'] : ''; $urls = array_map('trim', explode("\n", $raw_urls)); $sanitized_urls = []; foreach ($urls as $url) { if (!empty($url)) { $sanitized_urls[] = esc_url_raw(trim($url)); } } $new_input['push_webhook_urls'] = implode("\n", $sanitized_urls); if (isset($input['push_mentions'])) { $new_input['push_mentions'] = sanitize_textarea_field($input['push_mentions']); } else { $new_input['push_mentions'] = ''; } $new_input['trusted_signature_hashes'] = isset($input['trusted_signature_hashes']) ? sanitize_textarea_field($input['trusted_signature_hashes']) : ''; $new_input['notification_email'] = isset($input['notification_email']) ? sanitize_email($input['notification_email']) : $defaults['notification_email']; $api_keys = ['api_key_ipapicom', 'api_key_ipstackcom', 'api_key_ipinfocom', 'api_key_ip_apicom', 'maxmind_license_key']; foreach($api_keys as $key) { $new_input[$key] = isset($input[$key]) ? sanitize_text_field($input[$key]) : ''; } $new_input['geolocation_provider'] = isset($input['geolocation_provider']) && in_array($input['geolocation_provider'], ['ip-api.com', 'geoiplookup.net', 'ipinfo.io', 'ipapi.com', 'ipstack.com']) ? $input['geolocation_provider'] : $defaults['geolocation_provider']; if (isset($input['log_timezone']) && in_array($input['log_timezone'], timezone_identifiers_list(), true)) { $new_input['log_timezone'] = $input['log_timezone']; } else { $new_input['log_timezone'] = $defaults['log_timezone']; } if ( isset( $input['geoblock_countries'] ) ) { $countries = (array) $input['geoblock_countries']; $all_country_codes = array_keys( $this->get_country_list() ); $new_input['geoblock_countries'] = array_values( array_intersect( $countries, $all_country_codes ) ); } else { $new_input['geoblock_countries'] = []; } // La línea de recaptcha_enable se movió a la lista de checkboxes $new_input['recaptcha_version'] = isset($input['recaptcha_version']) && in_array($input['recaptcha_version'], ['v2', 'v3']) ? $input['recaptcha_version'] : 'v3'; $new_input['recaptcha_site_key'] = isset($input['recaptcha_site_key']) ? sanitize_text_field($input['recaptcha_site_key']) : ''; $new_input['recaptcha_secret_key'] = isset($input['recaptcha_secret_key']) ? sanitize_text_field($input['recaptcha_secret_key']) : ''; $new_input['recaptcha_score_threshold'] = isset($input['recaptcha_score_threshold']) ? $this->sanitize_score_threshold($input['recaptcha_score_threshold']) : 0.5; $new_input['xmlrpc_protection_mode'] = isset($input['xmlrpc_protection_mode']) && in_array($input['xmlrpc_protection_mode'], ['enabled', 'smart', 'disabled']) ? $input['xmlrpc_protection_mode'] : 'smart'; if ( isset( $input['geolocation_method'] ) && in_array( $input['geolocation_method'], ['api', 'local_db'], true ) ) { $new_input['geolocation_method'] = $input['geolocation_method']; } else { // Si el valor no es válido, volvemos al por defecto para evitar problemas. $new_input['geolocation_method'] = 'api'; } return $new_input; } public function text_field_callback($args){ $value = $this->options[$args['name']] ?? $args['default'] ?? ''; $class = $args['class'] ?? ''; $data_attr = ! empty( $args['data_provider'] ) ? 'data-provider="' . esc_attr( $args['data_provider'] ) . '"' : ''; if ( isset($args['name']) && strpos($args['name'], 'api_key_') === 0 ) { printf( ' ', esc_attr( $args['name'] ), esc_attr( 'advaipbl_settings[' . $args['name'] . ']' ), esc_attr( $value ), esc_attr( $class ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $data_attr, esc_attr( $args['data_provider'] ), esc_html__( 'Verify', 'advanced-ip-blocker' ) ); } else { $is_numeric = false; if (isset($args['name']) && is_string($args['name'])) { $numeric_keywords = ['duration', 'threshold', 'retention', 'score', 'points', 'frequency', 'limit', 'window', 'expiration', 'ttl']; foreach ($numeric_keywords as $keyword) { if (strpos($args['name'], $keyword) !== false) { $is_numeric = true; break; } } } $type = $is_numeric ? 'number' : 'text'; $min_attr = ( 'number' === $type ) ? 'min="0"' : ''; printf( '', esc_attr($type), esc_attr($args['name']), esc_attr('advaipbl_settings[' . $args['name'] . ']'), esc_attr($value), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $min_attr, esc_attr($class) ); } if ( isset($args['description']) ) { echo '

    ' . wp_kses_post($args['description']) . '

    '; } } public function checkbox_field_callback($args){ $checked = checked( '1', $this->options[$args['name']] ?? '0', false ); $id_attr = isset($args['id']) ? 'id="' . esc_attr($args['id']) . '"' : ''; $html = sprintf( '', esc_attr( 'advaipbl_settings[' . $args['name'] . ']' ), $checked, $id_attr, esc_html( $args['label'] ) ); echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped if ( ! empty( $args['description'] ) ) { echo '

    ' . wp_kses_post( $args['description'] ) . '

    '; } } /** * Muestra un campo de tipo interruptor (toggle/switch) para los ajustes. * Utiliza la estructura HTML necesaria para ser estilizado con CSS. */ public function switch_field_callback($args){ $value = $this->options[$args['name']] ?? '0'; $id_attr = isset($args['id']) ? 'id="' . esc_attr($args['id']) . '"' : 'advaipbl_switch_' . esc_attr($args['name']); // El input checkbox original se mantiene, pero se ocultará con CSS. // La estructura de label + span es para crear el interruptor visual. $html = sprintf( '', esc_attr( $id_attr ), esc_attr( 'advaipbl_settings[' . $args['name'] . ']' ), esc_attr( $id_attr ), checked( '1', $value, false ) ); // Imprimimos el interruptor // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $html; // Imprimimos la descripción si existe, que ahora actúa como el label visible. if ( ! empty( $args['description'] ) ) { echo '

    ' . wp_kses_post( $args['description'] ) . '

    '; } elseif ( ! empty( $args['label'] ) ) { echo ''; } } public function textarea_field_callback($args){ $value = $this->options[$args['name']] ?? ''; $rows = $args['rows'] ?? 5; // Hacemos la clase personalizable. Si no se especifica, usa 'large-text' por defecto. $class = $args['class'] ?? 'large-text'; printf( '', esc_attr( 'advaipbl_settings[' . $args['name'] . ']' ), esc_attr( $rows ), esc_textarea( $value ), esc_attr( $class ) // Usamos la nueva variable de clase. ); if ( isset($args['description']) ) { echo '

    ' . wp_kses_post($args['description']) . '

    '; } elseif (isset($args['label'])) { echo '

    ' . esc_html($args['label']) . '

    '; } } public function email_field_callback($args){ $value = $this->options[$args['name']] ?? ''; printf( '', esc_attr( 'advaipbl_settings[' . $args['name'] . ']' ), esc_attr( $value ), esc_attr( get_option('admin_email') ) ); if ( isset($args['label']) ) { printf( '

    %s

    ', esc_html($args['label']) ); } } public function timezone_select_callback() { $name_attr = 'advaipbl_settings[log_timezone]'; $current_timezone = $this->options['log_timezone'] ?? wp_timezone_string(); echo ''; echo '

    ' . esc_html__('Select the timezone to display in the logs.', 'advanced-ip-blocker') . '

    '; } public function notification_frequency_callback() { $value = $this->options['notification_frequency'] ?? 'disabled'; $frequencies = [ 'disabled' => __('Disabled', 'advanced-ip-blocker'), 'instant' => __('Instant', 'advanced-ip-blocker'), 'daily' => __('Daily Summary', 'advanced-ip-blocker'), 'weekly' => __('Weekly Summary', 'advanced-ip-blocker') ]; echo ''; echo '

    ' . esc_html__('Choose how often you want to receive alert emails.', 'advanced-ip-blocker') . '

    '; } /** * Muestra la descripción para la sección de Threat Scoring. */ public function threat_scoring_section_callback() { // La descripción ahora está en el HTML, esta función puede estar vacía. // Opcionalmente, podemos dejar el

    aquí, pero es más limpio en el HTML. } /** * Muestra la tabla de campos numéricos para configurar los puntos de cada evento de amenaza. */ public function threat_scores_callback() { $events = [ 'score_404' => ['label' => __('404 Error', 'advanced-ip-blocker'), 'default' => 5], 'score_403' => ['label' => __('403 Error', 'advanced-ip-blocker'), 'default' => 10], 'score_login' => ['label' => __('Failed Login', 'advanced-ip-blocker'), 'default' => 15], 'score_user_agent' => ['label' => __('Blocked User-Agent', 'advanced-ip-blocker'), 'default' => 25], 'score_waf' => ['label' => __('WAF Rule Triggered', 'advanced-ip-blocker'), 'default' => 50], 'score_honeypot' => ['label' => __('Honeypot Accessed', 'advanced-ip-blocker'), 'default' => 100], 'score_asn' => ['label' => __('Blocked ASN (Spamhaus)', 'advanced-ip-blocker'), 'default' => 100], ]; ?>

    $details) : ?> options[$name] ?? $details['default']; ?>

    options['geolocation_provider'] ?? 'ip-api.com'; $providers = [ 'ip-api.com' => __('ip-api.com (Default, no key for HTTP)', 'advanced-ip-blocker'), 'geoiplookup.net' => __('geoiplookup.net (Free, no key)', 'advanced-ip-blocker'), 'ipinfo.io' => __('ipinfo.io (API Key recommended)', 'advanced-ip-blocker'), 'ipapi.com' => __('ipapi.com (API Key required)', 'advanced-ip-blocker'), 'ipstack.com' => __('ipstack.com (API Key required)', 'advanced-ip-blocker') ]; // Añadimos un id único al select printf( ''; } /** * Displays the content for the Status & Debug tab. * This tab helps users diagnose issues with IP detection and server configuration. */ private function display_status_tab() { // Obtenemos los datos una sola vez para usarlos en toda la función. $ip_data = $this->get_ip_intelligence(); $server_ip = $this->get_server_ip(); $client_ip = $ip_data['visitor_ip'] ?? 'N/A'; ?>

    get_remote_addr(); $client_ip = $this->get_client_ip(); if ( $remote_addr && $client_ip !== $remote_addr && filter_var( $remote_addr, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) { echo '

    '; /* translators: %1$s: Client's real IP. %2$s: Proxy/CDN IP. */ echo '' . esc_html__( 'Info:', 'advanced-ip-blocker' ) . ' ' . sprintf( esc_html__( 'It seems your site is behind a reverse proxy or CDN. Your IP is correctly detected as %1$s, while your server connects through %2$s. This is a normal and correct configuration.', 'advanced-ip-blocker' ), '' . esc_html( $client_ip ) . '', '' . esc_html( $remote_addr ) . '' ); echo '

    '; $has_recommendations = true; } // Comprobación de la IP del servidor if ( ! $this->get_server_ip() ) { echo '

    '; echo '' . esc_html__( 'Warning:', 'advanced-ip-blocker' ) . ' ' . esc_html__( 'The plugin could not automatically determine your server\'s public IP. The feature that adds the server IP to the whitelist on activation will not work. Please add your server IP to the whitelist manually to prevent any issues with geoblocking or other functionalities.', 'advanced-ip-blocker' ); echo '

    '; $has_recommendations = true; } $xmlrpc_plugins = $this->get_xmlrpc_dependent_plugins(); if ( ! empty( $xmlrpc_plugins ) ) : ?>


    '; echo '' . esc_html__( 'All Clear!', 'advanced-ip-blocker' ) . ' ' . esc_html__( 'No potential configuration issues were detected.', 'advanced-ip-blocker' ); echo '

    '; } ?>



    'Permission denied.'] ); return; } check_ajax_referer( 'advaipbl_export_nonce', 'nonce' ); $export_type = isset( $_POST['export_type'] ) && in_array( $_POST['export_type'], ['template', 'full_backup'] ) ? $_POST['export_type'] : 'template'; $option_keys = [ self::OPTION_SETTINGS, self::OPTION_WHITELIST, self::OPTION_BLOCKED_MANUAL, self::OPTION_BLOCKED_404, self::OPTION_BLOCKED_403, self::OPTION_BLOCKED_LOGIN, self::OPTION_BLOCKED_GEO, self::OPTION_BLOCKED_HONEYPOT, self::OPTION_BLOCKED_USER_AGENT, self::OPTION_HONEYPOT_URLS, self::OPTION_BLOCKED_UAS, self::OPTION_WHITELISTED_UAS, self::OPTION_WAF_RULES, self::OPTION_BLOCKED_WAF, self::OPTION_BLOCKED_RATE_LIMIT, self::OPTION_BLOCKED_ASNS, self::OPTION_BLOCKED_ASN, self::OPTION_BLOCKED_XMLRPC, ]; $sensitive_keys = ['recaptcha_site_key', 'recaptcha_secret_key', 'api_key_ipapicom', 'api_key_ipstackcom', 'api_key_ipinfocom', 'api_key_ip_apicom']; $settings_to_export = []; foreach ($option_keys as $key) { $settings_to_export[$key] = get_option($key); } if ($export_type === 'template' && isset($settings_to_export[self::OPTION_SETTINGS])) { foreach ($sensitive_keys as $sensitive_key) { if (isset($settings_to_export[self::OPTION_SETTINGS][$sensitive_key])) { $settings_to_export[self::OPTION_SETTINGS][$sensitive_key] = ''; } } } /* translators: %1$s: Export file type, %2$s: Username. */ $this->log_event( sprintf( __( 'Plugin settings prepared for export as \'%1$s\' by %2$s.', 'advanced-ip-blocker' ), $export_type, $this->get_current_admin_username() ), 'info' ); wp_send_json_success( ['settings' => $settings_to_export, 'type' => $export_type] ); } catch (Throwable $e) { // Si ocurriera un error inesperado, lo registramos y devolvemos un error limpio. $this->log_event( 'An unexpected error occurred during settings export: ' . $e->getMessage(), 'critical' ); wp_send_json_error(['message' => 'An unexpected server error occurred. Please check the plugin logs.']); } } /** * Maneja la lógica de importación de ajustes desde un archivo JSON subido. * Esta función es segura, ya que solo actualiza las opciones que están explícitamente permitidas. */ public function handle_import_settings() { if ( ! isset( $_POST['advaipbl_import_nonce_field'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['advaipbl_import_nonce_field'] ) ), 'advaipbl_import_nonce' ) ) { wp_die( 'Security check failed.', 'Error', ['response' => 403] ); } if ( ! current_user_can( 'manage_options' ) ) { wp_die( 'Permission denied.', 'Error', ['response' => 403] ); } $message = ''; $type = 'error'; if ( isset( $_FILES['advaipbl_import_file'] ) && UPLOAD_ERR_OK === $_FILES['advaipbl_import_file']['error'] ) { $file_name = $_FILES['advaipbl_import_file']['name']; if ( 'json' !== pathinfo( $file_name, PATHINFO_EXTENSION ) ) { $message = __( 'Error: The uploaded file is not a .json file.', 'advanced-ip-blocker' ); /* translators: $s: Export file type. */ $this->log_event( sprintf( __( 'A failed settings import was attempted by %s (invalid file type).', 'advanced-ip-blocker' ), $this->get_current_admin_username() ), 'error' ); } else { $file_content = file_get_contents( $_FILES['advaipbl_import_file']['tmp_name'] ); $settings_to_import = json_decode( $file_content, true ); if ( JSON_ERROR_NONE === json_last_error() && is_array( $settings_to_import ) ) { $allowed_option_keys = [ self::OPTION_SETTINGS, self::OPTION_WHITELIST, self::OPTION_BLOCKED_MANUAL, self::OPTION_BLOCKED_404, self::OPTION_BLOCKED_403, self::OPTION_BLOCKED_LOGIN, self::OPTION_BLOCKED_GEO, self::OPTION_BLOCKED_HONEYPOT, self::OPTION_BLOCKED_USER_AGENT, self::OPTION_HONEYPOT_URLS, self::OPTION_BLOCKED_UAS, self::OPTION_WHITELISTED_UAS, self::OPTION_WAF_RULES, self::OPTION_BLOCKED_WAF, self::OPTION_BLOCKED_RATE_LIMIT, self::OPTION_BLOCKED_ASNS, self::OPTION_BLOCKED_ASN, self::OPTION_BLOCKED_XMLRPC, ]; $imported_count = 0; foreach ( $settings_to_import as $key => $value ) { if ( in_array( $key, $allowed_option_keys, true ) ) { update_option( $key, $value ); $imported_count++; } } if ($imported_count > 0) { $message = __( 'Settings imported successfully. Please review all tabs to ensure the configuration is correct.', 'advanced-ip-blocker' ); $type = 'success'; /* translators: $s: Username. */ $this->log_event( sprintf( __( 'Plugin settings successfully imported by %s.', 'advanced-ip-blocker' ), $this->get_current_admin_username() ), 'warning' ); } else { $message = __( 'Error: The imported file did not contain any valid settings for this plugin.', 'advanced-ip-blocker' ); $type = 'error'; /* translators: $s: Username. */ $this->log_event( sprintf( __( 'A failed settings import was attempted by %s (no valid keys found).', 'advanced-ip-blocker' ), $this->get_current_admin_username() ), 'error' ); } } else { $message = __( 'Error: The uploaded file is not a valid JSON file.', 'advanced-ip-blocker' ); /* translators: $s: Username. */ $this->log_event( sprintf( __( 'A failed settings import was attempted by %s (invalid JSON file).', 'advanced-ip-blocker' ), $this->get_current_admin_username() ), 'error' ); } } } else { $message = __( 'Error: No file was uploaded or an error occurred during upload.', 'advanced-ip-blocker' ); /* translators: $s: Username. */ $this->log_event( sprintf( __( 'A failed settings import was attempted by %s (file upload error).', 'advanced-ip-blocker' ), $this->get_current_admin_username() ), 'error' ); } set_transient( self::TRANSIENT_ADMIN_NOTICE, ['message' => $message, 'type' => $type], 30 ); wp_safe_redirect( admin_url( 'admin.php?page=advaipbl_settings_page-settings&sub-tab=import_export' ) ); exit; } /** * Muestra la sección de configuración de 2FA en la página de perfil del usuario. * * @param WP_User $user El objeto del usuario cuyo perfil se está editando. */ public function display_2fa_section_in_profile( $user ) { // 1. Comprobamos si la funcionalidad 2FA está activada globalmente. if ( empty( $this->options['enable_2fa'] ) || '1' !== $this->options['enable_2fa'] ) { return; // Si no está activado, no mostramos nada y salimos. } // 2. Nos aseguramos de que el manager de 2FA esté cargado (relevante para CLI). if ( ! $this->tfa_manager ) { return; } if ( ! current_user_can( 'edit_user', $user->ID ) ) { return; } $is_enabled = $this->tfa_manager->is_2fa_enabled_for_user( $user->ID ); ?>

    ID, ADVAIPBL_2fa_Manager::META_ENABLED_AT, true ); /* translators: $s: Enabled ago. */ printf( esc_html__( 'Enabled %s.', 'advanced-ip-blocker' ), esc_html( human_time_diff( $enabled_time ) ) . ' ' . esc_html__('ago', 'advanced-ip-blocker') ); ?>

    __( 'Permission denied.', 'advanced-ip-blocker' ) ] ); } $user = get_user_by( 'id', $user_id ); if ( ! $user ) { wp_send_json_error( [ 'message' => __( 'Invalid user.', 'advanced-ip-blocker' ) ] ); } $data = $this->tfa_manager->generate_new_secret_for_user( $user ); wp_send_json_success( $data ); } /** * AJAX callback para verificar y activar 2FA para un usuario. */ public function ajax_2fa_activate() { check_ajax_referer( 'advaipbl_2fa_activate_nonce', 'nonce' ); $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0; $code = isset( $_POST['code'] ) ? sanitize_text_field( $_POST['code'] ) : ''; $backup_codes = isset( $_POST['backup_codes'] ) && is_array( $_POST['backup_codes'] ) ? array_map( 'sanitize_text_field', $_POST['backup_codes'] ) : []; if ( ! $user_id || ! current_user_can( 'edit_user', $user_id ) ) { wp_send_json_error( [ 'message' => __( 'Permission denied.', 'advanced-ip-blocker' ) ] ); } $success = $this->tfa_manager->verify_and_activate( $user_id, $code, $backup_codes ); if ( $success ) { wp_send_json_success(); } else { wp_send_json_error( [ 'message' => __( 'Invalid verification code. Please try again.', 'advanced-ip-blocker' ) ] ); } } /** * AJAX callback para desactivar 2FA para un usuario. */ public function ajax_2fa_deactivate() { check_ajax_referer( 'advaipbl_2fa_deactivate_nonce', 'nonce' ); $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0; if ( ! $user_id || ! current_user_can( 'edit_user', $user_id ) ) { wp_send_json_error( [ 'message' => __( 'Permission denied.', 'advanced-ip-blocker' ) ] ); } $this->tfa_manager->deactivate_for_user( $user_id ); wp_send_json_success(); } /** * Se ejecuta al inicio de la carga del formulario de login. * Su única misión es verificar el código si se envía desde nuestro formulario del paso 2. */ public function handle_login_action() { if ( ! isset( $_POST['advaipbl_2fa_login_step'] ) ) { return; } $step = $_POST['advaipbl_2fa_login_step']; $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0; $code = isset( $_POST['advaipbl_2fa_code'] ) ? trim( sanitize_text_field( $_POST['advaipbl_2fa_code'] ) ) : ''; $nonce = $_POST['_wpnonce'] ?? ''; $user = get_user_by( 'id', $user_id ); if ( ! $user ) { wp_die( 'Authentication error: Invalid user.' ); } $is_valid = false; $nonce_action = ''; $error_action_redirect = ''; if ( '2' === $step ) { $nonce_action = 'advaipbl-2fa-verify-' . $user_id; $error_action_redirect = 'advaipbl_validate_2fa'; if ( wp_verify_nonce( $nonce, $nonce_action ) ) { $is_valid = $this->tfa_manager->verify_code( $user->ID, $code ); } } elseif ( 'backup' === $step ) { $nonce_action = 'advaipbl-2fa-verify-backup-' . $user_id; $error_action_redirect = 'advaipbl_validate_2fa_backup'; if ( wp_verify_nonce( $nonce, $nonce_action ) ) { $is_valid = $this->tfa_manager->is_valid_backup_code( $user->ID, $code ); } } if ( $is_valid ) { wp_set_auth_cookie( $user->ID, isset( $_POST['rememberme'] ) ); $redirect_to = isset( $_REQUEST['redirect_to'] ) && !empty($_REQUEST['redirect_to']) ? wp_unslash( $_REQUEST['redirect_to'] ) : admin_url(); wp_safe_redirect( $redirect_to ); exit; } else { // Guardamos el tipo de error en una cookie que dura solo unos segundos $error_message = ( 'backup' === $step ) ? __( 'ERROR: The recovery code is incorrect or has already been used.', 'advanced-ip-blocker' ) : __( 'ERROR: The verification code is incorrect.', 'advanced-ip-blocker' ); setcookie( 'advaipbl_login_error', $error_message, time() + 30, COOKIEPATH, COOKIE_DOMAIN ); $redirect_url = add_query_arg([ 'action' => $error_action_redirect, 'user_id' => $user->ID, 'wp_auth_nonce' => wp_create_nonce( 'advaipbl-2fa-interim-' . $user->ID ), 'redirect_to' => $_REQUEST['redirect_to'] ?? '', ], site_url( 'wp-login.php', 'login' ) ); wp_redirect( $redirect_url ); exit; } } /** * Intercepta el login normal (Paso 1). Si el usuario y contraseña son correctos y necesita 2FA, * redirige a nuestra pantalla del Paso 2 en lugar de iniciar sesión. */ public function intercept_login_step_1( $user, $username, $password ) { if ( is_wp_error( $user ) || ! $user instanceof WP_User ) { return $user; } $global_2fa_enabled = ! empty( $this->options['enable_2fa'] ) && '1' === $this->options['enable_2fa']; if ( ! $global_2fa_enabled || ! $this->tfa_manager ) { return $user; } $user_has_2fa_setup = $this->tfa_manager->is_2fa_enabled_for_user( $user->ID ); $user_is_forced = $this->tfa_manager->is_2fa_forced_for_user( $user ); // Si el usuario tiene 2FA configurado, le redirigimos al paso 2. if ( $user_has_2fa_setup ) { $redirect_url = add_query_arg([ 'action' => 'advaipbl_validate_2fa', 'user_id' => $user->ID, 'wp_auth_nonce' => wp_create_nonce( 'advaipbl-2fa-interim-' . $user->ID ), 'redirect_to' => $_REQUEST['redirect_to'] ?? '', 'rememberme' => $_REQUEST['rememberme'] ?? '', ], site_url( 'wp-login.php', 'login' ) ); wp_redirect( $redirect_url ); exit; } // Si el usuario NO tiene 2FA, pero está OBLIGADO a tenerlo... if ( ! $user_has_2fa_setup && $user_is_forced ) { update_user_meta( $user->ID, '_advaipbl_2fa_setup_required', true ); } // En todos los demás casos (no tiene 2FA y no está obligado), el login es normal. return $user; } /** * Muestra nuestro formulario personalizado para el Paso 2 de 2FA. */ public function display_2fa_login_form_step_2() { $user_id = isset( $_GET['user_id'] ) ? absint( $_GET['user_id'] ) : 0; $nonce = $_GET['wp_auth_nonce'] ?? ''; if ( ! $user_id || ! wp_verify_nonce( $nonce, 'advaipbl-2fa-interim-' . $user_id ) ) { wp_die( 'Invalid 2FA request.' ); } $message = ''; if ( isset( $_COOKIE['advaipbl_login_error'] ) ) { $message = '
    ' . wp_kses_post( $_COOKIE['advaipbl_login_error'] ) . '
    '; // Borramos la cookie para que no se muestre de nuevo unset( $_COOKIE['advaipbl_login_error'] ); setcookie( 'advaipbl_login_error', '', time() - 3600, COOKIEPATH, COOKIE_DOMAIN ); } else { $message = '

    ' . esc_html__( 'Enter the code generated by your authenticator app.', 'advanced-ip-blocker' ) . '

    '; } add_filter( 'login_message', function( $original_message ) use ( $message ) { return $message; }); login_header( __( 'Enter Verification Code', 'advanced-ip-blocker' ) ); ?>

    ' . wp_kses_post( $_COOKIE['advaipbl_login_error'] ) . ''; unset( $_COOKIE['advaipbl_login_error'] ); setcookie( 'advaipbl_login_error', '', time() - 3600, COOKIEPATH, COOKIE_DOMAIN ); } else { $message = '

    ' . esc_html__( 'Please enter one of your recovery codes.', 'advanced-ip-blocker' ) . '

    '; } add_filter( 'login_message', function( $original_message ) use ( $message ) { return $message; }); login_header( __( 'Enter Recovery Code', 'advanced-ip-blocker' ) ); ?>

    options['tfa_force_roles'] ?? []; $editable_roles = get_editable_roles(); ?>
    $role_details ) : ?>

    tfa_manager ) { return; } $user_id = get_current_user_id(); // Solo continuamos si la marca existe. if ( get_user_meta( $user_id, '_advaipbl_2fa_setup_required', true ) ) { $user = wp_get_current_user(); $global_2fa_enabled = ! empty( $this->options['enable_2fa'] ) && '1' === $this->options['enable_2fa']; $user_is_forced = $this->tfa_manager->is_2fa_forced_for_user( $user ); $user_has_2fa_setup = $this->tfa_manager->is_2fa_enabled_for_user( $user_id ); // CONDICIONES PARA MOSTRAR EL AVISO: // 1. 2FA debe estar activado globalmente. // 2. El rol del usuario debe estar en la lista de forzados. // 3. El usuario NO debe tener 2FA configurado todavía. if ( $global_2fa_enabled && $user_is_forced && ! $user_has_2fa_setup ) { $profile_url = get_edit_profile_url( $user_id ); ?>

    go to your profile to configure it now.', 'advanced-ip-blocker' ), [ 'a' => [ 'href' => [] ] ] ), esc_url( $profile_url ) ); ?>

    options['enable_email_notifications'] ) || '1' !== $this->options['enable_email_notifications'] ) { return; } $to = $user->user_email; $site_name = get_bloginfo( 'name' ); $subject = ''; $template_title = ''; $content_html = ''; switch ( $event ) { case 'activated': /* translators: $s: Site name. */ $subject = sprintf( __( '[%s] Two-Factor Authentication Activated', 'advanced-ip-blocker' ), $site_name ); $template_title = __( '2FA Activated', 'advanced-ip-blocker' ); $content_html = '

    ' . sprintf(/* translators: %s: Username. */ esc_html__( 'Hello %s,', 'advanced-ip-blocker' ), esc_html( $user->display_name ) ) . '

    ' . '

    ' . esc_html__( 'This is a confirmation that two-factor authentication has been successfully activated on your account. Your account is now more secure.', 'advanced-ip-blocker' ) . '

    ' . '

    ' . esc_html__( 'If you did not perform this action, please contact the site administrator immediately.', 'advanced-ip-blocker' ) . '

    '; break; case 'deactivated': /* translators: $s: Site name. */ $subject = sprintf( __( '[%s] SECURITY ALERT: Two-Factor Authentication Deactivated', 'advanced-ip-blocker' ), $site_name ); $template_title = __( 'Security Alert: 2FA Deactivated', 'advanced-ip-blocker' ); $content_html = '

    ' . sprintf(/* translators: %s: Username. */ esc_html__( 'Hello %s,', 'advanced-ip-blocker' ), esc_html( $user->display_name ) ) . '

    ' . '

    ' . esc_html__( 'This is a notification that two-factor authentication has been deactivated on your account. Your account is now less secure.', 'advanced-ip-blocker' ) . '

    ' . '

    ' . esc_html__( 'If you did not perform this action, please reset your password and contact the site administrator immediately.', 'advanced-ip-blocker' ) . '

    '; break; case 'reset': /* translators: $s: Site name. */ $subject = sprintf( __( '[%s] Your Two-Factor Authentication has been Reset', 'advanced-ip-blocker' ), $site_name ); $template_title = __( '2FA Reset by Administrator', 'advanced-ip-blocker' ); $content_html = '

    ' . sprintf(/* translators: %s: Username. */ esc_html__( 'Hello %s,', 'advanced-ip-blocker' ), esc_html( $user->display_name ) ) . '

    ' . '

    ' . esc_html__( 'A site administrator has reset the two-factor authentication configuration for your account. You can now log in using only your username and password.', 'advanced-ip-blocker' ) . '

    ' . '

    ' . esc_html__( 'We strongly recommend you set up 2FA again from your profile page as soon as possible.', 'advanced-ip-blocker' ) . '

    '; break; case 'backup_used': /* translators: $s: Site name. */ $subject = sprintf( __( '[%s] SECURITY NOTICE: A Recovery Code was Used', 'advanced-ip-blocker' ), $site_name ); $template_title = __( 'Security Notice', 'advanced-ip-blocker' ); $remaining_codes = $data['remaining_codes'] ?? 0; $content_html = '

    ' . sprintf(/* translators: %s: Username. */ esc_html__( 'Hello %s,', 'advanced-ip-blocker' ), esc_html( $user->display_name ) ) . '

    ' . '

    ' . esc_html__( 'A recovery code was used to access your account.', 'advanced-ip-blocker' ) . '

    ' . '

    ' . sprintf(/* translators: %d: Number of recovery codes. */ esc_html__( 'You have %d recovery codes remaining.', 'advanced-ip-blocker' ), $remaining_codes ) . '

    ' . '

    ' . esc_html__( 'If you did not perform this action, please reset your password immediately.', 'advanced-ip-blocker' ) . '

    '; break; } if ( ! empty( $content_html ) ) { $body = $this->get_html_email_template( $template_title, $content_html ); add_filter( 'wp_mail_content_type', [$this, 'set_html_mail_content_type'] ); wp_mail( $to, $subject, $body ); remove_filter( 'wp_mail_content_type', [$this, 'set_html_mail_content_type'] ); } } /** * Previene la enumeración de usuarios a través del endpoint oEmbed de la API REST. * Elimina los datos del autor de la respuesta si la protección está activada. * * @param array $data The response data. * @param WP_Post $post The post object. * @param int $width The requested width. * @param int $height The requested height. * @return array The modified response data. */ public function prevent_user_enumeration_via_oembed( $data, $post, $width, $height ) { // Solo actuar si la opción de protección está activada. if ( ! empty( $this->options['disable_user_enumeration'] ) ) { // Eliminamos las claves que podrían revelar información del autor. unset( $data['author_name'] ); unset( $data['author_url'] ); } return $data; } /** * Previene la enumeración de usuarios a través de los feeds RSS. * Si la protección de escaneo de autor está activada, reemplaza el login del autor * con su nombre público (display name) en los feeds. * * @param string $author_login El login del autor. * @return string El login del autor o su nombre público. */ public function prevent_user_enumeration_via_feeds( $author_login ) { // Solo actuar si la opción está activada y estamos en un feed. if ( ! empty( $this->options['prevent_author_scanning'] ) && is_feed() ) { $author = get_user_by( 'login', $author_login ); if ( $author ) { // Devolvemos el nombre público en lugar del login. return $author->display_name; } } return $author_login; } /** * Previene el "login hinting" interceptando los errores de autenticación * y reemplazándolos por un mensaje genérico. * * @param WP_User|WP_Error|null $user El objeto de usuario o error. * @return WP_User|WP_Error */ public function prevent_login_hinting( $user ) { // Solo actuar si la opción está activada. if ( empty( $this->options['prevent_login_hinting'] ) || '1' !== $this->options['prevent_login_hinting'] ) { return $user; } // Solo nos interesa modificar los errores, no los inicios de sesión correctos. if ( ! is_wp_error( $user ) ) { return $user; } // Lista de códigos de error que queremos ocultar. $error_codes_to_hide = [ 'invalid_username', 'invalid_email', 'incorrect_password', ]; // Comprobamos si el error actual es uno de los que dan pistas. $has_hinting_error = false; foreach ( $error_codes_to_hide as $code ) { if ( $user->get_error_code() === $code ) { $has_hinting_error = true; break; } } if ( $has_hinting_error ) { // Si es un error que da pistas, devolvemos un NUEVO WP_Error con nuestro mensaje genérico. // WordPress se encargará de mostrarlo correctamente en la banda roja. return new WP_Error( 'advaipbl_generic_login_error', __( 'ERROR: Your login details are incorrect. Please try again.', 'advanced-ip-blocker' ) ); } // Si no es un error de los que queremos ocultar (ej. el de 2FA), lo dejamos pasar. return $user; } /** * Guarda los cambios de la sección 2FA en el perfil de usuario. * Este hook es necesario para WordPress. Nuestra lógica de activación/desactivación * se maneja principalmente vía AJAX para una mejor experiencia de usuario. * * @param int $user_id El ID del usuario que se está actualizando. */ public function save_2fa_section_in_profile( $user_id ) { // No se requiere ninguna acción aquí, ya que el JS maneja las acciones de 2FA. // La función debe existir para que el hook 'personal_options_update' no cause un error fatal. if ( ! current_user_can( 'edit_user', $user_id ) ) { return; } // No hay nada que hacer, pero la función está aquí para evitar el error. } /** * Muestra el selector para el método de geolocalización. */ public function geolocation_method_callback() { $current_method = $this->options['geolocation_method'] ?? 'api'; ?>

    geoip_manager ) return; $status = $this->geoip_manager->get_database_status(); ?>

    : ()

    : ()

    : ()

    'Nonce verification failed.'], 403 ); } if ( ! current_user_can( 'manage_options' ) || ! $this->geoip_manager ) { wp_send_json_error( ['message' => 'Permission denied or module not available.'], 403 ); } // Aumentamos el límite de tiempo de ejecución para la descarga set_time_limit(300); $result = $this->geoip_manager->download_and_unpack_databases(); if ( $result['success'] ) { wp_send_json_success( ['message' => $result['message']] ); } else { wp_send_json_error( ['message' => $result['message']] ); } } /** * Tarea programada para actualizar las bases de datos de GeoIP. */ public function execute_geoip_db_update() { // Solo se ejecuta si el método es 'local_db' y hay una clave de licencia. if ( isset($this->options['geolocation_method']) && $this->options['geolocation_method'] === 'local_db' && !empty($this->options['maxmind_license_key']) && $this->geoip_manager ) { $this->log_event('Starting scheduled GeoIP database update.', 'info'); $result = $this->geoip_manager->download_and_unpack_databases(); if (!$result['success']) { $this->log_event('Scheduled GeoIP database update failed: ' . $result['message'], 'error'); } else { $this->log_event('Scheduled GeoIP database update completed successfully.', 'info'); } } } /** * Comprueba si una IP está actualmente bajo un bloqueo activo de cualquier tipo. * Esta función es una comprobación de alto rendimiento que prioriza los transients. * * @param string $ip La dirección IP a comprobar. * @return bool True si la IP está activamente bloqueada, false en caso contrario. */ public function is_ip_actively_blocked($ip) { $definitions = $this->get_all_block_type_definitions(); // 1. Comprobación de transients para todos los tipos que los usan. foreach ($definitions as $type => $def) { if ($def['uses_transient'] && get_transient('advaipbl_bloqueo_' . $type . '_' . md5($ip))) { return true; } } // 2. Comprobación específica para bloqueos manuales (que son permanentes). $manual_blocks = get_option(self::OPTION_BLOCKED_MANUAL, []); if (!empty($manual_blocks)) { foreach (array_keys($manual_blocks) as $entry) { if ($this->is_ip_in_range($ip, $entry)) { return true; } } } return false; } /** * Comprueba si la IP del visitante actual está activamente bloqueada. * Utiliza un caché estático por petición para un rendimiento máximo, * ya que esta función puede ser llamada varias veces durante una misma ejecución. * * @return bool True si la IP está bloqueada, false en caso contrario. */ private function is_visitor_actively_blocked() { // 1. Usar caché por petición para evitar comprobaciones repetidas en la misma carga de página. static $is_blocked = null; if ( null !== $is_blocked ) { return $is_blocked; } $ip = $this->get_client_ip(); // 2. Comprobación más rápida: el transient maestro. // Si este transient existe, sabemos que hay un bloqueo activo y no necesitamos más detalles. if ( get_transient( 'advaipbl_blocked_ip_' . md5( $ip ) ) ) { $is_blocked = true; return true; } // 3. Si el transient maestro no existe, realizamos una comprobación más exhaustiva // en la base de datos para bloqueos permanentes (como los manuales) que no usan transients. global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_blocked_ips'; // Buscamos cualquier entrada para esta IP que NO haya expirado. // expires_at = 0 se considera permanente. $is_blocked_in_db = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table_name} WHERE ip_range = %s AND (expires_at = 0 OR expires_at > %d)", $ip, time() ) ); if ( $is_blocked_in_db ) { $is_blocked = true; return true; } // Si no se encuentra ningún bloqueo, cacheamos el resultado y devolvemos false. $is_blocked = false; return false; } /** * Revisa y actualiza el estado de 'autoload' para las opciones del plugin que pueden * crecer mucho en tamaño, mejorando el rendimiento general del sitio. * Se ejecuta automáticamente una vez gracias a un sistema de versionado. */ public function update_option_autoload_states() { if ( get_option('advaipbl_autoload_version') === self::AUTOLOAD_OPTIMIZATION_VERSION ) { return; } global $wpdb; $options_to_optimize = [ // Listas y reglas que pueden crecer self::OPTION_WHITELIST, self::OPTION_BLOCKED_MANUAL, self::OPTION_BLOCKED_404, self::OPTION_BLOCKED_403, self::OPTION_BLOCKED_LOGIN, self::OPTION_BLOCKED_GEO, self::OPTION_BLOCKED_HONEYPOT, self::OPTION_BLOCKED_USER_AGENT, self::OPTION_BLOCKED_WAF, self::OPTION_BLOCKED_THREAT_SCORE, self::OPTION_BLOCKED_RATE_LIMIT, self::OPTION_BLOCKED_ASN, self::OPTION_BLOCKED_XMLRPC, self::OPTION_HONEYPOT_URLS, self::OPTION_BLOCKED_UAS, self::OPTION_WHITELISTED_UAS, self::OPTION_WAF_RULES, self::OPTION_BLOCKED_ASNS, self::OPTION_WHITELISTED_ASNS, 'advaipbl_spamhaus_asn_list', self::OPTION_ADMIN_IP_TRIGGER, 'advaipbl_spamhaus_last_update', ]; // Usamos una consulta directa a la BD, ya que update_option() no puede cambiar // el autoload de una opción existente. foreach ($options_to_optimize as $option_name) { $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->options} SET `autoload` = 'no' WHERE `option_name` = %s", $option_name ) ); } update_option('advaipbl_autoload_version', self::AUTOLOAD_OPTIMIZATION_VERSION); $this->log_event('Successfully optimized database option autoloading for performance.', 'info'); } /** * Obtiene y sanea la URI de la petición actual. * Cachea el resultado para evitar trabajo repetido en una misma petición. * * @return string La URI de la petición saneada. */ public function get_current_request_uri() { static $request_uri = null; if ( is_null($request_uri) ) { $request_uri = esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'] ?? '')); } return $request_uri; } /** * Obtiene y sanea el User Agent de la petición actual. * @return string El User Agent de la petición, saneado. */ public function get_user_agent() { static $user_agent = null; if (is_null($user_agent)) { $user_agent = sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'] ?? '')); } return $user_agent; } /** * Obtiene y sanea el método de la petición actual. * @return string */ public function get_request_method() { static $request_method = null; if (is_null($request_method)) { $request_method = sanitize_text_field($_SERVER['REQUEST_METHOD'] ?? 'GET'); } return $request_method; } /** * Obtiene y sanea el Referer de la petición actual. * @return string */ public function get_http_referer() { static $http_referer = null; if (is_null($http_referer)) { $http_referer = sanitize_text_field(wp_unslash($_SERVER['HTTP_REFERER'] ?? '')); } return $http_referer; } /** * Obtiene y sanea la IP de la conexión directa (REMOTE_ADDR). * @return string */ public function get_remote_addr() { static $remote_addr = null; if (is_null($remote_addr)) { $remote_addr = filter_var($_SERVER['REMOTE_ADDR'] ?? '', FILTER_VALIDATE_IP) ?: ''; } return $remote_addr; } /** * Incrementa el contador de un endpoint para el sistema de Lockdown. * Si el contador supera el umbral, activa el modo Lockdown. * * @param string $endpoint_key La clave del endpoint (ej. 'xmlrpc'). */ private function increment_lockdown_counter($endpoint_key) { $threshold = (int) ($this->options[$endpoint_key . '_lockdown_threshold'] ?? 10); $window_minutes = (int) ($this->options[$endpoint_key . '_lockdown_window'] ?? 15); $duration_minutes = (int) ($this->options[$endpoint_key . '_lockdown_duration'] ?? 60); if ($threshold <= 0 || $window_minutes <= 0) { return; } $cache_key = 'advaipbl_lockdown_trigger_' . $endpoint_key; // Obtenemos los datos actuales de la caché, que ahora serán un array. $trigger_data = $this->get_from_custom_cache($cache_key); if (!is_array($trigger_data)) { $trigger_data = ['count' => 0, 'ips' => []]; } $trigger_data['count']++; $ip = $this->get_client_ip(); // Guardamos las últimas ~15 IPs para tener contexto. $trigger_data['ips'][] = $ip; if (count($trigger_data['ips']) > 15) { $trigger_data['ips'] = array_slice($trigger_data['ips'], -15); } $trigger_data['ips'] = array_unique($trigger_data['ips']); // Volvemos a guardar en la caché. $ttl = $this->get_from_custom_cache($cache_key, true)['expires_at'] ?? (time() + $window_minutes * MINUTE_IN_SECONDS); $this->set_in_custom_cache($cache_key, $trigger_data, $ttl - time()); if ($trigger_data['count'] >= $threshold) { global $wpdb; $lockdowns_table = $wpdb->prefix . 'advaipbl_endpoint_lockdowns'; $now = time(); $reason = sprintf( /* translators: 1: The number of blocks. 2: The number of minutes in the detection window. */ __('Exceeded threshold: %1$d blocks in %2$d minutes.', 'advanced-ip-blocker'), $trigger_data['count'], $window_minutes ); $details = wp_json_encode(['triggering_ip_hashes' => $trigger_data['ips']]); // Insertamos el lockdown en nuestra nueva tabla. $wpdb->insert( $lockdowns_table, [ 'endpoint_key' => $endpoint_key, 'reason' => $reason, 'created_at' => $now, 'expires_at' => $now + ($duration_minutes * MINUTE_IN_SECONDS), 'details' => $details, ] ); // Limpiamos el contador de la caché, ya que el lockdown está activo. $wpdb->delete($wpdb->prefix . 'advaipbl_cache', ['cache_key' => $cache_key]); $this->log_event(sprintf('Endpoint Lockdown activated for "%s" for %d minutes due to %d suspicious blocks.', $endpoint_key, $duration_minutes, $trigger_data['count']), 'critical'); $this->send_lockdown_notification($endpoint_key, $duration_minutes, $threshold); } } /** * Incrementa el contador de fallos de login para el sistema de Lockdown de wp-login.php. * Activa el lockdown si se superan los umbrales de eventos y de IPs únicas. */ private function increment_login_lockdown_counter() { // Obtenemos los umbrales desde los ajustes (los añadiremos más tarde). // Por ahora, usamos valores por defecto razonables. $event_threshold = (int) ($this->options['login_lockdown_event_threshold'] ?? 50); $ip_threshold = (int) ($this->options['login_lockdown_ip_threshold'] ?? 10); $window_minutes = (int) ($this->options['login_lockdown_window'] ?? 5); $duration_minutes = (int) ($this->options['login_lockdown_duration'] ?? 60); if ($event_threshold <= 0 || $ip_threshold <= 0 || $window_minutes <= 0) { return; } $endpoint_key = 'login'; $cache_key = 'advaipbl_lockdown_trigger_' . $endpoint_key; // Obtenemos los datos actuales de la caché. $trigger_data = $this->get_from_custom_cache($cache_key); if (!is_array($trigger_data) || !isset($trigger_data['event_count'])) { $trigger_data = ['event_count' => 0, 'ip_hashes' => []]; } $trigger_data['event_count']++; $ip_hash = hash('sha256', $this->get_client_ip()); if (!in_array($ip_hash, $trigger_data['ip_hashes'])) { $trigger_data['ip_hashes'][] = $ip_hash; } // Guardamos los datos actualizados en la caché. $ttl_info = $this->get_from_custom_cache($cache_key, true); $ttl = $ttl_info ? $ttl_info['expires_at'] - time() : $window_minutes * MINUTE_IN_SECONDS; if ($ttl > 0) { $this->set_in_custom_cache($cache_key, $trigger_data, $ttl); } // Comprobamos si se han superado ambos umbrales. if ($trigger_data['event_count'] >= $event_threshold && count($trigger_data['ip_hashes']) >= $ip_threshold) { global $wpdb; $lockdowns_table = $wpdb->prefix . 'advaipbl_endpoint_lockdowns'; // Verificamos que no haya ya un lockdown activo para este endpoint para evitar duplicados. $is_already_active = $wpdb->get_var($wpdb->prepare( "SELECT id FROM {$lockdowns_table} WHERE endpoint_key = %s AND expires_at > %d", $endpoint_key, time() )); if (!$is_already_active) { $now = time(); $reason = sprintf( /* translators: 1: The number of failed logins. 2: The number of unique IPs. 3: The number of minutes in the detection window. */ __('Exceeded threshold: %1$d failed logins from %2$d unique IPs in %3$d minutes.', 'advanced-ip-blocker'), $trigger_data['event_count'], count($trigger_data['ip_hashes']), $window_minutes ); $details = wp_json_encode(['triggering_ip_hashes' => $trigger_data['ip_hashes']]); $wpdb->insert( $lockdowns_table, [ 'endpoint_key' => $endpoint_key, 'reason' => $reason, 'created_at' => $now, 'expires_at' => $now + ($duration_minutes * MINUTE_IN_SECONDS), 'details' => $details, ] ); $this->log_event(sprintf('Endpoint Lockdown activated for "%s" for %d minutes.', $endpoint_key, $duration_minutes), 'critical'); $this->send_lockdown_notification($endpoint_key, $duration_minutes, $event_threshold); } // Limpiamos el contador de la caché, ya que el lockdown está activo. $wpdb->delete($wpdb->prefix . 'advaipbl_cache', ['cache_key' => $cache_key]); } } /** * Comprueba si un endpoint crítico está bajo "Lockdown" y sirve un desafío si es necesario. * Se ejecuta en un hook temprano para interceptar el tráfico antes que otras comprobaciones. */ public function check_for_endpoint_lockdown() { // Solo ejecutamos si la función de Lockdown está activada para al menos un endpoint. // AÑADIMOS la nueva opción 'enable_login_lockdown'. if (empty($this->options['enable_xmlrpc_lockdown']) && empty($this->options['enable_login_lockdown'])) { return; } if ($this->request_is_asn_whitelisted) { return; } if (isset($_POST['_advaipbl_js_token'])) { return; } if (isset($_COOKIE['advaipbl_js_verified']) && $_COOKIE['advaipbl_js_verified'] === '1') { return; } $request_uri = $this->get_current_request_uri(); $endpoint_key = ''; // Determinamos qué endpoint se está intentando acceder. $is_xmlrpc_request = strpos($request_uri, 'xmlrpc.php') !== false; $is_login_request = strpos($request_uri, 'wp-login.php') !== false; if ($is_xmlrpc_request && !empty($this->options['enable_xmlrpc_lockdown'])) { $endpoint_key = 'xmlrpc'; } elseif ($is_login_request && !empty($this->options['enable_login_lockdown'])) { $endpoint_key = 'login'; } if (empty($endpoint_key)) { return; } // Comprobamos en la base de datos si hay un lockdown activo para este endpoint. global $wpdb; $lockdowns_table = $wpdb->prefix . 'advaipbl_endpoint_lockdowns'; $is_lockdown_active = $wpdb->get_var($wpdb->prepare( "SELECT id FROM {$lockdowns_table} WHERE endpoint_key = %s AND expires_at > %d", $endpoint_key, time() )); if ($is_lockdown_active) { $ip = $this->get_client_ip(); if ($this->is_whitelisted($ip)) { return; } $user_agent = $this->get_user_agent(); $is_trusted_service = false; if ('xmlrpc' === $endpoint_key) { $automattic_ua_patterns = [ '/^WordPress\/\d+\.\d+/i', '/jetpack by wordpress\.com/i', '/woocommerce/i' ]; foreach ($automattic_ua_patterns as $pattern) { if (preg_match($pattern, $user_agent)) { $is_trusted_service = true; break; } } } // Para 'login', no hay servicios de confianza que eximir por defecto. Todos los no-whitelisted son desafiados. if (!$is_trusted_service) { $this->log_specific_error( 'endpoint_challenge', $ip, [ 'endpoint' => $endpoint_key, 'reason' => ucfirst($endpoint_key) . ' Lockdown Mode Active', 'uri' => $request_uri ], 'warning' ); $this->serve_js_challenge(); } } } /** * Envía notificaciones (Email/Push) cuando el modo Lockdown se activa para un endpoint. * * @param string $endpoint_key La clave del endpoint (ej. 'xmlrpc'). * @param int $duration_minutes La duración del lockdown. * @param int $threshold El umbral de bloqueos que lo activó. */ public function send_lockdown_notification($endpoint_key, $duration_minutes, $threshold) { $site_name = get_bloginfo('name'); $endpoint_name = strtoupper($endpoint_key); // ej. XMLRPC // --- Notificación Push (Webhook) --- if (!empty($this->options['enable_push_notifications'])) { $message_lines = [ sprintf('*:lock: [%s] Endpoint Lockdown Activated*', $site_name), '> An automated defense system has been activated due to a sustained attack.', /* translators: $s: Endpoint name. */ sprintf('> • *Protected Endpoint:* `%s`', $endpoint_name), /* translators: $d: Threshold number. */ sprintf('> • *Reason:* More than %d suspicious blocks detected recently.', $threshold), /* translators: $d: Minutes. */ sprintf('> • *Action:* All new traffic to this endpoint will be challenged for the next %d minutes.', $duration_minutes), ]; $this->execute_webhook_send(implode("\n", $message_lines)); } // --- Notificación por Email --- if (!empty($this->options['enable_email_notifications']) && $this->options['notification_frequency'] === 'instant') { $to = !empty($this->options['notification_email']) ? $this->options['notification_email'] : get_option('admin_email'); /* translators: 1: Site name, 2: Username */ $email_subject = sprintf(__('[%1$s] Security Alert: Endpoint Lockdown Activated for %2$s', 'advanced-ip-blocker'), $site_name, $endpoint_name); $template_title = __('Endpoint Lockdown Activated', 'advanced-ip-blocker'); $button_url = admin_url('admin.php?page=advaipbl_settings_page&tab=logs&sub-tab=security_log'); $content_html = '

    ' . esc_html__("An automated defense mechanism, Endpoint Lockdown, has been activated on your website due to a sustained attack.", 'advanced-ip-blocker') . '

    ' . '' . '' . '' . '' . '
    ' . esc_html__('Protected Endpoint', 'advanced-ip-blocker') . '' . esc_html($endpoint_name) . '
    ' . esc_html__('Reason for Activation', 'advanced-ip-blocker') . '' . sprintf(/* translators: %d: The number of IPs. */esc_html__('More than %d suspicious IP blocks were detected in a short period.', 'advanced-ip-blocker'), $threshold) . '
    ' . esc_html__('Protective Action', 'advanced-ip-blocker') . '' . sprintf(/* translators: %d: The number minutes. */esc_html__('For the next %d minutes, all new, non-whitelisted traffic to this endpoint will be challenged with a JavaScript verifier to filter out bots.', 'advanced-ip-blocker'), $duration_minutes) . '
    ' . '

    ' . esc_html__('This is an automated response to protect your site. No immediate action is required on your part.', 'advanced-ip-blocker') . '

    '. '
    ' . '' . esc_html__( 'View Security Log', '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']); } } }