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'; $this->api_manager = new ADVAIPBL_Api_Manager(); $this->session_manager = new ADVAIPBL_User_Session_Manager($this, $this->api_manager); $this->waf_manager = new ADVAIPBL_Waf_Manager($this); $this->rate_limit_manager = new ADVAIPBL_Rate_Limiting_Manager($this); $this->asn_manager = new ADVAIPBL_Asn_Manager($this, $this->api_manager); $this->dashboard_manager = new ADVAIPBL_Dashboard_Manager($this, $this->session_manager); $this->add_hooks(); } private function add_hooks() { // Hooks del Frontend y Globales add_action('wp_enqueue_scripts', [$this, 'enqueue_live_feed_assets']); add_action('init', [$this, 'log_wp_cron_execution'], 1); add_action('init', [$this->rate_limit_manager, 'check_request_rate'], -1); add_action('init', [$this, 'run_all_block_checks'], 0); add_action('init', [$this, 'add_admin_ip_to_whitelist_on_first_run']); add_action('status_header', [$this, 'detect_http_error_status'], 10, 2); add_action('init', [$this, 'prevent_author_enumeration_redirect'], 9); add_action('wp_login_failed', [$this, 'registrar_intento_login_fallido']); add_action('login_init', [ $this, 'handle_login_page_restriction' ], 1 ); add_filter('rest_endpoints', [ $this, 'disable_rest_api_user_endpoints' ] ); add_action('rest_api_init', [$this, 'register_live_feed_api_endpoint']); add_shortcode('advaipbl_live_feed', [$this, 'render_live_feed_shortcode']); 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_filter('cron_schedules', [$this, 'add_cron_intervals']); add_action('advaipbl_clear_expired_blocks_event', [$this, 'limpiar_ips_expiradas']); // Hooks del Backend if (is_admin()) { add_action('admin_init', [$this, 'check_database_update']); add_action('admin_init', [$this, 'schedule_cron_jobs']); add_action('admin_init', [$this, 'conditionally_remove_admin_notices']); 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_admin_notice']); add_action('admin_enqueue_scripts', [$this, 'load_admin_scripts']); // Hooks de AJAX y Admin Post 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_get_dashboard_stats', [$this, 'ajax_get_dashboard_stats']); add_action('wp_ajax_advaipbl_export_settings_ajax', [ $this, 'handle_export_settings_ajax' ] ); 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); } } 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.']); } } /** * 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' => '__return_true', // Es público, solo lee datos no sensibles. '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'); // Añadimos "AND log_type != 'general'" a la consulta para excluir los logs de auditoría. $query = "SELECT log_id, timestamp, ip, log_type, message, details FROM {$table_name} WHERE level = 'critical' AND log_type != 'general'"; if ($since_id) { $query .= $wpdb->prepare(" AND log_id > %d", $since_id); } $query .= " ORDER BY log_id DESC LIMIT 20"; $results = $wpdb->get_results($query, 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); } /** * 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'), '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; } /** * Encola los assets del feed en vivo. * Actualmente obsoleto, ya que el shortcode 'render_live_feed_shortcode' * ahora inyecta los estilos y encola el script directamente para máxima compatibilidad con temas. */ public function enqueue_live_feed_assets() { // Esta función se mantiene por compatibilidad histórica pero la lógica // se ha movido al renderizado del shortcode. } /** * Detecta y registra las ejecuciones de WP-Cron. * Enganchada a 'init' con una prioridad alta para ejecutarse pronto. */ public function log_wp_cron_execution() { // wp-cron.php puede ser llamado de dos maneras. // 1. A través de una constante definida, que es la forma más común. // 2. A través de la URL, si alguien lo llama directamente. $is_doing_cron = ( defined( 'DOING_CRON' ) && DOING_CRON ); $is_cron_url = ( isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], 'wp-cron.php') !== false ); if ( $is_doing_cron || $is_cron_url ) { // Obtenemos los detalles de la petición $ip = $this->get_client_ip(); // Evitamos el "ruido" de registrar la misma IP repetidamente en un corto periodo. // Creamos un transient que dura 60 segundos. $transient_key = 'advaipbl_cron_log_lock_' . md5($ip); if ( false !== get_transient($transient_key) ) { return; // Ya hemos registrado esta IP recientemente. } set_transient($transient_key, true, 60); global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_logs'; $details = [ 'url' => $_SERVER['REQUEST_URI'] ?? 'N/A', 'method' => $_SERVER['REQUEST_METHOD'] ?? 'N/A', 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'N/A', ]; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery @$wpdb->insert( $table_name, [ 'timestamp' => current_time('mysql', 1), 'ip' => $ip, 'log_type' => 'wp_cron', // Usamos un tipo de log único '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() { // Añadimos una descripción para que el usuario sepa para qué es esta pestaña. echo '

'; esc_html_e( 'This log shows which IP addresses are triggering WP-Cron tasks on your site. This can be a visitor, a search engine bot, or your own server if you have configured a real cron job.', 'advanced-ip-blocker' ); echo '

'; $this->display_log_table_generic('wp_cron'); } /** * Enqueues the Google reCAPTCHA API script on the login page. */ public function enqueue_recaptcha_script() { $version = $this->options['recaptcha_version'] ?? 'v3'; $site_key = $this->options['recaptcha_site_key'] ?? ''; // La comprobación de si la clave está vacía ya se hace en add_hooks, // pero una doble comprobación no hace daño. if (empty($site_key)) { return; } $script_url = 'https://www.google.com/recaptcha/api.js'; if ('v3' === $version) { $script_url = add_query_arg('render', $site_key, $script_url); } 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. */ /** * 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) ); ?>



Settings tab.', 'advanced-ip-blocker'), ['a' => ['href' => []]] ), esc_url($settings_url) ); ?>

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. */ private 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 ); 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, ]; 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.'); } delete_transient(ADVAIPBL_USM_LOCATION_CACHE_KEY); /* 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' ) . '

' . '

' . /* translators: %s: a link to the Status & Debug tab. */ sprintf( wp_kses( __( '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' ) . '

' . '

' . /* translators: %s: a link to the User Agents tab. */ sprintf( wp_kses( __( '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 ) ) ) . '
' . /* translators: %s: a link to the Honeypot URLs tab. */ sprintf( wp_kses( __( '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' ) . '

' . '

