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('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_action('advaipbl_purge_old_logs_event', [$this, 'purge_old_logs']); add_filter('cron_schedules', [$this, 'add_cron_intervals']); add_filter( 'rest_endpoints', [ $this, 'disable_rest_api_user_endpoints' ] ); 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']); 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']); } } /** * 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' ); } /** * 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. 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. 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 lógica de whitelisting. $whitelist = get_option( 'advaipbl_ips_whitelist', [] ); if ( array_key_exists( $ip, $whitelist ) ) { /* translators: %s: The IP address that is already whitelisted. */ wp_send_json_success( [ 'message' => sprintf( __( '%s is already whitelisted.', 'advanced-ip-blocker' ), $ip ) ] ); } $whitelist[ $ip ] = [ 'timestamp' => time(), 'detail' => $detail ]; update_option( 'advaipbl_ips_whitelist', $whitelist ); $this->desbloquear_ip( $ip ); /* translators: 1: The IP address, 2: The reason/detail, 3: The admin username. */ $this->log_event( sprintf( __( 'IP %1$s added to whitelist via admin button. Reason: %2$s. Action by: %3$s', 'advanced-ip-blocker' ), $ip, $detail, $this->get_current_admin_username() ), 'info', $ip ); /* 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 ) ] ); } 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) { // 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 $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' ); 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 ); // 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'] ?? [], 'test_connection_nonce' => wp_create_nonce( 'advaipbl_test_connection_nonce' ), 'add_whitelist_nonce' => wp_create_nonce( 'advaipbl_add_whitelist_nonce' ), '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' ), 'add_to_whitelist_btn' => esc_html__( 'Add to Whitelist', 'advanced-ip-blocker' ), 'adding_to_whitelist' => esc_html__( 'Adding...', 'advanced-ip-blocker' ), 'added_to_whitelist' => esc_html__( 'Successfully Added!', 'advanced-ip-blocker' ), 'select2_placeholder' => esc_attr__( 'Search for a country to block...', 'advanced-ip-blocker' ), ], ] ); // 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'); } } /** * 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 '' . esc_html__( 'Your current IP address (%s) is NOT whitelisted.', 'advanced-ip-blocker' ) . ' ' . 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; } } } public function run_all_block_checks() { /** * Si estamos en un contexto de WP-CLI, nos saltamos todas las comprobaciones de bloqueo. */ if ( defined( 'WP_CLI' ) && WP_CLI ) { return; } $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. $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). $blocked_countries = $this->options['geoblock_countries'] ?? []; if ( ! empty( $blocked_countries ) ) { if ( ! isset( $this->api_manager ) ) { $this->api_manager = new ADVAIPBL_Api_Manager(); } $location = $this->api_manager->fetch_location( $ip ); 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'] ); $this->block_ip_instantly( $ip, 'geoblock', $reason, [ 'country' => $location['country'] ?? '' ] ); } } } // 4. Lógica 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'), '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; // Ahora 'geoblock' obtiene su duración de los ajustes, como los demás. // Solo 'manual' es verdaderamente permanente. $duration = (int) ($this->options[$duration_key] ?? 1440) * 60; if ( 'manual' === $type ) { $duration = 10 * YEAR_IN_SECONDS; } $option_key = 'advaipbl_ips_bloqueadas_' . $type; set_transient($block_key, true, $duration); $list = get_option($option_key, []); $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. $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); } } /** * 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, ] ); } private function log_specific_error($type, $ip, $extra_data = []) { 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', ]; $message = ''; if ( 'login' === $type && ! empty( $extra_data['username'] ) ) { $username = $extra_data['username']; $details['username'] = $username; /* translators: %s: The username that was attempted during a failed login. */ $message = sprintf( __( 'Failed login attempt for user: %s', 'advanced-ip-blocker' ), $username ); } else { /* translators: %s: The error type, e.g., 404 or 403. */ $message = sprintf( __( 'A %s error occurred.', 'advanced-ip-blocker' ), $type ); } @$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; } // Si ya está bloqueado, solo loguea el intento y sal. foreach (['geoblock', 'honeypot', 'manual', '404', '403', 'login', 'user_agent'] as $reason_type) { if (get_transient('advaipbl_bloqueo_' . $reason_type . '_' . md5($ip))) { $this->log_specific_error($type, $ip, $extra_data); 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'] : 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_name = get_bloginfo( 'name' ); /* translators: %s: The name of the website. */ $email_subject = sprintf(__('[%s] Security Alert: IP Automatically Blocked', 'advanced-ip-blocker'), $site_name); $template_title = __('Security Alert: IP Blocked', 'advanced-ip-blocker'); /* translators: %d: The duration of the block in minutes. */ $duration_text = sprintf(__('%d minutes', 'advanced-ip-blocker'), (int) ($options["duration_{$type}"] ?? 120)); $reason = ''; switch ($type) { case '404': case '403': /* translators: 1: Number of errors, 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'), $errors, strtoupper($type), $trigger); break; case 'login': /* translators: 1: Number of failed login attempts, 2: The username used in the attempts. */ $reason = sprintf(__('Made %1$d failed login attempts for user: %2$s', 'advanced-ip-blocker'), $errors, $trigger); break; } $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;"; $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'; $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'; $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) { 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'); 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. */ $email_subject = sprintf(__('[%s] Security Alert: IP Automatically Blocked', 'advanced-ip-blocker'), $site_name); $template_title = __('Security Alert: IP Blocked', 'advanced-ip-blocker'); if ( 'manual' === $type ) { $duration_text = __('Permanent', 'advanced-ip-blocker'); } else { // Todos los demás tipos (geoblock, honeypot, 404, etc.) usan su duración configurable. $duration_minutes = (int) ($this->options['duration_' . $type] ?? 1440); /* translators: %d: The duration of the block in minutes. */ $duration_text = sprintf(__('%d minutes', 'advanced-ip-blocker'), $duration_minutes); } $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;"; $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' ) . '' . '
'; // 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' ] ); 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) { 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: 1: The IP address, 2: The admin username. */ $this->log_event( sprintf( __( 'IP %1$s manually unblocked by %2$s.', 'advanced-ip-blocker' ), $ip, $this->get_current_admin_username() ) ); } public function limpiar_ips_expiradas() { // Array actualizado para incluir 'geoblock' foreach (['geoblock', '404', '403', 'login', 'honeypot', 'user_agent'] as $type) { $key = 'advaipbl_ips_bloqueadas_' . $type; $list = get_option($key, []); if ( ! is_array($list) || empty($list) ) { continue; } $list_changed = false; foreach ($list as $ip => $block_data) { // Si el transient de bloqueo ya no existe, la IP ha expirado. if (!get_transient("advaipbl_bloqueo_{$type}_" . md5($ip))) { $log_detail = ''; // Si es un geobloqueo, extraemos el detalle (el país). if ( $type === 'geoblock' && is_array($block_data) && !empty($block_data['detail']) ) { // El detalle es algo como "Country block: Spain". Extraemos solo "Spain". $detail_parts = explode(':', $block_data['detail']); $country_name = trim(end($detail_parts)); $log_detail = ' (' . $country_name . ')'; } /* translators: 1: The unblocked IP address, 2: The type of block that expired (e.g., geoblock), 3: Extra detail like the country name. */ $log_message = sprintf(__('IP %1$s automatically unblocked (expired %2$s%3$s).', 'advanced-ip-blocker'), $ip, $type, $log_detail); $this->log_event($log_message); unset($list[$ip]); $list_changed = true; } } if ( $list_changed ) { update_option($key, $list); } } } // REEMPLAZA tu función get_default_settings() completa public function get_default_settings() { // Lista de exclusiones por defecto para evitar falsos positivos comunes. $default_exclusions = [ 'wc-ajax=get_refreshed_fragments', '?wc-ajax=', 'undefinedjetpack', '/wp-cron.php', '.js.map', '.css.map', '/favicon.ico', '/.well-known/traffic-advice', ]; 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, 'disable_user_enumeration' => '1', 'prevent_author_scanning' => '1', 'duration_geoblock' => 1440, 'duration_honeypot' => 1440, 'duration_user_agent' => 1440, 'enable_logging' => '1', 'custom_block_message' => '', '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, 'restrict_login_page' => '0', ]; } 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

', esc_attr( $type ), esc_html( $notice['message'] ) ); delete_transient('advaipbl_admin_notice'); } } public function handle_admin_actions() { // 1. Validación de Nonce y Permisos. Salir si no es una de nuestras acciones. if ( empty( $_POST['action_type'] ) || ! isset( $_POST['advaipbl_admin_nonce_action'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['advaipbl_admin_nonce_action'] ) ), 'advaipbl_admin_nonce_action' ) ) { return; } if ( ! current_user_can('manage_options') ) { wp_die( 'Permission denied.' ); } // 2. Preparación de Variables $action = sanitize_key($_POST['action_type']); $message = ''; $type = 'info'; $current_user_login = $this->get_current_admin_username(); // 3. Lógica de Acciones if ($action === 'save_user_agents') { $blocked_raw = isset($_POST['blocked_user_agents']) ? sanitize_textarea_field(wp_unslash($_POST['blocked_user_agents'])) : ''; update_option('advaipbl_blocked_user_agents', 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('advaipbl_whitelisted_user_agents', array_filter(array_map('trim', explode("\n", $whitelisted_raw)))); /* translators: %s: The username of the user who updated the lists. */ $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'); $type = 'success'; } elseif ($action === 'save_honeypot_urls') { $urls_raw = isset($_POST['honeypot_urls']) ? sanitize_textarea_field(wp_unslash($_POST['honeypot_urls'])) : ''; update_option('advaipbl_honeypot_urls', array_filter(array_map('trim', explode("\n", $urls_raw)))); /* translators: %s: The username of the user who updated the list. */ $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'); $type = 'success'; } elseif ($action === 'clear_specific_logs') { global $wpdb; $log_type_to_clear = isset($_POST['log_type_to_clear']) ? sanitize_key($_POST['log_type_to_clear']) : ''; if (in_array($log_type_to_clear, ['404', '403'], true)) { $wpdb->delete($wpdb->prefix . 'advaipbl_logs', ['log_type' => $log_type_to_clear], ['%s']); $log_type_upper = strtoupper($log_type_to_clear); /* translators: 1: The log type (e.g., "404"). 2: The username of the user who deleted the logs. */ $this->log_event(sprintf(__('%1$s logs manually deleted by %2$s.', 'advanced-ip-blocker'), $log_type_upper, $current_user_login), 'warning'); /* translators: %s: The type of log that was deleted (e.g., "404"). */ $message = sprintf(__('%s logs deleted.', 'advanced-ip-blocker'), $log_type_upper); $type = 'success'; } } elseif ($action === 'clear_all_logs') { global $wpdb; $wpdb->query("TRUNCATE TABLE `{$wpdb->prefix}advaipbl_logs`"); $wpdb->query("TRUNCATE TABLE `{$wpdb->prefix}advaipbl_notifications_queue`"); /* translators: %s: The username of the user who deleted all logs. */ $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'); $type = 'success'; } else { $ip = isset($_POST['ip_address']) ? sanitize_text_field(trim(wp_unslash($_POST['ip_address']))) : null; if ($ip && !filter_var($ip, FILTER_VALIDATE_IP)) { $message = __('The entered IP is not valid.', 'advanced-ip-blocker'); $type = 'error'; } elseif ($ip) { switch ($action) { case 'add_manual_block': $list = get_option( 'advaipbl_ips_bloqueadas_manual', [] ); if ( ! array_key_exists( $ip, $list ) ) { $list[ $ip ] = [ 'timestamp' => time(), 'detail' => __( 'Manual Block', 'advanced-ip-blocker' ) ]; update_option( 'advaipbl_ips_bloqueadas_manual', $list ); set_transient( 'advaipbl_bloqueo_manual_' . md5( $ip ), true, 10 * YEAR_IN_SECONDS ); /* translators: 1: The IP address, 2: The admin username. */ $this->log_event( sprintf( __( 'IP %1$s manually blocked by %2$s.', 'advanced-ip-blocker' ), $ip, $current_user_login ) ); $message = sprintf( __( 'IP %s blocked.', 'advanced-ip-blocker' ), $ip ); $type = 'success'; } else { $message = sprintf( __( 'IP %s was already blocked.', 'advanced-ip-blocker' ), $ip ); } break; case 'remove_block': $this->desbloquear_ip( $ip ); $message = sprintf( __( 'IP %s unblocked.', 'advanced-ip-blocker' ), $ip ); $type = 'success'; break; case 'add_whitelist': $list = get_option( 'advaipbl_ips_whitelist', [] ); if ( ! array_key_exists( $ip, $list ) ) { $list[ $ip ] = [ 'timestamp' => time(), 'detail' => __('Manually added', 'advanced-ip-blocker') ]; update_option( 'advaipbl_ips_whitelist', $list ); $this->desbloquear_ip( $ip ); /* translators: 1: The IP address, 2: The admin username. */ $this->log_event( sprintf( __( 'IP %1$s added to whitelist by %2$s.', 'advanced-ip-blocker' ), $ip, $current_user_login ) ); $message = sprintf( __( 'IP %s added to whitelist and unblocked.', 'advanced-ip-blocker' ), $ip ); $type = 'success'; } else { $message = sprintf( __( 'IP %s was already in the whitelist.', 'advanced-ip-blocker' ), $ip ); } break; case 'remove_whitelist': $list = get_option( 'advaipbl_ips_whitelist', [] ); if ( array_key_exists( $ip, $list ) ) { unset( $list[ $ip ] ); update_option( 'advaipbl_ips_whitelist', $list ); /* translators: 1: The IP address, 2: The admin username. */ $this->log_event( sprintf( __( 'IP %1$s removed from whitelist by %2$s.', 'advanced-ip-blocker' ), $ip, $current_user_login ) ); $message = sprintf( __( 'IP %s removed from whitelist.', 'advanced-ip-blocker' ), $ip ); $type = 'success'; } break; } } } // 4. Redirección final if ($message) { set_transient('advaipbl_admin_notice', ['message' => $message, 'type' => $type], 30); } wp_safe_redirect(remove_query_arg(['settings-updated', 's'], wp_get_referer())); exit; } public function settings_page_content() { if (!current_user_can('manage_options')) return; $active_tab = isset($_GET['tab']) ? sanitize_key($_GET['tab']) : 'general_settings'; ?>
display_admin_notice(); ?>
display_user_agents_tab(); break; case 'honeypot': $this->display_honeypot_tab(); break; case 'blocked_ips': $this->display_blocked_ips_tab(); break; case 'whitelist': $this->display_whitelist_tab(); break; case '404_logs': $this->display_404_logs_tab(); break; case '403_logs': $this->display_403_logs_tab(); break; case 'login_logs': $this->display_login_logs_tab(); break; case 'user_sessions': $this->session_manager->display_admin_page(); break; case 'logs': $this->display_logs_tab(); break; case 'status': $this->display_status_tab(); break; case 'credits': $this->display_credits_tab(); break; case 'general': case 'general_settings': default: $this->display_general_settings_tab(); break; } ?>

