' . $js_script . '
';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $html;
exit;
}
/**
* Envía notificaciones (Email/Push) cuando una nueva firma maliciosa es identificada.
*
* @param string $signature_hash El hash de la firma.
* @param string $reason La razón por la que fue marcada.
* @param string $user_agent El User-Agent de ejemplo asociado a la firma.
*/
public function send_signature_flagged_notification($signature_hash, $reason, $user_agent = 'N/A') {
$short_hash = substr($signature_hash, 0, 12) . '...';
$site_name = get_bloginfo('name');
// --- Notificación Push (Webhook) ---
if (!empty($this->options['enable_push_notifications'])) {
$message_lines = [
sprintf('*:dna: [%s] New Attack Signature Identified*', $site_name),
'> A new attack pattern has been automatically identified and is now being challenged.',
sprintf('> • *Signature Hash:* `%s`', $short_hash),
sprintf('> • *Reason:* %s', $reason),
sprintf('> • *Sample User-Agent:* `%s`', $user_agent),
];
$this->execute_webhook_send(implode("\n", $message_lines));
}
// --- Notificación por Email ---
if (!empty($this->options['enable_email_notifications'])) {
$to = !empty($this->options['notification_email']) ? $this->options['notification_email'] : get_option('admin_email');
/* translators: %s: Site name. */
$email_subject = sprintf(__('[%s] Security Alert: New Attack Signature Identified', 'advanced-ip-blocker'), $site_name);
$template_title = __('New Attack Signature Identified', 'advanced-ip-blocker');
$button_url = admin_url('admin.php?page=advaipbl_settings_page&tab=ip_management&sub-tab=blocked_signatures');
$content_html =
'
' . esc_html__("The plugin's analysis engine has automatically identified a new distributed attack pattern. Requests matching this signature will now be challenged.", 'advanced-ip-blocker') . '
';
$body = $this->get_html_email_template($template_title, $content_html);
add_filter('wp_mail_content_type', [$this, 'set_html_mail_content_type']);
wp_mail($to, $email_subject, $body);
remove_filter('wp_mail_content_type', [$this, 'set_html_mail_content_type']);
}
}
/**
* Callback para el evento de cron que ejecuta el decaimiento de la puntuación de amenaza.
*/
public function execute_threat_score_decay() {
// Solo se ejecuta si el sistema de puntuación está activado.
if (empty($this->options['enable_threat_scoring'])) {
return;
}
$decay_points = (int) ($this->options['score_decay_points'] ?? 1);
$decay_frequency_hours = (int) ($this->options['score_decay_frequency'] ?? 1);
$inactive_for_seconds = $decay_frequency_hours * HOUR_IN_SECONDS;
if ($decay_points > 0 && $decay_frequency_hours > 0) {
$result = $this->threat_score_manager->decay_scores($decay_points, $inactive_for_seconds);
if (($result['updated'] ?? 0) > 0 || ($result['deleted'] ?? 0) > 0) {
$log_message = sprintf( /* translators: 1: Number of scores reduced, 2: Number of scores reset to zero. */
__('Threat score decay process ran. Reduced: %1$d IPs, Reset: %2$d IPs.', 'advanced-ip-blocker'),
$result['updated'],
$result['deleted']
);
$this->log_event($log_message, 'info');
}
}
}
/**
* Callback para el evento de cron que ejecuta el análisis de firmas de ataque.
*/
public function execute_signature_analysis() {
if (empty($this->options['enable_signature_analysis'])) {
return;
}
$ip_threshold = (int) ($this->options['signature_ip_threshold'] ?? 5);
$analysis_window_hours = (int) ($this->options['signature_analysis_window'] ?? 1);
$rule_ttl_hours = (int) ($this->options['signature_rule_ttl'] ?? 24);
$this->fingerprint_manager->analyze_and_flag_signatures(
$ip_threshold,
$analysis_window_hours * HOUR_IN_SECONDS,
$rule_ttl_hours * HOUR_IN_SECONDS
);
}
/**
* Registra el endpoint de la API REST para el feed de ataques en vivo.
*/
public function register_live_feed_api_endpoint() {
register_rest_route('advaipbl/v1', '/live-attacks', [
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_live_attacks_for_feed'],
'permission_callback' => function( $request ) {
// El nonce puede venir como un header o como un parámetro GET/POST
$nonce = $request->get_header('x-wp-nonce') ?? $request->get_param('nonce');
return wp_verify_nonce( $nonce, 'wp_rest' );
},
'args' => [
'since' => [
'description' => 'Get attacks since this log ID.',
'type' => 'integer',
'sanitize_callback' => 'absint',
],
],
]);
}
/**
* Callback para el endpoint de la API. Devuelve los últimos ataques.
*
* @param WP_REST_Request $request
* @return WP_REST_Response
*/
public function get_live_attacks_for_feed(WP_REST_Request $request) {
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_logs';
$since_id = $request->get_param('since');
$sql = "SELECT log_id, timestamp, ip, log_type, message, details
FROM {$table_name}
WHERE level = 'critical' AND log_type != 'general'";
$params = [];
if ($since_id) {
$sql .= " AND log_id > %d";
$params[] = $since_id;
}
$sql .= " ORDER BY log_id DESC LIMIT 20";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$results = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
$attacks = [];
$last_id = 0;
if (!empty($results)) {
$last_id = (int) $results[0]['log_id'];
foreach ($results as $row) {
$details_data = json_decode($row['details'], true) ?: [];
$location = 'Unknown Location';
if (!empty($details_data['city']) && !empty($details_data['country'])) {
$location = $details_data['city'] . ', ' . $details_data['country'];
} elseif (!empty($details_data['country'])) {
$location = $details_data['country'];
}
$details_message = $row['message']; // Mensaje por defecto
$log_type = $row['log_type'];
// Construimos un mensaje de detalles más descriptivo para tipos específicos
switch ($log_type) {
case 'abuseipdb':
$score = $details_data['abuse_score'] ?? 'N/A';
/* translators: %d: The abuse confidence score from AbuseIPDB. */
$details_message = sprintf(__('Blocked by AbuseIPDB with a score of %d%%.', 'advanced-ip-blocker'), $score);
break;
case 'advanced_rule':
// Para reglas avanzadas, el "reason" del bloqueo es el mensaje más descriptivo.
$block_entry = $wpdb->get_row($wpdb->prepare("SELECT reason FROM {$wpdb->prefix}advaipbl_blocked_ips WHERE ip_range = %s", $row['ip']), ARRAY_A);
if ($block_entry && !empty($block_entry['reason'])) {
$details_message = $block_entry['reason'];
}
break;
case 'waf':
$rule_triggered = $details_data['rule'] ?? 'Unknown Rule';
/* translators: %s: The WAF rule that was triggered. */
$details_message = sprintf(__('Triggered WAF rule: %s', 'advanced-ip-blocker'), $rule_triggered);
break;
case 'asn':
$asn_number = $details_data['asn_number'] ?? 'N/A';
/* translators: %s: The ASN that was blocked. */
$details_message = sprintf(__('Blocked ASN: %s', 'advanced-ip-blocker'), $asn_number);
break;
}
$attacks[] = [
'id' => (int) $row['log_id'],
'time' => human_time_diff(strtotime($row['timestamp'])) . ' ago',
'ip' => esc_html($row['ip']),
'type' => esc_html(ucwords(str_replace('_', ' ', $log_type))),
'location' => esc_html($location),
'method' => esc_html($details_data['method'] ?? 'N/A'),
'details' => esc_html($details_message),
'user_agent' => esc_html($details_data['user_agent'] ?? 'N/A'),
'uri' => esc_html($details_data['uri'] ?? ($details_data['url'] ?? '')),
];
}
}
$response_data = [
'attacks' => $attacks,
'last_id' => $last_id,
];
return new WP_REST_Response($response_data, 200);
}
/**
* Endpoint de la API REST para obtener un nonce fresco para el feed en vivo.
* Esto evita problemas de caché de página.
*/
public function get_live_feed_nonce() {
return new WP_REST_Response( [
'nonce' => wp_create_nonce( 'wp_rest' )
], 200 );
}
/**
* Función del shortcode [advaipbl_live_feed].
* Genera el HTML y el CSS necesario para el feed, y encola el script JS.
*
* @param array $atts Atributos del shortcode.
* @return string El HTML y CSS para el feed.
*/
public function render_live_feed_shortcode($atts) {
wp_enqueue_script('advaipbl-live-feed-js', plugin_dir_url(dirname(__FILE__)) . 'js/advaipbl-live-feed.js', ['jquery'], ADVAIPBL_VERSION, true);
wp_localize_script('advaipbl-live-feed-js', 'advaipbl_feed_data', [
'api_url' => get_rest_url(null, 'advaipbl/v1/live-attacks'),
'nonce_url' => get_rest_url(null, 'advaipbl/v1/live-feed-nonce'),
'text' => [
'blocked_from' => __('blocked from', 'advanced-ip-blocker'),
'type' => __('Type', 'advanced-ip-blocker'),
'method' => __('Method', 'advanced-ip-blocker'),
'details' => __('Details', 'advanced-ip-blocker'),
'uri' => __('URI', 'advanced-ip-blocker'),
'user_agent' => __('User Agent', 'advanced-ip-blocker'),
]
]);
$logo_url_desktop = plugin_dir_url(dirname(__FILE__)) . 'assets/img/logo-ligth.png';
$logo_url_mobile = plugin_dir_url(dirname(__FILE__)) . 'assets/img/icon-256x256.png';
$css = '';
$html = '
⬤
Live Security Feed
Awaiting security events...
';
return $css . $html;
}
/**
* Detecta y registra las ejecuciones de WP-Cron.
*/
public function log_wp_cron_execution() {
$is_doing_cron = ( defined( 'DOING_CRON' ) && DOING_CRON );
$is_cron_url = ( strpos($this->get_current_request_uri(), 'wp-cron.php') !== false );
if ( $is_doing_cron || $is_cron_url ) {
$ip = $this->get_client_ip();
$transient_key = 'advaipbl_cron_log_lock_' . md5($ip);
if ( false !== get_transient($transient_key) ) {
return;
}
set_transient($transient_key, true, 60);
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_logs';
$cron_array = _get_cron_array();
$due_hooks = [];
if ( is_array( $cron_array ) ) {
$current_time = time();
foreach ( $cron_array as $timestamp => $hooks ) {
if ( $timestamp <= $current_time ) {
foreach ( $hooks as $hook_name => $events ) {
$due_hooks[] = $hook_name;
}
}
}
}
$due_hooks = array_unique( $due_hooks );
$server_ip = $this->get_server_ip();
$source = 'External'; // Por defecto, asumimos que es externa.
if ( $ip === $server_ip || $ip === '127.0.0.1' || $ip === '::1' ) {
$source = 'Server'; // Si coincide con la IP del servidor o localhost, es interna.
}
$details = [
'url' => $this->get_current_request_uri(),
'uri' => $this->get_current_request_uri(),
'method' => $this->get_request_method(),
'user_agent' => $this->get_user_agent(),
'due_hooks' => $due_hooks,
'source' => $source, // Añadimos la nueva información
];
@$wpdb->insert(
$table_name,
[
'timestamp' => current_time('mysql', 1),
'ip' => $ip,
'log_type' => 'wp_cron',
'level' => 'info',
'message' => __('WP-Cron execution triggered.', 'advanced-ip-blocker'),
'details' => wp_json_encode($details)
]
);
}
}
/**
* Enqueues the Google reCAPTCHA API script on the login page.
*/
public function enqueue_recaptcha_script() {
$version = $this->options['recaptcha_version'] ?? 'v3';
$site_key = $this->options['recaptcha_site_key'] ?? '';
// La comprobación de si la clave está vacía ya se hace en add_hooks,
// pero una doble comprobación no hace daño.
if (empty($site_key)) {
return;
}
$script_url = 'https://www.google.com/recaptcha/api.js';
if ('v3' === $version) {
$script_url = add_query_arg('render', $site_key, $script_url);
}
wp_enqueue_script(
'google-recaptcha',
$script_url,
array(),
null,
true
);
}
/**
* Se dispara cuando las reglas del WAF son actualizadas desde la página de ajustes.
* Registra el evento en el log general.
*
* @param mixed $old_value El valor antiguo de la opción.
* @param mixed $new_value El nuevo valor de la opción.
*/
public function on_waf_rules_update($old_value, $new_value) {
// Solo registramos si el valor ha cambiado realmente.
if ($old_value !== $new_value) {
$message = sprintf(
/* translators: %s: El nombre de usuario del administrador que realizó el cambio. */
__('WAF rules list updated by %s.', 'advanced-ip-blocker'),
$this->get_current_admin_username()
);
$this->log_event($message, 'info');
}
}
/**
* Descarga y procesa la lista Spamhaus ASN DROP y la guarda en la base de datos.
* Es llamada por el WP-Cron y por la acción de refresco manual.
*/
public function update_spamhaus_list() {
$url = 'https://www.spamhaus.org/drop/asndrop.json';
$response = wp_remote_get($url, ['timeout' => 15]);
if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
$error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code($response);
$this->log_event(sprintf('Failed to download Spamhaus ASN list. Error: %s', $error_message), 'error');
return;
}
$body_string = wp_remote_retrieve_body($response);
// El archivo de Spamhaus es una secuencia de objetos JSON, no un único array JSON.
// Lo dividimos en objetos individuales.
// Usamos una expresión regular para encontrar cada objeto JSON { ... }
preg_match_all('/\{.*?\}/', $body_string, $matches);
if (empty($matches[0])) {
$this->log_event('Failed to parse Spamhaus ASN list: No valid JSON objects found in the response.', 'error');
return;
}
$json_objects = $matches[0];
$asns = [];
foreach ($json_objects as $json_string) {
$entry = json_decode($json_string, true);
if (json_last_error() === JSON_ERROR_NONE && isset($entry['asn'])) {
// El formato es correcto y tiene la clave 'asn'.
$asns[] = 'AS' . $entry['asn'];
}
}
if (empty($asns)) {
$this->log_event('Spamhaus ASN list was parsed but no ASN entries were extracted.', 'warning');
return;
}
update_option('advaipbl_spamhaus_asn_list', array_unique($asns));
update_option('advaipbl_spamhaus_last_update', time());
$this->log_event(sprintf('Successfully updated Spamhaus ASN list with %d entries.', count($asns)), 'info');
}
public function handle_spamhaus_refresh_action() {
if (!current_user_can('manage_options') || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'] ?? '')), 'advaipbl-refresh-spamhaus')) {
wp_die('Security check failed.');
}
$this->update_spamhaus_list();
set_transient(self::TRANSIENT_ADMIN_NOTICE, ['message' => __('Spamhaus ASN list has been updated.', 'advanced-ip-blocker'), 'type' => 'success'], 30);
wp_safe_redirect(wp_get_referer());
exit;
}
/**
* Displays the reCAPTCHA field on the login form.
*/
public function display_recaptcha_field() {
$site_key = $this->options['recaptcha_site_key'] ?? '';
$version = $this->options['recaptcha_version'] ?? 'v3';
if ('v2' === $version) {
echo '';
} else {
echo '';
?>
options['recaptcha_secret_key'] ?? '';
// La comprobación principal ya se hace en add_hooks,
// pero si alguien modificara la opción mientras tanto, esto es un seguro.
if (empty($secret_key)) {
return $user;
}
if (!isset($_POST['g-recaptcha-response']) || empty($_POST['g-recaptcha-response'])) {
return new WP_Error('recaptcha_empty', __('ERROR: Please complete the reCAPTCHA verification.', 'advanced-ip-blocker'));
}
$token = sanitize_text_field($_POST['g-recaptcha-response']);
$visitor_ip = $this->get_client_ip();
$response = wp_remote_post('https://www.google.com/recaptcha/api/siteverify', array(
'body' => array(
'secret' => $secret_key,
'response' => $token,
'remoteip' => $visitor_ip,
),
));
if (is_wp_error($response)) {
return new WP_Error('recaptcha_api_error', __('ERROR: Could not connect to the reCAPTCHA service.', 'advanced-ip-blocker'));
}
$result = json_decode(wp_remote_retrieve_body($response), true);
if (!isset($result['success']) || true !== $result['success']) {
return new WP_Error('recaptcha_failed', __('ERROR: reCAPTCHA verification failed. Please try again.', 'advanced-ip-blocker'));
}
$version = $this->options['recaptcha_version'] ?? 'v3';
if ('v3' === $version) {
$threshold = (float) ($this->options['recaptcha_score_threshold'] ?? 0.5);
if (!isset($result['score']) || $result['score'] < $threshold) {
return new WP_Error('recaptcha_low_score', __('ERROR: Your action was blocked as it was flagged as automated.', 'advanced-ip-blocker'));
}
}
return $user;
}
/**
* Detecta plugins activos que probablemente usan la interfaz XML-RPC.
*
* Utiliza una combinación de una lista de plugins conocidos y un análisis de los hooks de XML-RPC.
*
* @return array Un array con los nombres de los plugins detectados. Vacío si no se encuentra ninguno.
*/
public function get_xmlrpc_dependent_plugins() {
$dependent_plugins = [];
$active_plugins = get_option('active_plugins');
// 1. Lista de plugins conocidos que dependen de XML-RPC.
$known_slugs = [
'jetpack/jetpack.php',
// Si encuentras más en el futuro, como la app de WooCommerce si usa un plugin puente.
'xmlrpc-debugger/xmlrpc-debugger.php'
];
foreach ($known_slugs as $slug) {
if (in_array($slug, $active_plugins, true)) {
$plugin_path = WP_PLUGIN_DIR . '/' . $slug;
if (file_exists($plugin_path)) {
$plugin_data = get_plugin_data($plugin_path);
if (!empty($plugin_data['Name'])) {
$dependent_plugins[$slug] = $plugin_data['Name'];
}
}
}
}
// 2. Análisis de Hooks (Heurística).
// Buscamos plugins que se enganchen a los eventos de XML-RPC.
global $wp_filter;
$xmlrpc_hooks = [
'xmlrpc_methods',
'xmlrpc_call',
'xmlrpc_call_success',
'xmlrpc_call_failure',
'xmlrpc_before_insert_post',
];
foreach ($xmlrpc_hooks as $hook_name) {
if (isset($wp_filter[$hook_name])) {
foreach ($wp_filter[$hook_name]->callbacks as $priority => $callbacks) {
foreach ($callbacks as $callback) {
$function = $callback['function'];
$reflection = null;
try {
if (is_array($function) && is_object($function[0])) {
$reflection = new ReflectionClass($function[0]);
} elseif (is_string($function) && function_exists($function)) {
$reflection = new ReflectionFunction($function);
}
} catch (ReflectionException $e) {
// Ignorar funciones/clases que no se pueden reflejar (ej. closures)
continue;
}
if ($reflection) {
$file_path = $reflection->getFileName();
// Comprobamos si la ruta del archivo está dentro del directorio de plugins.
if ($file_path && strpos($file_path, WP_PLUGIN_DIR) !== false) {
$plugin_dir_path = str_replace('\\', '/', WP_PLUGIN_DIR);
$file_path = str_replace('\\', '/', $file_path);
$relative_path = ltrim(str_replace($plugin_dir_path, '', $file_path), '/');
$plugin_slug_parts = explode('/', $relative_path);
$plugin_folder = $plugin_slug_parts[0];
// Buscamos el nombre real del plugin para no mostrar solo el slug.
foreach ($active_plugins as $active_plugin_slug) {
if (strpos($active_plugin_slug, $plugin_folder) === 0) {
if (!isset($dependent_plugins[$active_plugin_slug])) {
$plugin_data = get_plugin_data(WP_PLUGIN_DIR . '/' . $active_plugin_slug);
if (!empty($plugin_data['Name'])) {
$dependent_plugins[$active_plugin_slug] = $plugin_data['Name'];
}
}
break;
}
}
}
}
}
}
}
}
return array_values($dependent_plugins);
}
/**
* Obtiene el número de IPs/rangos bloqueados.
* Cachea el resultado para mejorar el rendimiento.
*
* @return int El número total de entradas de IP bloqueadas.
*/
public function get_blocked_count() {
if ( null !== $this->blocked_count ) {
return $this->blocked_count;
}
// Usamos la caché de objetos de WP para un mejor rendimiento
$count = wp_cache_get('blocked_ips_count', 'advaipbl');
if (false === $count) {
$this->limpiar_ips_expiradas(); // La limpieza es crucial antes de contar
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_blocked_ips';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$count = (int) $wpdb->get_var("SELECT COUNT(id) FROM {$table_name}");
wp_cache_set('blocked_ips_count', $count, 'advaipbl', 300); // Cache por 5 minutos
}
$this->blocked_count = $count;
return $this->blocked_count;
}
/**
* Obtiene el número de firmas de ataque activas.
* @return int
*/
public function get_blocked_signatures_count() {
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_malicious_signatures';
// Usamos cache para evitar consultas repetidas en la misma petición
$count = wp_cache_get('blocked_signatures_count', 'advaipbl');
if (false === $count) {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$count = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(id) FROM {$table_name} WHERE expires_at > %d", time()));
wp_cache_set('blocked_signatures_count', $count, 'advaipbl', 300); // Cache por 5 minutos
}
return $count;
}
/**
* Obtiene el número de endpoints actualmente en lockdown.
* @return int
*/
public function get_blocked_endpoints_count() {
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_endpoint_lockdowns';
$count = wp_cache_get('blocked_endpoints_count', 'advaipbl');
if (false === $count) {
// Primero, limpiamos los expirados para un conteo preciso
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query($wpdb->prepare("DELETE FROM {$table_name} WHERE expires_at <= %d", time()));
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$count = (int) $wpdb->get_var("SELECT COUNT(id) FROM {$table_name}");
wp_cache_set('blocked_endpoints_count', $count, 'advaipbl', 300); // Cache por 5 minutos
}
return $count;
}
/**
* Gets the username of the current logged-in administrator.
*
* @return string The username or a default string if not available.
*/
public function get_current_admin_username() {
$user = wp_get_current_user();
// Si el usuario existe y tiene un ID, devuelve su login. Si no, devuelve un texto genérico.
return ( $user && $user->ID ) ? $user->user_login : __( 'an unknown user', 'advanced-ip-blocker' );
}
/**
* Añade una entrada (IP o rango) a la whitelist y se asegura de que sea eliminada
* de todas las listas de bloqueo. Este es el método centralizado para esta acción.
*
* @param string $entry_to_whitelist La IP o rango a añadir a la whitelist.
* @param string $detail Una descripción de por qué se añadió.
* @return bool True si la entrada se añadió con éxito, false si ya existía.
*/
public function add_to_whitelist_and_unblock( $entry_to_whitelist, $detail ) {
if ( ! $this->is_valid_ip_or_range( $entry_to_whitelist ) ) {
return false;
}
$whitelist = get_option( self::OPTION_WHITELIST, [] );
if ( array_key_exists( $entry_to_whitelist, $whitelist ) ) {
return false;
}
$whitelist[ $entry_to_whitelist ] = [ 'timestamp' => time(), 'detail' => $detail ];
update_option( self::OPTION_WHITELIST, $whitelist );
wp_cache_delete( self::OPTION_WHITELIST, 'options' );
// FIX: Always unblock from DB/Htaccess/Cloudflare (IPs AND Ranges)
$this->desbloquear_ip( $entry_to_whitelist );
// Legacy Cleanup: Only for ranges (to clean old wp_options arrays if they exist)
if ( ! filter_var( $entry_to_whitelist, FILTER_VALIDATE_IP ) ) {
// Usar el mapa de constantes para limpiar el rango de TODAS las listas.
$option_key_map = [
'geoblock' => self::OPTION_BLOCKED_GEO,
'honeypot' => self::OPTION_BLOCKED_HONEYPOT,
'user_agent' => self::OPTION_BLOCKED_USER_AGENT,
'manual' => self::OPTION_BLOCKED_MANUAL,
'404' => self::OPTION_BLOCKED_404,
'403' => self::OPTION_BLOCKED_403,
'login' => self::OPTION_BLOCKED_LOGIN,
'waf' => self::OPTION_BLOCKED_WAF,
'rate_limit' => self::OPTION_BLOCKED_RATE_LIMIT,
'asn' => self::OPTION_BLOCKED_ASN,
'xmlrpc_block' => self::OPTION_BLOCKED_XMLRPC,
'threat_score' => self::OPTION_BLOCKED_THREAT_SCORE,
];
foreach ( $option_key_map as $type => $option_key ) {
$list = get_option( $option_key, [] );
if ( is_array($list) && array_key_exists( $entry_to_whitelist, $list ) ) {
unset( $list[ $entry_to_whitelist ] );
update_option( $option_key, $list );
}
}
}/* translators: 1: The whitelisted IP/range, 2: The admin username. */
$this->log_event( sprintf( __( 'Entry %1$s added to whitelist by %2$s.', 'advanced-ip-blocker' ), $entry_to_whitelist, $this->get_current_admin_username() ), 'info', is_array($entry_to_whitelist) ? wp_json_encode($entry_to_whitelist) : $entry_to_whitelist );
return true;
}
public function handle_clear_cache_action() {
if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'advaipbl_clear_location_cache_nonce')) {
wp_die('Invalid nonce.');
}
if (!current_user_can('manage_options')) {
wp_die('Permission denied.');
}
// Vaciamos la tabla de caché.
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_cache';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query("TRUNCATE TABLE `{$table_name}`");
/* translators: %s: Admin username. */
$this->log_event( sprintf( __( 'User session location cache manually cleared by %s.', 'advanced-ip-blocker' ), $this->get_current_admin_username() ), 'info' );
set_transient('advaipbl_admin_notice', ['message' => __('Location cache has been cleared.', 'advanced-ip-blocker'), 'type' => 'success'], 30);
wp_safe_redirect(wp_get_referer());
exit;
}
/**
* Envuelve el contenido de un email en una plantilla HTML básica y profesional.
*
* @param string $subject El asunto del email.
* @param string $content_html El contenido principal del email en HTML.
* @return string El cuerpo completo del email en HTML.
*/
private function get_html_email_template( $subject, $content_html ) {
$site_name = get_bloginfo( 'name' );
$site_url = home_url();
$settings_url = admin_url( 'options-general.php?page=advaipbl_settings_page' );
$logo_url = plugin_dir_url( dirname( __FILE__ ) ) . 'assets/img/logo-email.png';
// Estilos CSS en línea
$body_style = "background-color: #f6f7f8; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';";
$wrapper_style = "background-color: #ffffff; border: 1px solid #e0e0e0; border-radius: 4px; padding: 40px; max-width: 600px; margin: 0 auto;";
$logo_wrapper_style = "text-align: center; margin-bottom: 25px;";
// Estilos flexibles para el logo
$logo_style = "max-width: 220px; height: auto; border: 0; display: inline-block; vertical-align: middle;";
$header_style = "font-size: 24px; font-weight: bold; color: #2271b1; margin-top: 0; margin-bottom: 20px; text-align: center;";
$footer_style = "font-size: 12px; color: #777777; text-align: center; margin-top: 30px; line-height: 1.6; border-top: 1px solid #e0e0e0; padding-top: 20px;";
ob_start();
?>
options['notification_email'] ) && is_email( $this->options['notification_email'] ) ? $this->options['notification_email'] : get_option( 'admin_email' );
$site_name = get_bloginfo( 'name' );
$settings_url = admin_url( 'options-general.php?page=advaipbl_settings_page' );
// Asunto para la línea "Subject:" del email (largo y descriptivo).
/* translators: %s: The name of the website. */
$email_subject = sprintf( __( '[%s] Welcome & Quick Setup Guide for Advanced IP Blocker', 'advanced-ip-blocker' ), $site_name );
// Título para el
dentro del cuerpo del email (corto y limpio).
$template_title = __( 'Welcome & Quick Setup Guide', 'advanced-ip-blocker' );
// 3. Construir el contenido HTML del email
$content_html =
'
' . esc_html__( 'Thank you for using Advanced IP Blocker! This email confirms your email settings are working. Here is a quick guide to get the most out of the plugin:', 'advanced-ip-blocker' ) . '
' .
sprintf(
wp_kses( /* translators: %s: a link to the Status & Debug tab. */
__( 'This is the most important step to prevent locking yourself out. Go to the Status & Debug tab to confirm that your IP address and your server\'s IP are correctly detected. If they are not on the whitelist, add them with a single click.', 'advanced-ip-blocker' ),
[ 'a' => [ 'href' => [] ] ]
),
esc_url( add_query_arg( 'tab', 'status', $settings_url ) )
) .
'
' .
sprintf(
wp_kses( /* translators: %s: a link to the User Agents tab. */
__( 'Go to the User Agents tab and copy the suggested list of malicious bots into the "Blocked" text area.', 'advanced-ip-blocker' ),
[ 'a' => [ 'href' => [] ] ]
),
esc_url( add_query_arg( 'tab', 'user_agents', $settings_url ) )
) . ' ' .
sprintf(
wp_kses( /* translators: %s: a link to the Honeypot URLs tab. */
__( 'Then, go to the Honeypot URLs tab and do the same. This will immediately block thousands of common automated attacks.', 'advanced-ip-blocker' ),
[ 'a' => [ 'href' => [] ] ]
),
esc_url( add_query_arg( 'tab', 'honeypot', $settings_url ) )
) .
'
' .
sprintf(
wp_kses( /* translators: %s: a link to the main Settings tab. */
__( 'Finally, visit the main Settings tab to enable logging and configure notification frequency to your liking. The default blocking rules are a great starting point for most sites.', 'advanced-ip-blocker' ),
[ 'a' => [ 'href' => [] ] ]
),
esc_url( add_query_arg( 'tab', 'general_settings', $settings_url ) )
) .
'
';
// 4. Envolver en la plantilla y enviar (usando las nuevas variables)
$body = $this->get_html_email_template( $template_title, $content_html );
add_filter( 'wp_mail_content_type', [ $this, 'set_html_mail_content_type' ] );
$sent = wp_mail( $to, $email_subject, $body );
remove_filter( 'wp_mail_content_type', [ $this, 'set_html_mail_content_type' ] );
// 5. Lógica de notificación y redirección
if ( $sent ) {
$message = __( 'The setup guide has been sent successfully to your configured email address.', 'advanced-ip-blocker' );
$type = 'success';
/* translators: 1: Recipient's email address, 2: Admin username */
$this->log_event( sprintf( __( 'Setup guide sent to %1$s by %2$s.', 'advanced-ip-blocker' ), $to, $this->get_current_admin_username() ), 'info' );
} else {
$message = __( 'Failed to send the setup guide email. Please check your site\'s email configuration.', 'advanced-ip-blocker' );
$type = 'error';
/* translators: 1: Recipient's email address, 2: Admin username */
$this->log_event( sprintf( __( 'FAILED to send setup guide to %1$s. Action by %2$s.', 'advanced-ip-blocker' ), $to, $this->get_current_admin_username() ), 'error' );
}
set_transient( 'advaipbl_admin_notice', [ 'message' => $message, 'type' => $type ], 30 );
wp_safe_redirect( wp_get_referer() );
exit;
}
public function load_admin_scripts($hook) {
// Cargar scripts en nuestras páginas Y en las páginas de perfil/edición de usuario.
$allowed_hooks = ['profile.php', 'user-edit.php'];
$is_plugin_page = strpos($hook, 'advaipbl_settings_page') !== false || strpos($hook, 'advaipbl-setup-wizard') !== false;
if ( !in_array($hook, $allowed_hooks) && !$is_plugin_page ) {
return;
}
// --- 1. Determinar la pestaña y sub-pestaña activas ---
$active_main_tab = isset($_GET['tab']) ? sanitize_key($_GET['tab']) : 'dashboard';
$active_sub_tab = isset($_GET['sub-tab']) ? sanitize_key($_GET['sub-tab']) : null;
// Si no hay sub-pestaña en la URL, la deducimos de la pestaña principal.
if (is_null($active_sub_tab)) {
switch ($active_main_tab) {
case 'dashboard':
$active_sub_tab = 'main_dashboard';
break;
case 'settings':
$active_sub_tab = 'general_settings';
break;
case 'rules':
$active_sub_tab = 'waf';
break;
case 'ip_management':
$active_sub_tab = 'blocked_ips';
break;
case 'logs':
$active_sub_tab = 'security_log';
break;
case 'about':
$active_sub_tab = 'credits';
break;
default:
$active_sub_tab = 'main_dashboard';
}
}
// --- 2. Cargar Assets Comunes (siempre necesarios en nuestras páginas) ---
if ($is_plugin_page || in_array($hook, $allowed_hooks)) {
$this->session_manager->enqueue_scripts_styles(); // Asumiendo que es necesario en el perfil también.
wp_enqueue_style( 'advaipbl-styles-main', plugin_dir_url( dirname( __FILE__ ) ) . 'css/advaipbl-styles.css', [], ADVAIPBL_VERSION );
wp_enqueue_style( 'advaipbl-select2-css', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/css/select2.min.css', [], '4.1.0-rc.0' );
wp_enqueue_script( 'advaipbl-select2-js', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/js/select2.min.js', [ 'jquery' ], '4.1.0-rc.0', true );
wp_enqueue_script( 'advaipbl-admin-js', plugin_dir_url( dirname( __FILE__ ) ) . 'js/advaipbl-admin.js', [ 'jquery', 'advaipbl-select2-js' ], ADVAIPBL_VERSION, true );
$floating_bar_css = " #advaipbl-floating-save-bar { position: fixed; bottom: 0; left: 160px; right: 0; background-color: #fff; box-shadow: 0 -2px 5px rgba(0,0,0,0.1); padding: 15px 30px; z-index: 999; transition: transform 0.3s ease-in-out; transform: translateY(100%); } #advaipbl-floating-save-bar.advaipbl-save-bar-visible { transform: translateY(0); } .advaipbl-save-bar-content { display: flex; justify-content: space-between; align-items: center; max-width: 1200px; margin: 0 auto; } .advaipbl-save-bar-text { font-size: 1.1em; font-weight: 600; } .advaipbl-save-bar-buttons .button { margin-left: 10px; } @media screen and (max-width: 960px) { #advaipbl-floating-save-bar { left: 36px; } } @media screen and (max-width: 782px) { #advaipbl-floating-save-bar { left: 0; padding: 10px 15px; } .advaipbl-save-bar-text { display: none; } .advaipbl-save-bar-content { justify-content: flex-end; } }";
wp_add_inline_style('advaipbl-styles-main', $floating_bar_css);
}
// --- 3. Cargar Assets Específicos (solo cuando sea necesario) ---
if ($is_plugin_page) {
// Cargar assets del Dashboard Principal
if ( 'main_dashboard' === $active_sub_tab ) {
wp_enqueue_script('chartjs', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/js/chart.min.js', [], '3.9.1', true);
wp_enqueue_style('leaflet-css', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/css/leaflet.css');
wp_enqueue_script('leaflet-js', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/js/leaflet.js', [], '1.9.4', true);
wp_enqueue_style('leaflet-markercluster-css', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/css/MarkerCluster.css');
wp_enqueue_style('leaflet-markercluster-default-css', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/css/MarkerCluster.Default.css');
wp_enqueue_script('leaflet-markercluster-js', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/js/leaflet.markercluster.js', ['leaflet-js'], '1.5.3', true);
wp_enqueue_script('advaipbl-dashboard-js', plugin_dir_url( dirname( __FILE__ ) ) . 'js/advaipbl-dashboard.js', ['jquery', 'chartjs', 'leaflet-markercluster-js'], ADVAIPBL_VERSION, true);
}
// Cargar assets de la página "About"
if ('credits' === $active_sub_tab) {
wp_enqueue_script('stripe-buy-button', 'https://js.stripe.com/v3/buy-button.js', [], null, true);
}
}
// --- 4. Localizar Datos para JS (siempre necesario en nuestras páginas) ---
if ($is_plugin_page) {
// --- Procesar información del servidor ---
$server_ip = $this->get_server_ip();
$server_info = [ 'ip' => $server_ip, 'country_code' => '', 'country_name' => '', 'is_whitelisted' => false ];
if ( $server_ip ) {
$location = $this->geolocation_manager->fetch_location( $server_ip );
if ( $location && empty( $location['error'] ) ) {
$server_info['country_code'] = $location['country_code'] ?? '';
$server_info['country_name'] = $location['country'] ?? '';
}
$server_info['is_whitelisted'] = $this->is_whitelisted( $server_ip );
}
// --- Procesar información del administrador (usuario actual) ---
$admin_ip = $this->get_client_ip();
$admin_info = [ 'ip' => $admin_ip, 'country_code' => '', 'country_name' => '', 'is_whitelisted' => false ];
if ( $admin_ip && filter_var( $admin_ip, FILTER_VALIDATE_IP ) ) {
$location = $this->geolocation_manager->fetch_location( $admin_ip );
if ( $location && empty( $location['error'] ) ) {
$admin_info['country_code'] = $location['country_code'] ?? '';
$admin_info['country_name'] = $location['country'] ?? '';
}
$admin_info['is_whitelisted'] = $this->is_whitelisted( $admin_ip );
}
// --- Localizar scripts con datos para el frontend ---
wp_localize_script('advaipbl-admin-js', 'advaipbl_admin_data', [
'counts' => [
'blocked' => count( $this->get_all_blocked_entries() ),
],
'geoblock' => [
'server' => $server_info,
'admin' => $admin_info,
'blocked_countries' => $this->options['geoblock_countries'] ?? [],
'challenged_countries' => $this->options['geo_challenge_countries'] ?? [],
],
'nonces' => [
'test_connection' => wp_create_nonce( 'advaipbl_test_connection_nonce' ),
'add_whitelist' => wp_create_nonce( 'advaipbl_add_whitelist_nonce' ),
'export' => wp_create_nonce( 'advaipbl_export_nonce' ),
'verify_api' => wp_create_nonce( 'advaipbl_verify_api_nonce' ),
'get_dashboard_stats' => wp_create_nonce( 'wp_ajax_advaipbl_get_dashboard_stats' ),
'telemetry' => wp_create_nonce('advaipbl_telemetry_nonce'),
'reset_score' => wp_create_nonce('advaipbl_reset_score_nonce'),
'get_history' => wp_create_nonce('advaipbl_get_history_nonce'),
'delete_signature' => wp_create_nonce('advaipbl_delete_signature_nonce'),
'get_signature_details' => wp_create_nonce('advaipbl_get_signature_details_nonce'),
'whitelist_signature' => wp_create_nonce('advaipbl_whitelist_signature_nonce'),
'get_lockdown_details' => wp_create_nonce('advaipbl_get_lockdown_details_nonce'),
'get_rules_nonce' => wp_create_nonce('advaipbl_get_rules_nonce'),
'save_rule_nonce' => wp_create_nonce('advaipbl_save_rule_nonce'),
'delete_rule_nonce' => wp_create_nonce('advaipbl_delete_rule_nonce'),
'bulk_delete_rules_nonce' => wp_create_nonce('advaipbl_bulk_delete_rules_nonce'),
'reorder_rules_nonce' => wp_create_nonce('advaipbl_reorder_rules_nonce'),
'verify_abuseipdb' => wp_create_nonce('advaipbl_verify_abuseipdb_nonce'),
],
'text' => [ /* translators: 1: Country, 2:IP. */
'server_whitelisted' => __( 'Info: Your server is located in %1$s and its IP (%2$s) is correctly whitelisted. You can safely block this country.', 'advanced-ip-blocker' ),
/* translators: 1: Country, 2:IP. */
'server_not_whitelisted' => __( 'CRITICAL WARNING! Your server is located in %1$s and its IP (%2$s) is NOT whitelisted. Blocking this country WILL likely break your site. Please add the server IP to the whitelist BEFORE blocking this country.', 'advanced-ip-blocker' ),
/* translators: 1: IP, 2:Country. */
'admin_whitelisted' => __( 'Info: Your current IP address (%1$s) from %2$s is whitelisted. You will not be locked out if you block this country.', 'advanced-ip-blocker' ),
/* translators: 1: IP, 2:Country. */
'admin_not_whitelisted' => __( 'Caution: Your current IP address (%1$s) from %2$s is NOT whitelisted. If you block this country, you may lose access to your admin panel.', 'advanced-ip-blocker' ),
/* translators: 1: IP, 2:Country. */
'remove_server_ip_warning' => __( 'CRITICAL WARNING: This IP (%1$s) belongs to your server, located in a blocked country (%2$s). REMOVING IT FROM THE WHITELIST WILL LIKELY BREAK YOUR SITE!', 'advanced-ip-blocker' ),
/* translators: 1: IP, 2:Country. */
'remove_admin_ip_warning' => __( 'CAUTION: This is your current IP address (%1$s) from a blocked country (%2$s). REMOVING THIS IP FROM THE WHITELIST MAY LOCK YOU OUT OF YOUR ADMIN PANEL.', 'advanced-ip-blocker' ),
'confirm_removal' => __( 'Are you absolutely sure you want to proceed?', 'advanced-ip-blocker' ),
'add_to_whitelist_btn' => __( 'Add to Whitelist', 'advanced-ip-blocker' ),
'adding_to_whitelist' => __( 'Adding...', 'advanced-ip-blocker' ),
'added_to_whitelist' => __( 'Successfully Added!', 'advanced-ip-blocker' ),
'select2_placeholder_block' => __( 'Search for a country to block...', 'advanced-ip-blocker' ),
'select2_placeholder_challenge' => __( 'Search for a country to challenge...', 'advanced-ip-blocker' ),
'verify_api_button' => __( 'Verify', 'advanced-ip-blocker' ),
'verifying_api' => __( 'Verifying...', 'advanced-ip-blocker' ),
'enter_api_key' => __( 'Please enter an API key.', 'advanced-ip-blocker' ),
'ajax_error' => __( 'AJAX error. Check browser console.', 'advanced-ip-blocker' ),
'discard_title' => __( 'Discard Changes?', 'advanced-ip-blocker' ),
'discard_message' => __( 'You have unsaved changes. Are you sure you want to discard them?', 'advanced-ip-blocker' ),
'discard_confirm_btn' => __( 'Yes, Discard', 'advanced-ip-blocker' ),
'attacks_label' => __( 'attacks', 'advanced-ip-blocker' ),
'blocks_label' => __( 'blocks', 'advanced-ip-blocker' ),
'confirm_bulk_action_title' => __( 'Confirm Bulk Action', 'advanced-ip-blocker' ),
/* translators: %d: The number IPs that were unblocked. */
'confirm_bulk_unblock_message' => __( 'Are you sure you want to unblock the selected %d entries? This action cannot be undone.', 'advanced-ip-blocker' ),
'confirm_bulk_unblock_button' => __( 'Yes, Unblock Selected', 'advanced-ip-blocker' ),
'alert_no_action' => __( 'Please select a bulk action.', 'advanced-ip-blocker' ),
'alert_no_items' => __( 'Please select at least one item to apply the action.', 'advanced-ip-blocker' ),
/* translators: %d: The number IPs that were removed from the whitelist. */
'confirm_bulk_whitelist_remove_message' => __( 'Are you sure you want to remove the selected %d entries from the whitelist?', 'advanced-ip-blocker' ),
'delete_rule_confirm_title' => __( 'Delete Rule?', 'advanced-ip-blocker' ),
/* translators: %s: The name of the rule to be deleted. */
'delete_rule_confirm_message' => __( 'Are you sure you want to permanently delete the rule "%s"? This action cannot be undone.', 'advanced-ip-blocker' ),
'delete_rule_confirm_button' => __( 'Yes, Delete Rule', 'advanced-ip-blocker' ),
'bulk_delete_rules_confirm_title' => __( 'Confirm Bulk Deletion', 'advanced-ip-blocker' ),
/* translators: %d: The number of rules to be deleted. */
'bulk_delete_rules_confirm_message' => __( 'Are you sure you want to delete the selected %d rule(s)? This action cannot be undone.', 'advanced-ip-blocker' ),
'bulk_delete_rules_confirm_button' => __( 'Yes, Delete Selected', 'advanced-ip-blocker' ),
'no_advanced_rules' => __( 'No advanced rules have been created yet.', 'advanced-ip-blocker' ),
'could_not_load_rules' => __( 'Could not load rules.', 'advanced-ip-blocker' ),
'scan_clean_title' => __('No known vulnerabilities found!', 'advanced-ip-blocker'),
'scan_clean_desc' => __('Your plugins appear secure.', 'advanced-ip-blocker'),
/* translators: %d: The number of Vulnerabilities Found. */
'scan_vuln_title' => __('%d Vulnerabilities Found!', 'advanced-ip-blocker'),
'scan_vuln_desc' => __('Please update these plugins immediately.', 'advanced-ip-blocker'),
'scan_error' => __('Server error during scan.', 'advanced-ip-blocker'),
'scan_checking' => __('Checking versions against vulnerability database...', 'advanced-ip-blocker'),
'scan_again' => __('Scan Again', 'advanced-ip-blocker'),
'rep_analyzing' => __('Analyzing...', 'advanced-ip-blocker'),
'rep_check_again' => __('Check Again', 'advanced-ip-blocker'),
'rep_clean_title' => __('Clean Reputation', 'advanced-ip-blocker'),
/* translators: %s: server IP. */
'rep_clean_desc' => __('Your server IP (%s) is not blacklisted.', 'advanced-ip-blocker'),
'rep_listed_title' => __('Issues Found!', 'advanced-ip-blocker'),
/* translators: %s: server IP. */
'rep_listed_desc' => __('Your server IP (%s) appears on one or more blocklists.', 'advanced-ip-blocker'),
'rep_error' => __('Server error during check.', 'advanced-ip-blocker'),
'status_clean' => __('Clean', 'advanced-ip-blocker'),
'status_blacklisted' => __('Blacklisted', 'advanced-ip-blocker'),
'status_skipped' => __('Skipped (Not Configured)', 'advanced-ip-blocker'),
'status_warning' => __('Warning', 'advanced-ip-blocker'),
'status_unknown' => __('Unknown', 'advanced-ip-blocker'),
],
'countries' => $this->get_country_list(),
]);
}
}
public function detect_http_error_status($status_header, $code) {
if ($this->request_is_asn_whitelisted) { return; }
if ($this->error_handled_this_request || is_admin()) {
return;
}
if ($this->is_request_uri_excluded()) {
return;
}
if ($code === 404) {
$this->error_handled_this_request = true;
$ip = $this->get_client_ip();
if ($this->is_whitelisted($ip)) return;
if (!empty($this->options['enable_honeypot_blocking'])) {
$honeypot_urls = get_option(self::OPTION_HONEYPOT_URLS, []);
if (!empty($honeypot_urls)) {
$requested_url = strtolower($this->get_current_request_uri());
foreach ($honeypot_urls as $trap_url) {
if (!empty($trap_url) && stripos($requested_url, strtolower(trim($trap_url))) !== false) {
/* translators: %d: Honeypot URL accessed. */
$reason = sprintf(__('Honeypot URL accessed: %s', 'advanced-ip-blocker'), $trap_url);
// Llamamos a nuestra función central y LUEGO terminamos.
$this->handle_threat_event($ip, 'honeypot', $reason, ['url' => $requested_url]);
return;
}
}
}
}
$this->handle_error('404');
} elseif ($code === 403) {
$this->error_handled_this_request = true;
$ip = $this->get_client_ip();
// Comprobación de whitelist.
if ($this->is_whitelisted($ip)) return;
$all_block_types = ['geoblock', 'honeypot', '404', '403', 'login', 'user_agent', 'waf', 'rate_limit', 'asn', 'xmlrpc_block', 'threat_score'];
foreach ($all_block_types as $type) {
if (get_transient('advaipbl_bloqueo_' . $type . '_' . md5($ip))) {
return;
}
}
$this->handle_error('403');
}
}
/**
* Bloquea de forma temprana y completa todas las peticiones a xmlrpc.php si la opción está en modo 'disabled'.
* Enganchado a un hook 'init' temprano para máxima eficacia.
*/
public function block_xmlrpc_requests_if_disabled() {
// Solo actuar si la opción está explícitamente en modo 'disabled'.
if ( empty( $this->options['xmlrpc_protection_mode'] ) || 'disabled' !== $this->options['xmlrpc_protection_mode'] ) {
return;
}
// Comprobamos si la petición actual es para xmlrpc.php.
$request_uri = $this->get_current_request_uri();
if ( strpos( $request_uri, 'xmlrpc.php' ) !== false ) {
$ip = $this->get_client_ip();
// Logueamos el intento de acceso (opcional pero recomendado para visibilidad)
$this->log_event(
sprintf( 'Access to disabled xmlrpc.php endpoint was denied for IP: %s', $ip ),
'warning',
['uri' => $request_uri]
);
// AIB Community Network Reporting
if ( ! empty( $this->options['enable_community_network'] ) ) {
$this->reporter_manager->queue_report( $ip, 'xmlrpc_block', ['uri' => $request_uri] );
}
// Enviamos una respuesta 403 Forbidden y terminamos la ejecución de forma robusta.
if (!headers_sent()) {
header('HTTP/1.1 403 Forbidden');
// Prevenimos que los navegadores intenten cachear esta respuesta de error.
header('Cache-Control: no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: Wed, 11 Jan 1984 05:00:00 GMT');
}
// Usamos exit() con un mensaje simple, que es más fiable que wp_die() en un hook tan temprano.
exit('XML-RPC services are disabled on this site.');
}
}
/**
* Handles the actual restriction of the login page.
* Hooked to 'login_init'.
*/
public function handle_login_page_restriction() {
// Solo actuar si la opción está activada
if ( ! empty( $this->options['restrict_login_page'] ) ) {
$client_ip = $this->get_client_ip();
// Si la IP del visitante no está en la whitelist...
if ( ! $this->is_whitelisted( $client_ip ) ) {
/* translators: %s: The IP address that was denied access. */
$this->log_event( sprintf( __( 'Access to wp-login.php denied for non-whitelisted IP: %s', 'advanced-ip-blocker' ), $client_ip ), 'critical', $client_ip );
// Mostramos un mensaje genérico de acceso denegado y terminamos.
wp_die(
esc_html__( 'Access to this page has been restricted by the administrator.', 'advanced-ip-blocker' ),
esc_html__( 'Access Denied', 'advanced-ip-blocker' ),
[ 'response' => 403 ]
);
}
}
}
/**
* Disables the REST API user endpoints for non-authenticated users if the option is enabled.
*
* @param array $endpoints The available REST API endpoints.
* @return array The modified endpoints.
*/
public function disable_rest_api_user_endpoints( $endpoints ) {
// Solo actuar si la opción está activada en los ajustes.
if ( ! empty( $this->options['disable_user_enumeration'] ) && ! is_user_logged_in() ) {
if ( isset( $endpoints['/wp/v2/users'] ) ) {
unset( $endpoints['/wp/v2/users'] );
}
if ( isset( $endpoints['/wp/v2/users/(?P[\d]+)'] ) ) {
unset( $endpoints['/wp/v2/users/(?P[\d]+)'] );
}
}
return $endpoints;
}
/**
* Prevents user enumeration by redirecting author archive scans at an early hook.
* Enganchado a 'init'.
*/
public function prevent_author_enumeration_redirect() {
// Solo actuar si la opción está activada y no estamos en el admin.
if ( ! empty( $this->options['prevent_author_scanning'] ) && ! is_admin() ) {
// Comprobamos directamente el parámetro GET 'author'. Es mucho más fiable.
if ( isset( $_GET['author'] ) && is_numeric( $_GET['author'] ) ) {
wp_safe_redirect( home_url(), 301 );
exit;
}
}
}
/**
* Checks if a given IP address falls within a specified range (CIDR, hyphenated, or single).
* Supports both IPv4 and IPv6.
*
* @param string $ip The IP address to check.
* @param string $range The range to check against.
* @return bool True if the IP is in the range, false otherwise.
*/
public function is_ip_in_range( $ip, $range ) {
$ip = trim($ip);
$range = trim($range);
// 1. IP Única (Comparación directa)
if ( $ip === $range ) {
return true;
}
// 2. Rango con guion (Solo IPv4 por simplicidad y rendimiento, raro en IPv6)
if ( strpos( $range, '-' ) !== false ) {
// Si alguno es IPv6, saltamos la lógica de guiones (compleja de calcular)
if ( strpos($ip, ':') !== false || strpos($range, ':') !== false ) {
return false;
}
list( $start_ip, $end_ip ) = explode( '-', $range, 2 );
$ip_long = ip2long( $ip );
$start_long = ip2long( trim( $start_ip ) );
$end_long = ip2long( trim( $end_ip ) );
return $ip_long >= $start_long && $ip_long <= $end_long;
}
// 3. Rango CIDR (IPv4 e IPv6)
if ( strpos( $range, '/' ) !== false ) {
list( $subnet, $bits ) = explode( '/', $range );
$bits = (int) $bits;
// Detectar versión de IP
$ip_is_v6 = (strpos($ip, ':') !== false);
$subnet_is_v6 = (strpos($subnet, ':') !== false);
// Si las versiones no coinciden, no puede estar en el rango
if ( $ip_is_v6 !== $subnet_is_v6 ) {
return false;
}
// Lógica IPv4
if ( ! $ip_is_v6 ) {
$ip_long = ip2long( $ip );
$subnet_long = ip2long( $subnet );
$mask = -1 << ( 32 - $bits );
$subnet_masked = $subnet_long & $mask;
return ( $ip_long & $mask ) === $subnet_masked;
}
// Lógica IPv6 (Matemática binaria)
if ( $ip_is_v6 ) {
$ip_bin = inet_pton( $ip );
$subnet_bin = inet_pton( $subnet );
if ( $ip_bin === false || $subnet_bin === false ) {
return false; // IP inválida
}
// Convertir a string binario de 128 bits
$ip_bits = '';
$subnet_bits = '';
foreach (str_split($ip_bin) as $char) {
$ip_bits .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
}
foreach (str_split($subnet_bin) as $char) {
$subnet_bits .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
}
// Comparar los primeros N bits
return substr($ip_bits, 0, $bits) === substr($subnet_bits, 0, $bits);
}
}
return false;
}
public function run_all_block_checks() {
if ($this->request_is_asn_whitelisted) { return; }
$ip = $this->get_client_ip();
if ($this->is_whitelisted($ip)) {
return;
}
if ($this->rules_engine->evaluate()) {
return;
}
if ( $this->is_visitor_actively_blocked() ) {
$this->access_denied_page(__('403 - Access Denied', 'advanced-ip-blocker'), $this->get_block_message('generic'));
exit;
}
if ( defined( 'WP_CLI' ) && WP_CLI ) {
return;
}
$request_uri = $this->get_current_request_uri();
$is_xmlrpc_request = (strpos($request_uri, 'xmlrpc.php') !== false);
// ENDPOINT LOCKDOWN (PRIMERA ACCIÓN)
if ($is_xmlrpc_request && !empty($this->options['enable_xmlrpc_lockdown'])) {
if (get_transient('advaipbl_lockdown_active_xmlrpc')) {
$user_agent = $this->get_user_agent();
$is_trusted_service = false;
$automattic_ua_patterns = [ '/^WordPress\/\d+\.\d+/i', '/jetpack by wordpress\.com/i', '/woocommerce/i' ];
foreach ($automattic_ua_patterns as $pattern) { if (preg_match($pattern, $user_agent)) { $is_trusted_service = true; break; } }
if (!$is_trusted_service) {
$this->log_specific_error('endpoint_challenge', $ip, ['endpoint' => 'xmlrpc.php', 'reason' => 'XML-RPC Lockdown Mode Active', 'uri' => $request_uri], 'warning');
$this->serve_js_challenge();
}
}
}
if ($is_xmlrpc_request && ($this->options['xmlrpc_protection_mode'] ?? 'smart') === 'smart') {
$is_trusted_request = false;
// Capa 2: Confianza en la Infraestructura (ASN de la conexión directa)
$remote_addr = $this->get_remote_addr();
if ($remote_addr) {
$location_data_remote = $this->geolocation_manager->fetch_location($remote_addr);
$remote_asn = $this->asn_manager->extract_asn_from_data($location_data_remote);
// Lista de ASNs de infraestructura de confianza.
$trusted_infra_asns = ['AS2635', 'AS13335']; // Automattic, Cloudflare
if ($remote_asn && in_array($remote_asn, $trusted_infra_asns, true)) {
$is_trusted_request = true;
}
}
// Capa 3: Confianza en el User-Agent (si la capa de infraestructura no lo confirmó)
if (!$is_trusted_request) {
$user_agent = $this->get_user_agent();
$automattic_ua_patterns = ['/^WordPress\/\d+\.\d+/i', '/jetpack by wordpress\.com/i', '/woocommerce/i'];
foreach ($automattic_ua_patterns as $pattern) {
if (preg_match($pattern, $user_agent)) {
$is_trusted_request = true;
break;
}
}
}
// Capa 4: Decisión final de bloqueo
if (!$is_trusted_request) {
/* translators: %s: User-Agent. */
$reason = sprintf(__('Blocked untrusted XML-RPC request from User-Agent: %s', 'advanced-ip-blocker'), $this->get_user_agent());
$log_data = ['user_agent' => $this->get_user_agent(), 'uri' => $request_uri, 'remote_addr' => $remote_addr];
if (!empty($this->options['enable_xmlrpc_lockdown'])) {
$this->increment_lockdown_counter('xmlrpc');
}
$this->block_ip_instantly($ip, 'xmlrpc_block', $reason, $log_data);
}
// Si es una petición XML-RPC, no continuamos con las demás comprobaciones
return;
}
// Comprobación de bloqueos manuales
$manual_blocks = get_option( self::OPTION_BLOCKED_MANUAL, [] );
if ( ! empty( $manual_blocks ) ) {
foreach ( array_keys( $manual_blocks ) as $entry ) {
if ( $this->is_ip_in_range( $ip, $entry ) ) {
$this->block_ip_instantly( $ip, 'manual', __('Access manually blocked.', 'advanced-ip-blocker') );
}
}
}
// AIB COMMUNITY CHECK
if (!empty($this->options['enable_community_blocking'])) {
if ($this->community_manager->is_ip_blocked($ip)) {
$action = $this->options['community_blocking_action'] ?? 'block';
$log_data = [
'source' => 'AIB Community Network',
'uri' => $this->get_current_request_uri()
];
if ($action === 'challenge') {
$this->log_specific_error('aib_network_challenge', $ip, $log_data, 'warning');
$this->serve_js_challenge('aib_network');
} else {
// Bloqueo
$reason = __('Blocked by AIB Community Network (Global Threat).', 'advanced-ip-blocker');
$this->block_ip_instantly($ip, 'aib_network', $reason, $log_data);
}
}
}
// GEOBLOCKING
if (!empty($this->options['enable_geoblocking'])) {
$blocked_countries = $this->options['geoblock_countries'] ?? [];
if ( ! empty( $blocked_countries ) ) {
$location = $this->geolocation_manager->fetch_location( $ip );
if ( $location && ! empty( $location['country_code'] ) ) {
if ( in_array( $location['country_code'], $blocked_countries, true ) ) {
/* translators: %s: Country block. */
$reason = sprintf( __( 'Country block: %s', 'advanced-ip-blocker' ), $location['country'] ?? $location['country_code'] );
$log_data = [ 'country' => $location['country'] ?? '', 'uri' => $this->get_current_request_uri() ];
$this->block_ip_instantly( $ip, 'geoblock', $reason, $log_data );
}
}
}
}
// USER-AGENT CHECK
if (!empty($this->options['enable_user_agent_blocking'])) {
$user_agent = $this->get_user_agent();
if (!empty($user_agent) && !$this->is_internal_request($user_agent)) {
$is_ua_whitelisted = false;
$whitelisted_user_agents = get_option('advaipbl_whitelisted_user_agents', []);
if (!empty($whitelisted_user_agents)) {
foreach ($whitelisted_user_agents as $whitelisted_ua_raw) {
$whitelisted_ua = trim(preg_replace('/#.*$/', '', $whitelisted_ua_raw));
if (!empty($whitelisted_ua) && stripos($user_agent, $whitelisted_ua) !== false) {
$is_ua_whitelisted = true;
break;
}
}
}
if (!$is_ua_whitelisted) {
$blocked_user_agents = get_option('advaipbl_blocked_user_agents', []);
if (!empty($blocked_user_agents)) {
foreach ($blocked_user_agents as $blocked_ua_raw) {
$blocked_ua = trim(preg_replace('/#.*$/', '', $blocked_ua_raw));
if (!empty($blocked_ua) && stripos($user_agent, $blocked_ua) !== false) {
/* translators: %s: User-Agent. */
$reason_message = sprintf(__('Blocked User-Agent pattern: %s', 'advanced-ip-blocker'), $blocked_ua);
$log_data = [
'user_agent' => $user_agent,
'detail' => $reason_message,
'uri' => $this->get_current_request_uri()
];
$this->handle_threat_event($ip, 'user_agent', $reason_message, $log_data);
return;
}
}
}
}
}
}
// WAF CHECK
if ( ! empty( $this->options['enable_waf'] ) ) {
$triggered_rule = $this->waf_manager->run_waf_scan();
if ( $triggered_rule !== false ) {
/* translators: %s: WAF Rule Triggered. */
$reason_message = sprintf(__('WAF Rule Triggered: %s', 'advanced-ip-blocker'), $triggered_rule);
$log_data = [
'rule' => $triggered_rule,
'uri' => $this->get_current_request_uri(),
'method' => $this->get_request_method()
];
$this->handle_threat_event($ip, 'waf', $reason_message, $log_data);
}
}
// ASN CHECK
if (!empty($this->options['enable_spamhaus_asn']) || !empty($this->options['enable_manual_asn'])) {
$provider = $this->options['geolocation_provider'] ?? '';
if (in_array($provider, ['ip-api.com', 'ipinfo.io'], true)) {
$asn_block_data = $this->asn_manager->check_asn_block($ip);
if ($asn_block_data) {
$this->handle_threat_event($ip, 'asn', $asn_block_data['reason_message'], $asn_block_data['log_data']);
}
}
}
}
public function is_whitelisted($ip) {
if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
return false; // No es una IP pública válida, no puede estar en la whitelist.
}
$whitelist = get_option( self::OPTION_WHITELIST, [] );
if ( empty( $whitelist ) ) {
return false;
}
// Comprobación rápida para IPs individuales (la mayoría de los casos)
if ( array_key_exists( $ip, $whitelist ) ) {
return true;
}
// Iterar sobre las claves de la whitelist para buscar rangos.
foreach ( array_keys( $whitelist ) as $entry ) {
// Si la entrada no es la IP exacta, podría ser un rango.
if ( $entry !== $ip && $this->is_ip_in_range( $ip, $entry ) ) {
return true;
}
}
return false;
}
/**
* Comprueba si el ASN del visitante actual está en la lista blanca.
* Si lo está, establece una bandera interna para saltar otras comprobaciones de seguridad.
* Esta función está diseñada para ejecutarse en un hook muy temprano.
*
* @return bool True si el ASN está en la lista blanca, false en caso contrario.
*/
public function is_visitor_asn_whitelisted() {
if ( ! $this->geoip_manager instanceof ADVAIPBL_GeoIP_Manager ) {
return false;
}
// Obtenemos la lista blanca. Si está vacía, no hay nada que hacer.
$whitelisted_asns = get_option(self::OPTION_WHITELISTED_ASNS, []);
if (empty($whitelisted_asns)) {
return false;
}
// Obtenemos la IP del visitante.
$ip = $this->get_client_ip();
if (!$ip) {
return false;
}
// Obtenemos el ASN de la IP.
$location_data = $this->geolocation_manager->fetch_location($ip);
$visitor_asn = $this->asn_manager->extract_asn_from_data($location_data);
if (!$visitor_asn) {
return false;
}
// Comprobamos si el ASN del visitante está en nuestra lista blanca.
if (in_array($visitor_asn, $whitelisted_asns, true)) {
// ¡Coincidencia! Establecemos la bandera y devolvemos true.
$this->request_is_asn_whitelisted = true;
return true;
}
return false;
}
private function is_internal_request($user_agent) { return strpos($user_agent, 'WordPress/') === 0; }
private function get_block_message($type) {
$messages = [
'generic' => __('Access denied by security policy.', 'advanced-ip-blocker'),
'geoblock' => __('Access from your country has been blocked.', 'advanced-ip-blocker'),
'honeypot' => __('Access blocked due to suspicious activity.', 'advanced-ip-blocker'),
'manual' => __('Access manually blocked.', 'advanced-ip-blocker'),
'404' => __('Access blocked due to repeated 404 errors.', 'advanced-ip-blocker'),
'403' => __('Access blocked due to repeated attempts on protected resources.', 'advanced-ip-blocker'),
'login' => __('Access blocked due to multiple failed login attempts.', 'advanced-ip-blocker'),
'user_agent' => __('Access blocked due to suspicious User-Agent.', 'advanced-ip-blocker'),
'waf' => __('Your request was blocked by the security firewall.', 'advanced-ip-blocker'),
'rate_limit' => __('Your connection has been temporarily suspended due to an excessive request rate.', 'advanced-ip-blocker'),
'asn' => __('Access from your network (ASN) has been blocked by the administrator.', 'advanced-ip-blocker'),
'xmlrpc_block' => __('Blocked suspicious XML-RPC request.', 'advanced-ip-blocker'),
'threat_score' => __('Your connection has been blocked due to a high threat score.', 'advanced-ip-blocker'),
'impersonation' => __('Access blocked for impersonating a known crawler (Googlebot, Bingbot, etc.).', 'advanced-ip-blocker'),
];
return $messages[$type] ?? __('Access blocked.', 'advanced-ip-blocker');
}
public function block_ip_instantly($ip, $type, $reason_message, $extra_data = [], $context = 'frontend_block', $custom_duration_seconds = null) {
if ( in_array( $ip, [ '127.0.0.1', '::1' ], true ) ) {
$remote_addr = $this->get_remote_addr();
if ( $remote_addr && ! in_array( $remote_addr, [ '127.0.0.1', '::1' ], true ) ) {
$original_ip = $ip;
$ip = $remote_addr;
$extra_data['_spoofed_ip'] = $original_ip;
/* translators: %s: The real attacker IP. */
$reason_message .= sprintf(' (Spoofing attempt from %s)', $ip);
} else {
return;
}
}
global $wpdb;
$lock_key = 'lock_blocking_' . md5($ip);
if (!$wpdb->query($wpdb->prepare("INSERT IGNORE INTO {$wpdb->prefix}advaipbl_cache (cache_key, cache_value, expires_at) VALUES (%s, '1', %d)", $lock_key, time() + 15))) {
if ($context === 'frontend_block') { $this->access_denied_page(__('403 - Access Denied', 'advanced-ip-blocker'), $this->get_block_message('generic')); exit; }
return;
}
$table_name = $wpdb->prefix . 'advaipbl_blocked_ips';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ($wpdb->get_var($wpdb->prepare("SELECT id FROM {$table_name} WHERE ip_range = %s", $ip))) {
$wpdb->delete("{$wpdb->prefix}advaipbl_cache", ['cache_key' => $lock_key]);
if ($context === 'frontend_block') { $this->access_denied_page(__('403 - Access Denied', 'advanced-ip-blocker'), $this->get_block_message('generic')); exit; }
return;
}
$definitions = $this->get_all_block_type_definitions();
$def = $definitions[$type] ?? null;
$timestamp = time();
// Por defecto, calculamos la duración según el tipo de bloqueo.
$duration_in_seconds = 10 * YEAR_IN_SECONDS; // Permanente por defecto
if ($def && !empty($def['duration_key'])) {
$duration_in_minutes = (int) ($this->options[$def['duration_key']] ?? 1440);
$duration_in_seconds = ($duration_in_minutes > 0) ? $duration_in_minutes * 60 : 0;
}
// Si se proporciona una duración personalizada, esta tiene prioridad.
if ($custom_duration_seconds !== null) {
$duration_in_seconds = (int) $custom_duration_seconds;
}
$expires_at = ($duration_in_seconds > 0) ? $timestamp + $duration_in_seconds : 0;
$wpdb->insert($table_name, [ 'ip_range' => $ip, 'block_type' => $type, 'timestamp' => $timestamp, 'expires_at' => $expires_at, 'reason' => $reason_message ]);
$this->log_specific_error($type, $ip, $extra_data, 'critical');
set_transient('advaipbl_blocked_ip_' . md5($ip), true, $duration_in_seconds);
if ($def && $def['uses_transient']) {
set_transient('advaipbl_bloqueo_' . $type . '_' . md5($ip), true, $duration_in_seconds);
}
$extra_data_for_notification = $extra_data;
if ($custom_duration_seconds !== null) {
$extra_data_for_notification['duration_seconds'] = $custom_duration_seconds;
}
$this->send_block_notification($ip, $type, 1, $extra_data_for_notification);
$this->clear_blocked_ips_cache();
// Lógica condicional completa para sincronización
$write_enabled = !empty($this->options['enable_htaccess_write']);
$sync_enabled = !empty($this->options['enable_htaccess_ip_blocking']);
$include_temps = !empty($this->options['enable_htaccess_all_ips']);
// Es permanente si es manual o la fecha de expiración es 0
$is_permanent = ($type === 'manual' || $expires_at == 0);
// Ejecutar actualización SI:
// 1. La escritura y la sincronización están activas
// 2. Y ADEMÁS: Es un bloqueo permanente O tenemos activada la inclusión de temporales
if ( $write_enabled && $sync_enabled ) {
if ( $is_permanent || $include_temps ) {
$this->htaccess_manager->update_htaccess();
}
}
$cf_enabled = !empty($this->options['enable_cloudflare']);
$cf_manual = !empty($this->options['cf_sync_manual']);
$cf_temps = !empty($this->options['cf_sync_temporary']);
// Lógica: Sincronizar si está activado globalmente Y
// (es manual y tenemos activado sync manual) O (es temporal y tenemos activado sync temporal)
if ( $cf_enabled ) {
$should_sync_cf = ($type === 'manual' && $cf_manual) || ($type !== 'manual' && $cf_temps);
if ( $should_sync_cf ) {
// Ejecutamos en segundo plano para no ralentizar la respuesta al usuario
// (Opcional: por ahora lo hacemos síncrono para simplificar la depuración,
// la API de CF es rápida).
$this->cloudflare_manager->block_ip( $ip, "Blocked by AIB: " . $reason_message );
}
}
// --- AIB Community Network Reporting ---
if ( ! empty( $this->options['enable_community_network'] ) ) {
$this->reporter_manager->queue_report( $ip, $type, $extra_data );
}
$wpdb->delete("{$wpdb->prefix}advaipbl_cache", ['cache_key' => $lock_key]);
if ($context === 'frontend_block') {
$this->access_denied_page(__('403 - Access Denied', 'advanced-ip-blocker'), $this->get_block_message($type));
exit;
}
}
public function add_admin_ip_to_whitelist_on_first_run() {
if ( get_option('advaipbl_admin_ip_whitelist_trigger') ) {
if ( ! wp_doing_cron() && is_user_logged_in() && current_user_can('manage_options') ) {
$whitelist = get_option('advaipbl_ips_whitelist', []);
$ips_to_add = [];
// 1. Obtener IP del administrador.
$admin_ip = $this->get_client_ip();
if ( filter_var($admin_ip, FILTER_VALIDATE_IP) && !array_key_exists($admin_ip, $whitelist) ) {
$ips_to_add[$admin_ip] = __('Admin IP (auto-added on activation)', 'advanced-ip-blocker');
}
// 2. Obtener IP del servidor.
$server_ip = $this->get_server_ip();
if ( $server_ip && !array_key_exists($server_ip, $whitelist) ) {
// Evitar añadir la misma IP dos veces si el admin y el servidor son el mismo.
if (!isset($ips_to_add[$server_ip])) {
$ips_to_add[$server_ip] = __('Server IP (auto-added on activation)', 'advanced-ip-blocker');
}
}
if ( ! empty($ips_to_add) ) {
foreach ($ips_to_add as $ip => $detail) {
$whitelist[$ip] = [ 'timestamp' => time(), 'detail' => $detail ];
/* translators: %1$s: The IP address, %2$s: The reason/detail for whitelisting. */
$this->log_event( sprintf( __( 'IP %1$s added to whitelist. Reason: %2$s', 'advanced-ip-blocker' ), $ip, $detail ), 'info', $ip );
}
update_option('advaipbl_ips_whitelist', $whitelist);
// Forzamos la invalidación de la caché aquí también.
wp_cache_delete('advaipbl_ips_whitelist', 'options');
}
delete_option('advaipbl_admin_ip_whitelist_trigger');
}
}
}
public function schedule_cron_jobs($force = false) {
// Optimization: Don't run this check on every admin load.
// Run only if forced (e.g., settings saved) or if the transient has expired (once a day).
// Update: Changed transient name to force reschedule on update to 8.6.6
if (!$force && get_transient('advaipbl_crons_scheduled_v866')) {
return;
}
// Tarea de AIB Json
$hook_update_community_list_event = 'advaipbl_update_community_list_event';
$schedule = wp_get_schedule($hook_update_community_list_event);
if ($schedule !== 'six_hours') {
if ($schedule) {
wp_clear_scheduled_hook($hook_update_community_list_event);
}
wp_schedule_event( time() + HOUR_IN_SECONDS, 'six_hours', $hook_update_community_list_event);
}
// Tarea de purga de logs (diaria)
if (!wp_next_scheduled('advaipbl_purge_old_logs_event')) {
wp_schedule_event(time(), 'daily', 'advaipbl_purge_old_logs_event');
}
// Tarea de resúmenes por email (diaria o semanal, según ajustes)
$freq = $this->options['notification_frequency'] ?? 'disabled';
if (($freq === 'daily' || $freq === 'weekly') && !wp_next_scheduled('advaipbl_send_summary_email')) {
$this->schedule_notification_cron();
}
// Tarea de telemetría (cada 3 días, si está activada)
if (!empty($this->options['allow_telemetry']) && '1' === $this->options['allow_telemetry']) {
if (!wp_next_scheduled('advaipbl_send_telemetry_data_event')) {
// Programamos la primera ejecución para dentro de 24 horas para no sobrecargar en la activación.
wp_schedule_event(time() + DAY_IN_SECONDS, '3_days', 'advaipbl_send_telemetry_data_event');
}
} else {
// Si la opción está desactivada, nos aseguramos de que no haya ninguna tarea programada.
wp_clear_scheduled_hook('advaipbl_send_telemetry_data_event');
}
// Tarea de decaimiento de la puntuación de amenaza (cada hora)
if (!wp_next_scheduled('advaipbl_threat_score_decay_event')) {
wp_schedule_event(time(), 'hourly', 'advaipbl_threat_score_decay_event');
}
// Tarea de análisis de firmas (cada 30 minutos)
if (!wp_next_scheduled('advaipbl_signature_analysis_event')) {
wp_schedule_event(time(), '10_minutes', 'advaipbl_signature_analysis_event');
}
// Tarea de actualización de la base de datos GeoIP (diaria)
if ( ! wp_next_scheduled( 'advaipbl_update_geoip_db_event' ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'advaipbl_update_geoip_db_event' );
}
// Tarea de limpieza de la caché (diaria)
if (!wp_next_scheduled('advaipbl_cleanup_expired_cache_event')) {
wp_schedule_event(time(), 'daily', 'advaipbl_cleanup_expired_cache_event');
}
// Tarea de envío de reportes a la comunidad (cada 6 horas)
if ( ! wp_next_scheduled( 'advaipbl_community_report_event_v2' ) ) {
wp_clear_scheduled_hook('advaipbl_community_report_event');
wp_schedule_event( time() + HOUR_IN_SECONDS, 'six_hours', 'advaipbl_community_report_event_v2' );
}
// Asegura que el cron de Spamhaus esté programado si la opción está activa.
if (!empty($this->options['enable_spamhaus_asn']) && '1' === $this->options['enable_spamhaus_asn']) {
if (!wp_next_scheduled('advaipbl_update_spamhaus_list_event')) {
// Si no está programado, lo programamos ahora para que se ejecute diariamente.
wp_schedule_event(time(), 'daily', 'advaipbl_update_spamhaus_list_event');
}
} else {
// Si la opción está desactivada, nos aseguramos de que no haya ningún cron programado.
wp_clear_scheduled_hook('advaipbl_update_spamhaus_list_event');
}
// Limpieza de seguridad para crons duplicados
$crons = _get_cron_array();
$count = 0;
if ( is_array($crons) ) {
foreach ( $crons as $timestamp => $cronhooks ) {
if ( isset( $cronhooks['advaipbl_update_community_list_event'] ) ) {
$count++;
}
}
}
if ( $count > 1 ) {
// Si hay más de 1, matamos todos y reprogramamos uno limpio.
wp_clear_scheduled_hook( 'advaipbl_update_community_list_event' );
wp_schedule_event( time() + HOUR_IN_SECONDS, 'six_hours', 'advaipbl_update_community_list_event' );
}
// Clean up old transient and set new one
delete_transient('advaipbl_crons_scheduled');
set_transient('advaipbl_crons_scheduled_v866', true, DAY_IN_SECONDS);
}
public function purge_old_logs() {
$this->options = get_option(self::OPTION_SETTINGS, []);
$retention_days = (int) ($this->options['log_retention_days'] ?? 30);
if ($retention_days <= 0) return;
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_logs';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query($wpdb->prepare("DELETE FROM $table_name WHERE timestamp < DATE_SUB(NOW(), INTERVAL %d DAY)", $retention_days));
}
public function check_database_update() {
static $already_checked = false;
if ( $already_checked ) {
return;
}
// Valor por defecto '1.0' para instalaciones muy antiguas
$current_db_version = get_option('advaipbl_db_version', '1.0');
// Si la versión guardada es menor que la versión actual del código ('1.9')
if ( version_compare($current_db_version, ADVAIPBL_DB_VERSION, '<') ) {
// 1. Crear/Actualizar tablas (incluida la nueva community_ips)
self::setup_database_tables();
// 2. Guardar la nueva versión
update_option('advaipbl_db_version', ADVAIPBL_DB_VERSION);
// 3. Migraciones y Limpiezas
$this->migrate_whitelist_format();
$this->migrate_blocked_ips_to_table();
$this->cleanup_legacy_options();
// 4. Forzar descarga inicial de la lista comunitaria (Sync)
// Esto solo ocurre si venían de una versión anterior a la 1.9
if (isset($this->community_manager)) {
$this->community_manager->update_list();
$this->log_event('Community list forced update during DB upgrade.', 'info');
}
}
$already_checked = true;
}
/**
* Limpia las opciones de la base de datos de versiones muy antiguas del plugin.
* Se ejecuta una sola vez gracias a un sistema de versionado.
*/
private function cleanup_legacy_options() {
if (get_option('advaipbl_legacy_options_cleaned') === self::LEGACY_OPTIONS_CLEANUP_VERSION) {
return;
}
$legacy_options_to_delete = [
'advaipbl_ips_bloqueadas_manual',
'advaipbl_ips_bloqueadas_404',
'advaipbl_ips_bloqueadas_403',
'advaipbl_ips_bloqueadas_login',
'advaipbl_ips_bloqueadas_geoblock',
'advaipbl_ips_bloqueadas_honeypot',
'advaipbl_ips_bloqueadas_user_agent',
'advaipbl_ips_bloqueadas_waf',
'advaipbl_ips_bloqueadas_threat_score',
'advaipbl_ips_bloqueadas_rate_limit',
'advaipbl_ips_bloqueadas_asn',
'advaipbl_ips_bloqueadas_xmlrpc_block',
'advanced-ip-blocker_ips_bloqueadas_404',
'advanced-ip-blocker_ips_bloqueadas_403',
'advanced-ip-blocker_ips_bloqueadas_login',
'advanced-ip-blocker_ips_bloqueadas_honeypot',
'advanced-ip-blocker_ips_bloqueadas_user_agent',
'advanced-ip-blocker_settings',
'advanced-ip-blocker_blocked_user_agents',
'advanced-ip-blocker_whitelisted_user_agents',
'advanced-ip-blocker_ips_whitelist',
'advanced-ip-blocker_ips_bloqueadas_manual',
'advanced-ip-blocker_honeypot_urls',
];
foreach ($legacy_options_to_delete as $option_name) {
delete_option($option_name);
}
update_option('advaipbl_legacy_options_cleaned', self::LEGACY_OPTIONS_CLEANUP_VERSION);
}
/**
* Migrates the old whitelist format ( ip => timestamp ) to the new format ( ip => [ 'timestamp' => ..., 'detail' => ... ] ).
* Runs only if needed.
*/
private function migrate_whitelist_format() {
$whitelist = get_option('advaipbl_ips_whitelist', []);
if ( empty($whitelist) ) {
return; // Nada que migrar.
}
// Comprobamos el formato de la primera entrada para ver si necesita migración.
$first_entry = reset($whitelist);
if ( is_array($first_entry) && isset($first_entry['timestamp']) ) {
return;
}
$migrated_whitelist = [];
foreach ($whitelist as $ip => $timestamp) {
if ( is_string($ip) && filter_var($ip, FILTER_VALIDATE_IP) ) {
$migrated_whitelist[$ip] = [
'timestamp' => is_numeric($timestamp) ? $timestamp : time(),
'detail' => __('Migrated from old format', 'advanced-ip-blocker')
];
}
}
update_option( 'advaipbl_ips_whitelist', $migrated_whitelist );
$this->log_event('Whitelist data format successfully migrated.', 'info');
}
/**
* Migra las IPs bloqueadas desde múltiples opciones de WP a la nueva tabla dedicada.
* Se ejecuta una sola vez y limpia las opciones antiguas después de la migración.
*/
private function migrate_blocked_ips_to_table() {
// Usamos una opción para asegurarnos de que esto solo se ejecute una vez.
if (get_option('advaipbl_ip_table_migration_complete')) {
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_blocked_ips';
$definitions = $this->get_all_block_type_definitions();
$migrated_count = 0;
foreach ($definitions as $type => $def) {
// Solo migramos tipos que tenían una lista persistente.
if (empty($def['option_key'])) {
continue;
}
$list = get_option($def['option_key'], []);
if (!is_array($list) || empty($list)) {
// Borramos la opción si está vacía para limpiar la BD.
delete_option($def['option_key']);
continue;
}
foreach ($list as $ip_or_range => $block_data) {
$timestamp = is_array($block_data) && isset($block_data['timestamp']) ? (int)$block_data['timestamp'] : time();
$reason = is_array($block_data) && isset($block_data['detail']) ? $block_data['detail'] : '';
$duration_in_seconds = 10 * YEAR_IN_SECONDS; // Permanente por defecto
if (!empty($def['duration_key'])) {
$duration_in_minutes = (int) ($this->options[$def['duration_key']] ?? 1440);
if ($duration_in_minutes > 0) {
$duration_in_seconds = $duration_in_minutes * 60;
}
}
$expires_at = $timestamp + $duration_in_seconds;
// Insertamos la fila en la nueva tabla.
$wpdb->insert(
$table_name,
[
'ip_range' => $ip_or_range,
'block_type' => $type,
'timestamp' => $timestamp,
'expires_at' => $expires_at,
'reason' => $reason,
]
);
$migrated_count++;
}
// Una vez migradas todas las IPs de esta opción, la borramos.
delete_option($def['option_key']);
}
if ($migrated_count > 0) {
$this->log_event(sprintf('Successfully migrated %d blocked entries to the new database table.', $migrated_count), 'info');
}
// Marcamos la migración como completada para no volver a ejecutarla.
update_option('advaipbl_ip_table_migration_complete', true);
}
public static function setup_database_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
// Tabla de Logs (existente)
$table_name_logs = $wpdb->prefix . 'advaipbl_logs';
$sql_logs = "CREATE TABLE $table_name_logs (
log_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
timestamp DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
ip VARCHAR(100) NOT NULL,
log_type VARCHAR(20) NOT NULL,
level VARCHAR(20) NOT NULL,
message TEXT NOT NULL,
details LONGTEXT NULL,
PRIMARY KEY (log_id),
KEY ip (ip(16)),
KEY log_type (log_type),
KEY timestamp (timestamp)
) $charset_collate;";
dbDelta($sql_logs);
// Tabla de Cola de Notificaciones (existente)
$table_name_queue = $wpdb->prefix . 'advaipbl_notifications_queue';
$sql_queue = "CREATE TABLE $table_name_queue (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
timestamp DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
ip VARCHAR(100) NOT NULL,
block_type VARCHAR(20) NOT NULL,
reason TEXT NOT NULL,
PRIMARY KEY (id),
KEY timestamp (timestamp)
) $charset_collate;";
dbDelta($sql_queue);
// Tabla de Puntuación de Amenaza
$table_name_scores = $wpdb->prefix . 'advaipbl_ip_scores';
$sql_scores = "CREATE TABLE $table_name_scores (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
ip VARCHAR(100) NOT NULL,
score INT(11) NOT NULL DEFAULT 0,
last_event_timestamp INT(11) UNSIGNED NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
log_details TEXT,
PRIMARY KEY (id),
UNIQUE KEY ip (ip),
KEY score (score)
) $charset_collate;";
dbDelta($sql_scores);
// Tabla de Logs de Peticiones y Firmas
$table_name_requests = $wpdb->prefix . 'advaipbl_request_log';
$sql_requests = "CREATE TABLE $table_name_requests (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
timestamp INT(11) UNSIGNED NOT NULL,
ip_hash CHAR(64) NOT NULL,
request_uri TEXT NOT NULL,
user_agent TEXT,
request_headers TEXT,
request_method VARCHAR(10) NOT NULL,
signature_hash CHAR(64) NOT NULL,
PRIMARY KEY (id),
KEY signature_hash (signature_hash),
KEY timestamp (timestamp)
) $charset_collate;";
dbDelta($sql_requests);
// Tabla de Firmas Maliciosas Identificadas
$table_name_signatures = $wpdb->prefix . 'advaipbl_malicious_signatures';
$sql_signatures = "CREATE TABLE $table_name_signatures (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
signature_hash CHAR(64) NOT NULL,
reason VARCHAR(255) NOT NULL,
first_seen INT(11) UNSIGNED NOT NULL,
last_seen INT(11) UNSIGNED NOT NULL,
expires_at INT(11) UNSIGNED NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY signature_hash (signature_hash),
KEY expires_at (expires_at)
) $charset_collate;";
dbDelta($sql_signatures);
// Tabla de Caché
$table_name_cache = $wpdb->prefix . 'advaipbl_cache';
$sql_cache = "CREATE TABLE $table_name_cache (
cache_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
cache_key VARCHAR(191) NOT NULL,
cache_value LONGTEXT NOT NULL,
expires_at INT(11) UNSIGNED NOT NULL,
PRIMARY KEY (cache_id),
UNIQUE KEY cache_key (cache_key)
) $charset_collate;";
dbDelta($sql_cache);
// Tabla de Endpoint Lockdowns
$table_name_lockdowns = $wpdb->prefix . 'advaipbl_endpoint_lockdowns';
$sql_lockdowns = "CREATE TABLE $table_name_lockdowns (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
endpoint_key VARCHAR(50) NOT NULL,
reason VARCHAR(255) NOT NULL,
created_at INT(11) UNSIGNED NOT NULL,
expires_at INT(11) UNSIGNED NOT NULL,
details TEXT,
PRIMARY KEY (id),
KEY endpoint_key (endpoint_key),
KEY expires_at (expires_at)
) $charset_collate;";
dbDelta($sql_lockdowns);
// Nueva Tabla de IP Bloqueadas
$table_name_blocked_ips = $wpdb->prefix . 'advaipbl_blocked_ips';
$sql_blocked_ips = "CREATE TABLE $table_name_blocked_ips (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
ip_range VARCHAR(100) NOT NULL,
block_type VARCHAR(20) NOT NULL,
timestamp INT(11) UNSIGNED NOT NULL,
expires_at INT(11) UNSIGNED NOT NULL,
reason TEXT,
PRIMARY KEY (id),
UNIQUE KEY ip_range (ip_range(40)),
KEY block_type (block_type),
KEY expires_at (expires_at)
) $charset_collate;";
dbDelta($sql_blocked_ips);
// Nueva Tabla de Reportes Pendientes (Inteligencia Colectiva)
$table_name_reports = $wpdb->prefix . 'advaipbl_pending_reports';
$sql_reports = "CREATE TABLE $table_name_reports (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
ip VARCHAR(100) NOT NULL,
report_type VARCHAR(50) NOT NULL,
timestamp INT(11) UNSIGNED NOT NULL,
context LONGTEXT NULL,
PRIMARY KEY (id),
KEY sent_status (timestamp)
) $charset_collate;";
dbDelta($sql_reports);
// Nueva Tabla Dedicada para Lista Comunitaria (Rendimiento)
$table_name_community = $wpdb->prefix . 'advaipbl_community_ips';
$sql_community = "CREATE TABLE $table_name_community (
ip VARCHAR(45) NOT NULL,
PRIMARY KEY (ip)
) $charset_collate;";
dbDelta($sql_community);
update_option('advaipbl_db_version', ADVAIPBL_DB_VERSION);
}
public static function get_formatted_datetime($timestamp) { if (!is_numeric($timestamp)) { $timestamp = strtotime($timestamp); } if ($timestamp === false) { return __('Invalid date', 'advanced-ip-blocker'); } $options = get_option('advaipbl_settings', []); $timezone_string = $options['log_timezone'] ?? wp_timezone_string(); try { $date = new DateTime('@' . $timestamp); $timezone = new DateTimeZone($timezone_string); $date->setTimezone($timezone); return $date->format('Y-m-d H:i:s T'); } catch (Exception $e) { $format = get_option('date_format') . ' ' . get_option('time_format'); return date_i18n($format, $timestamp); } }
/**
* Parsea una cadena de User-Agent y devuelve una descripción legible.
*
* @param string $ua La cadena de User-Agent.
* @return string Una descripción formateada (ej. "Chrome on Windows").
*/
private static function parse_user_agent($ua) {
if (empty($ua) || $ua === __('Unknown', 'advanced-ip-blocker')) {
return __('Unknown device', 'advanced-ip-blocker');
}
$os = __('Unknown OS', 'advanced-ip-blocker');
$browser = __('Unknown Browser', 'advanced-ip-blocker');
// Mapeos para una detección más precisa
$os_map = [
'/windows nt 10/i' => 'Windows 10/11',
'/windows nt 6.3/i' => 'Windows 8.1',
'/windows nt 6.2/i' => 'Windows 8',
'/windows nt 6.1/i' => 'Windows 7',
'/windows nt 6.0/i' => 'Windows Vista',
'/windows nt 5.1/i' => 'Windows XP',
'/macintosh|mac os x/i' => 'Mac OS',
'/linux/i' => 'Linux',
'/android/i' => 'Android',
'/iphone/i' => 'iPhone',
'/ipad/i' => 'iPad',
];
$browser_map = [
'/msie/i' => 'Internet Explorer',
'/firefox/i' => 'Firefox',
'/safari/i' => 'Safari',
'/chrome/i' => 'Chrome',
'/edge/i' => 'Edge',
'/opera/i' => 'Opera',
'/netscape/i' => 'Netscape',
'/maxthon/i' => 'Maxthon',
'/konqueror/i' => 'Konqueror',
'/mobile/i' => 'Mobile Browser',
];
foreach ($os_map as $regex => $value) {
if (preg_match($regex, $ua)) {
$os = $value;
break;
}
}
foreach ($browser_map as $regex => $value) {
if (preg_match($regex, $ua)) {
$browser = $value;
break;
}
}
// Corrección para que "Chrome" no se identifique como "Safari"
if ($browser === 'Safari' && strpos(strtolower($ua), 'chrome') !== false) {
$browser = 'Chrome';
}
/* translators: 1: Browser name, 2: Operating System name. */
return sprintf(__('%1$s on %2$s', 'advanced-ip-blocker'), $browser, $os);
}
/**
* Obtiene la IP del visitante actual. Actúa como un wrapper para get_ip_intelligence()
*
* @return string La IP del visitante.
*/
public function get_client_ip() {
if ( null !== $this->client_ip ) {
return $this->client_ip;
}
$ip_data = $this->get_ip_intelligence();
$this->client_ip = $ip_data['visitor_ip'] ?? '0.0.0.0';
return $this->client_ip;
}
/**
* Obtiene un análisis completo y verificado de las direcciones IP de una petición.
* Implementa la lógica de "Proxies de Confianza" para prevenir IP spoofing.
*/
public function get_ip_intelligence() {
static $result = null;
if (null !== $result) {
return $result;
}
$result = [
'visitor_ip' => '0.0.0.0',
'visitor_ip_source' => 'Unknown',
'proxy_chain' => [],
'is_proxied' => false,
'cdn_info' => ['provider' => 'None', 'ray_id' => null, 'country' => null],
];
$headers = [];
if (function_exists('getallheaders')) {
$headers = getallheaders();
} else {
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);
$remote_addr = $this->get_remote_addr();
$source_is_trusted = $this->is_source_trusted($remote_addr);
$visitor_ip = $remote_addr; // Por defecto, la IP es la de la conexión directa.
$result['visitor_ip_source'] = 'Direct Connection (REMOTE_ADDR)';
if ($source_is_trusted) {
$result['is_proxied'] = true;
$header_priority = [
'cf-connecting-ip' => 'Cloudflare',
'true-client-ip' => 'True-Client-IP',
'x-real-ip' => 'X-Real-IP',
'x-forwarded-for' => 'X-Forwarded-For'
];
foreach ($header_priority as $header_name => $source_label) {
if (isset($headers[$header_name])) {
$found_ip = $this->get_first_public_ip_from_string($headers[$header_name]);
if ($found_ip) {
$visitor_ip = $found_ip;
$result['visitor_ip_source'] = 'Header: ' . $source_label;
if ($source_label === 'Cloudflare') {
$result['cdn_info']['provider'] = 'Cloudflare';
$result['cdn_info']['ray_id'] = $headers['cf-ray'] ?? null;
$result['cdn_info']['country'] = $headers['cf-ipcountry'] ?? null;
}
break; // Salimos del bucle en cuanto encontramos una IP válida.
}
}
}
// Construimos la cadena de proxies
if ($remote_addr && $remote_addr !== $visitor_ip) {
$result['proxy_chain'][] = $remote_addr;
}
if (isset($headers['x-forwarded-for'])) {
$forwarded_ips = array_map('trim', explode(',', $headers['x-forwarded-for']));
foreach ($forwarded_ips as $proxy_ip) {
if (filter_var($proxy_ip, FILTER_VALIDATE_IP) && $proxy_ip !== $visitor_ip && !in_array($proxy_ip, $result['proxy_chain'])) {
$result['proxy_chain'][] = $proxy_ip;
}
}
}
} else {
// Si la fuente NO es de confianza, ignoramos todas las cabeceras de proxy.
// La IP del visitante es y será siempre REMOTE_ADDR.
$result['visitor_ip_source'] = 'Untrusted Source (REMOTE_ADDR)';
}
$result['visitor_ip'] = $visitor_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.
*/
public function get_server_ip() {
// Método 1: El más rápido, si está disponible y es una IP pública.
$server_ip = isset($_SERVER['SERVER_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_ADDR'])) : null;
if ( $server_ip && filter_var( $server_ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
return $server_ip;
}
// Método 2: Fallback a un servicio externo (muy fiable).
// Hacemos una petición a un servicio que nos devuelve nuestra propia IP.
$response = wp_remote_get( 'https://api.ipify.org' );
if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) {
$ip = trim( wp_remote_retrieve_body( $response ) );
if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
return $ip;
}
}
// Si ambos métodos fallan, no podemos determinar la IP.
return null;
}
public function log_event( $message, $level = 'info', $details = [] ) {
if ( empty( $this->options['enable_logging'] ) ) {
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_logs';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
@$wpdb->insert(
$table_name,
[
'timestamp' => current_time( 'mysql', 1 ),
'ip' => $this->get_client_ip(),
'log_type' => 'general',
'level' => $level,
'message' => $message,
'details' => ! empty( $details ) ? wp_json_encode( $details ) : null,
]
);
}
public function log_specific_error($type, $ip, $extra_data = [], $level = 'warning') {
if (empty($this->options['enable_logging'])) {
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_logs';
$details = [
'url' => $this->get_current_request_uri(),
'uri' => $this->get_current_request_uri(),
'method' => $this->get_request_method(),
'referrer' => $this->get_http_referer(),
'user_agent' => $this->get_user_agent(),
];
if (is_array($extra_data)) {
$details = array_merge($details, $extra_data);
}
$is_local_db_active = (($this->options['geolocation_method'] ?? 'api') === 'local_db' && $this->geoip_manager instanceof ADVAIPBL_GeoIP_Manager);
if (!isset($details['country_code']) || $is_local_db_active) {
$location_data = $this->geolocation_manager->fetch_location($ip);
if ($location_data && empty($location_data['error'])) {
$details = array_merge($location_data, $details);
}
}
$message = '';
switch ($type) {
/* translators: %s: Username. */
case 'login': $message = sprintf(__('Failed login attempt for user: %s', 'advanced-ip-blocker'), $details['username'] ?? __('unknown', 'advanced-ip-blocker')); break;
/* translators: %s: WAF rule. */
case 'waf': $message = sprintf(__('Request blocked by WAF rule: %s', 'advanced-ip-blocker'), $details['rule'] ?? __('unknown rule', 'advanced-ip-blocker')); break;
/* translators: 1: Number of request, 2: Seconds */
case 'rate_limit': $message = sprintf(__('Rate limit exceeded: %1$d requests in %2$d seconds.', 'advanced-ip-blocker'), $details['count'] ?? 0, $details['window'] ?? 0); break;
/* translators: %s: AS number. */
case 'asn': $message = sprintf(__('Request blocked due to blacklisted ASN: %s', 'advanced-ip-blocker'), $details['asn_number'] ?? 'N/A'); break;
/* translators: %s: User-Agent. */
case 'xmlrpc_block': $message = $details['_reason'] ?? sprintf(__('Blocked untrusted XML-RPC request from User-Agent: %s', 'advanced-ip-blocker'), $details['user_agent'] ?? 'N/A'); break;
/* translators: %s: Honeypot URL. */
case 'honeypot': $message = sprintf(__('Honeypot URL accessed: %s', 'advanced-ip-blocker'), $details['url'] ?? 'N/A'); break;
/* translators: %s: Country. */
case 'geoblock': $message = sprintf(__('Blocked access from country: %s', 'advanced-ip-blocker'), $details['country'] ?? 'N/A'); break;
/* translators: %s: User-Agent. */
case 'user_agent': $message = sprintf(__('Blocked due to User-Agent match: %s', 'advanced-ip-blocker'), $details['user_agent'] ?? 'N/A'); break;
case 'threat_score': $message = $details['_reason'] ?? __('Threat score threshold exceeded', 'advanced-ip-blocker'); break;
/* translators: %d: The abuse confidence score from AbuseIPDB. */
case 'abuseipdb': $message = sprintf(__('Blocked by AbuseIPDB with a confidence score of %d%%.', 'advanced-ip-blocker'), $details['abuse_score'] ?? 'N/A'); break;
case 'abuseipdb_challenge': $message = __('Challenged by AbuseIPDB', 'advanced-ip-blocker'); break;
case 'aib_network':
/* translators: %s: The trigger detail. */
$message = $details['_reason'] ?? __('Blocked by AIB Community Intelligence.', 'advanced-ip-blocker');
break;
case 'aib_network_challenge':
$message = __('Visitor challenged by AIB Community Intelligence.', 'advanced-ip-blocker');
break;
case 'advanced_rule':
if ($level === 'critical') {
/* translators: %s: Advanced Rule detail. */
$message = $details['_reason'] ?? sprintf(__('Blocked by Advanced Rule: %s', 'advanced-ip-blocker'), $details['rule_name'] ?? 'N/A');
} elseif ($level === 'warning') {
/* translators: %s: Advanced Rule detail. */
$message = sprintf(__('Challenged by Advanced Rule: %s', 'advanced-ip-blocker'), $details['rule_name'] ?? 'N/A');
} else {
/* translators: %s: Advanced Rule detail. */
$message = sprintf(__('Threat score added by Advanced Rule: %s', 'advanced-ip-blocker'), $details['rule_name'] ?? 'N/A');
}
break;
/* translators: %s: The name of the custom 'Allow' rule. */
case 'advanced_rule_allow': $message = sprintf(__('Allowed by Advanced Rule: %s', 'advanced-ip-blocker'), $details['rule_name'] ?? 'N/A'); break;
case 'signature_flagged': $message = $details['_reason'] ?? __('New Attack Signature Identified.', 'advanced-ip-blocker'); break;
case 'signature_challenge': $message = $details['_reason'] ?? __('Signature challenge.', 'advanced-ip-blocker'); break;
case 'endpoint_challenge': $message = $details['reason'] ?? __('Endpoint challenge served.', 'advanced-ip-blocker'); break;
/* translators: %s: Country name. */
case 'geo_challenge': $message = sprintf(__('Visitor from %s was challenged.', 'advanced-ip-blocker'), $details['country'] ?? 'N/A'); break;
case 'impersonation': $message = __('Impersonating a known crawler', 'advanced-ip-blocker'); break;
/* translators: %s: Default error reason. */
default: $message = sprintf(__('A %s error occurred.', 'advanced-ip-blocker'), strtoupper($type)); break;
}
@$wpdb->insert(
$table_name,
[
'timestamp' => current_time('mysql', 1),
'ip' => $ip,
'log_type' => $type,
'level' => $level,
'message' => $message,
'details' => wp_json_encode($details)
]
);
if ('critical' === $level) {
$type_label = $this->get_all_block_type_definitions()[$type]['label'] ?? ucwords(str_replace('_', ' ', $type));
$general_log_message = sprintf(
/* translators: 1: IP Address, 2: Block Type Label (e.g., "WAF Block") */
__('IP %1$s was automatically blocked. Reason: %2$s.', 'advanced-ip-blocker'),
$ip,
$type_label
);
$this->log_event($general_log_message, 'critical', ['original_log_type' => $type]);
}
}
public function registrar_intento_login_fallido( $username ) {
$this->handle_error( 'login', [ 'username' => $username ] );
}
private function handle_error($type, $extra_data = []) {
if (is_user_logged_in() && current_user_can('unfiltered_html')) {
return;
}
$ip = $this->get_client_ip();
if ($this->is_whitelisted($ip)) {
return;
}
$all_block_types = ['geoblock', 'honeypot', 'manual', '404', '403', 'login', 'user_agent', 'waf', 'rate_limit', 'asn', 'xmlrpc_block', 'threat_score'];
foreach ($all_block_types as $reason_type) {
if (get_transient('advaipbl_bloqueo_' . $reason_type . '_' . md5($ip))) {
$this->log_specific_error($type, $ip, $extra_data);
return;
}
}
// Si es un error 404 o 403, nos aseguramos de que $extra_data contenga la URL.
if (in_array($type, ['404', '403']) && !isset($extra_data['url'])) {
$extra_data['url'] = $this->get_current_request_uri();
}
$this->log_specific_error($type, $ip, $extra_data);
if ('login' === $type && !empty($this->options['enable_login_lockdown'])) { // Usaremos una nueva opción 'enable_login_lockdown'
$this->increment_login_lockdown_counter();
}
if (!empty($this->options['enable_threat_scoring'])) {
$points_to_add = (int) ($this->options['score_' . $type] ?? 0);
if ($points_to_add > 0) {
// Pasamos $extra_data, que ahora contiene la URL, a increment_score.
$new_score = $this->threat_score_manager->increment_score($ip, $points_to_add, $type, $extra_data);
$threshold = (int) ($this->options['threat_score_threshold'] ?? 100);
$should_block = false;
if ($new_score >= $threshold || $points_to_add >= 100) {
$should_block = true;
}
if ($should_block) {
$trigger_detail = '';
if ($type === 'login') {
$trigger_detail = 'Login attempt for user: ' . ($extra_data['username'] ?? 'N/A');
} else {
$trigger_detail = $this->get_current_request_uri();
}
/* translators: 1: Points number, 2: An IP will be blocked when its threat score reaches or exceeds this value. */
$score_summary = sprintf(__('Score %1$d/%2$d', 'advanced-ip-blocker'), $new_score, $threshold);
$reason_message = sprintf( /* translators: 1: Score, 2: Block type, 3: Triger. (eg. Score 100/100 via User agent | Trigger: Blocked User-Agent pattern: MJ12bot.) */
__('%1$s via %2$s | Trigger: %3$s', 'advanced-ip-blocker'),
$score_summary,
ucfirst($type),
$trigger_detail
);
$locations = $this->session_manager->get_cached_locations([$ip]);
$location_data = $locations[$ip] ?? null;
$geo_details = [];
if ($location_data && empty($location_data['error'])) {
$geo_details['country'] = $location_data['country'] ?? null;
$geo_details['country_code'] = $location_data['country_code'] ?? null;
$geo_details['city'] = $location_data['city'] ?? null;
$geo_details['lat'] = $location_data['lat'] ?? null;
$geo_details['lon'] = $location_data['lon'] ?? null;
}
$log_details = array_merge([
'final_score' => $new_score,
'triggering_event' => $type,
'_reason' => $reason_message,
'block_reason_code' => $block_reason_code // Dato extra para depuración
], $geo_details);
$this->block_ip_instantly($ip, 'threat_score', $reason_message, $log_details);
}
}
} else {
// LÓGICA ANTIGUA (FALLBACK)
$s = $this->options;
$minute_in_seconds = 60;
$threshold = (int) ($s["threshold_{$type}"] ?? 5);
$window = (int) ($s["transient_expiration_{$type}"] ?? 60) * $minute_in_seconds;
$count_key = "advaipbl_errores_{$type}_" . md5($ip);
$errors = (int) get_transient($count_key) + 1;
set_transient($count_key, $errors, $window);
if ($errors >= $threshold) {
$trigger_detail = ($type === 'login')
? ($extra_data['username'] ?? 'N/A')
: $this->get_current_request_uri();
self::$block_queue = [
'ip' => $ip,
'type' => $type,
'trigger' => $trigger_detail,
'to' => !empty($this->options['notification_email']) && is_email($this->options['notification_email']) ? $this->options['notification_email'] : get_option('admin_email'),
'options' => $this->options,
'errors' => $errors,
];
if (!self::$shutdown_hook_registered) {
register_shutdown_function([$this, 'execute_shutdown_block']);
self::$shutdown_hook_registered = true;
}
delete_transient($count_key);
}
}
}
/**
* Centraliza la decisión de cómo manejar un evento de amenaza instantáneo (WAF, Honeypot, etc.).
* Si el sistema de puntuación está activo, suma puntos. Si no, bloquea directamente.
*
* @param string $ip La dirección IP del atacante.
* @param string $type El tipo de evento (ej. 'waf', 'honeypot').
* @param string $reason_message La razón detallada del evento.
* @param array $log_data Datos adicionales para el log.
*/
public function handle_threat_event($ip, $type, $reason_message, $log_data) {
$scoring_system_active = !empty($this->options['enable_threat_scoring']);
$this->log_specific_error($type, $ip, $log_data, 'warning');
if ( isset($this->reporter_manager) ) {
$this->reporter_manager->queue_report( $ip, $type, $log_data );
}
if ($scoring_system_active) {
$points_to_add = (int) ($this->options['score_' . $type] ?? 0);
if ($points_to_add <= 0) { return; }
$new_score = $this->threat_score_manager->increment_score($ip, $points_to_add, $type, $log_data);
$threshold = (int) ($this->options['threat_score_threshold'] ?? 100);
if ($new_score >= $threshold) {
/* translators: 1: Points number, 2: An IP will be blocked when its threat score reaches or exceeds this value. */
$score_summary = sprintf(__('Score %1$d/%2$d', 'advanced-ip-blocker'), $new_score, $threshold);
$block_reason_detail = sprintf( /* translators: 1: Points number, 2: An IP will be blocked when its threat score reaches or exceeds this value, 3: Trigger. */
__('%1$s via %2$s | Trigger: %3$s', 'advanced-ip-blocker'),
$score_summary, ucfirst(str_replace('_', ' ', $type)), $reason_message
);
$locations = $this->session_manager->get_cached_locations([$ip]);
$location_data = $locations[$ip] ?? null;
$geo_details = [];
if ($location_data && empty($location_data['error'])) {
$geo_details = ['country' => $location_data['country'] ?? null, 'country_code' => $location_data['country_code'] ?? null, 'city' => $location_data['city'] ?? null];
}
$block_log_details = array_merge($log_data, $geo_details, ['_reason' => $block_reason_detail]);
$this->block_ip_instantly($ip, 'threat_score', $block_reason_detail, $block_log_details);
}
} else {
$this->block_ip_instantly($ip, $type, $reason_message, $log_data);
}
}
public function execute_shutdown_block() {
if (empty(self::$block_queue)) {
return;
}
$q = self::$block_queue;
$ip = $q['ip'];
$original_ip_for_log = $ip; // Guardamos la IP original para los logs
$extra_data_for_log = ['trigger' => $q['trigger'], 'count' => ($q['errors'] ?? 1)];
if ( in_array( $ip, [ '127.0.0.1', '::1' ], true ) ) {
$remote_addr = $this->get_remote_addr();
if ( $remote_addr && ! in_array( $remote_addr, [ '127.0.0.1', '::1' ], true ) ) {
$ip = $remote_addr; // Cambiamos el objetivo del bloqueo
$extra_data_for_log['_spoofed_ip'] = $original_ip_for_log;
} else {
return; // Petición interna genuina, no bloquear.
}
}
global $wpdb;
$lock_key = 'lock_blocking_shutdown_' . md5($ip);
$lock_acquired = $wpdb->query($wpdb->prepare(
"INSERT IGNORE INTO {$wpdb->prefix}advaipbl_cache (cache_key, cache_value, expires_at) VALUES (%s, %s, %d)",
$lock_key, '1', time() + 15
));
if ( ! $lock_acquired ) {
return;
}
$table_name_blocked = $wpdb->prefix . 'advaipbl_blocked_ips';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$existing_block = $wpdb->get_var($wpdb->prepare("SELECT id FROM {$table_name_blocked} WHERE ip_range = %s", $ip));
if ($existing_block) {
$wpdb->delete("{$wpdb->prefix}advaipbl_cache", ['cache_key' => $lock_key]);
return;
}
$type = $q['type'];
$trigger = $q['trigger'];
$to = $q['to'];
$options = $q['options'];
$errors = (isset($q['errors']) && is_numeric($q['errors'])) ? $q['errors'] : 1;
// 1. Mapear el tipo de bloqueo a su constante de opción correspondiente.
$option_key_map = [
'404' => self::OPTION_BLOCKED_404,
'403' => self::OPTION_BLOCKED_403,
'login' => self::OPTION_BLOCKED_LOGIN,
];
$option_key = $option_key_map[$type] ?? null;
if ( ! $option_key ) {
$wpdb->delete("{$wpdb->prefix}advaipbl_cache", ['cache_key' => $lock_key]); // Liberar cerrojo antes de salir
return;
}
// 2. Acciones de bloqueo (transient y opción persistente)
$duration = (int) ($options["duration_{$type}"] ?? 120) * 60;
$block_key = "advaipbl_bloqueo_{$type}_" . md5($ip);
set_transient($block_key, true, $duration);
$list = get_option($option_key, []);
if (!is_array($list)) $list = [];
$list[$ip] = ['timestamp' => time(), 'detail' => $trigger];
update_option($option_key, $list);
// 3. Obtener datos de geolocalización COMPLETOS
$location_data = $this->geolocation_manager->fetch_location($ip);
$geo_details = [];
if ($location_data && empty($location_data['error'])) {
$geo_details['country'] = $location_data['country'] ?? null;
$geo_details['country_code'] = $location_data['country_code'] ?? null;
$geo_details['city'] = $location_data['city'] ?? null;
$geo_details['lat'] = $location_data['lat'] ?? null;
$geo_details['lon'] = $location_data['lon'] ?? null;
}
// 4. Lógica de email y logs
$notification_enabled = !empty($options['enable_email_notifications']) && '1' === $options['enable_email_notifications'];
$frequency = $options['notification_frequency'] ?? 'disabled';
$email_sent = null;
$reason = '';
switch ($type) {
case '404':
case '403':
$reason = sprintf( /* translators: 1: %1\$d errors number, 2: %2\$s errors number, 3: Triggering URL. */ __("Generated %1\$d %2\$s errors. Triggering URL: %3\$s", 'advanced-ip-blocker'), $errors, strtoupper($type), $trigger);
break;
case 'login':
$reason = sprintf( /* translators: 1: %1$d failed login attempts, 2: %2$s Username. */ __('Made %1$d failed login attempts for user: %2$s', 'advanced-ip-blocker'), $errors, $trigger);
break;
}
if ($notification_enabled && $frequency === 'instant') {
$site_name = get_bloginfo( 'name' );
/* translators: %s: Site name. */
$email_subject = sprintf(__('[%s] Security Alert: IP Automatically Blocked', 'advanced-ip-blocker'), $site_name);
$template_title = __('Security Alert: IP Blocked', 'advanced-ip-blocker');
/* translators: %d: Minutes. */
$duration_text = sprintf(__('%d minutes', 'advanced-ip-blocker'), (int) ($options["duration_{$type}"] ?? 120));
$button_url = admin_url('admin.php?page=advaipbl_ip_management&sub-tab=blocked_ips');
$button_style = "background-color: #2271b1; color: #ffffff; padding: 12px 25px; text-decoration: none; border-radius: 4px; display: inline-block; font-weight: bold;";
$content_html =
'
' . esc_html__( "An IP address has been automatically blocked on your website.", 'advanced-ip-blocker' ) . '
' . esc_html__('Your request was blocked by the security firewall.', 'advanced-ip-blocker'),
'excluded_error_urls' => implode( "\n", $default_exclusions_404_403 ),
'waf_excluded_urls' => implode("\n", $default_waf_exclusions),
'rows_per_page' => 20, 'delete_data_on_uninstall' => '0', 'show_admin_bar_menu' => '1',
'trusted_proxies' => "# Cloudflare\nAS13335\nAS209242\n# Local Nginx/Varnish Proxy\n127.0.0.1\n::1",
// Notificaciones
'enable_email_notifications' => '0', 'notification_frequency' => 'disabled', 'notification_email' => '',
'enable_push_notifications' => '0',
'push_webhook_urls' => '',
'push_critical_only' => '0',
'push_mentions' => '',
// APIs Externas
'geolocation_provider' => 'ip-api.com', 'api_key_ipapicom' => '', 'api_key_ipstackcom' => '',
'api_key_ipinfocom' => '', 'api_key_ip_apicom' => '', 'geolocation_method' => 'api',
'maxmind_license_key' => '',
'enable_community_network' => '1', // Por defecto 0 hasta que el usuario acepte
// AIB Community Network
'enable_community_network' => '1',
'enable_community_blocking' => '1',
'community_blocking_action' => 'block',
'duration_aib_network' => 1440,
// AbuseIPDB Integration
'enable_abuseipdb' => '0',
'abuseipdb_api_key' => '',
'abuseipdb_threshold' => 90,
'duration_abuseipdb' => 1440,
'abuseipdb_action' => 'block',
// Htaccess Firewall
'enable_htaccess_write' => '0',
'enable_htaccess_ip_blocking' => '0',
'enable_htaccess_all_ips' => '0',
'htaccess_protect_system_files' => '0',
'htaccess_protect_wp_config' => '0',
'htaccess_protect_readme' => '0',
// Geobloqueo
'geoblock_countries' => [],
// Ajustes de reCAPTCHA
'recaptcha_enable' => '0', 'recaptcha_version' => 'v3', 'recaptcha_site_key' => '',
'recaptcha_secret_key' => '', 'recaptcha_score_threshold' => 0.5,
// CLAVES 2FA
'enable_2fa' => '0',
'tfa_force_roles' => [],
'prevent_login_hinting' => '1',
// Telemetría
'allow_telemetry' => '0',
// Sistema de Puntuación de Amenaza (IP Trust System)
'enable_threat_scoring' => '0',
'threat_score_threshold' => 100,
'duration_threat_score' => 1440,
'score_404' => 5,
'score_403' => 10,
'score_login' => 15,
'score_user_agent' => 100,
'score_waf' => 100,
'score_honeypot' => 100,
'score_asn' => 100,
'score_impersonation' => 100,
'score_decay_points' => 1,
'score_decay_frequency' => 1,
'enable_signature_engine' => '0',
'enable_signature_analysis' => '0',
'signature_ip_threshold' => 5,
'signature_analysis_window' => 1,
'signature_rule_ttl' => 24,
'enable_signature_blocking' => '0',
'trusted_signature_hashes' => '',
// Cloudflare Integration
'enable_cloudflare' => '0',
'cf_api_token' => '',
'cf_zone_id' => '',
'cf_sync_manual' => '0', // Sincronizar bloqueos manuales
'cf_sync_temporary' => '0', // Sincronizar bloqueos temporales (WAF, Login, etc)
];
}
/**
* Devuelve la lista por defecto de User-Agents maliciosos para el asistente.
* @return array
*/
public function get_default_user_agents() {
return [
'# === Vulnerability Scanners & Pentesting Tools ===',
'Acunetix', 'Arachni', 'Burp', 'Dirb', 'DirBuster', 'Feroxbuster', 'Go-http-client', 'Havij', 'Nessus', 'Nikto', 'Nmap', 'Netsparker', 'OpenVAS', 'Photon/1.0', 'sqlmap', 'Vega', 'Wfuzz', 'WhatWeb', 'WPScan', 'WPSec', 'ZAP/', 'masscan', 'ScanNG', 'PressVuln', 'PostmanRuntime', 'CensysInspect', 'Expanse', 'internet-measurement', 'JSScanner/',
'# === Generic Bots & Scripting Libraries ===',
'#curl', 'HTTrack', '#Java/', 'okhttp', 'perl', 'php/', 'Python', 'python-requests', 'Scrapy', 'wget', 'libwww', 'ruby',
'# === Aggressive Scrapers & Black Hat SEO Bots ===',
'#AhrefsBot', 'Bytespider', 'contabot', 'dataprovider', 'DigExt', '#DotBot', 'EmailCollector', 'ExtractorPro', 'MegaIndex', '#MJ12bot', 'SemrushBot', 'WebCollector', 'WebCopier', 'AliyunSecBot', 'AwarioBot', 'BW/', '#GoogleOther', 'IonCrawl', 'ISSCyberRiskCrawler',
'# === Spam, Low-Quality AI & Comment Bots ===',
'#Applebot-Extended', 'ClaudeBot', 'Diffbot', '#FacebookBot', 'FriendlyCrawler', '#Google-Extended', 'ImagesiftBot', 'Image2dataset', '#Meta-ExternalAgent', 'omgili', 'Timpibot', 'omgilibot', 'AcoonBot/', 'anthropic-ai', 'BoardReader', 'CCBot', '#ChatGPT-User', 'Claude-Web', 'DataForSeoBot', '#GPTBot', 'PerplexityBot', '#petalbot', '#YandexBot', 'ZmEu',
'# === Aggressive Regional Crawlers (optional) ===',
'Baiduspider', 'Baiduspider-image', 'Baiduspider-news', 'Barkrowler', 'msnbot-media', 'SeznamBot', 'Sogou', 'YisouSpider', 'BLEXBot', 'news-please', 'Orbbot', 'peer39_crawler', 'VelenPublicWebCrawler', '#wp_is_mobile', 'Zoominfobot',
'# === Suspicious or Malformed User-Agents ===',
'Dalvik/', 'morfeus', 'ShellBot', 'zgrab', 'Chrome/45', 'Mozilla/4.0', 'Empty', 'Mozlila', 'GRequests/'
];
}
/**
* Devuelve la lista por defecto de URLs Honeypot para el asistente.
* @return array
*/
public function get_default_honeypot_urls() {
return [
'/.env',
'/wp-config.php',
'/.git/',
'/phpinfo.php',
'/shell.php',
'/wso.php',
'/wordpress.zip',
'/phpunit',
'/eval-stdin.php'
];
}
/**
* Devuelve la lista por defecto de reglas WAF para el asistente.
* @return array
*/
public function get_default_waf_rules() {
return [
'# === SQL Injection (SQLi) ===',
'union\s+select',
'information_schema\.',
'# === Cross-Site Scripting (XSS) ===',
'<\s*script',
'on(error|load|click|mouseover)\s*=',
'javascript:', // Añadido: Protocolo peligroso en inputs
'# === Path Traversal & LFI ===',
'\.\.\/',
'/etc/passwd',
'php://input',
'# === Sensitive Files & Backups ===',
'/(wp-config\.php|\.env)',
'\.sql$',
'\.log$'
];
}
public static function activate_plugin() {
// 1. Obtener la instancia del plugin SIEMPRE al principio.
// Esto asegura que $instance siempre exista y que las propiedades (como $options) estén cargadas.
$instance = self::get_instance();
// 2. Lógica de la base de datos.
self::setup_database_tables();
if ( ! wp_next_scheduled( 'advaipbl_purge_old_logs_event' ) ) {
wp_schedule_event( time(), 'daily', 'advaipbl_purge_old_logs_event' );
}
if ( ! wp_next_scheduled( 'advaipbl_update_geoip_db_event' ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'advaipbl_update_geoip_db_event' );
}
if ( ! wp_next_scheduled( 'advaipbl_cleanup_expired_cache_event' ) ) {
wp_schedule_event( time(), 'daily', 'advaipbl_cleanup_expired_cache_event' );
}
// 3. Crear los ajustes por defecto SOLO si no existen.
if ( false === get_option( self::OPTION_SETTINGS ) ) {
$defaults = $instance->get_default_settings();
update_option( self::OPTION_SETTINGS, $defaults );
}
// 4. Programar los crons de notificación (usando la instancia).
$notification_frequency = $instance->options['notification_frequency'] ?? 'disabled';
if ( 'disabled' !== $notification_frequency && 'instant' !== $notification_frequency ) {
if ( ! wp_next_scheduled( 'advaipbl_send_summary_email' ) ) {
$instance->schedule_notification_cron();
}
}
// 5. Re-sincronizar transients (ahora $instance siempre existe).
$instance->resync_block_transients();
// 6. Añadir las demás opciones si no existen.
add_option( self::OPTION_BLOCKED_UAS, [], '', 'no' );
add_option( self::OPTION_WHITELISTED_UAS, [], '', 'no' );
add_option( self::OPTION_WHITELIST, [], '', 'no' );
add_option( self::OPTION_BLOCKED_MANUAL, [], '', 'no' );
add_option( self::OPTION_BLOCKED_HONEYPOT, [], '', 'no' );
add_option( self::OPTION_BLOCKED_USER_AGENT, [], '', 'no' );
add_option( self::OPTION_BLOCKED_GEO, [], '', 'no' );
add_option( ADVAIPBL_USM_OPTION_PER_PAGE, ADVAIPBL_USM_DEFAULT_PER_PAGE, '', 'no' );
add_option( self::OPTION_HONEYPOT_URLS, [], '', 'no' );
// 7. Inicializar los triggers de activación.
add_option( self::OPTION_ADMIN_IP_TRIGGER, 'yes', '', 'no' );
}
public static function deactivate_plugin() {
wp_clear_scheduled_hook('advaipbl_purge_old_logs_event');
wp_clear_scheduled_hook('advaipbl_update_spamhaus_list_event');
wp_clear_scheduled_hook('advaipbl_send_telemetry_data_event');
wp_clear_scheduled_hook('advaipbl_threat_score_decay_event');
wp_clear_scheduled_hook('advaipbl_signature_analysis_event');
wp_clear_scheduled_hook('advaipbl_update_geoip_db_event');
wp_clear_scheduled_hook('advaipbl_cleanup_expired_cache_event');
$instance = self::get_instance();
$instance->clear_notification_cron();
}
/**
* Comprueba si estamos en una de las páginas de nuestro plugin y, si es así,
* elimina todos los demás avisos de administrador para una interfaz limpia.
*/
public function conditionally_remove_admin_notices() {
$current_page_slug = isset($_GET['page']) ? sanitize_key($_GET['page']) : '';
$plugin_pages = [
'advaipbl_settings_page',
'advaipbl-setup-wizard'
];
// Comprobamos si el slug de la página actual es o empieza por uno de nuestros slugs.
foreach ($plugin_pages as $page_slug) {
if (strpos($current_page_slug, $page_slug) === 0) {
remove_all_actions('admin_notices');
remove_all_actions('all_admin_notices');
// Una vez que encontramos una coincidencia y limpiamos, no necesitamos seguir.
return;
}
}
}
public function admin_menu() {
$blocked_count = $this->get_blocked_count();
$bubble_html = $blocked_count > 0 ? ' ' . number_format_i18n($blocked_count) . '' : '';
// Slug base para la página principal. Es buena práctica tenerlo en una variable.
$main_page_slug = 'advaipbl_settings_page';
// 1. Menú Principal de Nivel Superior
add_menu_page(
__('Advanced IP Blocker', 'advanced-ip-blocker'),
__('Security', 'advanced-ip-blocker') . $bubble_html,
'manage_options',
$main_page_slug,
[$this, 'settings_page_content'],
'dashicons-shield',
'80.123'
);
// 2. Submenús
// El primer submenú (Dashboard) usa el mismo slug que el padre para ser la página por defecto.
// Al hacer clic en "Security", se cargará esta página.
add_submenu_page(
$main_page_slug,
__('Dashboard', 'advanced-ip-blocker'),
__('Dashboard', 'advanced-ip-blocker'),
'manage_options',
$main_page_slug, // Slug = padre. Correcto.
[$this, 'settings_page_content']
);
// Para los demás submenús, usamos slugs únicos y simples.
// WordPress generará automáticamente los enlaces correctos, por ejemplo:
// admin.php?page=advaipbl_settings_page-settings
add_submenu_page(
$main_page_slug,
__('Settings', 'advanced-ip-blocker'),
__('Settings', 'advanced-ip-blocker'),
'manage_options',
$main_page_slug . '-settings', // Slug único
[$this, 'settings_page_content']
);
add_submenu_page(
$main_page_slug,
__('Security Headers', 'advanced-ip-blocker'),
__('Security Headers', 'advanced-ip-blocker'),
'manage_options',
$main_page_slug . '-security-headers', // Slug único
[$this, 'settings_page_content']
);
add_submenu_page(
$main_page_slug,
__('Blocking Rules', 'advanced-ip-blocker'),
__('Blocking Rules', 'advanced-ip-blocker'),
'manage_options',
$main_page_slug . '-rules', // Slug único
[$this, 'settings_page_content']
);
add_submenu_page(
$main_page_slug,
__('IP Management', 'advanced-ip-blocker'),
__('IP Management', 'advanced-ip-blocker') . $bubble_html,
'manage_options',
$main_page_slug . '-ip-management', // Slug único
[$this, 'settings_page_content']
);
add_submenu_page(
$main_page_slug,
__('Site Scanner', 'advanced-ip-blocker'),
__('Site Scanner', 'advanced-ip-blocker'),
'manage_options',
$main_page_slug . '-scanner', // Slug único
[$this, 'settings_page_content']
);
add_submenu_page(
$main_page_slug,
__('Logs & Sessions', 'advanced-ip-blocker'),
__('Logs & Sessions', 'advanced-ip-blocker'),
'manage_options',
$main_page_slug . '-logs', // Slug único
[$this, 'settings_page_content']
);
add_submenu_page(
$main_page_slug,
__('About', 'advanced-ip-blocker'),
__('About', 'advanced-ip-blocker'),
'manage_options',
$main_page_slug . '-about', // Slug único
[$this, 'settings_page_content']
);
// Registrar la página oculta para el asistente de configuración.
add_submenu_page(
'', // No tener padre de menú la oculta.
__( 'Setup Wizard', 'advanced-ip-blocker' ),
__( 'Setup Wizard', 'advanced-ip-blocker' ),
'manage_options',
'advaipbl-setup-wizard', // Slug único para nuestra página.
[ $this->admin_pages, 'render_setup_wizard_page' ]
);
}
/**
* Removes all admin notices from other plugins and WordPress core
* ONLY on this plugin's settings page.
*/
public function remove_all_other_admin_notices() {
// Esta es la técnica que sugeriste, es perfecta.
remove_all_actions( 'admin_notices' );
remove_all_actions( 'all_admin_notices' );
}
public function display_admin_notice() {
// Primero, llamamos a la función que decide si mostrar el aviso de telemetría.
$this->display_telemetry_notice();
// Después, mostramos cualquier otro aviso de éxito/error que esté en el transient.
$notice = get_transient('advaipbl_admin_notice');
if ( $notice ) {
$type = ( 'success' === $notice['type'] ) ? 'success' : 'error';
printf(
'
%2$s
',
esc_attr( $type ),
esc_html( $notice['message'] )
);
delete_transient('advaipbl_admin_notice');
}
}
/**
* Validates if a string is a valid IP, CIDR range, or hyphenated range.
*
* @param string $input The string to validate.
* @return bool
*/
public function is_valid_ip_or_range($input) {
$input = trim($input);
// Check for single IP
if (filter_var($input, FILTER_VALIDATE_IP)) {
return true;
}
// Check for CIDR
if (strpos($input, '/') !== false) {
list($subnet, $bits) = explode('/', $input, 2);
return filter_var($subnet, FILTER_VALIDATE_IP) && is_numeric($bits) && $bits >= 0 && $bits <= 32;
}
// Check for hyphenated range
if (strpos($input, '-') !== false) {
list($start_ip, $end_ip) = explode('-', $input, 2);
return filter_var(trim($start_ip), FILTER_VALIDATE_IP) && filter_var(trim($end_ip), FILTER_VALIDATE_IP);
}
return false;
}
/**
* Checks if a given IP address is from a trusted proxy source.
* This is the core function for the Trusted Proxies feature. It checks against
* a user-defined list of IPs, CIDR ranges, and ASNs.
*
* @param string $ip_to_check The IP address to verify (usually REMOTE_ADDR).
* @return bool True if the IP is from a trusted source, false otherwise.
*/
private function is_source_trusted($ip_to_check) {
// Si no hay IP para comprobar, no puede ser de confianza.
if (empty($ip_to_check)) {
return false;
}
$raw_trusted_list = $this->options['trusted_proxies'] ?? '';
if (empty($raw_trusted_list)) {
return false;
}
$trusted_list = array_filter(array_map('trim', explode("\n", $raw_trusted_list)));
if (empty($trusted_list)) {
return false;
}
// Dividimos la lista en IPs/CIDRs y ASNs para un procesamiento eficiente.
$trusted_ips_cidrs = [];
$trusted_asns = [];
foreach ($trusted_list as $entry) {
if (strpos(strtoupper($entry), 'AS') === 0) {
$trusted_asns[] = strtoupper($entry);
} else {
$trusted_ips_cidrs[] = $entry;
}
}
// 1. Comprobación directa de IP/CIDR (la más rápida).
foreach ($trusted_ips_cidrs as $trusted_entry) {
if ($this->is_ip_in_range($ip_to_check, $trusted_entry)) {
return true;
}
}
// 2. Si no hubo coincidencia de IP y hay ASNs para comprobar, procedemos con la búsqueda de ASN.
if (!empty($trusted_asns)) {
// Aprovechamos nuestro gestor de geolocalización y su caché.
$location_data = $this->geolocation_manager->fetch_location($ip_to_check);
$source_asn = $this->asn_manager->extract_asn_from_data($location_data);
if ($source_asn && in_array(strtoupper($source_asn), $trusted_asns, true)) {
return true;
}
}
return false;
}
/**
* Finds the first valid, public IP address from a comma-separated string of IPs.
*
* @param string $ip_string The string from a proxy header (e.g., X-Forwarded-For).
* @return string|null The first valid public IP, or null if none are found.
*/
private function get_first_public_ip_from_string($ip_string) {
$ips = array_map('trim', explode(',', $ip_string));
foreach ($ips as $ip) {
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip;
}
}
return null;
}
//public function handle_admin_actions() {}
public function settings_page_content() {
$this->admin_pages->settings_page_content();
}
/**
* Imprime una cabecera de columna de tabla HTML que permite ordenar.
*/
public function print_sortable_header($label, $column_key, $orderby, $order) {
$next_order = ($orderby === $column_key && 'asc' === $order) ? 'desc' : 'asc';
$arrow_class = ($orderby === $column_key) ? 'sorted ' . $order : 'sortable desc';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$filter_type_sanitized = isset($_GET['filter_type']) ? sanitize_key(wp_unslash($_GET['filter_type'])) : 'all';
$url = add_query_arg([
'orderby' => $column_key,
'order' => $next_order,
'filter_type' => $filter_type_sanitized,
]);
echo '
';
}
/**
* Renders a dropdown to select the number of items per page.
*
* @param int $current_per_page The currently selected number of items per page.
*/
public function render_per_page_selector( $current_per_page ) {
$per_page_options = [ 20, 50, 100, 200 ];
?>
$column_key,
'order' => $next_order,
]);
$url = add_query_arg($url_params, admin_url('admin.php'));
echo '
';
// Borramos la cookie para que no se muestre de nuevo
unset( $_COOKIE['advaipbl_login_error'] );
setcookie( 'advaipbl_login_error', '', time() - 3600, COOKIEPATH, COOKIE_DOMAIN );
} else {
$message = '
' . esc_html__( 'Enter the code generated by your authenticator app.', 'advanced-ip-blocker' ) . '
tfa_manager ) {
return;
}
$user_id = get_current_user_id();
// Solo continuamos si la marca existe.
if ( get_user_meta( $user_id, '_advaipbl_2fa_setup_required', true ) ) {
$user = wp_get_current_user();
$global_2fa_enabled = ! empty( $this->options['enable_2fa'] ) && '1' === $this->options['enable_2fa'];
$user_is_forced = $this->tfa_manager->is_2fa_forced_for_user( $user );
$user_has_2fa_setup = $this->tfa_manager->is_2fa_enabled_for_user( $user_id );
// CONDICIONES PARA MOSTRAR EL AVISO:
// 1. 2FA debe estar activado globalmente.
// 2. El rol del usuario debe estar en la lista de forzados.
// 3. El usuario NO debe tener 2FA configurado todavía.
if ( $global_2fa_enabled && $user_is_forced && ! $user_has_2fa_setup ) {
$profile_url = get_edit_profile_url( $user_id );
?>
go to your profile to configure it now.', 'advanced-ip-blocker' ),
[ 'a' => [ 'href' => [] ] ]
),
esc_url( $profile_url )
);
?>
' . esc_html__( 'This is a confirmation that two-factor authentication has been successfully activated on your account. Your account is now more secure.', 'advanced-ip-blocker' ) . '
'
. '
' . esc_html__( 'If you did not perform this action, please contact the site administrator immediately.', 'advanced-ip-blocker' ) . '
' . esc_html__( 'This is a notification that two-factor authentication has been deactivated on your account. Your account is now less secure.', 'advanced-ip-blocker' ) . '
'
. '
' . esc_html__( 'If you did not perform this action, please reset your password and contact the site administrator immediately.', 'advanced-ip-blocker' ) . '
';
break;
case 'reset':
/* translators: $s: Site name. */
$subject = sprintf( __( '[%s] Your Two-Factor Authentication has been Reset', 'advanced-ip-blocker' ), $site_name );
$template_title = __( '2FA Reset by Administrator', 'advanced-ip-blocker' );
$content_html = '
' . esc_html__( 'A site administrator has reset the two-factor authentication configuration for your account. You can now log in using only your username and password.', 'advanced-ip-blocker' ) . '
'
. '
' . esc_html__( 'We strongly recommend you set up 2FA again from your profile page as soon as possible.', 'advanced-ip-blocker' ) . '
' . esc_html__( 'A recovery code was used to access your account.', 'advanced-ip-blocker' ) . '
'
. '
' . sprintf(/* translators: %d: Number of recovery codes. */ esc_html__( 'You have %d recovery codes remaining.', 'advanced-ip-blocker' ), $remaining_codes ) . '
'
. '
' . esc_html__( 'If you did not perform this action, please reset your password immediately.', 'advanced-ip-blocker' ) . '
';
break;
}
if ( ! empty( $content_html ) ) {
$body = $this->get_html_email_template( $template_title, $content_html );
add_filter( 'wp_mail_content_type', [$this, 'set_html_mail_content_type'] );
wp_mail( $to, $subject, $body );
remove_filter( 'wp_mail_content_type', [$this, 'set_html_mail_content_type'] );
}
}
/**
* Previene la enumeración de usuarios a través del endpoint oEmbed de la API REST.
* Elimina los datos del autor de la respuesta si la protección está activada.
*
* @param array $data The response data.
* @param WP_Post $post The post object.
* @param int $width The requested width.
* @param int $height The requested height.
* @return array The modified response data.
*/
public function prevent_user_enumeration_via_oembed( $data, $post, $width, $height ) {
// Solo actuar si la opción de protección está activada.
if ( ! empty( $this->options['disable_user_enumeration'] ) ) {
// Eliminamos las claves que podrían revelar información del autor.
unset( $data['author_name'] );
unset( $data['author_url'] );
}
return $data;
}
/**
* Previene la enumeración de usuarios a través de los feeds RSS.
* Si la protección de escaneo de autor está activada, reemplaza el login del autor
* con su nombre público (display name) en los feeds.
*
* @param string $author_login El login del autor.
* @return string El login del autor o su nombre público.
*/
public function prevent_user_enumeration_via_feeds( $author_login ) {
// Solo actuar si la opción está activada y estamos en un feed.
if ( ! empty( $this->options['prevent_author_scanning'] ) && is_feed() ) {
$author = get_user_by( 'login', $author_login );
if ( $author ) {
// Devolvemos el nombre público en lugar del login.
return $author->display_name;
}
}
return $author_login;
}
/**
* Previene el "login hinting" interceptando los errores de autenticación
* y reemplazándolos por un mensaje genérico.
*
* @param WP_User|WP_Error|null $user El objeto de usuario o error.
* @return WP_User|WP_Error
*/
public function prevent_login_hinting( $user ) {
// Solo actuar si la opción está activada.
if ( empty( $this->options['prevent_login_hinting'] ) || '1' !== $this->options['prevent_login_hinting'] ) {
return $user;
}
// Solo nos interesa modificar los errores, no los inicios de sesión correctos.
if ( ! is_wp_error( $user ) ) {
return $user;
}
// Lista de códigos de error que queremos ocultar.
$error_codes_to_hide = [
'invalid_username',
'invalid_email',
'incorrect_password',
];
// Comprobamos si el error actual es uno de los que dan pistas.
$has_hinting_error = false;
foreach ( $error_codes_to_hide as $code ) {
if ( $user->get_error_code() === $code ) {
$has_hinting_error = true;
break;
}
}
if ( $has_hinting_error ) {
return new WP_Error(
'advaipbl_generic_login_error',
__( 'ERROR: Your login details are incorrect. Please try again.', 'advanced-ip-blocker' )
);
}
// Si no es un error de los que queremos ocultar (ej. el de 2FA), lo dejamos pasar.
return $user;
}
/**
* Guarda los cambios de la sección 2FA en el perfil de usuario.
* Este hook es necesario para WordPress. Nuestra lógica de activación/desactivación
* se maneja principalmente vía AJAX para una mejor experiencia de usuario.
*
* @param int $user_id El ID del usuario que se está actualizando.
*/
public function save_2fa_section_in_profile( $user_id ) {
// No se requiere ninguna acción aquí, ya que el JS maneja las acciones de 2FA.
// La función debe existir para que el hook 'personal_options_update' no cause un error fatal.
if ( ! current_user_can( 'edit_user', $user_id ) ) {
return;
}
// No hay nada que hacer, pero la función está aquí para evitar el error.
}
/**
* Tarea programada para actualizar las bases de datos de GeoIP.
*/
public function execute_geoip_db_update() {
// Solo se ejecuta si el método es 'local_db' y hay una clave de licencia.
if (
isset($this->options['geolocation_method']) && $this->options['geolocation_method'] === 'local_db' &&
!empty($this->options['maxmind_license_key']) && $this->geoip_manager
) {
$this->log_event('Starting scheduled GeoIP database update.', 'info');
$result = $this->geoip_manager->download_and_unpack_databases();
if (!$result['success']) {
$this->log_event('Scheduled GeoIP database update failed: ' . $result['message'], 'error');
} else {
$this->log_event('Scheduled GeoIP database update completed successfully.', 'info');
}
}
}
/**
* Comprueba si una IP está actualmente bajo un bloqueo activo de cualquier tipo.
* Esta función es una comprobación de alto rendimiento que prioriza los transients.
*
* @param string $ip La dirección IP a comprobar.
* @return bool True si la IP está activamente bloqueada, false en caso contrario.
*/
public function is_ip_actively_blocked($ip) {
$definitions = $this->get_all_block_type_definitions();
// 1. Comprobación de transients para todos los tipos que los usan.
foreach ($definitions as $type => $def) {
if ($def['uses_transient'] && get_transient('advaipbl_bloqueo_' . $type . '_' . md5($ip))) {
return true;
}
}
// 2. Comprobación específica para bloqueos manuales (que son permanentes).
$manual_blocks = get_option(self::OPTION_BLOCKED_MANUAL, []);
if (!empty($manual_blocks)) {
foreach (array_keys($manual_blocks) as $entry) {
if ($this->is_ip_in_range($ip, $entry)) {
return true;
}
}
}
return false;
}
/**
* Comprueba si la IP del visitante actual está activamente bloqueada.
* Utiliza un caché estático por petición para un rendimiento máximo,
* ya que esta función puede ser llamada varias veces durante una misma ejecución.
*
* @return bool True si la IP está bloqueada, false en caso contrario.
*/
private function is_visitor_actively_blocked() {
static $is_blocked = null;
if ( null !== $is_blocked ) {
return $is_blocked;
}
$ip = $this->get_client_ip();
if ( get_transient( 'advaipbl_blocked_ip_' . md5( $ip ) ) ) {
$is_blocked = true;
return true;
}
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_blocked_ips';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$is_blocked_in_db = $wpdb->get_var( $wpdb->prepare(
"SELECT id FROM {$table_name} WHERE ip_range = %s AND (expires_at = 0 OR expires_at > %d)",
$ip,
time()
) );
if ( $is_blocked_in_db ) {
$is_blocked = true;
return true;
}
$is_blocked = false;
return false;
}
/**
* Revisa y actualiza el estado de 'autoload' para las opciones del plugin que pueden
* crecer mucho en tamaño, mejorando el rendimiento general del sitio.
* Se ejecuta automáticamente una vez gracias a un sistema de versionado.
*/
public function update_option_autoload_states() {
if ( get_option('advaipbl_autoload_version') === self::AUTOLOAD_OPTIMIZATION_VERSION ) {
return;
}
global $wpdb;
$options_to_optimize = [
self::OPTION_WHITELIST,
self::OPTION_BLOCKED_MANUAL,
self::OPTION_BLOCKED_404,
self::OPTION_BLOCKED_403,
self::OPTION_BLOCKED_LOGIN,
self::OPTION_BLOCKED_GEO,
self::OPTION_BLOCKED_HONEYPOT,
self::OPTION_BLOCKED_USER_AGENT,
self::OPTION_BLOCKED_WAF,
self::OPTION_BLOCKED_THREAT_SCORE,
self::OPTION_BLOCKED_RATE_LIMIT,
self::OPTION_BLOCKED_ASN,
self::OPTION_BLOCKED_XMLRPC,
self::OPTION_HONEYPOT_URLS,
self::OPTION_BLOCKED_UAS,
self::OPTION_WHITELISTED_UAS,
self::OPTION_WAF_RULES,
self::OPTION_BLOCKED_ASNS,
self::OPTION_WHITELISTED_ASNS,
'advaipbl_spamhaus_asn_list',
self::OPTION_ADMIN_IP_TRIGGER,
'advaipbl_spamhaus_last_update',
];
foreach ($options_to_optimize as $option_name) {
$wpdb->query( $wpdb->prepare(
"UPDATE {$wpdb->options} SET `autoload` = 'no' WHERE `option_name` = %s",
$option_name
) );
}
update_option('advaipbl_autoload_version', self::AUTOLOAD_OPTIMIZATION_VERSION);
$this->log_event('Successfully optimized database option autoloading for performance.', 'info');
}
/**
* Obtiene y sanea la URI de la petición actual.
* Cachea el resultado para evitar trabajo repetido en una misma petición.
*
* @return string La URI de la petición saneada.
*/
public function get_current_request_uri() {
static $request_uri = null;
if ( is_null($request_uri) ) {
$request_uri = esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'] ?? ''));
}
return $request_uri;
}
/**
* Checks if the current request URI is in the global exclusion list.
*
* @return bool True if the URI should be excluded, false otherwise.
*/
private function is_request_uri_excluded() {
$excluded_urls = $this->options['excluded_error_urls'] ?? '';
if (empty($excluded_urls)) {
return false;
}
$excluded_list = array_filter(array_map('trim', explode("\n", $excluded_urls)));
$current_url = $this->get_current_request_uri();
foreach ($excluded_list as $excluded_item) {
if (!empty($excluded_item) && strpos($current_url, $excluded_item) !== false) {
return true;
}
}
return false;
}
/**
* Obtiene y sanea el User Agent de la petición actual.
* @return string El User Agent de la petición, saneado.
*/
public function get_user_agent() {
static $user_agent = null;
if (is_null($user_agent)) {
$user_agent = sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'] ?? ''));
}
return $user_agent;
}
/**
* Obtiene y sanea el método de la petición actual.
* @return string
*/
public function get_request_method() {
static $request_method = null;
if (is_null($request_method)) {
$request_method = sanitize_text_field($_SERVER['REQUEST_METHOD'] ?? 'GET');
}
return $request_method;
}
/**
* Obtiene y sanea el Referer de la petición actual.
* @return string
*/
public function get_http_referer() {
static $http_referer = null;
if (is_null($http_referer)) {
$http_referer = sanitize_text_field(wp_unslash($_SERVER['HTTP_REFERER'] ?? ''));
}
return $http_referer;
}
/**
* Obtiene y sanea la IP de la conexión directa (REMOTE_ADDR).
* @return string
*/
public function get_remote_addr() {
static $remote_addr = null;
if (is_null($remote_addr)) {
$remote_addr = filter_var($_SERVER['REMOTE_ADDR'] ?? '', FILTER_VALIDATE_IP) ?: '';
}
return $remote_addr;
}
/**
* Incrementa el contador de un endpoint para el sistema de Lockdown.
* Si el contador supera el umbral, activa el modo Lockdown.
*
* @param string $endpoint_key La clave del endpoint (ej. 'xmlrpc').
*/
private function increment_lockdown_counter($endpoint_key) {
$threshold = (int) ($this->options[$endpoint_key . '_lockdown_threshold'] ?? 10);
$window_minutes = (int) ($this->options[$endpoint_key . '_lockdown_window'] ?? 15);
$duration_minutes = (int) ($this->options[$endpoint_key . '_lockdown_duration'] ?? 60);
if ($threshold <= 0 || $window_minutes <= 0) {
return;
}
$cache_key = 'advaipbl_lockdown_trigger_' . $endpoint_key;
// Obtenemos los datos actuales de la caché, que ahora serán un array.
$trigger_data = $this->get_from_custom_cache($cache_key);
if (!is_array($trigger_data)) {
$trigger_data = ['count' => 0, 'ips' => []];
}
$trigger_data['count']++;
$ip = $this->get_client_ip();
// Guardamos las últimas ~15 IPs para tener contexto.
$trigger_data['ips'][] = $ip;
if (count($trigger_data['ips']) > 15) {
$trigger_data['ips'] = array_slice($trigger_data['ips'], -15);
}
$trigger_data['ips'] = array_unique($trigger_data['ips']);
// Volvemos a guardar en la caché.
$ttl = $this->get_from_custom_cache($cache_key, true)['expires_at'] ?? (time() + $window_minutes * MINUTE_IN_SECONDS);
$this->set_in_custom_cache($cache_key, $trigger_data, $ttl - time());
if ($trigger_data['count'] >= $threshold) {
global $wpdb;
$lockdowns_table = $wpdb->prefix . 'advaipbl_endpoint_lockdowns';
$now = time();
$reason = sprintf(
/* translators: 1: The number of blocks. 2: The number of minutes in the detection window. */
__('Exceeded threshold: %1$d blocks in %2$d minutes.', 'advanced-ip-blocker'),
$trigger_data['count'],
$window_minutes
);
$details = wp_json_encode(['triggering_ip_hashes' => $trigger_data['ips']]);
// Insertamos el lockdown en nuestra nueva tabla.
$wpdb->insert(
$lockdowns_table,
[
'endpoint_key' => $endpoint_key,
'reason' => $reason,
'created_at' => $now,
'expires_at' => $now + ($duration_minutes * MINUTE_IN_SECONDS),
'details' => $details,
]
);
// Limpiamos el contador de la caché, ya que el lockdown está activo.
$wpdb->delete($wpdb->prefix . 'advaipbl_cache', ['cache_key' => $cache_key]);
$this->log_event(sprintf('Endpoint Lockdown activated for "%s" for %d minutes due to %d suspicious blocks.', $endpoint_key, $duration_minutes, $trigger_data['count']), 'critical');
$this->send_lockdown_notification($endpoint_key, $duration_minutes, $threshold);
}
}
/**
* Incrementa el contador de fallos de login para el sistema de Lockdown de wp-login.php.
* Activa el lockdown si se superan los umbrales de eventos y de IPs únicas.
*/
private function increment_login_lockdown_counter() {
// Obtenemos los umbrales desde los ajustes (los añadiremos más tarde).
// Por ahora, usamos valores por defecto razonables.
$event_threshold = (int) ($this->options['login_lockdown_event_threshold'] ?? 50);
$ip_threshold = (int) ($this->options['login_lockdown_ip_threshold'] ?? 10);
$window_minutes = (int) ($this->options['login_lockdown_window'] ?? 5);
$duration_minutes = (int) ($this->options['login_lockdown_duration'] ?? 60);
if ($event_threshold <= 0 || $ip_threshold <= 0 || $window_minutes <= 0) {
return;
}
$endpoint_key = 'login';
$cache_key = 'advaipbl_lockdown_trigger_' . $endpoint_key;
// Obtenemos los datos actuales de la caché.
$trigger_data = $this->get_from_custom_cache($cache_key);
if (!is_array($trigger_data) || !isset($trigger_data['event_count'])) {
$trigger_data = ['event_count' => 0, 'ip_hashes' => []];
}
$trigger_data['event_count']++;
$ip_hash = hash('sha256', $this->get_client_ip());
if (!in_array($ip_hash, $trigger_data['ip_hashes'])) {
$trigger_data['ip_hashes'][] = $ip_hash;
}
// Guardamos los datos actualizados en la caché.
$ttl_info = $this->get_from_custom_cache($cache_key, true);
$ttl = $ttl_info ? $ttl_info['expires_at'] - time() : $window_minutes * MINUTE_IN_SECONDS;
if ($ttl > 0) {
$this->set_in_custom_cache($cache_key, $trigger_data, $ttl);
}
// Comprobamos si se han superado ambos umbrales.
if ($trigger_data['event_count'] >= $event_threshold && count($trigger_data['ip_hashes']) >= $ip_threshold) {
global $wpdb;
$lockdowns_table = $wpdb->prefix . 'advaipbl_endpoint_lockdowns';
// Verificamos que no haya ya un lockdown activo para este endpoint para evitar duplicados.
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$is_already_active = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$lockdowns_table} WHERE endpoint_key = %s AND expires_at > %d",
$endpoint_key, time()
));
if (!$is_already_active) {
$now = time();
$reason = sprintf(
/* translators: 1: The number of failed logins. 2: The number of unique IPs. 3: The number of minutes in the detection window. */
__('Exceeded threshold: %1$d failed logins from %2$d unique IPs in %3$d minutes.', 'advanced-ip-blocker'),
$trigger_data['event_count'],
count($trigger_data['ip_hashes']),
$window_minutes
);
$details = wp_json_encode(['triggering_ip_hashes' => $trigger_data['ip_hashes']]);
$wpdb->insert(
$lockdowns_table,
[
'endpoint_key' => $endpoint_key,
'reason' => $reason,
'created_at' => $now,
'expires_at' => $now + ($duration_minutes * MINUTE_IN_SECONDS),
'details' => $details,
]
);
$this->log_event(sprintf('Endpoint Lockdown activated for "%s" for %d minutes.', $endpoint_key, $duration_minutes), 'critical');
$this->send_lockdown_notification($endpoint_key, $duration_minutes, $event_threshold);
}
// Limpiamos el contador de la caché, ya que el lockdown está activo.
$wpdb->delete($wpdb->prefix . 'advaipbl_cache', ['cache_key' => $cache_key]);
}
}
/**
* Comprueba si un endpoint crítico está bajo "Lockdown" y sirve un desafío si es necesario.
* Se ejecuta en un hook temprano para interceptar el tráfico antes que otras comprobaciones.
*/
public function check_for_endpoint_lockdown() {
if ($this->is_request_uri_excluded()) { return; }
if (empty($this->options['enable_xmlrpc_lockdown']) && empty($this->options['enable_login_lockdown'])) {
return;
}
// Si el usuario acaba de pasar un desafío, le damos un pase de gracia de 15s.
if (get_transient('advaipbl_grace_pass_' . md5($this->get_client_ip()))) {
return;
}
if ($this->request_is_asn_whitelisted) { return; }
// Procesa la respuesta del desafío si es para este tipo.
if (isset($_POST['_advaipbl_challenge_type']) && $_POST['_advaipbl_challenge_type'] === 'endpoint') {
// Un pase de 1 hora es suficiente para un endpoint crítico.
$this->verify_js_challenge('advaipbl_js_verified', 1 * HOUR_IN_SECONDS);
}
// Si ya está verificado, no hacer nada.
if (isset($_COOKIE['advaipbl_js_verified']) && $_COOKIE['advaipbl_js_verified'] === '1') {
return;
}
$request_uri = $this->get_current_request_uri();
$endpoint_key = '';
$is_xmlrpc_request = strpos($request_uri, 'xmlrpc.php') !== false;
$is_login_request = strpos($request_uri, 'wp-login.php') !== false;
if ($is_xmlrpc_request && !empty($this->options['enable_xmlrpc_lockdown'])) {
$endpoint_key = 'xmlrpc';
} elseif ($is_login_request && !empty($this->options['enable_login_lockdown'])) {
$endpoint_key = 'login';
}
if (empty($endpoint_key)) {
return;
}
global $wpdb;
$lockdowns_table = $wpdb->prefix . 'advaipbl_endpoint_lockdowns';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$is_lockdown_active = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$lockdowns_table} WHERE endpoint_key = %s AND expires_at > %d",
$endpoint_key,
time()
));
if ($is_lockdown_active) {
$ip = $this->get_client_ip();
if ($this->is_whitelisted($ip)) {
return;
}
$user_agent = $this->get_user_agent();
$is_trusted_service = false;
if ('xmlrpc' === $endpoint_key) {
$automattic_ua_patterns = [ '/^WordPress\/\d+\.\d+/i', '/jetpack by wordpress\.com/i', '/woocommerce/i' ];
foreach ($automattic_ua_patterns as $pattern) {
if (preg_match($pattern, $user_agent)) {
$is_trusted_service = true;
break;
}
}
}
if (!$is_trusted_service) {
$this->log_specific_error('endpoint_challenge', $ip, ['endpoint' => $endpoint_key, 'reason' => ucfirst($endpoint_key) . ' Lockdown Mode Active', 'uri' => $request_uri], 'warning');
// AIB Community Network Reporting
if ( ! empty( $this->options['enable_community_network'] ) ) {
$report_type = ($endpoint_key === 'xmlrpc') ? 'xmlrpc_block' : 'login_lockdown';
$this->reporter_manager->queue_report( $ip, $report_type, ['uri' => $request_uri] );
}
$this->serve_js_challenge('endpoint');
}
}
}
/**
* Envía notificaciones (Email/Push) cuando el modo Lockdown se activa para un endpoint.
*
* @param string $endpoint_key La clave del endpoint (ej. 'xmlrpc').
* @param int $duration_minutes La duración del lockdown.
* @param int $threshold El umbral de bloqueos que lo activó.
*/
public function send_lockdown_notification($endpoint_key, $duration_minutes, $threshold) {
$site_name = get_bloginfo('name');
$endpoint_name = strtoupper($endpoint_key); // ej. XMLRPC
// --- Notificación Push (Webhook) ---
if (!empty($this->options['enable_push_notifications'])) {
$message_lines = [
sprintf('*:lock: [%s] Endpoint Lockdown Activated*', $site_name),
'> An automated defense system has been activated due to a sustained attack.',
/* translators: $s: Endpoint name. */
sprintf('> • *Protected Endpoint:* `%s`', $endpoint_name),
/* translators: $d: Threshold number. */
sprintf('> • *Reason:* More than %d suspicious blocks detected recently.', $threshold),
/* translators: $d: Minutes. */
sprintf('> • *Action:* All new traffic to this endpoint will be challenged for the next %d minutes.', $duration_minutes),
];
$this->execute_webhook_send(implode("\n", $message_lines));
}
// --- Notificación por Email ---
if (!empty($this->options['enable_email_notifications']) && $this->options['notification_frequency'] === 'instant') {
$to = !empty($this->options['notification_email']) ? $this->options['notification_email'] : get_option('admin_email');
/* translators: 1: Site name, 2: Username */
$email_subject = sprintf(__('[%1$s] Security Alert: Endpoint Lockdown Activated for %2$s', 'advanced-ip-blocker'), $site_name, $endpoint_name);
$template_title = __('Endpoint Lockdown Activated', 'advanced-ip-blocker');
$button_url = admin_url('admin.php?page=advaipbl_settings_page&tab=logs&sub-tab=security_log');
$content_html =
'
' . esc_html__("An automated defense mechanism, Endpoint Lockdown, has been activated on your website due to a sustained attack.", 'advanced-ip-blocker') . '
' . esc_html__('Reason for Activation', 'advanced-ip-blocker') . '
' . sprintf(/* translators: %d: The number of IPs. */esc_html__('More than %d suspicious IP blocks were detected in a short period.', 'advanced-ip-blocker'), $threshold) . '
' . sprintf(/* translators: %d: The number minutes. */esc_html__('For the next %d minutes, all new, non-whitelisted traffic to this endpoint will be challenged with a JavaScript verifier to filter out bots.', 'advanced-ip-blocker'), $duration_minutes) . '
' .
'
' .
'
' . esc_html__('This is an automated response to protect your site. No immediate action is required on your part.', 'advanced-ip-blocker') . '
';
$body = $this->get_html_email_template($template_title, $content_html);
add_filter('wp_mail_content_type', [$this, 'set_html_mail_content_type']);
wp_mail($to, $email_subject, $body);
remove_filter('wp_mail_content_type', [$this, 'set_html_mail_content_type']);
}
}
/**
* Comprueba la bandera de activación y redirige al asistente si es necesario.
* Se ejecuta en admin_init.
*/
public function maybe_redirect_to_wizard() {
if ( get_option( 'advaipbl_run_setup_wizard' ) ) {
// Eliminamos la opción para que no se redirija en bucle
delete_option( 'advaipbl_run_setup_wizard' );
if ( ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || isset( $_GET['activate-multi'] ) ) {
// Si es una activación masiva, volvemos a añadir la opción para que se muestre el aviso.
add_option( 'advaipbl_run_setup_wizard', true );
return;
}
wp_safe_redirect( admin_url( 'admin.php?page=advaipbl-setup-wizard' ) );
exit;
}
}
/**
* Muestra un aviso en el panel de administración si el asistente de configuración no se ha completado.
*/
public function display_setup_wizard_notice() {
// Solo mostrar el aviso si la bandera existe, el usuario puede gestionar opciones, y no estamos ya en el asistente.
if ( get_option( 'advaipbl_run_setup_wizard' ) && current_user_can( 'manage_options' ) && ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'advaipbl-setup-wizard' ) ) {
$wizard_url = admin_url( 'admin.php?page=advaipbl-setup-wizard' );
?>
options['enable_abuseipdb']) || empty($this->options['abuseipdb_api_key'])) {
return;
}
// Explicitly allowed by Advanced Rules? Skip.
if ($this->is_advanced_rule_allowed) {
return;
}
if ($this->request_is_asn_whitelisted || (isset($_COOKIE['advaipbl_js_verified']) && $_COOKIE['advaipbl_js_verified'] === '1')) {
return;
}
$ip = $this->get_client_ip();
if ($this->is_whitelisted($ip)) {
return;
}
$result = $this->abuseipdb_manager->check_ip($ip);
if ($result === false) {
return;
}
$threshold = (int) ($this->options['abuseipdb_threshold'] ?? 90);
if ($result['score'] >= $threshold) {
$action_to_take = $this->options['abuseipdb_action'] ?? 'block'; // 'block' es el valor por defecto
$log_data = [
'abuse_score' => $result['score'],
'uri' => $this->get_current_request_uri()
];
if ($action_to_take === 'challenge') {
// Si la acción es 'challenge', registramos el evento y mostramos el desafío.
$this->log_specific_error('abuseipdb_challenge', $ip, $log_data, 'warning');
$this->serve_js_challenge('abuseipdb'); // El tipo es importante para un futuro manejo
} else {
// Si no, procedemos con el bloqueo como antes.
$reason = sprintf(
/* translators: %d: The abuse confidence score from AbuseIPDB. */
__('Blocked by AbuseIPDB with a confidence score of %d%%.', 'advanced-ip-blocker'),
$result['score']
);
if ( isset($this->reporter_manager) ) {
$this->reporter_manager->queue_report( $ip, 'abuseipdb', $log_data );
}
$this->block_ip_instantly($ip, 'abuseipdb', $reason, $log_data);
}
}
}
/**
* Envía una notificación por email al administrador cuando se alcanza el límite de la API de AbuseIPDB.
* Se asegura de enviar solo una notificación por día.
*/
public function send_abuseipdb_limit_email() {
// Solo enviar si las notificaciones por email están activadas
if (empty($this->options['enable_email_notifications']) || '1' !== $this->options['enable_email_notifications']) {
return;
}
$to = !empty($this->options['notification_email']) ? $this->options['notification_email'] : get_option('admin_email');
$site_name = get_bloginfo('name');
/* translators: %s: Site name. */
$email_subject = sprintf(__('[%s] Security Alert: AbuseIPDB API Limit Reached', 'advanced-ip-blocker'), $site_name);
$template_title = __('AbuseIPDB API Limit Reached', 'advanced-ip-blocker');
$settings_url = admin_url('admin.php?page=advaipbl_settings_page&tab=settings&sub-tab=threat_intelligence');
$content_html =
'
' . esc_html__("This is an automated notification to inform you that your website has reached the daily request limit for the AbuseIPDB API.", 'advanced-ip-blocker') . '
' .
'
' . esc_html__("As a result, the real-time IP reputation checking has been temporarily paused. The system will automatically resume checks when the quota resets (typically at 00:00 UTC).", 'advanced-ip-blocker') . '
' .
'
' . esc_html__('What this means:', 'advanced-ip-blocker') . '
' .
'
' .
'
' . esc_html__('Your site remains protected by all other security layers (WAF, Advanced Rules, etc.).', 'advanced-ip-blocker') . '
' .
'
' . esc_html__('This is often an indicator of high bot traffic or a distributed attack.', 'advanced-ip-blocker') . '
' . /* translators: %s: Link to documentation. Do not translate the URLs. */sprintf(wp_kses(__('Consider verifying your domain with AbuseIPDB to increase your free daily limit from 1,000 to 3,000 checks. Learn how.', 'advanced-ip-blocker'), ['a' => ['href' => [], 'target' => []]]), 'https://advaipbl.com/docs/abuseipdb-integration-guide/') . '
' .
'
' . /* translators: %s: Link to pricing page. Do not translate the URLs. */sprintf(wp_kses(__('For high-traffic sites, consider upgrading to a paid AbuseIPDB plan for a higher limit.', 'advanced-ip-blocker'), ['a' => ['href' => [], 'target' => []]]), 'https://www.abuseipdb.com/pricing') . '