' . /* translators: %s: a link to the main Settings tab. */ sprintf( wp_kses( __( '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) { if ( strpos( $hook, 'advaipbl_settings_page' ) === false ) { return; } $this->session_manager->enqueue_scripts_styles(); 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); // --- 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->api_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->api_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 ); } // --- Obtener el conteo de entradas bloqueadas --- $blocked_count = count( $this->get_all_blocked_entries() ); // --- Localizar scripts con datos para el frontend --- wp_localize_script('advaipbl-admin-js', 'advaipbl_admin_data', [ 'counts' => [ 'blocked' => $blocked_count, ], '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' ) ], 'text' => [ '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' ), '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' ), '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' ), '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' ), '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' ), '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' ), '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' ), ], ]); // --- Determinar la sub-pestaña activa del panel de administración --- $active_sub_tab = null; $current_page_slug = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; if ( isset( $_GET['sub-tab'] ) ) { $active_sub_tab = sanitize_key( $_GET['sub-tab'] ); } else { $page_slug_to_sub_tab_map = [ 'advaipbl_settings_page' => 'main_dashboard', 'advaipbl_settings_page-settings' => 'general_settings', 'advaipbl_settings_page-rules' => 'waf', 'advaipbl_settings_page-ip-management' => 'blocked_ips', 'advaipbl_settings_page-logs' => 'security_log', 'advaipbl_settings_page-about' => 'credits', ]; if ( isset( $page_slug_to_sub_tab_map[ $current_page_slug ] ) ) { $active_sub_tab = $page_slug_to_sub_tab_map[ $current_page_slug ]; } } // --- Lógica condicional basada en la sub-pestaña activa --- if ( 'main_dashboard' === $active_sub_tab ) { // Código para el dashboard principal // Enqueue de scripts y estilos específicos para el dashboard wp_enqueue_script('chartjs', 'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js', [], '3.9.1', true); // Leaflet Core wp_enqueue_style('leaflet-css', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'); wp_enqueue_script('leaflet-js', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], '1.9.4', true); // Leaflet MarkerCluster wp_enqueue_style('leaflet-markercluster-css', 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css'); wp_enqueue_style('leaflet-markercluster-default-css', 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css'); wp_enqueue_script('leaflet-markercluster-js', 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js', ['leaflet-js'], '1.5.3', true); // Nuestro script del dashboard, que ahora depende de markercluster wp_enqueue_script('advaipbl-dashboard-js', plugin_dir_url( dirname( __FILE__ ) ) . 'js/advaipbl-dashboard.js', ['jquery', 'chartjs', 'leaflet-markercluster-js'], ADVAIPBL_VERSION, true); } // <-- ¡Aquí cierra correctamente el primer 'if'! // Ahora, el siguiente 'if' está en su lugar correcto. if ('credits' === $active_sub_tab) { wp_enqueue_script('stripe-buy-button', 'https://js.stripe.com/v3/buy-button.js', [], null, true);} } public function detect_http_error_status($status_header, $code) { 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 = isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['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; // La lógica del Honeypot solo se ejecuta si está activado if (!empty($this->options['enable_honeypot_blocking'])) { $honeypot_urls = get_option(self::OPTION_HONEYPOT_URLS, []); if (!empty($honeypot_urls)) { $requested_url = isset($_SERVER['REQUEST_URI']) ? strtolower(esc_url_raw(wp_unslash($_SERVER['REQUEST_URI']))) : ''; foreach ($honeypot_urls as $trap_url) { if (!empty($trap_url) && stripos($requested_url, strtolower(trim($trap_url))) !== false) { $reason = sprintf(__('Honeypot URL accessed: %s', 'advanced-ip-blocker'), $trap_url); $this->block_ip_instantly($ip, 'honeypot', $reason, ['url' => $requested_url]); } } } } $this->handle_error('404'); } elseif ($code === 403) { $this->error_handled_this_request = true; $ip = $this->get_client_ip(); $all_block_types = ['geoblock', 'honeypot', '404', '403', 'login', 'user_agent', 'waf', 'rate_limit', 'asn', 'xmlrpc_block']; 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 '