geoblocking_section_callback(); } ?>


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


get_all_blocked_ips_for_cli(); $all_ips = wp_list_pluck($all_blocked_data, 'ip'); $locations = $this->session_manager->get_cached_locations($all_ips); // 1. Obtener los parámetros de la URL de forma segura. // phpcs:disable WordPress.Security.NonceVerification.Recommended $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; // Obtenemos el nuevo valor de "per page" de la URL, con un valor por defecto de 20. $per_page = isset($_GET['advaipbl_per_page']) ? absint($_GET['advaipbl_per_page']) : 20; // phpcs:enable $filtered_data = $all_blocked_data; if ('all' !== $filter_type) { $filtered_data = array_filter($all_blocked_data, function($block) use ($filter_type) { // Comparamos convirtiendo ambos a string y eliminando espacios. // Esto previene errores si uno es numérico (404) y el otro es string ('404'). return trim((string)$block['type']) == trim((string)$filter_type); }); } usort($filtered_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') ? ($a_val < $b_val ? -1 : 1) : ($a_val > $b_val ? -1 : 1); }); $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); ?>
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_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); ?>

$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, 's' => $search_term_sanitized, ]); 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 nuevo 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('404'); } /** * Muestra la tabla de logs específica para errores 403. */ private function display_403_logs_tab() { $this->display_log_table_generic('403'); } /** * Muestra la tabla de logs específica para fallos de login. */ private function display_login_logs_tab() { $this->display_log_table_generic('login'); } private function display_log_table_generic($type) { // 1. Definir títulos y configuración $titles = [ '404' => __('404 Error Log', 'advanced-ip-blocker'), '403' => __('403 Error Log', 'advanced-ip-blocker'), 'login' => __('Login Failure Log', 'advanced-ip-blocker'), ]; $title = $titles[$type] ?? ucfirst($type) . ' Log'; // 2. Obtener parámetros de la URL // phpcs:disable WordPress.Security.NonceVerification.Recommended $search_term = isset($_GET['s']) ? sanitize_text_field(wp_unslash($_GET['s'])) : ''; $orderby = isset($_GET['orderby']) ? sanitize_key($_GET['orderby']) : 'timestamp'; $order = isset($_GET['order']) ? 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; // phpcs:enable // 3. Construir la consulta SQL global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_logs'; $where_clauses = [$wpdb->prepare("log_type = %s", $type)]; if (!empty($search_term)) { $search_like = '%' . $wpdb->esc_like($search_term) . '%'; // Búsqueda inteligente: si es log de login, busca también en el username. if ($type === 'login') { $where_clauses[] = $wpdb->prepare("(ip LIKE %s OR details LIKE %s)", $search_like, $search_like); } else { $where_clauses[] = $wpdb->prepare("(ip LIKE %s OR details LIKE %s)", $search_like, $search_like); } } $where_sql = implode(' AND ', $where_clauses); $total_items = $wpdb->get_var("SELECT COUNT(log_id) FROM $table_name WHERE $where_sql"); $total_pages = ceil($total_items / $per_page); $offset = ($current_page - 1) * $per_page; $logs = $wpdb->get_results($wpdb->prepare("SELECT * FROM $table_name WHERE $where_sql ORDER BY " . esc_sql($orderby) . " " . esc_sql($order) . " LIMIT %d OFFSET %d", $per_page, $offset), ARRAY_A); // 4. Renderizado del HTML ?>

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

print_log_sortable_header(__('Date/Time', 'advanced-ip-blocker'), 'timestamp', $orderby, $order); ?> print_log_sortable_header(__('IP', 'advanced-ip-blocker'), 'ip', $orderby, $order); ?>
' . esc_html($details['username']) . '' : '' . esc_html__('N/A', 'advanced-ip-blocker') . ''; ?>
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 de forma segura. // phpcs:disable WordPress.Security.NonceVerification.Recommended $search_term = isset($_GET['s']) ? sanitize_text_field(wp_unslash($_GET['s'])) : ''; $sortable_columns = ['timestamp', 'level', 'ip', 'message']; // 'message' añadido para ordenar por mensaje $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'; $current_page = isset($_GET['paged']) ? absint($_GET['paged']) : 1; $per_page = isset($_GET['advaipbl_per_page']) ? absint($_GET['advaipbl_per_page']) : 25; // Usamos 25 por defecto como en tu código original // phpcs:enable // 2. Construir la consulta a la base de datos con el filtro de búsqueda. 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) . '%'; // Buscamos en las columnas IP y Message $where_clauses[] = $wpdb->prepare("(ip LIKE %s OR message LIKE %s)", $search_like, $search_like); } $where_sql = implode(' AND ', $where_clauses); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $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; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $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); // 3. Renderizado del HTML ?>

render_per_page_selector( $per_page ); ?>
add_query_arg(['paged' => '%#%', 'orderby' => $orderby, 'order' => $order, 's' => $search_term]), '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); // 'message' ahora es ordenable ?>

  • Stripe
  • bc1qxfsmpqk0q7an82ckm7380pzpsea2sa294824lg
  • 0xAF8CC1B71EAbF5dffDa2aF58AEF43b2559163284


# 

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  

  • MaxMind.', 'advanced-ip-blocker'), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [] ] ] ), 'https://www.maxmind.com' ); ?>
  • Select2 -
'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 to use 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. Useful for ignoring false positives from plugins.', 'advanced-ip-blocker')]); add_settings_section('advaipbl_notifications_section', __('Email Notifications', 'advanced-ip-blocker'), null, $page); add_settings_field('advaipbl_enable_email_notifications', __('Enable Email Notifications', 'advanced-ip-blocker'), [$this, 'checkbox_field_callback'], $page, 'advaipbl_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_notifications_section'); add_settings_field('advaipbl_notification_email', __('Notification Email', 'advanced-ip-blocker'), [$this, 'email_field_callback'], $page, 'advaipbl_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_notifications_section' ); add_settings_section('advaipbl_api_settings_section', __('External API Settings', 'advanced-ip-blocker'), 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_section', __( 'Country Blocking (Geoblocking)', 'advanced-ip-blocker' ), [ $this, 'geoblocking_section_callback' ], $page ); add_settings_field( 'advaipbl_geoblock_countries', __( 'Blocked Countries', 'advanced-ip-blocker' ), [ $this, 'geoblock_countries_callback' ], $page, 'advaipbl_geoblocking_section' ); add_settings_field( 'advaipbl_duration_geoblock', __( 'Block Duration (min)', 'advanced-ip-blocker' ), [ $this, 'text_field_callback' ], $page, 'advaipbl_geoblocking_section', [ 'name' => 'duration_geoblock', 'default' => 1440, // 24 horas, igual que honeypot 'description' => __( 'How long an IP from a blocked country will be blocked.', 'advanced-ip-blocker' ) ] ); add_settings_section('advaipbl_honeypot_settings_section', __('Honeypot Blocking', 'advanced-ip-blocker'), null, $page); add_settings_field('advaipbl_duration_honeypot', __('Block 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.', 'advanced-ip-blocker')]); add_settings_section('advaipbl_user_agent_settings_section', __('User-Agent Blocking', 'advanced-ip-blocker'), null, $page); add_settings_field('advaipbl_duration_user_agent', __('Block 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 whose User-Agent is on the blacklist.', 'advanced-ip-blocker')]); add_settings_section('advaipbl_404_settings_section', __('404 Error Blocking', 'advanced-ip-blocker'), null, $page); add_settings_field( 'advaipbl_threshold_404', __('Error Threshold', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_404_settings_section', [ 'name' => 'threshold_404', 'default' => 10, /* translators: Description for an input field. */ 'description' => __('Number of 404 errors an IP must generate to trigger a block.', 'advanced-ip-blocker') ] ); add_settings_field( 'advaipbl_transient_expiration_404', __('Time Window (min)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_404_settings_section', [ 'name' => 'transient_expiration_404', 'default' => 60, /* translators: Description for an input field. */ 'description' => __('The time period in which the error count must be reached.', 'advanced-ip-blocker') ] ); add_settings_field( 'advaipbl_duration_404', __('Block Duration (min)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_404_settings_section', [ 'name' => 'duration_404', 'default' => 120, /* translators: Description for an input field. */ 'description' => __('How long the IP will be blocked after reaching the threshold.', 'advanced-ip-blocker') ] ); add_settings_section('advaipbl_403_settings_section', __('403 Error Blocking', 'advanced-ip-blocker'), null, $page); add_settings_field( 'advaipbl_threshold_403', __('Error Threshold', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_403_settings_section', [ 'name' => 'threshold_403', 'default' => 5, /* translators: Description for an input field. */ 'description' => __('Number of 403 errors an IP must generate to trigger a block.', 'advanced-ip-blocker') ] ); add_settings_field( 'advaipbl_transient_expiration_403', __('Time Window (min)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_403_settings_section', [ 'name' => 'transient_expiration_403', 'default' => 30, /* translators: Description for an input field. */ 'description' => __('The time period in which the error count must be reached.', 'advanced-ip-blocker') ] ); add_settings_field( 'advaipbl_duration_403', __('Block Duration (min)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_403_settings_section', [ 'name' => 'duration_403', 'default' => 60, /* translators: Description for an input field. */ 'description' => __('How long the IP will be blocked after reaching the threshold.', 'advanced-ip-blocker') ] ); add_settings_section('advaipbl_login_settings_section', __('Failed Login Blocking', 'advanced-ip-blocker'), null, $page); add_settings_field( 'advaipbl_threshold_login', __('Failure Threshold', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_login_settings_section', [ 'name' => 'threshold_login', 'default' => 5, /* translators: Description for an input field. */ 'description' => __('Number of failed logins an IP must generate to trigger a block.', 'advanced-ip-blocker') ] ); add_settings_field( 'advaipbl_transient_expiration_login', __('Time Window (min)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_login_settings_section', [ 'name' => 'transient_expiration_login', 'default' => 60, /* translators: Description for an input field. */ 'description' => __('The time period in which the failure count must be reached.', 'advanced-ip-blocker') ] ); add_settings_field( 'advaipbl_duration_login', __('Block Duration (min)', 'advanced-ip-blocker'), [$this, 'text_field_callback'], $page, 'advaipbl_login_settings_section', [ 'name' => 'duration_login', 'default' => 120, /* translators: Description for an input field. */ 'description' => __('How long the IP will be blocked after reaching the threshold.', 'advanced-ip-blocker') ] ); add_settings_field( 'advaipbl_disable_user_enumeration', __( 'REST API User Protection', 'advanced-ip-blocker' ), [ $this, 'checkbox_field_callback' ], $page, 'advaipbl_login_settings_section', [ 'name' => 'disable_user_enumeration', 'label' => __( 'Disable User Enumeration via REST API', 'advanced-ip-blocker' ), 'description' => __( 'Blocks access to user data endpoints for non-logged-in users, preventing attackers from easily discovering usernames. Highly recommended.', 'advanced-ip-blocker' ) ] ); add_settings_field( 'advaipbl_prevent_author_scanning', // Nuevo nombre para la opción __( 'Author Scan Protection', 'advanced-ip-blocker' ), [ $this, 'checkbox_field_callback' ], $page, 'advaipbl_login_settings_section', [ 'name' => 'prevent_author_scanning', 'label' => __( 'Prevent user enumeration via author scans', 'advanced-ip-blocker' ), 'description' => __( 'Redirects requests like `?author=1` to the homepage, preventing bots from discovering usernames through author archives. Highly recommended.', 'advanced-ip-blocker' ) ] ); add_settings_field( 'advaipbl_restrict_login_page', __( 'Whitelist Login Access', 'advanced-ip-blocker' ), [ $this, 'restrict_login_page_callback' ], $page, 'advaipbl_login_settings_section' ); add_settings_section('advaipbl_uninstall_section', __('Uninstallation', 'advanced-ip-blocker'), 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 la descripción para la sección de Geoblocking. */ public function geoblocking_section_callback() { echo '

' . 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 = [ 'duration_geoblock', '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' ]; 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 ); $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 '

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

'; } } 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() { // 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; } // Si no se ha mostrado ninguna recomendación, mostramos un mensaje de "todo OK". if ( ! $has_recommendations ) { echo '

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

'; } ?>