options = get_option('advaipbl_settings', []); $this->session_manager = new ADVAIPBL_User_Session_Manager($this); $this->add_hooks(); } private function add_hooks() { 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('wp_login_failed', [$this, 'registrar_intento_login_fallido']); add_action('advaipbl_purge_old_logs_event', [$this, 'purge_old_logs']); add_filter('cron_schedules', [$this, 'add_cron_intervals']); add_action('advaipbl_send_summary_email', [$this, 'process_and_send_summary']); 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' ] ); if (is_admin()) { add_action('admin_init', [$this, 'check_database_update']); add_action('admin_init', [$this, 'schedule_cron_jobs']); add_action('admin_menu', [$this, 'admin_menu']); add_action('admin_init', [$this, 'register_settings']); add_action('admin_init', [$this, 'handle_admin_actions']); add_action('admin_init', [$this, 'limpiar_ips_expiradas']); add_action('update_option_' . 'advaipbl_settings', [$this, 'on_settings_update'], 10, 2); add_action('admin_notices', [$this, 'display_admin_notice']); add_action('admin_enqueue_scripts', [$this, 'load_admin_scripts']); } } 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); $this->log_event(__('User session location cache manually cleared.', 'advanced-ip-blocker'), 'info'); // Creamos un aviso para el usuario. set_transient('advaipbl_admin_notice', ['message' => __('Location cache has been cleared.', 'advanced-ip-blocker'), 'type' => 'success'], 30); // Redirigimos de vuelta a la página de ajustes. wp_safe_redirect(wp_get_referer()); exit; } /** * Handles the request to send a test email from the settings page. */ public function handle_send_test_email() { // 1. Security check: Nonce and user permissions. if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'advaipbl_send_test_email_nonce' ) ) { wp_die( 'Invalid nonce.' ); } if ( ! current_user_can( 'manage_options' ) ) { wp_die( 'Permission denied.' ); } // 2. Determine a quién enviar el correo. $to = ! empty( $this->options['notification_email'] ) && is_email( $this->options['notification_email'] ) ? $this->options['notification_email'] : get_option( 'admin_email' ); // 3. Preparar el contenido del correo. $site_name = get_bloginfo( 'name' ); /* translators: %s: The name of the website. */ $subject = sprintf( __( '[%s] Test Email from Advanced IP Blocker', 'advanced-ip-blocker' ), $site_name ); $body_lines = [ __( 'Hello,', 'advanced-ip-blocker' ), '', __( 'This is a test email to confirm that your WordPress site is able to send notifications from the Advanced IP Blocker plugin.', 'advanced-ip-blocker' ), '', __( 'If you received this, your email settings are working correctly!', 'advanced-ip-blocker' ), '', '--', /* translators: 1: Plugin name, 2: Plugin version. */ sprintf( __( 'Sent by %1$s v%2$s', 'advanced-ip-blocker' ), 'Advanced IP Blocker', ADVAIPBL_VERSION ), ]; $body = implode( "\n", $body_lines ); // 4. Enviar el correo. $sent = wp_mail( $to, $subject, $body ); // 5. Crear una notificación para el administrador y redirigir. if ( $sent ) { /* translators: %s: The email address the test email was sent to. */ $message = sprintf( __( 'Test email sent successfully to %s.', 'advanced-ip-blocker' ), $to ); $type = 'success'; $this->log_event( $message, 'info' ); } else { /* translators: %s: The email address the test email failed to send to. */ $message = sprintf( __( 'Failed to send test email to %s. Please check your site\'s email configuration or contact your hosting provider.', 'advanced-ip-blocker' ), $to ); $type = 'error'; $this->log_event( $message, 'error' ); } set_transient( 'advaipbl_admin_notice', [ 'message' => $message, 'type' => $type ], 30 ); wp_safe_redirect( wp_get_referer() ); exit; } public function load_admin_scripts($hook) { // Solo cargamos los scripts en nuestra página de ajustes. if ( 'settings_page_advaipbl_settings_page' !== $hook ) { return; } // El encolado de scripts del Session Manager, que ya tenías. $this->session_manager->enqueue_scripts_styles(); // Encolar los assets para el selector de países (Select2) wp_enqueue_style( 'advaipbl-select2-css', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/css/select2.min.css', [], '4.1.0-rc.0' // O la versión que hayas descargado ); 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' ], // Dependencia para que se cargue después ADVAIPBL_VERSION, true ); // 1. Obtener datos del servidor. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $server_ip = $this->get_server_ip(); $server_info = [ 'ip' => $server_ip, 'country_code' => '', 'country_name' => '', 'is_whitelisted' => false ]; // La comprobación ahora es más simple, porque get_server_ip() ya devuelve una IP válida o null. if ( $server_ip ) { // Usamos el Api_Manager para obtener la localización. if ( ! isset( $this->api_manager ) ) { $this->api_manager = new ADVAIPBL_Api_Manager(); } $location = $this->api_manager->fetch_location( $server_ip ); if ( $location ) { $server_info['country_code'] = $location['country_code'] ?? ''; $server_info['country_name'] = $location['country'] ?? ''; } // Comprobamos si la IP del servidor está en la whitelist. $server_info['is_whitelisted'] = $this->is_whitelisted( $server_ip ); } // 2. Obtener datos del administrador 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 ) ) { if ( ! isset( $this->api_manager ) ) { $this->api_manager = new ADVAIPBL_Api_Manager(); } $location = $this->api_manager->fetch_location( $admin_ip ); if ( $location ) { $admin_info['country_code'] = $location['country_code'] ?? ''; $admin_info['country_name'] = $location['country'] ?? ''; } // Comprobamos si la IP del admin está en la whitelist. $admin_info['is_whitelisted'] = $this->is_whitelisted( $admin_ip ); } // 3. Pasar todos los datos y los textos pre-traducidos al script. wp_localize_script( 'advaipbl-admin-js', 'advaipbl_geoblock_data', [ 'server' => $server_info, 'admin' => $admin_info, 'blocked_countries' => $this->options['geoblock_countries'] ?? [], 'text' => [ /* translators: %1$s: Server's Country Name, %2$s: Server's IP Address. */ 'server_whitelisted' => __( 'Info: Your server is located in %1$s and its IP (%2$s) is correctly whitelisted. You can safely block this country.', 'advanced-ip-blocker' ), /* translators: %1$s: Server's Country Name, %2$s: Server's IP Address. */ 'server_not_whitelisted' => __( 'CRITICAL WARNING! Your server is located in %1$s and its IP (%2$s) is NOT whitelisted. Blocking this country WILL likely break your site. Please add the server IP to the whitelist BEFORE blocking this country.', 'advanced-ip-blocker' ), /* translators: %1$s: Admin's IP Address, %2$s: Admin's Country Name. */ 'admin_whitelisted' => __( 'Info: Your current IP address (%1$s) from %2$s is whitelisted. You will not be locked out if you block this country.', 'advanced-ip-blocker' ), /* translators: %1$s: Admin's IP Address, %2$s: Admin's Country Name. */ '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' ), ], ] ); // Encolar el script de Stripe si estamos en la pestaña de créditos. // Lo movemos aquí para centralizar todo el encolado de la página. // phpcs:ignore WordPress.Security.NonceVerification.Recommended $current_tab = isset($_GET['tab']) ? sanitize_key($_GET['tab']) : 'general'; if ( 'credits' === $current_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))); // Saneamos la variable $_SERVER. $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; $honeypot_urls = get_option('advaipbl_honeypot_urls', []); if (!empty($honeypot_urls)) { // Saneamos la variable $_SERVER. $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) { /* translators: %s: The specific honeypot URL that was accessed. */ $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; $this->handle_error('403'); } } public function run_all_block_checks() { $ip = $this->get_client_ip(); // 1. La lista blanca tiene prioridad absoluta. if ($this->is_whitelisted($ip)) { return; } // 2. Comprobamos si la IP ya está bloqueada por CUALQUIER motivo. // Esto es mucho más rápido que una llamada a la API. $block_reasons = ['geoblock', 'honeypot', 'manual', '404', '403', 'login', 'user_agent']; foreach ($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) ); } } // 3. Comprobación de Geobloqueo (solo para IPs nuevas). // Se ejecuta solo si la IP no está ya en una lista de bloqueo. $blocked_countries = $this->options['geoblock_countries'] ?? []; if ( ! empty( $blocked_countries ) ) { // Nota: El Api_Manager ya debería estar instanciado, pero si no, asegúrate de que exista. // En tu código, se instancia en el constructor de ADVAIPBL_User_Session_Manager, lo cual es un poco indirecto. // Para mayor robustez, nos aseguramos de que exista. if ( ! isset( $this->api_manager ) ) { $this->api_manager = new ADVAIPBL_Api_Manager(); } $location = $this->api_manager->fetch_location( $ip ); // Llamada a la API if ( $location && ! empty( $location['country_code'] ) ) { if ( in_array( $location['country_code'], $blocked_countries, true ) ) { /* translators: %s: The name of the blocked country. */ $reason = sprintf( __( 'Country block: %s', 'advanced-ip-blocker' ), $location['country'] ?? $location['country_code'] ); // Usamos la función de bloqueo instantáneo, pero con un nuevo tipo 'geoblock'. $this->block_ip_instantly( $ip, 'geoblock', $reason, [ 'country' => $location['country'] ] ); // block_ip_instantly() termina la ejecución. } } } // 4. Lógica original de User-Agent. $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) { if (!empty($whitelisted_ua) && stripos($user_agent, trim($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) { if (!empty($blocked_ua) && stripos($user_agent, trim($blocked_ua)) !== false) { /* translators: %s: The User-Agent pattern that was blocked. */ $reason_message = sprintf(__('Blocked User-Agent pattern: %s', 'advanced-ip-blocker'), $blocked_ua); $this->block_ip_instantly($ip, 'user_agent', $reason_message, ['user_agent' => $user_agent]); } } } } } } public function get_all_blocked_ips_for_cli() { $all_blocked = []; $block_types = [ 'geoblock' => __('Geoblock', 'advanced-ip-blocker'), 'honeypot' => __('Honeypot', 'advanced-ip-blocker'), 'user_agent' => __('User-Agent', 'advanced-ip-blocker'), 'manual' => __('Manual', 'advanced-ip-blocker'), '404' => __('404 Error', 'advanced-ip-blocker'), '403' => __('403 Error', 'advanced-ip-blocker'), 'login' => __('Login Failure', 'advanced-ip-blocker'), ]; foreach ( $block_types as $type => $label ) { $key = 'advaipbl_ips_bloqueadas_' . $type; $ips = get_option( $key, [] ); if ( ! is_array($ips) ) continue; foreach ( $ips as $ip => $block_data ) { $timestamp = is_array($block_data) ? ($block_data['timestamp'] ?? time()) : $block_data; $detail = is_array($block_data) ? ($block_data['detail'] ?? '') : ''; $all_blocked[] = [ 'ip' => $ip, 'type' => $type, 'type_label' => $label, 'timestamp' => self::get_formatted_datetime($timestamp), 'detail' => $detail, ]; } } usort($all_blocked, function($a, $b) { return strtotime($b['timestamp']) <=> strtotime($a['timestamp']); }); return $all_blocked; } private function is_whitelisted($ip) { $whitelist = get_option('advaipbl_ips_whitelist', []); return array_key_exists($ip, $whitelist); } 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'), // <<< LÍNEA NUEVA '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') ]; return $messages[$type] ?? __('Access blocked.', 'advanced-ip-blocker'); } private function block_ip_instantly($ip, $type, $reason_message, $extra_data = []) { $block_key = 'advaipbl_bloqueo_' . $type . '_' . md5($ip); if (get_transient($block_key)) return; $duration_key = 'duration_' . $type; // Para geoblock y otros bloqueos instantáneos, podemos definir una duración muy larga o usar un valor por defecto. $duration = (int) ($this->options[$duration_key] ?? 1440) * 60; // 1 día por defecto // Si es geoblock, el bloqueo es "permanente" (un transient muy largo) if ( $type === 'geoblock' ) { $duration = 10 * YEAR_IN_SECONDS; } $option_key = 'advaipbl_ips_bloqueadas_' . $type; set_transient($block_key, true, $duration); $list = get_option($option_key, []); // =================== LÓGICA CORREGIDA =================== $detail_to_store = ''; 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']; } else { // Fallback: usamos el mensaje de la razón como el detalle. // Esto funciona perfectamente para Geoblock y cualquier futuro tipo. $detail_to_store = $reason_message; } $block_details = [ 'timestamp' => time(), 'detail' => $detail_to_store, ]; // ======================================================== $list[$ip] = $block_details; update_option($option_key, $list); /* translators: 1: The blocked IP address, 2: The reason for the block. */ $log_message = sprintf(__('IP %1$s instantly blocked. Reason: %2$s', 'advanced-ip-blocker'), $ip, $reason_message); $this->log_event($log_message, 'critical', $extra_data); $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 usando el nuevo método robusto. $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('advaipbl_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); } } public function get_client_ip() { if ( null !== $this->client_ip ) { return $this->client_ip; } // Lista de cabeceras en orden de prioridad (de la más fiable a la menos fiable). $headers_to_check = [ 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'HTTP_X_REAL_IP', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR' // Siempre al final como último recurso. ]; foreach ( $headers_to_check as $header ) { if ( isset( $_SERVER[ $header ] ) ) { // Las cabeceras como X-Forwarded-For pueden contener múltiples IPs (proxy1, proxy2, cliente). // La IP del cliente real suele ser la primera de la lista. $ips = explode( ',', $_SERVER[ $header ] ); $ip = trim( reset( $ips ) ); // Obtenemos la primera IP. // Validamos que es una IP pública válida. if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) { $this->client_ip = $ip; return $this->client_ip; } } } // Si todo falla, usamos REMOTE_ADDR incluso si es una IP privada (mejor que nada). $this->client_ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; return $this->client_ip; } /** * 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, ] ); } private function log_specific_error($type, $ip) { if (empty($this->options['enable_logging'])) { return; } global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_logs'; $details = [ 'url' => isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : '', 'method' => isset($_SERVER['REQUEST_METHOD']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_METHOD'])) : 'N/A', 'referrer' => isset($_SERVER['HTTP_REFERER']) ? esc_url_raw(wp_unslash($_SERVER['HTTP_REFERER'])) : 'N/A', 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : 'N/A', ]; /* translators: %s: The error type, e.g., 404 or 403. */ $message = sprintf(__("A %s error occurred.", 'advanced-ip-blocker'), $type); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery @$wpdb->insert( $table_name, [ 'timestamp' => current_time('mysql', 1), 'ip' => $ip, 'log_type' => $type, 'level' => 'warning', 'message' => $message, 'details' => wp_json_encode($details) ] ); } 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; } $block_reasons = ['honeypot', 'manual', '404', '403', 'login', 'user_agent']; foreach ($block_reasons 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); $detail_key = "advaipbl_detail_{$type}_" . md5($ip); $errors = (int) get_transient($count_key) + 1; if ($errors === 1) { $detail_to_store = ($type === 'login') ? ($extra_data['username'] ?? '') : (isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : ''); set_transient($detail_key, $detail_to_store, $window); } set_transient($count_key, $errors, $window); if ($errors >= $threshold) { $trigger_detail = get_transient($detail_key) ?: 'N/A'; self::$block_queue = [ 'ip' => $ip, 'type' => $type, 'trigger' => $trigger_detail, 'to' => !empty($this->options['notification_email']) && is_email($this->options['notification_email']) ? $this->options['notification_email'] : get_option('admin_email'), 'options' => $this->options, 'errors' => $errors, ]; if (!self::$shutdown_hook_registered) { register_shutdown_function([$this, 'execute_shutdown_block']); self::$shutdown_hook_registered = true; } delete_transient($count_key); delete_transient($detail_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'] : (isset($q['count']) ? $q['count'] : 1); // 1. Acciones de bloqueo $duration = (int) ($options["duration_{$type}"] ?? 120) * 60; $block_key = "advaipbl_bloqueo_{$type}_" . md5($ip); $option_key = "advaipbl_ips_bloqueadas_{$type}"; 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); // 2. Lógica de email $notification_enabled = !empty($options['enable_email_notifications']) && '1' === $options['enable_email_notifications']; $frequency = $options['notification_frequency'] ?? 'disabled'; $email_sent = null; if ($notification_enabled && $frequency === 'instant') { $site_host = wp_parse_url(home_url(), PHP_URL_HOST) ?: 'Your Website'; /* translators: %s: Site name. */ $subject = sprintf(__('[%s] Security Alert: IP Automatically Blocked', 'advanced-ip-blocker'), $site_host); $duration_minutes = (int) ($options["duration_{$type}"] ?? 120); $reason_text = "Type: " . strtoupper($type) . ", Trigger: " . $trigger; if($errors > 1) { $reason_text .= ", Errors: " . $errors; } $body_lines = [ __("Hello,", 'advanced-ip-blocker'), "", /* translators: %s: Site name. */ sprintf(__("An IP address has been automatically blocked on your website '%s'.", 'advanced-ip-blocker'), $site_host), "", __("Block Details:", 'advanced-ip-blocker'), "-------------------------------------", /* translators: %s: IP Address. */ sprintf(__("Blocked IP: %s", 'advanced-ip-blocker'), $ip), /* translators: %s: The date and time of the block. */ sprintf(__("Date and Time: %s", 'advanced-ip-blocker'), self::get_formatted_datetime(time())), /* translators: %s: Reason for the block. */ sprintf(__("Reason: %s", 'advanced-ip-blocker'), $reason_text), /* translators: %d: Duration in minutes. */ sprintf(__("Block Duration: %d minutes", 'advanced-ip-blocker'), $duration_minutes), "-------------------------------------", "", __("No action is required on your part.", 'advanced-ip-blocker'), "", "--", /* translators: 1: Plugin name, 2: Plugin version. */ sprintf(__('Sent by %1$s v%2$s', 'advanced-ip-blocker'), 'Advanced IP Blocker', ADVAIPBL_VERSION) ]; $body = implode("\n", $body_lines); $email_sent = wp_mail($to, $subject, $body); } elseif ($notification_enabled && in_array($frequency, ['daily', 'weekly'])) { global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_notifications_queue'; $reason_for_summary = sprintf( /* translators: 1: Number of errors, 2: Error type (e.g., 404), 3: Triggering URL. */ __('Generated %1$d %2$s errors. Triggering URL: %3$s', 'advanced-ip-blocker'), $errors, strtoupper($type), $trigger ); // phpcs:ignore WordPress.WP.I18n.InterpolatedVariableText.Found, WordPress.DB.DirectDatabaseQuery.DirectQuery @$wpdb->insert( $table_name, ['timestamp' => current_time('mysql', 1), 'ip' => $ip, 'block_type' => $type, 'reason' => $reason_for_summary], ['%s', '%s', '%s', '%s'] ); } // 3. Loguear los eventos de forma segura if (!empty($options['enable_logging']) && '1' === $options['enable_logging']) { global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_logs'; $log_message_critical = sprintf( 'IP %1$s blocked via shutdown: %2$d %3$s errors. Notification action: %4$s.', $ip, $errors, strtoupper($type), $frequency ); // phpcs:ignore WordPress.WP.I18n.InterpolatedVariableText.Found, WordPress.DB.DirectDatabaseQuery.DirectQuery @$wpdb->insert($table_name, [ 'timestamp' => current_time('mysql', 1), 'ip' => $ip, 'log_type' => 'general', 'level' => 'critical', 'message' => $log_message_critical, 'details' => wp_json_encode(['trigger' => $trigger]) ]); if (is_bool($email_sent)) { $email_log_level = $email_sent ? 'info' : 'error'; $email_log_message = $email_sent ? /* translators: 1: The blocked IP address. 2: The email address the notification was sent to. */ sprintf(__('Block notification for IP %1$s sent to %2$s.', 'advanced-ip-blocker'), $ip, $to) : /* translators: 1: The blocked IP address. 2: The email address the notification was sent to. */ sprintf(__('FAILED to send block notification for IP %1$s to %2$s.', 'advanced-ip-blocker'), $ip, $to); // phpcs:ignore WordPress.WP.I18n.InterpolatedVariableText.Found, WordPress.DB.DirectDatabaseQuery.DirectQuery @$wpdb->insert($table_name, [ 'timestamp' => current_time('mysql', 1), 'ip' => $ip, 'log_type' => 'general', 'level' => $email_log_level, 'message' => $email_log_message, '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'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $notifications = $wpdb->get_results("SELECT * FROM $table_name ORDER BY timestamp ASC"); if (empty($notifications)) { return; } $total_blocks = count($notifications); $blocks_by_type = []; foreach ($notifications as $notification) { $blocks_by_type[$notification->block_type][] = $notification; } $to = !empty($this->options['notification_email']) && is_email($this->options['notification_email']) ? $this->options['notification_email'] : get_option('admin_email'); $site_name = get_bloginfo('name'); /* translators: 1: Site name, 2: Total number of blocked IPs. */ $subject = sprintf(__('[%1$s] Security Summary: %2$d IPs Blocked', 'advanced-ip-blocker'), $site_name, $total_blocks); $body_lines = [ __('Hello,', 'advanced-ip-blocker'), '', /* translators: %s: Site name. */ sprintf(__("This is your security summary for the site '%s'.", 'advanced-ip-blocker'), $site_name), '', /* translators: %d: Total number of blocked IPs. */ sprintf(__('Total IPs blocked: %d', 'advanced-ip-blocker'), $total_blocks), '-------------------------------------', '' ]; foreach ( $blocks_by_type as $type => $blocks ) { $type_capitalized = ucfirst( $type ); $block_count = count( $blocks ); // Separamos la traducción del sprintf para evitar el falso positivo del linter. /* translators: 1: Block type (e.g. Honeypot, 404). 2: Number of blocks. */ $summary_line = __( 'Blocks by %1$s: %2$d', 'advanced-ip-blocker' ); $body_lines[] = sprintf( $summary_line, $type_capitalized, $block_count ); } $body_lines[] = ''; $body_lines[] = '--- ' . __('Some recent blocks', 'advanced-ip-blocker') . ' ---'; $details_limit = 10; $limited_notifications = array_slice($notifications, -$details_limit); foreach ( $limited_notifications as $notification ) { $formatted_date = self::get_formatted_datetime( $notification->timestamp ); $ip_address = $notification->ip; $block_reason = $notification->reason; // Separamos la traducción del sprintf para evitar el falso positivo. /* translators: 1: Date and time, 2: IP address, 3: Reason. */ $detail_line = __( '- %1$s | IP: %2$s | Reason: %3$s', 'advanced-ip-blocker' ); $body_lines[] = sprintf( $detail_line, $formatted_date, $ip_address, $block_reason ); } $body_lines[] = ''; $body_lines[] = '-------------------------------------'; $body_lines[] = ''; $body_lines[] = '--'; /* translators: 1: Plugin name, 2: Plugin version. */ $body_lines[] = sprintf(__('Sent by %1$s v%2$s', 'advanced-ip-blocker'), 'Advanced IP Blocker', ADVAIPBL_VERSION); $body = implode("\n", $body_lines); $sent = wp_mail($to, $subject, $body); if ($sent) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query("TRUNCATE TABLE $table_name"); $this->log_event(sprintf('CRON aibp_send_summary_email: Summary sent to %s and queue cleared.', $to), 'info'); } else { $this->log_event(sprintf('CRON aibp_send_summary_email: FAILED to send summary to %s. Queue NOT cleared.', $to), 'error'); } } private function send_block_notification($ip, $type, $count, $extra_data) { if ( empty($this->options['enable_email_notifications']) || '1' !== $this->options['enable_email_notifications'] ) { return; } $frequency = $this->options['notification_frequency'] ?? 'disabled'; if ('disabled' === $frequency) { return; } $reason = __('Unknown reason', 'advanced-ip-blocker'); // --- (La lógica del switch para la variable $reason sigue igual y está correcta) --- switch ($type) { case '404': case '403': $error_type_upper = strtoupper($type); $trigger_url = $extra_data['trigger'] ?? 'N/A'; /* translators: 1: Number of errors (integer), 2: Error type (e.g., 404), 3: The URL that triggered the block. */ $reason = sprintf(__("Generated %1\$d %2\$s errors. Triggering URL: %3\$s", 'advanced-ip-blocker'), $count, $error_type_upper, $trigger_url); break; case 'login': $username = isset($extra_data['trigger']) ? sanitize_text_field($extra_data['trigger']) : __('unknown', 'advanced-ip-blocker'); /* translators: 1: Number of failed login attempts (integer), 2: The username used in the attempts. */ $reason_format = __('Made %1$d failed login attempts for user: %2$s', 'advanced-ip-blocker'); $reason = sprintf($reason_format, $count, $username); break; case 'honeypot': $honeypot_url = $extra_data['url'] ?? 'N/A'; /* translators: %s: The honeypot URL that was accessed. */ $reason = sprintf(__('Accessed Honeypot URL: %s', 'advanced-ip-blocker'), $honeypot_url); break; case 'user_agent': $user_agent_string = $extra_data['user_agent'] ?? 'N/A'; /* translators: %s: The blocked User-Agent string. */ $reason = sprintf(__('Used a blocked User-Agent: %s', 'advanced-ip-blocker'), $user_agent_string); break; case 'geoblock': $country_name = $extra_data['country'] ?? 'Unknown Country'; /* translators: %s: The name of the blocked country. */ $reason = sprintf( __( 'Blocked due to country policy: %s', 'advanced-ip-blocker' ), $country_name ); break; } if ($frequency === 'instant') { $to = !empty($this->options['notification_email']) && is_email($this->options['notification_email']) ? $this->options['notification_email'] : get_option('admin_email'); $site_name = get_bloginfo('name'); /* translators: %s: The name of the website. */ $subject = sprintf(__('[%s] Security Alert: IP Automatically Blocked', 'advanced-ip-blocker'), $site_name); // =================== LÓGICA DE DURACIÓN RE-CORREGIDA =================== $permanent_block_types = ['manual', 'geoblock']; if ( in_array( $type, $permanent_block_types, true ) ) { $duration_text = __('Permanent', 'advanced-ip-blocker'); } else { // Para todos los demás tipos (honeypot, user_agent, 404, etc.), buscamos su duración en los ajustes. $duration_minutes = (int) ($this->options['duration_' . $type] ?? 1440); // 1440 (1 día) como fallback seguro. /* translators: %d: The duration of the block in minutes. */ $duration_text = sprintf(__('%d minutes', 'advanced-ip-blocker'), $duration_minutes); } // ======================================================================= $body_lines = [ __("Hello,", 'advanced-ip-blocker'), "", /* translators: %s: The name of the website. */ sprintf(__("An IP address has been automatically blocked on your website '%s'.", 'advanced-ip-blocker'), $site_name), "", __("Block Details:", 'advanced-ip-blocker'), "-------------------------------------", /* translators: %s: The blocked IP address. */ sprintf(__("Blocked IP: %s", 'advanced-ip-blocker'), esc_html($ip)), /* translators: %s: The date and time of the block. */ sprintf(__("Date and Time: %s", 'advanced-ip-blocker'), self::get_formatted_datetime(time())), /* translators: %s: The reason for the block. */ sprintf(__("Reason: %s", 'advanced-ip-blocker'), esc_html($reason)), /* translators: %s: The duration of the block (e.g., "120 minutes" or "Permanent"). */ sprintf(__("Block Duration: %s", 'advanced-ip-blocker'), $duration_text), "-------------------------------------", "", __("No action is required on your part.", 'advanced-ip-blocker'), "", "--", /* translators: 1: Plugin name, 2: Plugin version. */ sprintf(__('Sent by %1$s v%2$s', 'advanced-ip-blocker'), 'Advanced IP Blocker', ADVAIPBL_VERSION) ]; $body = implode("\n", $body_lines); $sent = wp_mail($to, $subject, $body); if ($sent) { /* translators: 1: The blocked IP address. 2: The email address the notification was sent to. */ $this->log_event(sprintf(__('Block notification for IP %1$s sent to %2$s.', 'advanced-ip-blocker'), $ip, $to), 'info'); } else { /* translators: 1: The blocked IP address. 2: The email address the notification failed to send to. */ $this->log_event(sprintf(__('FAILED to send block notification for IP %1$s to %2$s.', 'advanced-ip-blocker'), $ip, $to), 'error'); } } else { // Para notificaciones 'daily' o 'weekly' global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_notifications_queue'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery @$wpdb->insert($table_name, [ 'timestamp' => current_time('mysql', 1), 'ip' => $ip, 'block_type' => $type, 'reason' => $reason ]); } } 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(); } } } public function log_settings_change($old_value, $new_value) { $user = wp_get_current_user(); $username = ($user && $user->ID) ? $user->user_login : __('an unknown user', 'advanced-ip-blocker'); $changed_fields = []; foreach ($new_value as $key => $value) { if (!isset($old_value[$key]) || $old_value[$key] !== $value) { $changed_fields[] = $key; } } if (empty($changed_fields)) { return; } $fields_string = implode(', ', $changed_fields); // phpcs:ignore WordPress.WP.I18n.InterpolatedVariableText $message = sprintf( /* translators: 1: The username. 2: A comma-separated list of changed fields. */ __('Plugin settings updated by %1$s. Changed fields: %2$s', 'advanced-ip-blocker'), $username, $fields_string ); $this->log_event($message, 'info'); } private function access_denied_page($title, $message) { if (isset($this->block_response_initiated) && $this->block_response_initiated) { exit; } $this->block_response_initiated = true; if ( ! headers_sent() ) { status_header(403); } $custom_message = $this->options['custom_block_message'] ?? ''; $display_message = ! empty($custom_message) ? nl2br(esc_html($custom_message)) : esc_html($message); // La función wp_die escapa el título, pero es buena práctica escapar el mensaje antes. wp_die( wp_kses_post($display_message), esc_html($title) ); } public function desbloquear_ip($ip) { // La lista ya incluye 'manual', por lo que esta función es correcta. // Solo verificamos que la lista esté completa. foreach (['geoblock', '404', '403', 'login', 'manual', 'honeypot', 'user_agent'] as $type) { delete_transient("advaipbl_bloqueo_{$type}_" . md5($ip)); // Los contadores solo existen para los bloqueos por umbral. if (in_array($type, ['404', '403', 'login'])) { delete_transient("advaipbl_errores_{$type}_" . md5($ip)); delete_transient("advaipbl_detail_{$type}_" . md5($ip)); } $key = 'advaipbl_ips_bloqueadas_' . $type; $list = get_option($key, []); if ( is_array($list) && array_key_exists($ip, $list) ) { unset($list[$ip]); update_option($key, $list); } } /* translators: %s: The IP address that was manually unblocked. */ $this->log_event(sprintf(__('IP %s manually unblocked.', 'advanced-ip-blocker'), $ip)); } public function limpiar_ips_expiradas() { foreach (['404', '403', 'login', 'honeypot', 'user_agent'] as $type) { $key = ADVAIPBL_PLUGIN_NAME . "_ips_bloqueadas_{$type}"; $list = get_option($key, []); if ( ! is_array($list) ) continue; foreach ($list as $ip => $block_data) { if (!get_transient("advaipbl_bloqueo_{$type}_" . md5($ip))) { unset($list[$ip]); /* translators: 1: The unblocked IP address. 2: The type of block that expired (e.g., 404, login). */ $this->log_event(sprintf(__('IP %1$s automatically unblocked (expired %2$s).', 'advanced-ip-blocker'), $ip, $type)); } } update_option($key, $list); } } public function get_default_settings() { // Lista de exclusiones por defecto para evitar falsos positivos comunes. $default_exclusions = [ 'wc-ajax=get_refreshed_fragments', // WooCommerce '?wc-ajax=', // WooCommerce 'undefinedjetpack', // Bug común de Jetpack '/wp-cron.php', // Cron de WordPress '.js.map', // Archivos de mapa de JavaScript '.css.map', // Archivos de mapa de CSS '/favicon.ico', // Icono de favoritos '/.well-known/traffic-advice', // Google Chrome Prefetch Proxy ]; return [ '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_honeypot' => 1440, 'duration_user_agent' => 1440, 'enable_logging' => '1', 'custom_block_message' => '', // La lista de exclusiones ahora se rellena por defecto. 'excluded_error_urls' => implode( "\n", $default_exclusions ), 'enable_email_notifications' => '0', 'notification_frequency' => 'disabled', 'notification_email' => '', 'log_timezone' => wp_timezone_string(), 'log_retention_days' => 30, 'geolocation_provider' => 'ip-api.com', 'api_key_ipapicom' => '', 'api_key_ipstackcom' => '', 'api_key_ipinfocom' => '', 'api_key_ip_apicom' => '', 'delete_data_on_uninstall' => '0', 'geoblock_countries' => [], 'rows_per_page' => 20, ]; } public static function activate_plugin() { self::setup_database_tables(); if ( ! wp_next_scheduled( 'advaipbl_purge_old_logs_event' ) ) { wp_schedule_event( time(), 'daily', 'advaipbl_purge_old_logs_event' ); } $existing_options = get_option( 'advaipbl_settings', [] ); $instance = self::get_instance(); $defaults = $instance->get_default_settings(); $updated_options = wp_parse_args( $existing_options, $defaults ); if ( $updated_options !== $existing_options ) { update_option( 'advaipbl_settings', $updated_options ); } $notification_frequency = $updated_options['notification_frequency'] ?? 'disabled'; if ( 'disabled' !== $notification_frequency && 'instant' !== $notification_frequency ) { if ( ! wp_next_scheduled( 'advaipbl_send_summary_email' ) ) { $instance->schedule_notification_cron(); } } add_option( 'advaipbl_blocked_user_agents', [], '', 'no' ); add_option( 'advaipbl_whitelisted_user_agents', [], '', 'no' ); add_option( 'advaipbl_ips_whitelist', [], '', 'no' ); add_option( 'advaipbl_ips_bloqueadas_manual', [], '', 'no' ); add_option( 'advaipbl_ips_bloqueadas_honeypot', [], '', 'no' ); add_option( 'advaipbl_ips_bloqueadas_user_agent', [], '', 'no' ); add_option( 'advaipbl_ips_bloqueadas_geoblock', [], '', 'no' ); add_option( ADVAIPBL_USM_OPTION_PER_PAGE, ADVAIPBL_USM_DEFAULT_PER_PAGE, '', 'no' ); add_option( 'advaipbl_honeypot_urls', [], '', 'no' ); update_option( 'ip_blocker_pro_log_migration_status', 'pending' ); add_option( 'advaipbl_admin_ip_whitelist_trigger', 'yes', '', 'no' ); } public static function deactivate_plugin() { wp_clear_scheduled_hook('advaipbl_purge_old_logs_event'); $instance = self::get_instance(); $instance->clear_notification_cron(); } public function admin_menu() { $page_hook = add_options_page( __( 'Advanced IP Blocker Settings', 'advanced-ip-blocker' ), __( 'Advanced IP Blocker', 'advanced-ip-blocker' ), 'manage_options', 'advaipbl_settings_page', [ $this, 'settings_page_content' ] ); // SOLO en nuestra página, enganchamos la función que limpia los avisos. if ( $page_hook ) { add_action( 'load-' . $page_hook, [ $this, 'remove_all_other_admin_notices' ] ); } } /** * 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
/blog/ or /contact/). Use attack-specific patterns.', 'advanced-ip-blocker') ); ?>
| 0 ? esc_html(self::get_formatted_datetime($item['timestamp'])) : '...'; ?> | |||
' . esc_html__('General event logging is disabled in the "Settings" tab.', 'advanced-ip-blocker') . '
bc1qxfsmpqk0q7an82ckm7380pzpsea2sa294824lg0xAF8CC1B71EAbF5dffDa2aF58AEF43b2559163284
#
wp help advaipbl
# --- ---
wp advaipbl block [--reason=]
wp advaipbl unblock
# --- ---
wp advaipbl whitelist list [--format=]
wp advaipbl whitelist add
wp advaipbl whitelist remove
# --- ---
wp advaipbl geoblock list
wp advaipbl geoblock add
wp advaipbl geoblock remove
# --- ---
wp advaipbl session list [--role=]
wp advaipbl session terminate [--user-id=] [--user-login=] [--role=] [--all]
# --- ---
wp advaipbl blocked [--type=] [--format=]
# --- ---
wp advaipbl log list [--type=] [--count=]
wp advaipbl log clear [--type=] [--force]
# --- ---
wp advaipbl config get []
wp advaipbl config set
' . 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' ); ?>
get_default_settings(); $numeric_fields = [ '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 = [ 'enable_logging', 'delete_data_on_uninstall', 'enable_email_notifications' ]; 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['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'] ) && is_array( $input['geoblock_countries'] ) ) { $all_country_codes = array_keys( $this->get_country_list() ); // Nos aseguramos de que solo se guarden códigos de país válidos. $new_input['geoblock_countries'] = array_intersect( $input['geoblock_countries'], $all_country_codes ); } else { $new_input['geoblock_countries'] = []; // Si no se envía nada, es un array vacío. } return $new_input; } public function text_field_callback($args){ $value = $this->options[$args['name']] ?? $args['default'] ?? ''; $class = isset($args['class']) ? $args['class'] : ''; $type = 'text'; if ( strpos($args['name'], 'duration') !== false || strpos($args['name'], 'threshold') !== false || strpos($args['name'], 'retention') !== false ) { $type = 'number'; } $min_attr = ( 'number' === $type ) ? 'min="0"' : ''; $data_attr = ! empty( $args['data_provider'] ) ? 'data-provider="' . esc_attr( $args['data_provider'] ) . '"' : ''; // Construimos el HTML y lo imprimimos con echo para escapar cada atributo de forma segura. $html = sprintf( '', esc_attr( $type ), esc_attr( $args['name'] ), esc_attr( 'advaipbl_settings[' . $args['name'] . ']' ), esc_attr( $value ), $min_attr, // $min_attr es seguro, es 'min="0"' o ''. esc_attr( $class ), $data_attr // $data_attr ya está escapado. ); echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped if ( isset($args['description']) ) { echo '
' . esc_html($args['description']) . '
'; } } public function checkbox_field_callback($args){ $checked = checked( '1', $this->options[$args['name']] ?? '0', false ); // Construimos el HTML y lo imprimimos con echo para mayor claridad. $html = sprintf( '', esc_attr( 'advaipbl_settings[' . $args['name'] . ']' ), $checked, // La función checked() de WordPress ya devuelve una cadena segura. esc_html( $args['label'] ) ); echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } public function textarea_field_callback($args){ $value = $this->options[$args['name']] ?? ''; printf( '%3$s
', esc_attr( 'advaipbl_settings[' . $args['name'] . ']' ), esc_textarea( $value ), 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') ]; 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() { ?>
get_client_ip() ); ?>
|
|
get_server_ip();
if ( $server_ip ) {
echo '' . esc_html( $server_ip ) . '';
} else {
echo '' . esc_html__( 'Could not be determined.', 'advanced-ip-blocker' ) . '';
}
?>
|
|
|
';
/* translators: %s: The IP address of the proxy/CDN. */
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 '
'; 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 '