'; } } /** * 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( __( 'Access to this page has been restricted by the administrator.', 'advanced-ip-blocker' ), __( '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 ( defined( 'WP_CLI' ) && WP_CLI ) { return; } $ip = $this->get_client_ip(); if ($this->is_whitelisted($ip)) { return; } $automatic_block_reasons = ['geoblock', 'honeypot', '404', '403', 'login', 'user_agent', 'waf', 'rate_limit', 'asn', 'xmlrpc_block']; foreach ($automatic_block_reasons as $type) { if (get_transient('advaipbl_bloqueo_' . $type . '_' . md5($ip))) { $this->access_denied_page(__('403 - Access Denied', 'advanced-ip-blocker'), $this->get_block_message($type)); } } $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->access_denied_page( __('403 - Access Denied', 'advanced-ip-blocker'), $this->get_block_message('manual') ); } } } $is_xmlrpc_request = (isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], 'xmlrpc.php') !== false); if ($is_xmlrpc_request && ($this->options['xmlrpc_protection_mode'] ?? 'smart') === 'smart') { $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; $visitor_asn = false; if (in_array($this->options['geolocation_provider'] ?? '', ['ip-api.com', 'ipinfo.io'], true)) { $location_data = $this->api_manager->fetch_location($ip); if ($location_data && empty($location_data['error'])) { $visitor_asn = $this->asn_manager->extract_asn_from_data($location_data); } } if ($visitor_asn === 'AS2635') { return; } $automattic_ua_patterns = [ '/^WordPress\/\d+\.\d+/i', '/jetpack by wordpress\.com/i', '/woocommerce/i' ]; $third_party_allowed_ua = [ 'Microsoft-Live-Writer' ]; $ua_is_automattic = false; foreach ($automattic_ua_patterns as $pattern) { if (preg_match($pattern, $user_agent)) { $ua_is_automattic = true; break; } } if ($visitor_asn && $ua_is_automattic) { $reason = sprintf(__('Blocked spoofed XML-RPC request. IP from ASN %s claimed to be Automattic.', 'advanced-ip-blocker'), $visitor_asn); $log_data = ['user_agent' => $user_agent, 'uri' => $_SERVER['REQUEST_URI'] ?? 'N/A', 'asn' => $visitor_asn]; $this->block_ip_instantly($ip, 'xmlrpc_block', $reason, $log_data); } $is_third_party_allowed = false; foreach ($third_party_allowed_ua as $allowed_ua) { if (stripos($user_agent, $allowed_ua) !== false) { $is_third_party_allowed = true; break; } } if ($is_third_party_allowed) { return; } if (false === $visitor_asn && $ua_is_automattic) { return; } if (!$ua_is_automattic && !$is_third_party_allowed) { $reason = sprintf(__('Blocked untrusted XML-RPC request from User-Agent: %s', 'advanced-ip-blocker'), $user_agent); $log_data = ['user_agent' => $user_agent, 'uri' => $_SERVER['REQUEST_URI'] ?? 'N/A', 'asn' => $visitor_asn ?: 'API Failed/Unavailable']; $this->block_ip_instantly($ip, 'xmlrpc_block', $reason, $log_data); } return; } if ( ! empty( $this->options['enable_waf'] ) ) { $triggered_rule = $this->waf_manager->run_waf_scan(); if ( $triggered_rule !== false ) { $reason_message = sprintf(__('WAF Rule Triggered: %s', 'advanced-ip-blocker'), $triggered_rule); $log_data = ['rule' => $triggered_rule, 'uri' => $_SERVER['REQUEST_URI'] ?? 'N/A', 'method' => $_SERVER['REQUEST_METHOD'] ?? 'N/A']; $this->block_ip_instantly($ip, 'waf', $reason_message, $log_data); } } if (!empty($this->options['enable_geoblocking'])) { $blocked_countries = $this->options['geoblock_countries'] ?? []; if ( ! empty( $blocked_countries ) ) { $location = $this->api_manager->fetch_location( $ip ); if ( $location && ! empty( $location['country_code'] ) ) { if ( in_array( $location['country_code'], $blocked_countries, true ) ) { $reason = sprintf( __( 'Country block: %s', 'advanced-ip-blocker' ), $location['country'] ?? $location['country_code'] ); $log_data = [ 'country' => $location['country'] ?? '', 'uri' => $_SERVER['REQUEST_URI'] ?? 'N/A' ]; $this->block_ip_instantly( $ip, 'geoblock', $reason, $log_data ); } } } } 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)) { $this->asn_manager->check_asn_block($ip); } } if (!empty($this->options['enable_user_agent_blocking'])) { $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_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) { // Limpiamos la regla: quitamos el comentario y los espacios. $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) { // Limpiamos la regla: quitamos el comentario y los espacios. $blocked_ua = trim(preg_replace('/#.*$/', '', $blocked_ua_raw)); if (!empty($blocked_ua) && stripos($user_agent, $blocked_ua) !== false) { $reason_message = sprintf(__('Blocked User-Agent pattern: %s', 'advanced-ip-blocker'), $blocked_ua); $log_data = [ 'user_agent' => $user_agent, 'detail' => $reason_message, 'uri' => $_SERVER['REQUEST_URI'] ?? 'N/A' ]; $this->block_ip_instantly($ip, 'user_agent', $reason_message, $log_data); } } } } } } } public function get_all_blocked_ips_for_cli() { $all_blocked = []; // Mapeo de tipos técnicos a etiquetas legibles y constantes de opción. // Esto centraliza la configuración y es fácil de leer. $block_type_definitions = [ 'geoblock' => [ 'label' => __('Geoblock', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_GEO, ], 'honeypot' => [ 'label' => __('Honeypot', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_HONEYPOT, ], 'user_agent' => [ 'label' => __('User-Agent', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_USER_AGENT, ], 'manual' => [ 'label' => __('Manual Block', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_MANUAL, ], '404' => [ 'label' => __('404 Error', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_404, ], '403' => [ 'label' => __('403 Error', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_403, ], 'login' => [ 'label' => __('Login Failure', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_LOGIN, ], 'xmlrpc_block' => [ 'label' => __('XML-RPC Block', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_XMLRPC ], ]; // Iteramos sobre las definiciones de cada tipo de bloqueo. foreach ( $block_type_definitions as $type => $definition ) { $entries = get_option( $definition['option'], [] ); if ( ! is_array($entries) || empty($entries) ) { continue; } // Iteramos sobre cada IP/rango dentro de esa lista. foreach ( $entries as $ip_or_range => $block_data ) { $timestamp = is_array($block_data) && isset($block_data['timestamp']) ? $block_data['timestamp'] : time(); $detail = is_array($block_data) && isset($block_data['detail']) ? $block_data['detail'] : ''; $all_blocked[] = [ 'ip' => $ip_or_range, 'type' => $type, // El tipo técnico 'type_label' => $definition['label'], // La etiqueta legible 'timestamp' => $timestamp, // Timestamp numérico para ordenar 'detail' => $detail, ]; } } // Ordenar por timestamp descendente después de haber recopilado todo. if (!empty($all_blocked)) { usort($all_blocked, function($a, $b) { return $b['timestamp'] <=> $a['timestamp']; }); } return $all_blocked; } 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; } private function is_internal_request($user_agent) { return strpos($user_agent, 'WordPress/') === 0; } private function get_block_message($type) { $messages = [ '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'), ]; return $messages[$type] ?? __('Access blocked.', 'advanced-ip-blocker'); } public function block_ip_instantly($ip, $type, $reason_message, $extra_data = []) { if (get_transient('advaipbl_bloqueo_' . $type . '_' . md5($ip))) { return; } // --- Lógica de Bloqueo (Transients y Opciones Persistentes) --- $duration_key = 'duration_' . $type; $duration_in_minutes = (int) ($this->options[$duration_key] ?? 1440); $duration_in_seconds = $duration_in_minutes * 60; if ('manual' === $type || ('asn' === $type && $duration_in_minutes <= 0)) { $duration_in_seconds = 10 * YEAR_IN_SECONDS; } set_transient('advaipbl_bloqueo_' . $type . '_' . md5($ip), true, $duration_in_seconds); // Manejo de casos especiales donde el nombre de la constante no sigue el patrón. $option_constant_name = ''; if ('xmlrpc_block' === $type) { // Usamos el nombre de la constante correcta directamente. $option_constant_name = 'OPTION_BLOCKED_XMLRPC'; } elseif ('geoblock' === $type) { // Usamos el nombre de la constante correcta directamente. $option_constant_name = 'OPTION_BLOCKED_GEO'; } else { // Para todos los demás tipos, usamos la lógica dinámica que ya funcionaba. $option_constant_name = 'OPTION_BLOCKED_' . strtoupper($type); } if (defined("self::{$option_constant_name}")) { $option_key = constant("self::{$option_constant_name}"); $list = get_option($option_key, []); if (!is_array($list)) $list = []; $detail_to_store = $reason_message; if ($type === 'user_agent' && isset($extra_data['user_agent'])) { $detail_to_store = $extra_data['user_agent']; } elseif ($type === 'honeypot' && isset($extra_data['url'])) { $detail_to_store = $extra_data['url']; } $list[$ip] = ['timestamp' => time(), 'detail' => $detail_to_store]; update_option($option_key, $list); } // --- LÓGICA DE LOG UNIFICADO Y ENRIQUECIDO --- $location_data = $this->api_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; } // Añadimos la razón específica a los datos del log antes de pasarlos. $extra_data['_reason'] = $reason_message; $log_details = array_merge($extra_data, $geo_details); $this->log_specific_error($type, $ip, $log_details, 'critical'); // --- NOTIFICAR Y TERMINAR --- $this->send_block_notification($ip, $type, 1, $extra_data); $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); } delete_option('advaipbl_admin_ip_whitelist_trigger'); } } } public function schedule_cron_jobs() { if (!wp_next_scheduled('advaipbl_purge_old_logs_event')) { wp_schedule_event(time(), 'daily', 'advaipbl_purge_old_logs_event'); } $freq = $this->options['notification_frequency'] ?? 'disabled'; if (($freq === 'daily' || $freq === 'weekly') && !wp_next_scheduled('advaipbl_send_summary_email')) { $this->schedule_notification_cron(); } } 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() { if (get_option('ip_blocker_pro_db_version') != ADVAIPBL_DB_VERSION) { self::setup_database_tables(); } if (get_option('ip_blocker_pro_log_migration_status') !== 'complete') { $this->migrate_old_logs(); } $this->migrate_whitelist_format(); } private function migrate_old_logs() { global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_logs'; $logs_migrated = 0; $log_types_to_migrate = ['404', '403']; foreach ($log_types_to_migrate as $type) { $option_name = ADVAIPBL_PLUGIN_NAME . "_{$type}_errors_log"; $old_logs = get_option($option_name, []); if (empty($old_logs)) { delete_option($option_name); continue; } foreach ($old_logs as $log_entry) { if (!is_array($log_entry) || !isset($log_entry['ip']) || !isset($log_entry['timestamp'])) continue; $details = ['url' => $log_entry['url'] ?? '', 'method' => $log_entry['method'] ?? 'N/A', 'referrer' => $log_entry['referrer'] ?? 'N/A', 'user_agent' => $log_entry['user_agent'] ?? 'N/A']; /* translators: %s: The error type, e.g., 404 or 403. */ $message = sprintf(__("A %s error occurred (migrated).", 'advanced-ip-blocker'), $type); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery if ($wpdb->insert($table_name, ['timestamp' => date_i18n('Y-m-d H:i:s', $log_entry['timestamp']), 'ip' => $log_entry['ip'], 'log_type' => $type, 'level' => 'warning', 'message' => $message, 'details' => wp_json_encode($details)])) { $logs_migrated++; } } delete_option($option_name); } if ($logs_migrated > 0) { /* translators: %d: The number of migrated log records. */ $this->log_event(sprintf(__('%d old log records have been migrated to the new table.', 'advanced-ip-blocker'), $logs_migrated), 'info'); } update_option('ip_blocker_pro_log_migration_status', 'complete'); } /** * 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'); } public static function setup_database_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); $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); $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); update_option('ip_blocker_pro_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() { // 1. Inicializar la estructura de datos de retorno $result = [ 'visitor_ip' => null, 'visitor_ip_source' => null, 'proxy_chain' => [], 'is_proxied' => false, 'cdn_info' => [ 'provider' => 'None', 'ray_id' => null, 'country' => null, ], ]; // 2. Unificar todas las cabeceras disponibles en un solo array (insensible a mayúsculas) $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 para determinar la IP del visitante // Nivel 1: Cabeceras de Cloudflare (Máxima Confianza) if (isset($headers['cf-connecting-ip'])) { $result['visitor_ip'] = 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'; $result['cdn_info']['ray_id'] = $headers['cf-ray'] ?? null; $result['cdn_info']['country'] = $headers['cf-ipcountry'] ?? null; } // Nivel 2: Cabeceras de otros Proxies/CDNs conocidos (Alta Confianza) elseif (isset($headers['true-client-ip'])) { $result['visitor_ip'] = filter_var($headers['true-client-ip'], FILTER_VALIDATE_IP); $result['visitor_ip_source'] = 'Header: True-Client-IP'; $result['is_proxied'] = true; $result['cdn_info']['provider'] = 'Akamai (probable)'; } elseif (isset($headers['fastly-client-ip'])) { $result['visitor_ip'] = filter_var($headers['fastly-client-ip'], FILTER_VALIDATE_IP); $result['visitor_ip_source'] = 'Header: Fastly-Client-IP'; $result['is_proxied'] = true; $result['cdn_info']['provider'] = 'Fastly'; } elseif (isset($headers['x-real-ip'])) { // Común en Nginx y otros proxies $result['visitor_ip'] = filter_var($headers['x-real-ip'], FILTER_VALIDATE_IP); $result['visitor_ip_source'] = 'Header: X-Real-IP'; $result['is_proxied'] = true; $result['cdn_info']['provider'] = 'Proxy Inverso (Nginx/Otro)'; } elseif (isset($headers['x-cluster-client-ip'])) { // Común en Google Cloud $result['visitor_ip'] = filter_var($headers['x-cluster-client-ip'], FILTER_VALIDATE_IP); $result['visitor_ip_source'] = 'Header: X-Cluster-Client-IP'; $result['is_proxied'] = true; $result['cdn_info']['provider'] = 'Proxy de Cluster (Google Cloud?)'; } // Añadimos variaciones de X-Forwarded-For que a veces se usan. elseif (isset($headers['x-forwarded'])) { $forwarded_ips = array_map('trim', explode(',', $headers['x-forwarded'])); $result['visitor_ip'] = filter_var($forwarded_ips[0], FILTER_VALIDATE_IP); $result['visitor_ip_source'] = 'Header: X-Forwarded'; $result['is_proxied'] = true; } elseif (isset($headers['forwarded-for'])) { $forwarded_ips = array_map('trim', explode(',', $headers['forwarded-for'])); $result['visitor_ip'] = filter_var($forwarded_ips[0], FILTER_VALIDATE_IP); $result['visitor_ip_source'] = 'Header: Forwarded-For'; $result['is_proxied'] = true; } // Nivel 3: Cabecera estándar X-Forwarded-For (Confianza Media) elseif (isset($headers['x-forwarded-for'])) { // X-Forwarded-For puede ser una lista: "cliente, proxy1, proxy2" $forwarded_ips = array_map('trim', explode(',', $headers['x-forwarded-for'])); // La IP del cliente real es siempre la primera de la lista. $result['visitor_ip'] = filter_var($forwarded_ips[0], FILTER_VALIDATE_IP); $result['visitor_ip_source'] = 'Header: X-Forwarded-For'; $result['is_proxied'] = true; // El resto de IPs en la lista son los proxies intermedios. if (count($forwarded_ips) > 1) { $result['proxy_chain'] = array_slice($forwarded_ips, 1); } } // Nivel 4: Conexión Directa (Baja Confianza si se espera un proxy) if ($result['visitor_ip'] === null) { $result['visitor_ip'] = filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP); $result['visitor_ip_source'] = 'Direct Connection (REMOTE_ADDR)'; } // 4. Poblar la cadena de proxies // Si la petición fue a través de un proxy, REMOTE_ADDR contendrá la IP del último proxy. if ($result['is_proxied'] && isset($_SERVER['REMOTE_ADDR'])) { $last_proxy_ip = filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP); if ($last_proxy_ip && !in_array($last_proxy_ip, $result['proxy_chain'])) { // Añadir al final de la cadena de proxies. $result['proxy_chain'][] = $last_proxy_ip; } } 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' => $_SERVER['REQUEST_URI'] ?? '', 'method' => $_SERVER['REQUEST_METHOD'] ?? 'N/A', 'referrer' => $_SERVER['HTTP_REFERER'] ?? 'N/A', 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'N/A', ]; if (is_array($extra_data)) { $details = array_merge($details, $extra_data); } if (!isset($details['lat']) || !isset($details['lon'])) { $location_data = $this->api_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) { case 'login': $message = sprintf(__('Failed login attempt for user: %s', 'advanced-ip-blocker'), $details['username'] ?? __('unknown', 'advanced-ip-blocker')); break; case 'waf': $message = sprintf(__('Request blocked by WAF rule: %s', 'advanced-ip-blocker'), $details['rule'] ?? __('unknown rule', 'advanced-ip-blocker')); break; 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; case 'asn': $message = sprintf(__('Request blocked due to blacklisted ASN: %s', 'advanced-ip-blocker'), $details['asn_number'] ?? 'N/A'); break; 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; case 'honeypot': $message = sprintf(__('Honeypot URL accessed: %s', 'advanced-ip-blocker'), $details['url'] ?? 'N/A'); break; case 'geoblock': $message = sprintf(__('Blocked access from country: %s', 'advanced-ip-blocker'), $details['country'] ?? 'N/A'); break; case 'user_agent': $message = sprintf(__('Blocked due to User-Agent match: %s', 'advanced-ip-blocker'), $details['user_agent'] ?? 'N/A'); break; 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 = []) { $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']; 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; } } $this->log_specific_error($type, $ip, $extra_data); $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); // No se guarda en un transient, se pasa directamente a la cola de bloqueo. if ($errors >= $threshold) { // Obtenemos el detalle del evento actual, no de un transient antiguo. $trigger_detail = ($type === 'login') ? ($extra_data['username'] ?? 'N/A') : (isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : 'N/A'); self::$block_queue = [ 'ip' => $ip, 'type' => $type, 'trigger' => $trigger_detail, // Usamos el detalle actual. '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); } } /** * Versión simplificada para procesar el bloqueo en el shutdown. * Envía un email de texto plano sin usar funciones complejas de WP. */ public function execute_shutdown_block() { if (empty(self::$block_queue)) { return; } $q = self::$block_queue; $ip = $q['ip']; $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 ) { 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->api_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(__("Generated %1\$d %2\$s errors. Triggering URL: %3\$s", 'advanced-ip-blocker'), $errors, strtoupper($type), $trigger); break; case 'login': $reason = sprintf(__('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' ); $email_subject = sprintf(__('[%s] Security Alert: IP Automatically Blocked', 'advanced-ip-blocker'), $site_name); $template_title = __('Security Alert: IP Blocked', 'advanced-ip-blocker'); $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'])) { 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]); } // 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); } } public function add_cron_intervals($schedules) { $schedules['weekly'] = ['interval' => 604800, '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->api_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(__("Generated %d %s errors. Triggering URL: %s", 'advanced-ip-blocker'), $count, strtoupper($type), $extra_data['trigger'] ?? 'N/A'); break; case 'login': $reason = sprintf(__('Made %d failed login attempts for user: %s', 'advanced-ip-blocker'), $count, isset($extra_data['trigger']) ? sanitize_text_field($extra_data['trigger']) : __('unknown', 'advanced-ip-blocker')); break; case 'honeypot': $reason = sprintf(__('Accessed Honeypot URL: %s', 'advanced-ip-blocker'), $extra_data['url'] ?? 'N/A'); break; case 'user_agent': $reason = sprintf(__('Used a blocked User-Agent: %s', 'advanced-ip-blocker'), $extra_data['user_agent'] ?? 'N/A'); break; case 'geoblock': $reason = sprintf( __( 'Blocked due to country policy: %s', 'advanced-ip-blocker' ), $extra_data['country'] ?? 'Unknown Country' ); break; case 'waf': // Para WAF, la "razón" es el "detalle" del bloqueo, que es más informativo. $reason = $extra_data['detail'] ?? sprintf(__('WAF Rule Triggered: %s', 'advanced-ip-blocker'), $extra_data['rule'] ?? 'Unknown'); break; case 'rate_limit': $reason = sprintf(__('Rate limit exceeded: %d requests in %d seconds', 'advanced-ip-blocker'), $extra_data['count'] ?? 0, $extra_data['window'] ?? 0); break; case 'asn': $reason = sprintf(__('Blocked ASN: %s (%s) - Source: %s', 'advanced-ip-blocker'), $extra_data['asn_number'] ?? 'N/A', $extra_data['asn_name'] ?? 'Unknown', $extra_data['source'] ?? 'N/A'); break; case 'xmlrpc_block': $reason = sprintf(__('Blocked untrusted XML-RPC request from User-Agent: %s', 'advanced-ip-blocker'), $extra_data['user_agent'] ?? 'N/A'); 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('> • *IP Blocked:* `%s`', $ip); 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); // Usamos la razón detallada que ya construimos. $duration_minutes = (int) ($this->options['duration_' . $type] ?? 1440); $duration_text = ($type === 'manual' || $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'] ?? ($_SERVER['REQUEST_URI'] ?? ''); if (!empty($request_uri)) { $message_lines[] = sprintf('> • *URI:* `%s`', $request_uri); } if (!empty($_SERVER['HTTP_USER_AGENT'])) { $message_lines[] = sprintf('> • *User Agent:* `%s`', $_SERVER['HTTP_USER_AGENT']); } $mentions = $this->options['push_mentions'] ?? ''; $this->execute_webhook_send(implode("\n", $message_lines), $mentions); } } // --- PASO 3: Lógica de Email (ahora la variable $reason está 100% correcta) --- $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'); $email_subject = sprintf(__('[%s] Security Alert: IP Automatically Blocked', 'advanced-ip-blocker'), $site_name); $template_title = __('Security Alert: IP Blocked', 'advanced-ip-blocker'); $duration_minutes = (int) ($this->options['duration_' . $type] ?? 1440); $duration_text = ($type === 'manual' || $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 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' ] ); 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); $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 usando la nueva clave de opción. $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 y no hay una tarea programada, la programamos. if (!wp_next_scheduled('advaipbl_update_spamhaus_list_event')) { wp_schedule_event(time(), 'daily', 'advaipbl_update_spamhaus_list_event'); // Opcional: ejecutarla inmediatamente la primera vez. $this->update_spamhaus_list(); } } else { // Si se desactiva, limpiamos la tarea programada. wp_clear_scheduled_hook('advaipbl_update_spamhaus_list_event'); } } } /** * 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_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'), '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_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'), // 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', 'prevent_author_scanning', 'restrict_login_page', 'recaptcha_enable', 'enable_waf', 'rate_limiting_enable', 'delete_data_on_uninstall', 'disable_xmlrpc', 'show_admin_bar_menu' ]; 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) { $changed_fields_log[] = sprintf(__('%s: updated', 'advanced-ip-blocker'), $name); } continue; } if ($old !== $new) { if (strpos($key, '_key') !== false && !empty($new)) { $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'); $changed_fields_log[] = sprintf(__('%1$s: %2$s', 'advanced-ip-blocker'), $name, $status); } else { $changed_fields_log[] = sprintf(__('%1$s: set to "%2$s"', 'advanced-ip-blocker'), $name, esc_html($new)); } } } if (!empty($changed_fields_log)) { $message = sprintf( __('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 ) { // La variable ahora se llama $entry_to_unblock para mayor claridad. if ( ! $this->is_valid_ip_or_range( $entry_to_unblock ) ) { return; } $block_types = ['geoblock', '404', '403', 'login', 'manual', 'honeypot', 'user_agent', 'waf', 'rate_limit', 'asn', 'xmlrpc_block']; $is_single_ip = filter_var($entry_to_unblock, FILTER_VALIDATE_IP); foreach ($block_types as $type) { // Para IPs individuales, limpiamos transients y contadores de error. if ($is_single_ip) { 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)); delete_transient("advaipbl_detail_{$type}_" . md5($entry_to_unblock)); } } $key = 'advaipbl_ips_bloqueadas_' . $type; $list = get_option($key, []); if ( ! is_array($list) || empty($list) ) { continue; } $list_changed = false; if ($is_single_ip) { // Si es una IP individual, buscamos en qué rango está contenida. foreach ( array_keys($list) as $blocked_entry ) { if ( $this->is_ip_in_range( $entry_to_unblock, $blocked_entry ) ) { unset($list[$blocked_entry]); $list_changed = true; } } } else { // Si es un rango, buscamos una coincidencia exacta para eliminar. if ( array_key_exists($entry_to_unblock, $list) ) { unset($list[$entry_to_unblock]); $list_changed = true; } } if ( $list_changed ) { update_option($key, $list); } } // Limpiamos la IP (si es una IP individual) de la caché de ubicaciones. if ($is_single_ip) { $location_cache = get_transient(ADVAIPBL_USM_LOCATION_CACHE_KEY); if (is_array($location_cache) && isset($location_cache[$entry_to_unblock])) { unset($location_cache[$entry_to_unblock]); set_transient(ADVAIPBL_USM_LOCATION_CACHE_KEY, $location_cache, ADVAIPBL_USM_LOCATION_CACHE_TTL); } } $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 todas las listas de bloqueo y limpia todos los transients. * 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') { $block_type_definitions = $this->get_all_block_type_definitions(); // 1. Vaciar todas las listas de bloqueo persistentes en wp_options. foreach ($block_type_definitions as $type => $definition) { update_option($definition['option'], []); } // 2. Limpiar todos los transients de bloqueo de la base de datos. global $wpdb; $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE `option_name` LIKE %s OR `option_name` LIKE %s", $wpdb->esc_like('_transient_advaipbl_bloqueo_') . '%', $wpdb->esc_like('_transient_timeout_advaipbl_bloqueo_') . '%' ) ); // 3. Registrar el evento. $this->log_event(sprintf('All blocked IPs have been unblocked. Action via %s.', $source), 'critical'); } public function limpiar_ips_expiradas() { $block_types_with_duration = ['geoblock', '404', '403', 'login', 'honeypot', 'user_agent', 'waf', 'rate_limit', 'xmlrpc_block', 'asn']; $now = time(); $ips_to_remove_from_location_cache = []; // Array para guardar las IPs que expiran foreach ($block_types_with_duration as $type) { $duration_minutes = (int) ($this->options['duration_' . $type] ?? 1440); if ($duration_minutes <= 0) { continue; } $option_key = 'advaipbl_ips_bloqueadas_' . $type; $list = get_option($option_key, []); if ( ! is_array($list) || empty($list) ) continue; $duration_seconds = $duration_minutes * 60; $list_changed = false; foreach ($list as $ip_or_range => $block_data) { if ( ! filter_var($ip_or_range, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) ) { continue; } $timestamp = is_array($block_data) && isset($block_data['timestamp']) ? $block_data['timestamp'] : 0; if ($timestamp === 0) continue; if ( ($timestamp + $duration_seconds) < $now ) { $this->log_event( sprintf( __('IP %1$s automatically unblocked (expired %2$s).', 'advanced-ip-blocker'), $ip_or_range, $type ), 'info' ); unset($list[$ip_or_range]); $list_changed = true; delete_transient("advaipbl_bloqueo_{$type}_" . md5($ip_or_range)); // Guardamos la IP expirada para limpiarla de la caché de ubicaciones. $ips_to_remove_from_location_cache[] = $ip_or_range; } } if ( $list_changed ) { update_option($option_key, $list); } } // Después de procesar todas las listas, si hemos encontrado IPs expiradas, limpiamos la caché de ubicaciones. if (!empty($ips_to_remove_from_location_cache)) { $location_cache = get_transient(ADVAIPBL_USM_LOCATION_CACHE_KEY); if (is_array($location_cache)) { $cache_changed = false; foreach ($ips_to_remove_from_location_cache as $ip) { if (isset($location_cache[$ip])) { unset($location_cache[$ip]); $cache_changed = true; } } if ($cache_changed) { set_transient(ADVAIPBL_USM_LOCATION_CACHE_KEY, $location_cache, ADVAIPBL_USM_LOCATION_CACHE_TTL); } } } } /** * 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() { $block_types_with_duration = ['geoblock', '404', '403', 'login', 'honeypot', 'user_agent', 'waf', 'rate_limit', 'xmlrpc_block', 'asn']; $now = time(); foreach ($block_types_with_duration as $type) { $duration_minutes = (int) ($this->options['duration_' . $type] ?? 1440); // Si la duración es 0, es permanente, así que no resincronizamos un transient temporal. if ($duration_minutes <= 0) { continue; } if ( ! is_array($list) || empty($list) ) continue; $duration_seconds = (int) ($this->options['duration_' . $type] ?? 1440) * 60; foreach ($list as $ip_or_range => $block_data) { // Solo operamos sobre IPs individuales, ignoramos los rangos. if ( ! filter_var($ip_or_range, FILTER_VALIDATE_IP) ) { continue; } $timestamp = is_array($block_data) && isset($block_data['timestamp']) ? $block_data['timestamp'] : 0; if ($timestamp === 0) continue; $time_left = ($timestamp + $duration_seconds) - $now; if ($time_left > 0) { $transient_key = "advaipbl_bloqueo_{$type}_" . md5($ip_or_range); if ( false === get_transient($transient_key) ) { set_transient($transient_key, true, $time_left); } } } } } 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, // Protección de Login y Módulos 'disable_user_enumeration' => '1', 'prevent_author_scanning' => '1', 'restrict_login_page' => '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' => '1', 'push_mentions' => '', // APIs Externas 'geolocation_provider' => 'ip-api.com', 'api_key_ipapicom' => '', 'api_key_ipstackcom' => '', 'api_key_ipinfocom' => '', 'api_key_ip_apicom' => '', // Geobloqueo 'geoblock_countries' => [], // Ajustes de reCAPTCHA 'recaptcha_enable' => '0', 'recaptcha_version' => 'v3', 'recaptcha_site_key' => '', 'recaptcha_secret_key' => '', 'recaptcha_score_threshold' => 0.5, ]; } 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 (no cambia). self::setup_database_tables(); if ( ! wp_next_scheduled( 'advaipbl_purge_old_logs_event' ) ) { wp_schedule_event( time(), 'daily', 'advaipbl_purge_old_logs_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 (no cambia). 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 (no cambia). update_option( self::OPTION_MIGRATION_STATUS, 'pending' ); 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'); $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() { // Obtenemos el slug de la página actual. $current_page_slug = isset($_GET['page']) ? sanitize_key($_GET['page']) : ''; // Condición corregida: Usamos strpos() para ver si el slug de la página // COMIENZA CON el slug base de nuestro plugin. if (strpos($current_page_slug, 'advaipbl_settings_page') === 0) { // Estamos en una de nuestras páginas, así que eliminamos los avisos. remove_all_actions('admin_notices'); remove_all_actions('all_admin_notices'); // Volvemos a añadir nuestro propio hook de avisos para que se muestren // los mensajes de éxito/error de nuestro plugin (como "Settings saved."). add_action('admin_notices', [$this, 'display_admin_notice']); add_action('admin_notices', function() { settings_errors('advaipbl_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 (Versión Corregida con Slugs Únicos) // 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() { $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&sub-tab=general_settings'); ?>

%s. You can change this in the Settings tab.', 'advanced-ip-blocker'), ['strong' => [], 'a' => ['href' => []]] ), $is_enabled ? __('Enabled', 'advanced-ip-blocker') : __('Disabled', 'advanced-ip-blocker'), 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() { $nonce = $_REQUEST['_wpnonce'] ?? ''; $action = $_REQUEST['action'] ?? ''; $action_type = $_POST['action_type'] ?? ''; // Determinar qué nonce verificar $nonce_is_valid = false; if (!empty($action) && strpos($action, 'advaipbl_') === 0 && wp_verify_nonce($nonce, $action)) { $action_type = $action; // Usamos la acción GET como la principal $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'; // Prioridad 1: Acciones masivas if ($bulk_action !== '-1' && isset($_POST['ips_to_process'])) { $ips_to_process = array_map('sanitize_text_field', (array) $_POST['ips_to_process']); $processed_count = 0; if ($bulk_action === 'unblock') { 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) { $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'; } } // Prioridad 2: Otras acciones (de formularios y enlaces) } 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); $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)))); $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)))); $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_list': $asns_raw = isset($_POST['blocked_asns']) ? sanitize_textarea_field(wp_unslash($_POST['blocked_asns'])) : ''; $asns_lines = array_filter(array_map('trim', explode("\n", $asns_raw))); $valid_asns = []; foreach ($asns_lines as $line) { if (preg_match('/^AS\d+$/i', $line)) { $valid_asns[] = strtoupper($line); } } update_option(self::OPTION_BLOCKED_ASNS, array_unique($valid_asns)); $this->log_event(sprintf(__('ASN blocklist updated by %s.', 'advanced-ip-blocker'), $current_user_login), 'info'); $message = __('ASN blocklist saved successfully.', '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')); $query = $wpdb->prepare("DELETE FROM {$wpdb->prefix}advaipbl_logs WHERE log_type IN ({$placeholders})", $log_types_to_clear); $wpdb->query($query); $types_string = implode(', ', $log_types_to_clear); $this->log_event(sprintf(__('Logs of type(s) [%1$s] manually deleted by %2$s.', 'advanced-ip-blocker'), $types_string, $current_user_login), 'warning'); $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`"); $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) { // Usamos action_type de nuevo aquí case 'add_manual_block': $list = get_option( self::OPTION_BLOCKED_MANUAL, [] ); if ( ! array_key_exists( $ip_or_range, $list ) ) { $list[ $ip_or_range ] = [ 'timestamp' => time(), 'detail' => __( 'Manual Block', 'advanced-ip-blocker' ) ]; update_option( self::OPTION_BLOCKED_MANUAL, $list ); if (filter_var($ip_or_range, FILTER_VALIDATE_IP)) { set_transient( 'advaipbl_bloqueo_manual_' . md5( $ip_or_range ), true, 10 * YEAR_IN_SECONDS ); } $this->log_event( sprintf( __( 'IP %1$s manually blocked by %2$s.', 'advanced-ip-blocker' ), $ip_or_range, $current_user_login ), 'warning', $ip_or_range ); $message = sprintf( __( 'IP %s blocked.', 'advanced-ip-blocker' ), $ip_or_range ); } else { $message = sprintf( __( 'IP %s was already blocked.', 'advanced-ip-blocker' ), $ip_or_range ); $type = 'info'; } break; case 'remove_block': $this->desbloquear_ip($ip_or_range); $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 ) { $message = sprintf( __( 'Entry %s added to whitelist and unblocked.', 'advanced-ip-blocker' ), $ip_or_range ); } else { $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 ); $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 ); $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'), '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'), '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'), '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') ] ] ]; // ========================================================================= // 2. LÓGICA PARA DETERMINAR LA PESTAÑA ACTIVA // ========================================================================= $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 '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 '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 '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['enable_user_agent_blocking']); $settings_url = admin_url('admin.php?page=advaipbl_settings_page&tab=settings&sub-tab=general_settings'); ?>

%s. You can change this in the Settings tab.', 'advanced-ip-blocker'), ['strong' => [], 'a' => ['href' => []]] ), $is_enabled ? __('Enabled', 'advanced-ip-blocker') : __('Disabled', 'advanced-ip-blocker'), esc_url($settings_url) ); ?>


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

%s. You can change this in the Settings tab.', 'advanced-ip-blocker'), ['strong' => [], 'a' => ['href' => []]] ), $is_enabled ? __('Enabled', 'advanced-ip-blocker') : __('Disabled', 'advanced-ip-blocker'), esc_url($settings_url) ); ?>

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


get_all_blocked_entries(); $filter_type = isset($_GET['filter_type']) ? sanitize_key($_GET['filter_type']) : 'all'; $orderby = isset($_GET['orderby']) && in_array($_GET['orderby'], ['ip', 'type_label', 'timestamp', 'detail'], 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; $filtered_data = ('all' !== $filter_type) ? array_filter($all_blocked_data, function($block) use ($filter_type) { return trim((string)$block['type']) === trim((string)$filter_type); }) : $all_blocked_data; usort($filtered_data, function($a, $b) use ($orderby, $order) { $a_val = $a[$orderby] ?? ''; $b_val = $b[$orderby] ?? ''; $comparison = ($orderby === 'timestamp') ? ($a_val <=> $b_val) : strnatcasecmp((string)$a_val, (string)$b_val); return ($order === 'asc') ? $comparison : -$comparison; }); $total_items = count($filtered_data); $total_pages = ceil($total_items / $per_page); $items_for_page = array_slice($filtered_data, ($current_page - 1) * $per_page, $per_page); $all_ips_on_page = wp_list_pluck($items_for_page, 'ip'); $locations = $this->session_manager->get_cached_locations($all_ips_on_page); ?>

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', $orderby, $order); ?> print_sortable_header(__('Block Type', 'advanced-ip-blocker'), 'type_label', $orderby, $order); ?> print_sortable_header(__('Since', 'advanced-ip-blocker'), 'timestamp', $orderby, $order); ?>
options['duration_' . $type] ?? 1440); echo ($type === 'manual' || $duration_minutes <= 0) ? esc_html__('Permanent', 'advanced-ip-blocker') : sprintf(esc_html__('%d minutes', 'advanced-ip-blocker'), $duration_minutes); ?> '; } else { esc_html_e('Not available', 'advanced-ip-blocker'); } ?> 'advaipbl_unblock_ip', 'ip' => $block['ip'] ]), 'advaipbl_unblock_ip' ); ?>
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); ?>

