' . $js_script . '
';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $html;
exit;
}
/**
* AJAX callback para eliminar una firma maliciosa.
*/
public function ajax_delete_signature() {
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => __('Permission denied.', 'advanced-ip-blocker')]);
}
check_ajax_referer('advaipbl_delete_signature_nonce', 'nonce');
$signature_hash = isset($_POST['hash']) ? sanitize_text_field(wp_unslash($_POST['hash'])) : '';
// Validamos que el hash tenga la longitud correcta de un sha256.
if (strlen($signature_hash) !== 64) {
wp_send_json_error(['message' => __('Invalid signature hash format.', 'advanced-ip-blocker')]);
}
$success = $this->fingerprint_manager->delete_signature($signature_hash);
if ($success) {
/* translators: %1$s: Hash, %2$s: Username */
$this->log_event(sprintf(__('Malicious signature %1$s... was manually deleted by %2$s.', 'advanced-ip-blocker'), substr($signature_hash, 0, 12), $this->get_current_admin_username()), 'warning');
wp_send_json_success(['message' => __('Signature deleted successfully.', 'advanced-ip-blocker')]);
} else {
wp_send_json_error(['message' => __('Failed to delete signature. It might have already expired or been removed.', 'advanced-ip-blocker')]);
}
}
/**
* AJAX callback para obtener los detalles de una firma maliciosa.
*/
public function ajax_get_signature_details() {
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => __('Permission denied.', 'advanced-ip-blocker')]);
}
check_ajax_referer('advaipbl_get_signature_details_nonce', 'nonce');
$signature_hash = isset($_POST['hash']) ? sanitize_text_field(wp_unslash($_POST['hash'])) : '';
if (strlen($signature_hash) !== 64) {
wp_send_json_error(['message' => __('Invalid signature hash format.', 'advanced-ip-blocker')]);
}
$details = $this->fingerprint_manager->get_signature_details($signature_hash);
if ($details !== false) {
wp_send_json_success(['details' => $details]);
} else {
wp_send_json_error(['message' => __('Could not retrieve signature details.', 'advanced-ip-blocker')]);
}
}
/**
* AJAX callback para obtener los detalles de un Endpoint Lockdown.
*/
public function ajax_get_lockdown_details() {
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => __('Permission denied.', 'advanced-ip-blocker')]);
}
check_ajax_referer('advaipbl_get_lockdown_details_nonce', 'nonce');
$lockdown_id = isset($_POST['id']) ? absint($_POST['id']) : 0;
if (!$lockdown_id) {
wp_send_json_error(['message' => __('Invalid Lockdown ID.', 'advanced-ip-blocker')]);
}
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_endpoint_lockdowns';
$lockdown = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table_name} WHERE id = %d", $lockdown_id), ARRAY_A);
if ($lockdown) {
wp_send_json_success(['details' => $lockdown]);
} else {
wp_send_json_error(['message' => __('Could not retrieve lockdown details.', 'advanced-ip-blocker')]);
}
}
/**
* Envía notificaciones (Email/Push) cuando una nueva firma maliciosa es identificada.
*
* @param string $signature_hash El hash de la firma.
* @param string $reason La razón por la que fue marcada.
* @param string $user_agent El User-Agent de ejemplo asociado a la firma.
*/
public function send_signature_flagged_notification($signature_hash, $reason, $user_agent = 'N/A') {
$short_hash = substr($signature_hash, 0, 12) . '...';
$site_name = get_bloginfo('name');
// --- Notificación Push (Webhook) ---
if (!empty($this->options['enable_push_notifications'])) {
$message_lines = [
sprintf('*:dna: [%s] New Attack Signature Identified*', $site_name),
'> A new attack pattern has been automatically identified and is now being challenged.',
sprintf('> • *Signature Hash:* `%s`', $short_hash),
sprintf('> • *Reason:* %s', $reason),
sprintf('> • *Sample User-Agent:* `%s`', $user_agent),
];
$this->execute_webhook_send(implode("\n", $message_lines));
}
// --- Notificación por Email ---
if (!empty($this->options['enable_email_notifications'])) {
$to = !empty($this->options['notification_email']) ? $this->options['notification_email'] : get_option('admin_email');
/* translators: %s: Site name. */
$email_subject = sprintf(__('[%s] Security Alert: New Attack Signature Identified', 'advanced-ip-blocker'), $site_name);
$template_title = __('New Attack Signature Identified', 'advanced-ip-blocker');
$button_url = admin_url('admin.php?page=advaipbl_settings_page&tab=ip_management&sub-tab=blocked_signatures');
$content_html =
'
' . esc_html__("The plugin's analysis engine has automatically identified a new distributed attack pattern. Requests matching this signature will now be challenged.", 'advanced-ip-blocker') . '
';
$body = $this->get_html_email_template($template_title, $content_html);
add_filter('wp_mail_content_type', [$this, 'set_html_mail_content_type']);
wp_mail($to, $email_subject, $body);
remove_filter('wp_mail_content_type', [$this, 'set_html_mail_content_type']);
}
}
/**
* AJAX callback para obtener el historial de eventos de una IP.
*/
public function ajax_get_score_history() {
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => __('Permission denied.', 'advanced-ip-blocker')]);
}
check_ajax_referer('advaipbl_get_history_nonce', 'nonce');
$ip = isset($_POST['ip']) ? sanitize_text_field(wp_unslash($_POST['ip'])) : '';
if (!$ip) {
wp_send_json_error(['message' => __('Invalid IP address.', 'advanced-ip-blocker')]);
}
$history = $this->threat_score_manager->get_log_details($ip);
if ($history !== false) {
wp_send_json_success(['history' => $history]);
} else {
wp_send_json_error(['message' => __('Could not retrieve history.', 'advanced-ip-blocker')]);
}
}
/**
* Callback para el evento de cron que ejecuta el decaimiento de la puntuación de amenaza.
*/
public function execute_threat_score_decay() {
// Solo se ejecuta si el sistema de puntuación está activado.
if (empty($this->options['enable_threat_scoring'])) {
return;
}
$decay_points = (int) ($this->options['score_decay_points'] ?? 1);
$decay_frequency_hours = (int) ($this->options['score_decay_frequency'] ?? 1);
$inactive_for_seconds = $decay_frequency_hours * HOUR_IN_SECONDS;
if ($decay_points > 0 && $decay_frequency_hours > 0) {
$result = $this->threat_score_manager->decay_scores($decay_points, $inactive_for_seconds);
if (($result['updated'] ?? 0) > 0 || ($result['deleted'] ?? 0) > 0) {
$log_message = sprintf( /* translators: 1: Number of scores reduced, 2: Number of scores reset to zero. */
__('Threat score decay process ran. Reduced: %1$d IPs, Reset: %2$d IPs.', 'advanced-ip-blocker'),
$result['updated'],
$result['deleted']
);
$this->log_event($log_message, 'info');
}
}
}
/**
* Callback para el evento de cron que ejecuta el análisis de firmas de ataque.
*/
public function execute_signature_analysis() {
if (empty($this->options['enable_signature_analysis'])) {
return;
}
$ip_threshold = (int) ($this->options['signature_ip_threshold'] ?? 5);
$analysis_window_hours = (int) ($this->options['signature_analysis_window'] ?? 1);
$rule_ttl_hours = (int) ($this->options['signature_rule_ttl'] ?? 24);
$this->fingerprint_manager->analyze_and_flag_signatures(
$ip_threshold,
$analysis_window_hours * HOUR_IN_SECONDS,
$rule_ttl_hours * HOUR_IN_SECONDS
);
}
/**
* Registra el endpoint de la API REST para el feed de ataques en vivo.
*/
public function register_live_feed_api_endpoint() {
register_rest_route('advaipbl/v1', '/live-attacks', [
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_live_attacks_for_feed'],
'permission_callback' => function( $request ) {
// El nonce puede venir como un header o como un parámetro GET/POST
$nonce = $request->get_header('x-wp-nonce') ?? $request->get_param('nonce');
return wp_verify_nonce( $nonce, 'wp_rest' );
},
'args' => [
'since' => [
'description' => 'Get attacks since this log ID.',
'type' => 'integer',
'sanitize_callback' => 'absint',
],
],
]);
}
/**
* Callback para el endpoint de la API. Devuelve los últimos ataques.
*
* @param WP_REST_Request $request
* @return WP_REST_Response
*/
public function get_live_attacks_for_feed(WP_REST_Request $request) {
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_logs';
$since_id = $request->get_param('since');
// Construimos la cadena de la consulta base. Usamos %i para el nombre de la tabla.
$sql = "SELECT log_id, timestamp, ip, log_type, message, details
FROM {$table_name}
WHERE level = 'critical' AND log_type != 'general'";
// Creamos un array para los parámetros que se van a preparar.
$params = [];
if ($since_id) {
$sql .= " AND log_id > %d";
$params[] = $since_id;
}
$sql .= " ORDER BY log_id DESC LIMIT 20";
// Preparamos y ejecutamos la consulta en una sola línea.
$results = $wpdb->get_results(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->prepare($sql, $params),
ARRAY_A
);
$attacks = [];
$last_id = 0;
if (!empty($results)) {
$last_id = (int) $results[0]['log_id'];
foreach ($results as $row) {
$details = json_decode($row['details'], true) ?: [];
$location = 'Unknown Location';
if (!empty($details['city']) && !empty($details['country'])) {
$location = $details['city'] . ', ' . $details['country'];
} elseif (!empty($details['country'])) {
$location = $details['country'];
}
$attacks[] = [
'id' => (int) $row['log_id'],
'time' => human_time_diff(strtotime($row['timestamp'])) . ' ago',
'ip' => esc_html($row['ip']),
'type' => esc_html(ucwords(str_replace('_', ' ', $row['log_type']))),
'location' => esc_html($location),
'method' => esc_html($details['method'] ?? 'N/A'),
'details' => esc_html($row['message']),
'user_agent' => esc_html($details['user_agent'] ?? 'N/A'),
'uri' => esc_html($details['uri'] ?? ($details['url'] ?? '')),
];
}
}
$response_data = [
'attacks' => $attacks,
'last_id' => $last_id,
];
return new WP_REST_Response($response_data, 200);
}
/**
* Endpoint de la API REST para obtener un nonce fresco para el feed en vivo.
* Esto evita problemas de caché de página.
*/
public function get_live_feed_nonce() {
return new WP_REST_Response( [
'nonce' => wp_create_nonce( 'wp_rest' )
], 200 );
}
/**
* Función del shortcode [advaipbl_live_feed].
* Genera el HTML y el CSS necesario para el feed, y encola el script JS.
*
* @param array $atts Atributos del shortcode.
* @return string El HTML y CSS para el feed.
*/
public function render_live_feed_shortcode($atts) {
wp_enqueue_script('advaipbl-live-feed-js', plugin_dir_url(dirname(__FILE__)) . 'js/advaipbl-live-feed.js', ['jquery'], ADVAIPBL_VERSION, true);
wp_localize_script('advaipbl-live-feed-js', 'advaipbl_feed_data', [
'api_url' => get_rest_url(null, 'advaipbl/v1/live-attacks'),
'nonce_url' => get_rest_url(null, 'advaipbl/v1/live-feed-nonce'),
'text' => [
'blocked_from' => __('blocked from', 'advanced-ip-blocker'),
'type' => __('Type', 'advanced-ip-blocker'),
'method' => __('Method', 'advanced-ip-blocker'),
'details' => __('Details', 'advanced-ip-blocker'),
'uri' => __('URI', 'advanced-ip-blocker'),
'user_agent' => __('User Agent', 'advanced-ip-blocker'),
]
]);
$logo_url_desktop = plugin_dir_url(dirname(__FILE__)) . 'assets/img/logo-ligth.png';
$logo_url_mobile = plugin_dir_url(dirname(__FILE__)) . 'assets/img/icon-256x256.png';
$css = '';
$html = '
⬤
Live Security Feed
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(),
'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($_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 bloqueadas.
* La primera vez que se llama, ejecuta la limpieza de IPs expiradas y cachea el resultado.
*
* @return int El número total de entradas bloqueadas.
*/
public function get_blocked_count() {
// Si ya hemos calculado el conteo en esta petición, lo devolvemos directamente.
if ( null !== $this->blocked_count ) {
return $this->blocked_count;
}
// 1. Ejecutar la limpieza de IPs expiradas PRIMERO.
$this->limpiar_ips_expiradas();
// 2. Obtener las entradas limpias y contarlas.
$entries = $this->get_all_blocked_entries();
$this->blocked_count = count( $entries );
// 3. Devolver el conteo.
return $this->blocked_count;
}
/**
* AJAX callback to test the server's outbound connection.
*/
public function ajax_test_outbound_connection() {
if ( ! current_user_can('manage_options') ) {
wp_send_json_error( [ 'message' => 'Permission denied.' ] );
}
check_ajax_referer( 'advaipbl_test_connection_nonce', 'nonce' );
$response = wp_remote_get('https://api.ipify.org?format=json', [ 'timeout' => 10 ]);
if ( is_wp_error($response) ) {
wp_send_json_error( [ 'message' => 'Error: ' . $response->get_error_message() ] );
}
$http_code = wp_remote_retrieve_response_code( $response );
if ( $http_code !== 200 ) {
wp_send_json_error( [ 'message' => sprintf( 'Error: Received HTTP status code %d.', $http_code ) ] );
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( isset( $body['ip'] ) && filter_var( $body['ip'], FILTER_VALIDATE_IP ) ) {
wp_send_json_success( [ 'message' => sprintf( 'Success! Connection established from IP: %s', $body['ip'] ) ] );
}
wp_send_json_error( [ 'message' => 'Error: The response from the test service was invalid.' ] );
}
/**
* Gets the username of the current logged-in administrator.
*
* @return string The username or a default string if not available.
*/
public function get_current_admin_username() {
$user = wp_get_current_user();
// Si el usuario existe y tiene un ID, devuelve su login. Si no, devuelve un texto genérico.
return ( $user && $user->ID ) ? $user->user_login : __( 'an unknown user', 'advanced-ip-blocker' );
}
/**
* Añade una entrada (IP o rango) a la whitelist y se asegura de que sea eliminada
* de todas las listas de bloqueo. Este es el método centralizado para esta acción.
*
* @param string $entry_to_whitelist La IP o rango a añadir a la whitelist.
* @param string $detail Una descripción de por qué se añadió.
* @return bool True si la entrada se añadió con éxito, false si ya existía.
*/
public function add_to_whitelist_and_unblock( $entry_to_whitelist, $detail ) {
if ( ! $this->is_valid_ip_or_range( $entry_to_whitelist ) ) {
return false;
}
$whitelist = get_option( self::OPTION_WHITELIST, [] );
if ( array_key_exists( $entry_to_whitelist, $whitelist ) ) {
return false;
}
$whitelist[ $entry_to_whitelist ] = [ 'timestamp' => time(), 'detail' => $detail ];
update_option( self::OPTION_WHITELIST, $whitelist );
wp_cache_delete( self::OPTION_WHITELIST, 'options' );
if ( filter_var( $entry_to_whitelist, FILTER_VALIDATE_IP ) ) {
$this->desbloquear_ip( $entry_to_whitelist );
} else {
// Usar el mapa de constantes para limpiar el rango de TODAS las listas.
$option_key_map = [
'geoblock' => self::OPTION_BLOCKED_GEO,
'honeypot' => self::OPTION_BLOCKED_HONEYPOT,
'user_agent' => self::OPTION_BLOCKED_USER_AGENT,
'manual' => self::OPTION_BLOCKED_MANUAL,
'404' => self::OPTION_BLOCKED_404,
'403' => self::OPTION_BLOCKED_403,
'login' => self::OPTION_BLOCKED_LOGIN,
'waf' => self::OPTION_BLOCKED_WAF,
'rate_limit' => self::OPTION_BLOCKED_RATE_LIMIT,
'asn' => self::OPTION_BLOCKED_ASN,
'xmlrpc_block' => self::OPTION_BLOCKED_XMLRPC,
'threat_score' => self::OPTION_BLOCKED_THREAT_SCORE,
];
foreach ( $option_key_map as $type => $option_key ) {
$list = get_option( $option_key, [] );
if ( is_array($list) && array_key_exists( $entry_to_whitelist, $list ) ) {
unset( $list[ $entry_to_whitelist ] );
update_option( $option_key, $list );
}
}
}
/* translators: 1: The whitelisted IP/range, 2: The admin username. */
$this->log_event( sprintf( __( 'Entry %1$s added to whitelist by %2$s.', 'advanced-ip-blocker' ), $entry_to_whitelist, $this->get_current_admin_username() ), 'info', is_array($entry_to_whitelist) ? wp_json_encode($entry_to_whitelist) : $entry_to_whitelist );
return true;
}
/**
* AJAX callback to add a specific IP to the whitelist.
* This is used by the interactive buttons in the admin interface.
*/
public function ajax_add_ip_to_whitelist() {
// 1. Validar permisos y nonce de seguridad (esto no cambia).
if ( ! current_user_can('manage_options') ) {
wp_send_json_error( [ 'message' => __( 'Permission denied.', 'advanced-ip-blocker' ) ] );
}
check_ajax_referer( 'advaipbl_add_whitelist_nonce', 'nonce' );
// 2. Obtener y validar la IP del POST. Esta función AJAX solo maneja IPs individuales.
if ( ! isset( $_POST['ip'] ) || ! filter_var( wp_unslash( $_POST['ip'] ), FILTER_VALIDATE_IP ) ) {
wp_send_json_error( [ 'message' => __( 'Invalid or missing IP address.', 'advanced-ip-blocker' ) ] );
}
$ip = sanitize_text_field( wp_unslash( $_POST['ip'] ) );
$detail = isset( $_POST['detail'] ) ? sanitize_text_field( wp_unslash( $_POST['detail'] ) ) : __('Added via admin action', 'advanced-ip-blocker');
// 3. Reutilizar nuestra nueva lógica centralizada.
$success = $this->add_to_whitelist_and_unblock( $ip, $detail );
if ( $success ) {
/* translators: %s: The IP address that was successfully whitelisted. */
wp_send_json_success( [ 'message' => sprintf( __( '%s successfully added to the whitelist.', 'advanced-ip-blocker' ), $ip ) ] );
} else {
/* translators: %s: The IP address that is already whitelisted. */
wp_send_json_success( [ 'message' => sprintf( __( '%s is already whitelisted.', 'advanced-ip-blocker' ), $ip ) ] );
}
}
public function handle_clear_cache_action() {
if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'advaipbl_clear_location_cache_nonce')) {
wp_die('Invalid nonce.');
}
if (!current_user_can('manage_options')) {
wp_die('Permission denied.');
}
// Vaciamos la tabla de caché.
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_cache';
$wpdb->query("TRUNCATE TABLE `{$table_name}`");
/* translators: %s: Admin username. */
$this->log_event( sprintf( __( 'User session location cache manually cleared by %s.', 'advanced-ip-blocker' ), $this->get_current_admin_username() ), 'info' );
set_transient('advaipbl_admin_notice', ['message' => __('Location cache has been cleared.', 'advanced-ip-blocker'), 'type' => 'success'], 30);
wp_safe_redirect(wp_get_referer());
exit;
}
/**
* Envuelve el contenido de un email en una plantilla HTML básica y profesional.
*
* @param string $subject El asunto del email.
* @param string $content_html El contenido principal del email en HTML.
* @return string El cuerpo completo del email en HTML.
*/
private function get_html_email_template( $subject, $content_html ) {
$site_name = get_bloginfo( 'name' );
$site_url = home_url();
$settings_url = admin_url( 'options-general.php?page=advaipbl_settings_page' );
$logo_url = plugin_dir_url( dirname( __FILE__ ) ) . 'assets/img/logo-email.png';
// Estilos CSS en línea
$body_style = "background-color: #f6f7f8; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';";
$wrapper_style = "background-color: #ffffff; border: 1px solid #e0e0e0; border-radius: 4px; padding: 40px; max-width: 600px; margin: 0 auto;";
$logo_wrapper_style = "text-align: center; margin-bottom: 25px;";
// Estilos flexibles para el logo
$logo_style = "max-width: 220px; height: auto; border: 0; display: inline-block; vertical-align: middle;";
$header_style = "font-size: 24px; font-weight: bold; color: #2271b1; margin-top: 0; margin-bottom: 20px; text-align: center;";
$footer_style = "font-size: 12px; color: #777777; text-align: center; margin-top: 30px; line-height: 1.6; border-top: 1px solid #e0e0e0; padding-top: 20px;";
ob_start();
?>
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;
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'),
],
'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' ),
],
]);
}
}
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]
);
// Devolvemos una respuesta 403 Forbidden y terminamos la ejecución.
wp_die(
'XML-RPC services are disabled on this site.',
'Forbidden',
[ 'response' => 403 ]
);
}
}
/**
* Handles the actual restriction of the login page.
* Hooked to 'login_init'.
*/
public function handle_login_page_restriction() {
// Solo actuar si la opción está activada
if ( ! empty( $this->options['restrict_login_page'] ) ) {
$client_ip = $this->get_client_ip();
// Si la IP del visitante no está en la whitelist...
if ( ! $this->is_whitelisted( $client_ip ) ) {
/* translators: %s: The IP address that was denied access. */
$this->log_event( sprintf( __( 'Access to wp-login.php denied for non-whitelisted IP: %s', 'advanced-ip-blocker' ), $client_ip ), 'critical', $client_ip );
// Mostramos un mensaje genérico de acceso denegado y terminamos.
wp_die(
esc_html__( 'Access to this page has been restricted by the administrator.', 'advanced-ip-blocker' ),
esc_html__( 'Access Denied', 'advanced-ip-blocker' ),
[ 'response' => 403 ]
);
}
}
}
/**
* Disables the REST API user endpoints for non-authenticated users if the option is enabled.
*
* @param array $endpoints The available REST API endpoints.
* @return array The modified endpoints.
*/
public function disable_rest_api_user_endpoints( $endpoints ) {
// Solo actuar si la opción está activada en los ajustes.
if ( ! empty( $this->options['disable_user_enumeration'] ) && ! is_user_logged_in() ) {
if ( isset( $endpoints['/wp/v2/users'] ) ) {
unset( $endpoints['/wp/v2/users'] );
}
if ( isset( $endpoints['/wp/v2/users/(?P[\d]+)'] ) ) {
unset( $endpoints['/wp/v2/users/(?P[\d]+)'] );
}
}
return $endpoints;
}
/**
* Prevents user enumeration by redirecting author archive scans at an early hook.
* Enganchado a 'init'.
*/
public function prevent_author_enumeration_redirect() {
// Solo actuar si la opción está activada y no estamos en el admin.
if ( ! empty( $this->options['prevent_author_scanning'] ) && ! is_admin() ) {
// Comprobamos directamente el parámetro GET 'author'. Es mucho más fiable.
if ( isset( $_GET['author'] ) && is_numeric( $_GET['author'] ) ) {
wp_safe_redirect( home_url(), 301 );
exit;
}
}
}
/**
* Checks if a given IP address falls within a specified range (CIDR or hyphenated).
*
* @param string $ip The IP address to check.
* @param string $range The range to check against (e.g., "192.168.1.0/24" or "192.168.1.100-192.168.1.200").
* @return bool True if the IP is in the range, false otherwise.
*/
public function is_ip_in_range( $ip, $range ) {
// 1. Comprobar si es un rango CIDR
if ( strpos( $range, '/' ) !== false ) {
list( $subnet, $bits ) = explode( '/', $range );
if ( filter_var( $subnet, FILTER_VALIDATE_IP ) && is_numeric( $bits ) ) {
$ip_long = ip2long( $ip );
$subnet_long = ip2long( $subnet );
$mask = -1 << ( 32 - (int) $bits );
$subnet_masked = $subnet_long & $mask;
return ( $ip_long & $mask ) === $subnet_masked;
}
}
// 2. Comprobar si es un rango con guion
if ( strpos( $range, '-' ) !== false ) {
list( $start_ip, $end_ip ) = explode( '-', $range, 2 );
$start_ip = trim( $start_ip );
$end_ip = trim( $end_ip );
if ( filter_var( $start_ip, FILTER_VALIDATE_IP ) && filter_var( $end_ip, FILTER_VALIDATE_IP ) ) {
$ip_long = ip2long( $ip );
$start_long = ip2long( $start_ip );
$end_long = ip2long( $end_ip );
return $ip_long >= $start_long && $ip_long <= $end_long;
}
}
// 3. Si no es un rango, es una IP individual. Hacemos una comparación directa.
return $ip === $range;
}
public function run_all_block_checks() {
if ($this->request_is_asn_whitelisted) { return; }
if ( $this->is_visitor_actively_blocked() ) {
$this->access_denied_page(__('403 - Access Denied', 'advanced-ip-blocker'), $this->get_block_message('generic'));
exit;
}
if ( defined( 'WP_CLI' ) && WP_CLI ) {
return;
}
$ip = $this->get_client_ip();
if ($this->is_whitelisted($ip)) {
return;
}
$request_uri = $this->get_current_request_uri();
$is_xmlrpc_request = (strpos($request_uri, 'xmlrpc.php') !== false);
// ENDPOINT LOCKDOWN (PRIMERA ACCIÓN)
if ($is_xmlrpc_request && !empty($this->options['enable_xmlrpc_lockdown'])) {
if (get_transient('advaipbl_lockdown_active_xmlrpc')) {
$user_agent = $this->get_user_agent();
$is_trusted_service = false;
$automattic_ua_patterns = [ '/^WordPress\/\d+\.\d+/i', '/jetpack by wordpress\.com/i', '/woocommerce/i' ];
foreach ($automattic_ua_patterns as $pattern) { if (preg_match($pattern, $user_agent)) { $is_trusted_service = true; break; } }
if (!$is_trusted_service) {
$this->log_specific_error('endpoint_challenge', $ip, ['endpoint' => 'xmlrpc.php', 'reason' => 'XML-RPC Lockdown Mode Active', 'uri' => $request_uri], 'warning');
$this->serve_js_challenge();
}
}
}
if ($is_xmlrpc_request && ($this->options['xmlrpc_protection_mode'] ?? 'smart') === 'smart') {
$is_trusted_request = false;
// Capa 2: Confianza en la Infraestructura (ASN de la conexión directa)
$remote_addr = $this->get_remote_addr();
if ($remote_addr) {
$location_data_remote = $this->geolocation_manager->fetch_location($remote_addr);
$remote_asn = $this->asn_manager->extract_asn_from_data($location_data_remote);
// Lista de ASNs de infraestructura de confianza.
$trusted_infra_asns = ['AS2635', 'AS13335']; // Automattic, Cloudflare
if ($remote_asn && in_array($remote_asn, $trusted_infra_asns, true)) {
$is_trusted_request = true;
}
}
// Capa 3: Confianza en el User-Agent (si la capa de infraestructura no lo confirmó)
if (!$is_trusted_request) {
$user_agent = $this->get_user_agent();
$automattic_ua_patterns = ['/^WordPress\/\d+\.\d+/i', '/jetpack by wordpress\.com/i', '/woocommerce/i'];
foreach ($automattic_ua_patterns as $pattern) {
if (preg_match($pattern, $user_agent)) {
$is_trusted_request = true;
break;
}
}
}
// Capa 4: Decisión final de bloqueo
if (!$is_trusted_request) {
/* translators: %s: User-Agent. */
$reason = sprintf(__('Blocked untrusted XML-RPC request from User-Agent: %s', 'advanced-ip-blocker'), $this->get_user_agent());
$log_data = ['user_agent' => $this->get_user_agent(), 'uri' => $request_uri, 'remote_addr' => $remote_addr];
if (!empty($this->options['enable_xmlrpc_lockdown'])) {
$this->increment_lockdown_counter('xmlrpc');
}
$this->block_ip_instantly($ip, 'xmlrpc_block', $reason, $log_data);
}
// Si es una petición XML-RPC, no continuamos con las demás comprobaciones
return;
}
// Comprobación de bloqueos manuales
$manual_blocks = get_option( self::OPTION_BLOCKED_MANUAL, [] );
if ( ! empty( $manual_blocks ) ) {
foreach ( array_keys( $manual_blocks ) as $entry ) {
if ( $this->is_ip_in_range( $ip, $entry ) ) {
$this->block_ip_instantly( $ip, 'manual', __('Access manually blocked.', 'advanced-ip-blocker') );
}
}
}
// GEOBLOCKING
if (!empty($this->options['enable_geoblocking'])) {
$blocked_countries = $this->options['geoblock_countries'] ?? [];
if ( ! empty( $blocked_countries ) ) {
$location = $this->geolocation_manager->fetch_location( $ip );
if ( $location && ! empty( $location['country_code'] ) ) {
if ( in_array( $location['country_code'], $blocked_countries, true ) ) {
/* translators: %s: Country block. */
$reason = sprintf( __( 'Country block: %s', 'advanced-ip-blocker' ), $location['country'] ?? $location['country_code'] );
$log_data = [ 'country' => $location['country'] ?? '', 'uri' => $this->get_current_request_uri() ];
$this->block_ip_instantly( $ip, 'geoblock', $reason, $log_data );
}
}
}
}
// USER-AGENT CHECK
if (!empty($this->options['enable_user_agent_blocking'])) {
$user_agent = $this->get_user_agent();
if (!empty($user_agent) && !$this->is_internal_request($user_agent)) {
$is_ua_whitelisted = false;
$whitelisted_user_agents = get_option('advaipbl_whitelisted_user_agents', []);
if (!empty($whitelisted_user_agents)) {
foreach ($whitelisted_user_agents as $whitelisted_ua_raw) {
$whitelisted_ua = trim(preg_replace('/#.*$/', '', $whitelisted_ua_raw));
if (!empty($whitelisted_ua) && stripos($user_agent, $whitelisted_ua) !== false) {
$is_ua_whitelisted = true;
break;
}
}
}
if (!$is_ua_whitelisted) {
$blocked_user_agents = get_option('advaipbl_blocked_user_agents', []);
if (!empty($blocked_user_agents)) {
foreach ($blocked_user_agents as $blocked_ua_raw) {
$blocked_ua = trim(preg_replace('/#.*$/', '', $blocked_ua_raw));
if (!empty($blocked_ua) && stripos($user_agent, $blocked_ua) !== false) {
/* translators: %s: User-Agent. */
$reason_message = sprintf(__('Blocked User-Agent pattern: %s', 'advanced-ip-blocker'), $blocked_ua);
$log_data = [
'user_agent' => $user_agent,
'detail' => $reason_message,
'uri' => $this->get_current_request_uri()
];
$this->handle_threat_event($ip, 'user_agent', $reason_message, $log_data);
return;
}
}
}
}
}
}
// WAF CHECK
if ( ! empty( $this->options['enable_waf'] ) ) {
$triggered_rule = $this->waf_manager->run_waf_scan();
if ( $triggered_rule !== false ) {
/* translators: %s: WAF Rule Triggered. */
$reason_message = sprintf(__('WAF Rule Triggered: %s', 'advanced-ip-blocker'), $triggered_rule);
$log_data = [
'rule' => $triggered_rule,
'uri' => $this->get_current_request_uri(),
'method' => $this->get_request_method()
];
$this->handle_threat_event($ip, 'waf', $reason_message, $log_data);
}
}
// ASN CHECK
if (!empty($this->options['enable_spamhaus_asn']) || !empty($this->options['enable_manual_asn'])) {
$provider = $this->options['geolocation_provider'] ?? '';
if (in_array($provider, ['ip-api.com', 'ipinfo.io'], true)) {
$asn_block_data = $this->asn_manager->check_asn_block($ip);
if ($asn_block_data) {
$this->handle_threat_event($ip, 'asn', $asn_block_data['reason_message'], $asn_block_data['log_data']);
}
}
}
}
public function is_whitelisted($ip) {
if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
return false; // No es una IP pública válida, no puede estar en la whitelist.
}
$whitelist = get_option( self::OPTION_WHITELIST, [] );
if ( empty( $whitelist ) ) {
return false;
}
// Comprobación rápida para IPs individuales (la mayoría de los casos)
if ( array_key_exists( $ip, $whitelist ) ) {
return true;
}
// Iterar sobre las claves de la whitelist para buscar rangos.
foreach ( array_keys( $whitelist ) as $entry ) {
// Si la entrada no es la IP exacta, podría ser un rango.
if ( $entry !== $ip && $this->is_ip_in_range( $ip, $entry ) ) {
return true;
}
}
return false;
}
/**
* Comprueba si el ASN del visitante actual está en la lista blanca.
* Si lo está, establece una bandera interna para saltar otras comprobaciones de seguridad.
* Esta función está diseñada para ejecutarse en un hook muy temprano.
*
* @return bool True si el ASN está en la lista blanca, false en caso contrario.
*/
public function is_visitor_asn_whitelisted() {
if ( ! $this->geoip_manager instanceof ADVAIPBL_GeoIP_Manager ) {
return false;
}
// Obtenemos la lista blanca. Si está vacía, no hay nada que hacer.
$whitelisted_asns = get_option(self::OPTION_WHITELISTED_ASNS, []);
if (empty($whitelisted_asns)) {
return false;
}
// Obtenemos la IP del visitante.
$ip = $this->get_client_ip();
if (!$ip) {
return false;
}
// Obtenemos el ASN de la IP.
$location_data = $this->geolocation_manager->fetch_location($ip);
$visitor_asn = $this->asn_manager->extract_asn_from_data($location_data);
if (!$visitor_asn) {
return false;
}
// Comprobamos si el ASN del visitante está en nuestra lista blanca.
if (in_array($visitor_asn, $whitelisted_asns, true)) {
// ¡Coincidencia! Establecemos la bandera y devolvemos true.
$this->request_is_asn_whitelisted = true;
return true;
}
return false;
}
private function is_internal_request($user_agent) { return strpos($user_agent, 'WordPress/') === 0; }
private function get_block_message($type) {
$messages = [
'generic' => __('Access denied by security policy.', 'advanced-ip-blocker'),
'geoblock' => __('Access from your country has been blocked.', 'advanced-ip-blocker'),
'honeypot' => __('Access blocked due to suspicious activity.', 'advanced-ip-blocker'),
'manual' => __('Access manually blocked.', 'advanced-ip-blocker'),
'404' => __('Access blocked due to repeated 404 errors.', 'advanced-ip-blocker'),
'403' => __('Access blocked due to repeated attempts on protected resources.', 'advanced-ip-blocker'),
'login' => __('Access blocked due to multiple failed login attempts.', 'advanced-ip-blocker'),
'user_agent' => __('Access blocked due to suspicious User-Agent.', 'advanced-ip-blocker'),
'waf' => __('Your request was blocked by the security firewall.', 'advanced-ip-blocker'),
'rate_limit' => __('Your connection has been temporarily suspended due to an excessive request rate.', 'advanced-ip-blocker'),
'asn' => __('Access from your network (ASN) has been blocked by the administrator.', 'advanced-ip-blocker'),
'xmlrpc_block' => __('Blocked suspicious XML-RPC request.', 'advanced-ip-blocker'),
'threat_score' => __('Your connection has been blocked due to a high threat score.', 'advanced-ip-blocker'),
];
return $messages[$type] ?? __('Access blocked.', 'advanced-ip-blocker');
}
public function block_ip_instantly($ip, $type, $reason_message, $extra_data = [], $context = 'frontend_block') {
if ( in_array( $ip, [ '127.0.0.1', '::1' ], true ) ) {
$remote_addr = $this->get_remote_addr();
if ( $remote_addr && ! in_array( $remote_addr, [ '127.0.0.1', '::1' ], true ) ) {
$original_ip = $ip;
$ip = $remote_addr;
$extra_data['_spoofed_ip'] = $original_ip;
/* translators: %s: The real attacker IP. */
$reason_message .= sprintf(' (Spoofing attempt from %s)', $ip);
} else {
return;
}
}
global $wpdb;
$lock_key = 'lock_blocking_' . md5($ip);
if (!$wpdb->query($wpdb->prepare("INSERT IGNORE INTO {$wpdb->prefix}advaipbl_cache (cache_key, cache_value, expires_at) VALUES (%s, '1', %d)", $lock_key, time() + 15))) {
if ($context === 'frontend_block') { $this->access_denied_page(__('403 - Access Denied', 'advanced-ip-blocker'), $this->get_block_message('generic')); exit; }
return;
}
$table_name = $wpdb->prefix . 'advaipbl_blocked_ips';
if ($wpdb->get_var($wpdb->prepare("SELECT id FROM {$table_name} WHERE ip_range = %s", $ip))) {
$wpdb->delete("{$wpdb->prefix}advaipbl_cache", ['cache_key' => $lock_key]);
if ($context === 'frontend_block') { $this->access_denied_page(__('403 - Access Denied', 'advanced-ip-blocker'), $this->get_block_message('generic')); exit; }
return;
}
$definitions = $this->get_all_block_type_definitions();
$def = $definitions[$type] ?? null;
$timestamp = time();
$duration_in_seconds = 10 * YEAR_IN_SECONDS;
if ($def && !empty($def['duration_key'])) {
$duration_in_minutes = (int) ($this->options[$def['duration_key']] ?? 1440);
$duration_in_seconds = ($duration_in_minutes > 0) ? $duration_in_minutes * 60 : 0;
}
$expires_at = ($duration_in_seconds > 0) ? $timestamp + $duration_in_seconds : 0;
$wpdb->insert($table_name, [ 'ip_range' => $ip, 'block_type' => $type, 'timestamp' => $timestamp, 'expires_at' => $expires_at, 'reason' => $reason_message ]);
$this->log_specific_error($type, $ip, $extra_data, 'critical');
set_transient('advaipbl_blocked_ip_' . md5($ip), true, $duration_in_seconds);
if ($def && $def['uses_transient']) {
set_transient('advaipbl_bloqueo_' . $type . '_' . md5($ip), true, $duration_in_seconds);
}
$this->send_block_notification($ip, $type, 1, $extra_data);
$this->clear_blocked_ips_cache();
$wpdb->delete("{$wpdb->prefix}advaipbl_cache", ['cache_key' => $lock_key]);
if ($context === 'frontend_block') {
$this->access_denied_page(__('403 - Access Denied', 'advanced-ip-blocker'), $this->get_block_message($type));
exit;
}
}
public function add_admin_ip_to_whitelist_on_first_run() {
if ( get_option('advaipbl_admin_ip_whitelist_trigger') ) {
if ( ! wp_doing_cron() && is_user_logged_in() && current_user_can('manage_options') ) {
$whitelist = get_option('advaipbl_ips_whitelist', []);
$ips_to_add = [];
// 1. Obtener IP del administrador.
$admin_ip = $this->get_client_ip();
if ( filter_var($admin_ip, FILTER_VALIDATE_IP) && !array_key_exists($admin_ip, $whitelist) ) {
$ips_to_add[$admin_ip] = __('Admin IP (auto-added on activation)', 'advanced-ip-blocker');
}
// 2. Obtener IP del servidor.
$server_ip = $this->get_server_ip();
if ( $server_ip && !array_key_exists($server_ip, $whitelist) ) {
// Evitar añadir la misma IP dos veces si el admin y el servidor son el mismo.
if (!isset($ips_to_add[$server_ip])) {
$ips_to_add[$server_ip] = __('Server IP (auto-added on activation)', 'advanced-ip-blocker');
}
}
if ( ! empty($ips_to_add) ) {
foreach ($ips_to_add as $ip => $detail) {
$whitelist[$ip] = [ 'timestamp' => time(), 'detail' => $detail ];
/* translators: %1$s: The IP address, %2$s: The reason/detail for whitelisting. */
$this->log_event( sprintf( __( 'IP %1$s added to whitelist. Reason: %2$s', 'advanced-ip-blocker' ), $ip, $detail ), 'info', $ip );
}
update_option('advaipbl_ips_whitelist', $whitelist);
// Forzamos la invalidación de la caché aquí también.
wp_cache_delete('advaipbl_ips_whitelist', 'options');
}
delete_option('advaipbl_admin_ip_whitelist_trigger');
}
}
}
public function schedule_cron_jobs() {
// Tarea de purga de logs (diaria)
if (!wp_next_scheduled('advaipbl_purge_old_logs_event')) {
wp_schedule_event(time(), 'daily', 'advaipbl_purge_old_logs_event');
}
// Tarea de resúmenes por email (diaria o semanal, según ajustes)
$freq = $this->options['notification_frequency'] ?? 'disabled';
if (($freq === 'daily' || $freq === 'weekly') && !wp_next_scheduled('advaipbl_send_summary_email')) {
$this->schedule_notification_cron();
}
// Tarea de telemetría (cada 3 días, si está activada)
if (!empty($this->options['allow_telemetry']) && '1' === $this->options['allow_telemetry']) {
if (!wp_next_scheduled('advaipbl_send_telemetry_data_event')) {
// Programamos la primera ejecución para dentro de 24 horas para no sobrecargar en la activación.
wp_schedule_event(time() + DAY_IN_SECONDS, '3_days', 'advaipbl_send_telemetry_data_event');
}
} else {
// Si la opción está desactivada, nos aseguramos de que no haya ninguna tarea programada.
wp_clear_scheduled_hook('advaipbl_send_telemetry_data_event');
}
// Tarea de decaimiento de la puntuación de amenaza (cada hora)
if (!wp_next_scheduled('advaipbl_threat_score_decay_event')) {
wp_schedule_event(time(), 'hourly', 'advaipbl_threat_score_decay_event');
}
// Tarea de análisis de firmas (cada 30 minutos)
if (!wp_next_scheduled('advaipbl_signature_analysis_event')) {
wp_schedule_event(time(), '10_minutes', 'advaipbl_signature_analysis_event');
}
// Tarea de actualización de la base de datos GeoIP (diaria)
if ( ! wp_next_scheduled( 'advaipbl_update_geoip_db_event' ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'advaipbl_update_geoip_db_event' );
}
// Tarea de limpieza de la caché (diaria)
if (!wp_next_scheduled('advaipbl_cleanup_expired_cache_event')) {
wp_schedule_event(time(), 'daily', 'advaipbl_cleanup_expired_cache_event');
}
// Asegura que el cron de Spamhaus esté programado si la opción está activa.
if (!empty($this->options['enable_spamhaus_asn']) && '1' === $this->options['enable_spamhaus_asn']) {
if (!wp_next_scheduled('advaipbl_update_spamhaus_list_event')) {
// Si no está programado, lo programamos ahora para que se ejecute diariamente.
wp_schedule_event(time(), 'daily', 'advaipbl_update_spamhaus_list_event');
}
} else {
// Si la opción está desactivada, nos aseguramos de que no haya ningún cron programado.
wp_clear_scheduled_hook('advaipbl_update_spamhaus_list_event');
}
}
public function purge_old_logs() {
$this->options = get_option(self::OPTION_SETTINGS, []);
$retention_days = (int) ($this->options['log_retention_days'] ?? 30);
if ($retention_days <= 0) return;
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_logs';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query($wpdb->prepare("DELETE FROM $table_name WHERE timestamp < DATE_SUB(NOW(), INTERVAL %d DAY)", $retention_days));
}
public function check_database_update() {
static $already_checked = false;
if ( $already_checked ) {
return;
}
$current_db_version = get_option('advaipbl_db_version', '1.0');
if ( version_compare($current_db_version, ADVAIPBL_DB_VERSION, '<') ) {
self::setup_database_tables();
update_option('advaipbl_db_version', ADVAIPBL_DB_VERSION);
}
// Ejecutamos nuestras migraciones de datos aquí.
$this->migrate_whitelist_format();
$this->migrate_blocked_ips_to_table();
$this->cleanup_legacy_options();
$already_checked = true;
}
/**
* Limpia las opciones de la base de datos de versiones muy antiguas del plugin.
* Se ejecuta una sola vez gracias a un sistema de versionado.
*/
private function cleanup_legacy_options() {
if (get_option('advaipbl_legacy_options_cleaned') === self::LEGACY_OPTIONS_CLEANUP_VERSION) {
return;
}
$legacy_options_to_delete = [
'advaipbl_ips_bloqueadas_manual',
'advaipbl_ips_bloqueadas_404',
'advaipbl_ips_bloqueadas_403',
'advaipbl_ips_bloqueadas_login',
'advaipbl_ips_bloqueadas_geoblock',
'advaipbl_ips_bloqueadas_honeypot',
'advaipbl_ips_bloqueadas_user_agent',
'advaipbl_ips_bloqueadas_waf',
'advaipbl_ips_bloqueadas_threat_score',
'advaipbl_ips_bloqueadas_rate_limit',
'advaipbl_ips_bloqueadas_asn',
'advaipbl_ips_bloqueadas_xmlrpc_block',
'advanced-ip-blocker_ips_bloqueadas_404',
'advanced-ip-blocker_ips_bloqueadas_403',
'advanced-ip-blocker_ips_bloqueadas_login',
'advanced-ip-blocker_ips_bloqueadas_honeypot',
'advanced-ip-blocker_ips_bloqueadas_user_agent',
'advanced-ip-blocker_settings',
'advanced-ip-blocker_blocked_user_agents',
'advanced-ip-blocker_whitelisted_user_agents',
'advanced-ip-blocker_ips_whitelist',
'advanced-ip-blocker_ips_bloqueadas_manual',
'advanced-ip-blocker_honeypot_urls',
];
foreach ($legacy_options_to_delete as $option_name) {
delete_option($option_name);
}
update_option('advaipbl_legacy_options_cleaned', self::LEGACY_OPTIONS_CLEANUP_VERSION);
}
/**
* Migrates the old whitelist format ( ip => timestamp ) to the new format ( ip => [ 'timestamp' => ..., 'detail' => ... ] ).
* Runs only if needed.
*/
private function migrate_whitelist_format() {
$whitelist = get_option('advaipbl_ips_whitelist', []);
if ( empty($whitelist) ) {
return; // Nada que migrar.
}
// Comprobamos el formato de la primera entrada para ver si necesita migración.
$first_entry = reset($whitelist);
if ( is_array($first_entry) && isset($first_entry['timestamp']) ) {
return; // Ya está en el nuevo formato.
}
$migrated_whitelist = [];
foreach ($whitelist as $ip => $timestamp) {
if ( is_string($ip) && filter_var($ip, FILTER_VALIDATE_IP) ) {
$migrated_whitelist[$ip] = [
'timestamp' => is_numeric($timestamp) ? $timestamp : time(),
'detail' => __('Migrated from old format', 'advanced-ip-blocker')
];
}
}
update_option( 'advaipbl_ips_whitelist', $migrated_whitelist );
$this->log_event('Whitelist data format successfully migrated.', 'info');
}
/**
* Migra las IPs bloqueadas desde múltiples opciones de WP a la nueva tabla dedicada.
* Se ejecuta una sola vez y limpia las opciones antiguas después de la migración.
*/
private function migrate_blocked_ips_to_table() {
// Usamos una opción para asegurarnos de que esto solo se ejecute una vez.
if (get_option('advaipbl_ip_table_migration_complete')) {
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_blocked_ips';
$definitions = $this->get_all_block_type_definitions();
$migrated_count = 0;
foreach ($definitions as $type => $def) {
// Solo migramos tipos que tenían una lista persistente.
if (empty($def['option_key'])) {
continue;
}
$list = get_option($def['option_key'], []);
if (!is_array($list) || empty($list)) {
// Borramos la opción si está vacía para limpiar la BD.
delete_option($def['option_key']);
continue;
}
foreach ($list as $ip_or_range => $block_data) {
$timestamp = is_array($block_data) && isset($block_data['timestamp']) ? (int)$block_data['timestamp'] : time();
$reason = is_array($block_data) && isset($block_data['detail']) ? $block_data['detail'] : '';
$duration_in_seconds = 10 * YEAR_IN_SECONDS; // Permanente por defecto
if (!empty($def['duration_key'])) {
$duration_in_minutes = (int) ($this->options[$def['duration_key']] ?? 1440);
if ($duration_in_minutes > 0) {
$duration_in_seconds = $duration_in_minutes * 60;
}
}
$expires_at = $timestamp + $duration_in_seconds;
// Insertamos la fila en la nueva tabla.
$wpdb->insert(
$table_name,
[
'ip_range' => $ip_or_range,
'block_type' => $type,
'timestamp' => $timestamp,
'expires_at' => $expires_at,
'reason' => $reason,
]
);
$migrated_count++;
}
// Una vez migradas todas las IPs de esta opción, la borramos.
delete_option($def['option_key']);
}
if ($migrated_count > 0) {
$this->log_event(sprintf('Successfully migrated %d blocked entries to the new database table.', $migrated_count), 'info');
}
// Marcamos la migración como completada para no volver a ejecutarla.
update_option('advaipbl_ip_table_migration_complete', true);
}
public static function setup_database_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
// Tabla de Logs (existente)
$table_name_logs = $wpdb->prefix . 'advaipbl_logs';
$sql_logs = "CREATE TABLE $table_name_logs (
log_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
timestamp DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
ip VARCHAR(100) NOT NULL,
log_type VARCHAR(20) NOT NULL,
level VARCHAR(20) NOT NULL,
message TEXT NOT NULL,
details LONGTEXT NULL,
PRIMARY KEY (log_id),
KEY ip (ip(16)),
KEY log_type (log_type),
KEY timestamp (timestamp)
) $charset_collate;";
dbDelta($sql_logs);
// Tabla de Cola de Notificaciones (existente)
$table_name_queue = $wpdb->prefix . 'advaipbl_notifications_queue';
$sql_queue = "CREATE TABLE $table_name_queue (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
timestamp DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
ip VARCHAR(100) NOT NULL,
block_type VARCHAR(20) NOT NULL,
reason TEXT NOT NULL,
PRIMARY KEY (id),
KEY timestamp (timestamp)
) $charset_collate;";
dbDelta($sql_queue);
// Tabla de Puntuación de Amenaza
$table_name_scores = $wpdb->prefix . 'advaipbl_ip_scores';
$sql_scores = "CREATE TABLE $table_name_scores (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
ip VARCHAR(100) NOT NULL,
score INT(11) NOT NULL DEFAULT 0,
last_event_timestamp INT(11) UNSIGNED NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
log_details TEXT,
PRIMARY KEY (id),
UNIQUE KEY ip (ip),
KEY score (score)
) $charset_collate;";
dbDelta($sql_scores);
// 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);
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 = $_SERVER['SERVER_ADDR'] ?? null;
if ( $server_ip && filter_var( $server_ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
return $server_ip;
}
// Método 2: Fallback a un servicio externo (muy fiable).
// Hacemos una petición a un servicio que nos devuelve nuestra propia IP.
$response = wp_remote_get( 'https://api.ipify.org' );
if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) {
$ip = trim( wp_remote_retrieve_body( $response ) );
if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
return $ip;
}
}
// Si ambos métodos fallan, no podemos determinar la IP.
return null;
}
public function log_event( $message, $level = 'info', $details = [] ) {
if ( empty( $this->options['enable_logging'] ) ) {
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_logs';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
@$wpdb->insert(
$table_name,
[
'timestamp' => current_time( 'mysql', 1 ),
'ip' => $this->get_client_ip(),
'log_type' => 'general',
'level' => $level,
'message' => $message,
'details' => ! empty( $details ) ? wp_json_encode( $details ) : null,
]
);
}
public function log_specific_error($type, $ip, $extra_data = [], $level = 'warning') {
if (empty($this->options['enable_logging'])) {
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_logs';
$details = [
'url' => $this->get_current_request_uri(),
'method' => $this->get_request_method(),
'referrer' => $this->get_http_referer(),
'user_agent' => $this->get_user_agent(),
];
if (is_array($extra_data)) {
$details = array_merge($details, $extra_data);
}
if (!isset($details['lat']) || !isset($details['lon'])) {
$location_data = $this->geolocation_manager->fetch_location($ip);
if ($location_data && empty($location_data['error'])) {
$details['country'] = $location_data['country'] ?? null;
$details['country_code'] = $location_data['country_code'] ?? null;
$details['city'] = $location_data['city'] ?? null;
$details['lat'] = $location_data['lat'] ?? null;
$details['lon'] = $location_data['lon'] ?? null;
}
}
$message = '';
switch ($type) {
/* translators: %s: Username. */
case 'login': $message = sprintf(__('Failed login attempt for user: %s', 'advanced-ip-blocker'), $details['username'] ?? __('unknown', 'advanced-ip-blocker')); break;
/* translators: %s: WAF rule. */
case 'waf': $message = sprintf(__('Request blocked by WAF rule: %s', 'advanced-ip-blocker'), $details['rule'] ?? __('unknown rule', 'advanced-ip-blocker')); break;
/* translators: 1: Number of request, 2: Seconds */
case 'rate_limit': $message = sprintf(__('Rate limit exceeded: %1$d requests in %2$d seconds.', 'advanced-ip-blocker'), $details['count'] ?? 0, $details['window'] ?? 0); break;
/* translators: %s: AS number. */
case 'asn': $message = sprintf(__('Request blocked due to blacklisted ASN: %s', 'advanced-ip-blocker'), $details['asn_number'] ?? 'N/A'); break;
/* translators: %s: User-Agent. */
case 'xmlrpc_block': $message = $details['_reason'] ?? sprintf(__('Blocked untrusted XML-RPC request from User-Agent: %s', 'advanced-ip-blocker'), $details['user_agent'] ?? 'N/A'); break;
/* translators: %s: Honeypot URL. */
case 'honeypot': $message = sprintf(__('Honeypot URL accessed: %s', 'advanced-ip-blocker'), $details['url'] ?? 'N/A'); break;
/* translators: %s: Country. */
case 'geoblock': $message = sprintf(__('Blocked access from country: %s', 'advanced-ip-blocker'), $details['country'] ?? 'N/A'); break;
/* translators: %s: User-Agent. */
case 'user_agent': $message = sprintf(__('Blocked due to User-Agent match: %s', 'advanced-ip-blocker'), $details['user_agent'] ?? 'N/A'); break;
case 'threat_score': $message = $details['_reason'] ?? __('Threat score threshold exceeded', 'advanced-ip-blocker'); break;
case 'signature_flagged': $message = $details['_reason'] ?? __('New Attack Signature Identified.', 'advanced-ip-blocker'); break;
case 'signature_challenge': $message = $details['_reason'] ?? __('Signature challenge not overcome.', 'advanced-ip-blocker'); break;
case 'endpoint_challenge': $message = $details['reason'] ?? __('Endpoint challenge served.', 'advanced-ip-blocker'); break;
case 'geo_challenge': $message = sprintf(/* translators: %s: Country name */ __('Visitor from %s was challenged.', 'advanced-ip-blocker'), $details['country'] ?? 'N/A'); break;
/* translators: %s: Default error reason. */
default: $message = sprintf(__('A %s error occurred.', 'advanced-ip-blocker'), strtoupper($type)); break;
}
// 1. Insertar el log específico del evento (comportamiento actual)
@$wpdb->insert(
$table_name,
[
'timestamp' => current_time('mysql', 1),
'ip' => $ip,
'log_type' => $type,
'level' => $level,
'message' => $message,
'details' => wp_json_encode($details)
]
);
// 2. Si el evento es un bloqueo (nivel 'critical'), añadimos una entrada de auditoría al log general.
if ('critical' === $level) {
$type_label = ucwords(str_replace('_', ' ', $type));
$general_log_message = sprintf(
/* translators: 1: IP Address, 2: Block Type Label (e.g., "WAF Block") */
__('IP %1$s was automatically blocked. Reason: %2$s.', 'advanced-ip-blocker'),
$ip,
$type_label
);
$this->log_event($general_log_message, 'critical', ['original_log_type' => $type]);
}
}
public function registrar_intento_login_fallido( $username ) {
$this->handle_error( 'login', [ 'username' => $username ] );
}
private function handle_error($type, $extra_data = []) {
if (is_user_logged_in() && current_user_can('unfiltered_html')) {
return;
}
$ip = $this->get_client_ip();
if ($this->is_whitelisted($ip)) {
return;
}
$all_block_types = ['geoblock', 'honeypot', 'manual', '404', '403', 'login', 'user_agent', 'waf', 'rate_limit', 'asn', 'xmlrpc_block', 'threat_score'];
foreach ($all_block_types as $reason_type) {
if (get_transient('advaipbl_bloqueo_' . $reason_type . '_' . md5($ip))) {
$this->log_specific_error($type, $ip, $extra_data);
return;
}
}
// Si es un error 404 o 403, nos aseguramos de que $extra_data contenga la URL.
if (in_array($type, ['404', '403']) && !isset($extra_data['url'])) {
$extra_data['url'] = $this->get_current_request_uri();
}
$this->log_specific_error($type, $ip, $extra_data);
if ('login' === $type && !empty($this->options['enable_login_lockdown'])) { // Usaremos una nueva opción 'enable_login_lockdown'
$this->increment_login_lockdown_counter();
}
if (!empty($this->options['enable_threat_scoring'])) {
$points_to_add = (int) ($this->options['score_' . $type] ?? 0);
if ($points_to_add > 0) {
// Pasamos $extra_data, que ahora contiene la URL, a increment_score.
$new_score = $this->threat_score_manager->increment_score($ip, $points_to_add, $type, $extra_data);
$threshold = (int) ($this->options['threat_score_threshold'] ?? 100);
$should_block = false;
if ($new_score >= $threshold || $points_to_add >= 100) {
$should_block = true;
}
if ($should_block) {
$trigger_detail = '';
if ($type === 'login') {
$trigger_detail = 'Login attempt for user: ' . ($extra_data['username'] ?? 'N/A');
} else {
$trigger_detail = $this->get_current_request_uri();
}
/* translators: 1: Points number, 2: An IP will be blocked when its threat score reaches or exceeds this value. */
$score_summary = sprintf(__('Score %1$d/%2$d', 'advanced-ip-blocker'), $new_score, $threshold);
$reason_message = sprintf( /* translators: 1: Score, 2: Block type, 3: Triger. (eg. Score 100/100 via User agent | Trigger: Blocked User-Agent pattern: MJ12bot.) */
__('%1$s via %2$s | Trigger: %3$s', 'advanced-ip-blocker'),
$score_summary,
ucfirst($type),
$trigger_detail
);
$locations = $this->session_manager->get_cached_locations([$ip]);
$location_data = $locations[$ip] ?? null;
$geo_details = [];
if ($location_data && empty($location_data['error'])) {
$geo_details['country'] = $location_data['country'] ?? null;
$geo_details['country_code'] = $location_data['country_code'] ?? null;
$geo_details['city'] = $location_data['city'] ?? null;
$geo_details['lat'] = $location_data['lat'] ?? null;
$geo_details['lon'] = $location_data['lon'] ?? null;
}
$log_details = array_merge([
'final_score' => $new_score,
'triggering_event' => $type,
'_reason' => $reason_message,
'block_reason_code' => $block_reason_code // Dato extra para depuración
], $geo_details);
$this->block_ip_instantly($ip, 'threat_score', $reason_message, $log_details);
}
}
} else {
// LÓGICA ANTIGUA (FALLBACK)
$s = $this->options;
$minute_in_seconds = 60;
$threshold = (int) ($s["threshold_{$type}"] ?? 5);
$window = (int) ($s["transient_expiration_{$type}"] ?? 60) * $minute_in_seconds;
$count_key = "advaipbl_errores_{$type}_" . md5($ip);
$errors = (int) get_transient($count_key) + 1;
set_transient($count_key, $errors, $window);
if ($errors >= $threshold) {
$trigger_detail = ($type === 'login')
? ($extra_data['username'] ?? 'N/A')
: $this->get_current_request_uri();
self::$block_queue = [
'ip' => $ip,
'type' => $type,
'trigger' => $trigger_detail,
'to' => !empty($this->options['notification_email']) && is_email($this->options['notification_email']) ? $this->options['notification_email'] : get_option('admin_email'),
'options' => $this->options,
'errors' => $errors,
];
if (!self::$shutdown_hook_registered) {
register_shutdown_function([$this, 'execute_shutdown_block']);
self::$shutdown_hook_registered = true;
}
delete_transient($count_key);
}
}
}
/**
* Centraliza la decisión de cómo manejar un evento de amenaza instantáneo (WAF, Honeypot, etc.).
* Si el sistema de puntuación está activo, suma puntos. Si no, bloquea directamente.
*
* @param string $ip La dirección IP del atacante.
* @param string $type El tipo de evento (ej. 'waf', 'honeypot').
* @param string $reason_message La razón detallada del evento.
* @param array $log_data Datos adicionales para el log.
*/
public function handle_threat_event($ip, $type, $reason_message, $log_data) {
$scoring_system_active = !empty($this->options['enable_threat_scoring']);
$this->log_specific_error($type, $ip, $log_data, 'warning');
if ($scoring_system_active) {
$points_to_add = (int) ($this->options['score_' . $type] ?? 0);
if ($points_to_add <= 0) { return; }
$new_score = $this->threat_score_manager->increment_score($ip, $points_to_add, $type, $log_data);
$threshold = (int) ($this->options['threat_score_threshold'] ?? 100);
if ($new_score >= $threshold) {
/* translators: 1: Points number, 2: An IP will be blocked when its threat score reaches or exceeds this value. */
$score_summary = sprintf(__('Score %1$d/%2$d', 'advanced-ip-blocker'), $new_score, $threshold);
$block_reason_detail = sprintf( /* translators: 1: Points number, 2: An IP will be blocked when its threat score reaches or exceeds this value, 3: Trigger. */
__('%1$s via %2$s | Trigger: %3$s', 'advanced-ip-blocker'),
$score_summary, ucfirst(str_replace('_', ' ', $type)), $reason_message
);
$locations = $this->session_manager->get_cached_locations([$ip]);
$location_data = $locations[$ip] ?? null;
$geo_details = [];
if ($location_data && empty($location_data['error'])) {
$geo_details = ['country' => $location_data['country'] ?? null, 'country_code' => $location_data['country_code'] ?? null, 'city' => $location_data['city'] ?? null];
}
$block_log_details = array_merge($log_data, $geo_details, ['_reason' => $block_reason_detail]);
$this->block_ip_instantly($ip, 'threat_score', $block_reason_detail, $block_log_details);
}
} else {
$this->block_ip_instantly($ip, $type, $reason_message, $log_data);
}
}
public function execute_shutdown_block() {
if (empty(self::$block_queue)) {
return;
}
$q = self::$block_queue;
$ip = $q['ip'];
$original_ip_for_log = $ip; // Guardamos la IP original para los logs
$extra_data_for_log = ['trigger' => $q['trigger'], 'count' => ($q['errors'] ?? 1)];
if ( in_array( $ip, [ '127.0.0.1', '::1' ], true ) ) {
$remote_addr = $this->get_remote_addr();
if ( $remote_addr && ! in_array( $remote_addr, [ '127.0.0.1', '::1' ], true ) ) {
$ip = $remote_addr; // Cambiamos el objetivo del bloqueo
$extra_data_for_log['_spoofed_ip'] = $original_ip_for_log;
} else {
return; // Petición interna genuina, no bloquear.
}
}
global $wpdb;
$lock_key = 'lock_blocking_shutdown_' . md5($ip);
$lock_acquired = $wpdb->query($wpdb->prepare(
"INSERT IGNORE INTO {$wpdb->prefix}advaipbl_cache (cache_key, cache_value, expires_at) VALUES (%s, %s, %d)",
$lock_key, '1', time() + 15
));
if ( ! $lock_acquired ) {
return;
}
$table_name_blocked = $wpdb->prefix . 'advaipbl_blocked_ips';
$existing_block = $wpdb->get_var($wpdb->prepare("SELECT id FROM {$table_name_blocked} WHERE ip_range = %s", $ip));
if ($existing_block) {
$wpdb->delete("{$wpdb->prefix}advaipbl_cache", ['cache_key' => $lock_key]);
return;
}
$type = $q['type'];
$trigger = $q['trigger'];
$to = $q['to'];
$options = $q['options'];
$errors = (isset($q['errors']) && is_numeric($q['errors'])) ? $q['errors'] : 1;
// 1. Mapear el tipo de bloqueo a su constante de opción correspondiente.
$option_key_map = [
'404' => self::OPTION_BLOCKED_404,
'403' => self::OPTION_BLOCKED_403,
'login' => self::OPTION_BLOCKED_LOGIN,
];
$option_key = $option_key_map[$type] ?? null;
if ( ! $option_key ) {
$wpdb->delete("{$wpdb->prefix}advaipbl_cache", ['cache_key' => $lock_key]); // Liberar cerrojo antes de salir
return;
}
// 2. Acciones de bloqueo (transient y opción persistente)
$duration = (int) ($options["duration_{$type}"] ?? 120) * 60;
$block_key = "advaipbl_bloqueo_{$type}_" . md5($ip);
set_transient($block_key, true, $duration);
$list = get_option($option_key, []);
if (!is_array($list)) $list = [];
$list[$ip] = ['timestamp' => time(), 'detail' => $trigger];
update_option($option_key, $list);
// 3. Obtener datos de geolocalización COMPLETOS
$location_data = $this->geolocation_manager->fetch_location($ip);
$geo_details = [];
if ($location_data && empty($location_data['error'])) {
$geo_details['country'] = $location_data['country'] ?? null;
$geo_details['country_code'] = $location_data['country_code'] ?? null;
$geo_details['city'] = $location_data['city'] ?? null;
$geo_details['lat'] = $location_data['lat'] ?? null;
$geo_details['lon'] = $location_data['lon'] ?? null;
}
// 4. Lógica de email y logs
$notification_enabled = !empty($options['enable_email_notifications']) && '1' === $options['enable_email_notifications'];
$frequency = $options['notification_frequency'] ?? 'disabled';
$email_sent = null;
$reason = '';
switch ($type) {
case '404':
case '403':
$reason = sprintf( /* translators: 1: %1\$d errors number, 2: %2\$s errors number, 3: Triggering URL. */ __("Generated %1\$d %2\$s errors. Triggering URL: %3\$s", 'advanced-ip-blocker'), $errors, strtoupper($type), $trigger);
break;
case 'login':
$reason = sprintf( /* translators: 1: %1$d failed login attempts, 2: %2$s Username. */ __('Made %1$d failed login attempts for user: %2$s', 'advanced-ip-blocker'), $errors, $trigger);
break;
}
if ($notification_enabled && $frequency === 'instant') {
$site_name = get_bloginfo( 'name' );
/* translators: %s: Site name. */
$email_subject = sprintf(__('[%s] Security Alert: IP Automatically Blocked', 'advanced-ip-blocker'), $site_name);
$template_title = __('Security Alert: IP Blocked', 'advanced-ip-blocker');
/* translators: %d: Minutes. */
$duration_text = sprintf(__('%d minutes', 'advanced-ip-blocker'), (int) ($options["duration_{$type}"] ?? 120));
$button_url = admin_url('admin.php?page=advaipbl_ip_management&sub-tab=blocked_ips');
$button_style = "background-color: #2271b1; color: #ffffff; padding: 12px 25px; text-decoration: none; border-radius: 4px; display: inline-block; font-weight: bold;";
$content_html =
'
' . esc_html__( "An IP address has been automatically blocked on your website.", 'advanced-ip-blocker' ) . '
';
}
/**
* 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 ) {
// Si es un error que da pistas, devolvemos un NUEVO WP_Error con nuestro mensaje genérico.
// WordPress se encargará de mostrarlo correctamente en la banda roja.
return new WP_Error(
'advaipbl_generic_login_error',
__( 'ERROR: Your login details are incorrect. Please try again.', 'advanced-ip-blocker' )
);
}
// Si no es un error de los que queremos ocultar (ej. el de 2FA), lo dejamos pasar.
return $user;
}
/**
* Guarda los cambios de la sección 2FA en el perfil de usuario.
* Este hook es necesario para WordPress. Nuestra lógica de activación/desactivación
* se maneja principalmente vía AJAX para una mejor experiencia de usuario.
*
* @param int $user_id El ID del usuario que se está actualizando.
*/
public function save_2fa_section_in_profile( $user_id ) {
// No se requiere ninguna acción aquí, ya que el JS maneja las acciones de 2FA.
// La función debe existir para que el hook 'personal_options_update' no cause un error fatal.
if ( ! current_user_can( 'edit_user', $user_id ) ) {
return;
}
// No hay nada que hacer, pero la función está aquí para evitar el error.
}
/**
* AJAX callback para iniciar la descarga de la base de datos GeoIP.
*/
public function ajax_update_geoip_db() {
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'advaipbl_update_geoip_nonce' ) ) {
wp_send_json_error( ['message' => 'Nonce verification failed.'], 403 );
}
if ( ! current_user_can( 'manage_options' ) || ! $this->geoip_manager ) {
wp_send_json_error( ['message' => 'Permission denied or module not available.'], 403 );
}
// Aumentamos el límite de tiempo de ejecución para la descarga
set_time_limit(300);
$result = $this->geoip_manager->download_and_unpack_databases();
if ( $result['success'] ) {
wp_send_json_success( ['message' => $result['message']] );
} else {
wp_send_json_error( ['message' => $result['message']] );
}
}
/**
* Tarea programada para actualizar las bases de datos de GeoIP.
*/
public function execute_geoip_db_update() {
// Solo se ejecuta si el método es 'local_db' y hay una clave de licencia.
if (
isset($this->options['geolocation_method']) && $this->options['geolocation_method'] === 'local_db' &&
!empty($this->options['maxmind_license_key']) && $this->geoip_manager
) {
$this->log_event('Starting scheduled GeoIP database update.', 'info');
$result = $this->geoip_manager->download_and_unpack_databases();
if (!$result['success']) {
$this->log_event('Scheduled GeoIP database update failed: ' . $result['message'], 'error');
} else {
$this->log_event('Scheduled GeoIP database update completed successfully.', 'info');
}
}
}
/**
* Comprueba si una IP está actualmente bajo un bloqueo activo de cualquier tipo.
* Esta función es una comprobación de alto rendimiento que prioriza los transients.
*
* @param string $ip La dirección IP a comprobar.
* @return bool True si la IP está activamente bloqueada, false en caso contrario.
*/
public function is_ip_actively_blocked($ip) {
$definitions = $this->get_all_block_type_definitions();
// 1. Comprobación de transients para todos los tipos que los usan.
foreach ($definitions as $type => $def) {
if ($def['uses_transient'] && get_transient('advaipbl_bloqueo_' . $type . '_' . md5($ip))) {
return true;
}
}
// 2. Comprobación específica para bloqueos manuales (que son permanentes).
$manual_blocks = get_option(self::OPTION_BLOCKED_MANUAL, []);
if (!empty($manual_blocks)) {
foreach (array_keys($manual_blocks) as $entry) {
if ($this->is_ip_in_range($ip, $entry)) {
return true;
}
}
}
return false;
}
/**
* Comprueba si la IP del visitante actual está activamente bloqueada.
* Utiliza un caché estático por petición para un rendimiento máximo,
* ya que esta función puede ser llamada varias veces durante una misma ejecución.
*
* @return bool True si la IP está bloqueada, false en caso contrario.
*/
private function is_visitor_actively_blocked() {
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';
$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.
$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;
}
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';
$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');
$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') . '