$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() { // 1. Obtener y procesar los datos $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') ]; } } } // 2. Lógica de Ordenación // phpcs:disable WordPress.Security.NonceVerification.Recommended $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'; // phpcs:enable 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); }); // 3. Lógica de Paginación (ahora lee el parámetro de la URL) // phpcs:ignore WordPress.Security.NonceVerification.Recommended $current_page = isset($_GET['paged']) ? absint($_GET['paged']) : 1; // Leemos el valor de la URL, con un valor por defecto de 20. $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); // 4. Renderizado del HTML ?>

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', 'advanced-ip-blocker'), 'ip', $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); ?>
0 ? esc_html(self::get_formatted_datetime($item['timestamp'])) : '...'; ?>
display_log_table_generic($security_log_types); } /** * 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; } // 1. Obtener parámetros de la URL $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; // 2. Construir la consulta SQL 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); ?>
get_all_blocked_entries(); // Reutilizamos esta función foreach ($definitions as $def) { $labels[$def['type']] = $def['type_label']; } return $labels; } private function display_log_table_generic($log_types) { $is_unified_log = is_array($log_types); $current_filter = isset($_GET['filter_log_type']) ? sanitize_key($_GET['filter_log_type']) : 'all'; $title = ''; $current_sub_tab = ''; if ($is_unified_log) { $title = __('Security Log', 'advanced-ip-blocker'); $current_sub_tab = 'security_log'; } else { $titles = [ 'general' => __('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 = []; 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] ?? 1440); // Lógica de duración unificada: si es manual o la duración es 0, es permanente. if ($log_type === 'manual' || $duration_minutes <= 0) { $duration_text = __('Permanent', 'advanced-ip-blocker'); } else { $duration_text = sprintf(__('%d min', 'advanced-ip-blocker'), $duration_minutes); } echo '
' . esc_html__('Blocked:', 'advanced-ip-blocker') . ' ' . esc_html($duration_text); } ?>
' . implode(', ', $location_parts) . ''; } ?> ' . 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'); 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; default: // Para rate_limit, user_agent, xmlrpc_block y cualquier otro caso. $detail_display = esc_html($entry['message']); break; } // Si hay una URI, la añadimos como información secundaria. if (!empty($uri)) { $detail_display .= '
' . $uri . ''; } } echo $detail_display; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
options['duration_' . $log_type] ?? 1440); $duration_text = ''; if ($log_type === 'manual' || ($log_type === 'asn' && $duration_minutes <= 0)) { $duration_text = __('Permanent', 'advanced-ip-blocker'); } else { $duration_text = sprintf(__('%d min', 'advanced-ip-blocker'), $duration_minutes); } echo '
' . esc_html__('Blocked:', 'advanced-ip-blocker') . ' ' . esc_html($duration_text); } ?>
' . implode(', ', $location_parts) . ''; } ?> ' . esc_html__('User:', 'advanced-ip-blocker') . ' ' . esc_html($details['username']); } elseif ($log_type === 'waf' && !empty($details['rule'])) { $detail_display = '' . esc_html__('Rule:', 'advanced-ip-blocker') . ' ' . esc_html($details['rule']); } elseif (in_array($log_type, ['404', '403', 'honeypot']) && !empty($details['url'])) { $detail_display = esc_html($details['url']); } elseif ($log_type === 'xmlrpc_block') { $detail_display = esc_html__('Untrusted XML-RPC Request', 'advanced-ip-blocker'); } elseif ($log_type === 'asn' && !empty($details['asn_number'])) { $detail_display = esc_html($details['asn_number']) . ' (' . esc_html($details['asn_name'] ?? '') . ')'; if (!empty($details['source'])) { $detail_display .= ' - ' . __('Source:', 'advanced-ip-blocker') . ' ' . esc_html($details['source']); } } // Ahora, añadimos la URI si existe para los tipos relevantes if (in_array($log_type, ['waf', 'xmlrpc_block', 'user_agent', 'rate_limit', 'asn']) && !empty($details['uri'])) { // Para estos tipos, la URI es información secundaria y va en una nueva línea pequeña. $detail_display .= '
' . esc_html($details['uri']) . ''; } elseif (in_array($log_type, ['404', '403', 'honeypot'])) { // Para estos, la URI es el detalle principal y no lleva etiquetas extra. $detail_display = esc_html($details['url'] ?? ''); } echo $detail_display; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
[ 'label' => __('Geoblock', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_GEO ], 'honeypot' => [ 'label' => __('Honeypot', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_HONEYPOT ], 'user_agent' => [ 'label' => __('User-Agent', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_USER_AGENT ], 'manual' => [ 'label' => __('Manual Block', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_MANUAL ], '404' => [ 'label' => __('404 Error', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_404 ], '403' => [ 'label' => __('403 Error', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_403 ], 'login' => [ 'label' => __('Login Failure', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_LOGIN ], 'waf' => [ 'label' => __('WAF Block', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_WAF ], 'rate_limit' => [ 'label' => __('Rate Limit', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_RATE_LIMIT ], 'asn' => [ 'label' => __('ASN Block', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_ASN ], 'xmlrpc_block' => [ 'label' => __('XML-RPC Block', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_XMLRPC ], ]; } public function get_all_blocked_entries() { $all_blocked = []; $block_type_definitions = [ 'geoblock' => [ 'label' => __('Geoblock', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_GEO ], 'honeypot' => [ 'label' => __('Honeypot', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_HONEYPOT ], 'user_agent' => [ 'label' => __('User-Agent', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_USER_AGENT ], 'manual' => [ 'label' => __('Manual Block', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_MANUAL ], '404' => [ 'label' => __('404 Error', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_404 ], '403' => [ 'label' => __('403 Error', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_403 ], 'login' => [ 'label' => __('Login Failure', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_LOGIN ], 'waf' => [ 'label' => __('WAF Block', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_WAF ], 'rate_limit' => [ 'label' => __('Rate Limit', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_RATE_LIMIT ], 'asn' => [ 'label' => __('ASN Block', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_ASN ], 'xmlrpc_block' => [ 'label' => __('XML-RPC Block', 'advanced-ip-blocker'), 'option' => self::OPTION_BLOCKED_XMLRPC ], ]; foreach ( $block_type_definitions as $type => $definition ) { $option_key = $definition['option']; // Forzamos la eliminación de esta opción de la caché de objetos de WP. // Esto garantiza que la siguiente llamada a get_option() leerá el valor // fresco directamente de la base de datos, evitando datos obsoletos. wp_cache_delete($option_key, 'options'); $entries = get_option( $option_key, [] ); if ( ! is_array($entries) || empty($entries) ) continue; foreach ( $entries as $ip_or_range => $block_data ) { $timestamp = is_array($block_data) && isset($block_data['timestamp']) ? $block_data['timestamp'] : time(); $detail = is_array($block_data) && isset($block_data['detail']) ? $block_data['detail'] : ''; $all_blocked[] = [ 'ip' => $ip_or_range, 'type' => $type, 'type_label' => $definition['label'], 'timestamp' => $timestamp, 'detail' => $detail, ]; } } 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


# 

wp help advaipbl

# ---  ---

wp advaipbl block  [--reason=]
wp advaipbl unblock 

# ---  ---

wp advaipbl whitelist  []

# ---  ---

wp advaipbl waf  [] [--from-file=]
wp advaipbl waf-enable
wp advaipbl waf-disable
wp advaipbl xmlrpc-mode 

# ---  ---

wp advaipbl geoblock  
wp advaipbl asn  
wp advaipbl spamhaus-enable
wp advaipbl spamhaus-disable
wp advaipbl provider  [] []

# ---  ---

wp advaipbl session  [--role=] [--user-id=]

# ---  ---

wp advaipbl blocked [--type=] [--format=]
wp advaipbl log  [--type=] [--count=]

# ---  ---

wp advaipbl config get []
wp advaipbl config set  

'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, 'checkbox_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, 'checkbox_field_callback' ], $page, 'advaipbl_general_settings_section', ['name' => 'show_admin_bar_menu', 'label' => __( 'Show security menu in the WordPress admin bar', '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, 'checkbox_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, 'checkbox_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, 'checkbox_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' ); add_settings_section('advaipbl_api_settings_section', null, null, $page); add_settings_field('advaipbl_geolocation_provider', __('Geolocation Provider', 'advanced-ip-blocker'), [$this, 'geolocation_provider_callback'], $page, 'advaipbl_api_settings_section'); add_settings_field('advaipbl_api_key_ip_apicom', 'ip-api.com ' . __('API Key', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_api_settings_section', ['name' => 'api_key_ip_apicom', 'description' => __('Optional key for HTTPS access.', 'advanced-ip-blocker'), 'class' => 'api-key-field', '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_api_settings_section', ['name' => 'api_key_ipinfocom', 'description' => __('API key is recommended for higher limits.', 'advanced-ip-blocker'), 'class' => 'api-key-field', 'data_provider' => 'ipinfo.io']); add_settings_field('advaipbl_api_key_ipapicom', 'ipapi.com ' . __('API Key', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_api_settings_section', ['name' => 'api_key_ipapicom', 'description' => __('API Key is required.', 'advanced-ip-blocker'), 'class' => 'api-key-field', 'data_provider' => 'ipapi.com']); add_settings_field('advaipbl_api_key_ipstackcom', 'ipstack.com ' . __('API Key', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_api_settings_section', ['name' => 'api_key_ipstackcom', 'description' => __('API Key is required.', 'advanced-ip-blocker'), 'class' => 'api-key-field', 'data_provider' => 'ipstack.com']); add_settings_field('advaipbl_clear_location_cache_button', __('Geolocation Cache', 'advanced-ip-blocker'), [$this, 'clear_cache_button_callback'], $page, 'advaipbl_api_settings_section'); add_settings_section('advaipbl_geoblocking_settings_section', null, null, $page ); add_settings_field('advaipbl_enable_geoblocking', __('Enable Geoblocking', 'advanced-ip-blocker'), [$this, 'checkbox_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, 'checkbox_field_callback'], $page, 'advaipbl_honeypot_settings_section', ['name' => 'enable_honeypot_blocking', 'label' => __('Activate Honeypot trap protection.', 'advanced-ip-blocker'), 'description' => sprintf(__('Manage Honeypot URLs in the Blocking Rules tab.', 'advanced-ip-blocker'), admin_url('admin.php?page=advaipbl_settings_page-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, 'checkbox_field_callback'], $page, 'advaipbl_user_agent_settings_section', ['name' => 'enable_user_agent_blocking', 'label' => __('Activate User-Agent based blocking.', 'advanced-ip-blocker'), 'description' => sprintf(__('Manage User-Agent lists in the Blocking Rules tab.', 'advanced-ip-blocker'), admin_url('admin.php?page=advaipbl_settings_page-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, 'checkbox_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, 'checkbox_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(__('Manage your custom list in the Blocking Rules tab.', 'advanced-ip-blocker'), admin_url('admin.php?page=advaipbl_settings_page-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, 'checkbox_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(__('Manage WAF rules in the Blocking Rules tab.', 'advanced-ip-blocker'), admin_url('admin.php?page=advaipbl_settings_page-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, 'checkbox_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_disable_user_enumeration', __( 'REST API User Protection', 'advanced-ip-blocker' ), [ $this, 'checkbox_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, 'checkbox_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_restrict_login_page', __( 'Whitelist Login Access', 'advanced-ip-blocker' ), [ $this, 'restrict_login_page_callback' ], $page, 'advaipbl_advanced_login_section'); add_settings_field('advaipbl_xmlrpc_protection_mode', __('XML-RPC Protection Mode', 'advanced-ip-blocker'), [$this, 'xmlrpc_protection_mode_callback'], $page, 'advaipbl_advanced_login_section'); add_settings_field('advaipbl_duration_xmlrpc_block', __('XML-RPC Block Duration (min)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_advanced_login_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')]); 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'); 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, 'checkbox_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')]); } /** * 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->api_manager->set_transient_api_key($provider, $api_key); $result = $this->api_manager->fetch_location('8.8.8.8'); $this->api_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 Api_Manager. $error_message = $result['error_message'] ?? __('Invalid API Key or connection error.', 'advanced-ip-blocker'); wp_send_json_error(['message' => $error_message]); } } 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' ]; 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', ]; foreach ($checkbox_fields as $field) { $new_input[$field] = (isset($input[$field]) && $input[$field] === '1') ? '1' : '0'; } $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); // Sanitizamos el nuevo campo de menciones. Permitimos @, letras, números, guiones y saltos de línea. if (isset($input['push_mentions'])) { // Usamos sanitize_textarea_field que es suficientemente seguro para este propósito. $new_input['push_mentions'] = sanitize_textarea_field($input['push_mentions']); } else { $new_input['push_mentions'] = ''; } $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']; 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'] = []; } $new_input['recaptcha_enable'] = (isset($input['recaptcha_enable']) && $input['recaptcha_enable'] === '1') ? '1' : '0'; $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'; 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'] ) . '"' : ''; // Comprobamos si es un campo de API Key if ( strpos($args['name'], 'api_key_') === 0 ) { printf( ' ', esc_attr( $args['name'] ), esc_attr( 'advaipbl_settings[' . $args['name'] . ']' ), esc_attr( $value ), esc_attr( $class ), $data_attr, esc_attr( $args['data_provider'] ), esc_html__( 'Verify', 'advanced-ip-blocker' ) ); } else { // Lógica para campos de texto normales (números, etc.) $type = ( strpos($args['name'], 'duration') !== false || strpos($args['name'], 'threshold') !== false || strpos($args['name'], 'retention') !== false ) ? '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), $min_attr, esc_attr($class) ); } if ( isset($args['description']) ) { // Usamos wp_kses_post para permitir etiquetas HTML seguras como echo '

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

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

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

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

'; } public function geolocation_provider_callback() { $value = $this->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_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] = ''; } } } $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' ); $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'; $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'; $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' ); $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' ); $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; } }