plugin = $plugin_instance;
}
/**
* Obtiene la ruta completa al archivo .htaccess.
*
* @return string
*/
public function get_htaccess_path() {
if ( ! function_exists( 'get_home_path' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
return get_home_path() . '.htaccess';
}
/**
* Verifica si el archivo .htaccess existe y es escribible.
* Utiliza wp_is_writable() para cumplir con los estándares de WP.
*
* @return bool
*/
public function is_writable() {
$path = $this->get_htaccess_path();
return ( file_exists( $path ) && wp_is_writable( $path ) ) || ( ! file_exists( $path ) && wp_is_writable( dirname( $path ) ) );
}
/**
* Crea una copia de seguridad del archivo .htaccess actual en una carpeta segura.
* Mantiene solo los últimos 30 backups.
*
* @return bool|string Ruta del backup si tiene éxito, false si falla.
*/
public function create_backup() {
$htaccess_path = $this->get_htaccess_path();
if ( ! file_exists( $htaccess_path ) ) {
return false;
}
$upload_dir = wp_upload_dir();
$backup_dir = $upload_dir['basedir'] . '/advaipbl-backups';
if ( ! file_exists( $backup_dir ) ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir
if ( ! wp_mkdir_p( $backup_dir ) ) {
$this->plugin->log_event( 'Backup failed: Could not create directory ' . $backup_dir, 'error' );
return false;
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
file_put_contents( $backup_dir . '/.htaccess', 'deny from all' );
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
file_put_contents( $backup_dir . '/index.php', 'plugin->log_event( 'Backup failed: Directory ' . $backup_dir . ' is not writable.', 'error' );
return false;
}
// Usamos gmdate() para evitar problemas de zona horaria (Standard WP).
$backup_filename = 'htaccess_backup_' . gmdate( 'Y-m-d_H-i-s' ) . '.txt';
$backup_path = $backup_dir . '/' . $backup_filename;
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_copy
if ( copy( $htaccess_path, $backup_path ) ) {
$files = glob( $backup_dir . '/htaccess_backup_*.txt' );
// AUMENTADO: Mantenemos las últimas 30 copias en lugar de 5.
if ( $files && count( $files ) > 30 ) {
array_multisort( array_map( 'filemtime', $files ), SORT_NUMERIC, SORT_ASC, $files );
$to_delete = array_slice( $files, 0, count( $files ) - 30 );
foreach ( $to_delete as $file ) {
// Usamos wp_delete_file() en lugar de unlink() (Standard WP).
wp_delete_file( $file );
}
}
return $backup_path;
} else {
$this->plugin->log_event( 'Backup failed: Could not copy .htaccess to ' . $backup_path, 'error' );
}
return false;
}
/**
* Obtiene las IPs para bloquear.
* Excluye rangos con guión (-) ya que Apache no los soporta nativamente en Require ip.
*/
private function get_ips_to_block() {
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_blocked_ips';
// Opciones
$include_all = !empty($this->plugin->options['enable_htaccess_all_ips']);
// Construcción de la Query
$sql = "SELECT ip_range FROM {$table_name} WHERE ip_range NOT LIKE '%-%'";
// Si NO está marcado "Include Temporary", filtramos solo manuales, importaciones masivas o permanentes
if ( ! $include_all ) {
$sql .= " AND (block_type IN ('manual', 'bulk_import') OR expires_at = 0)";
}
// Siempre aplicamos un límite de seguridad para proteger Apache
// Si incluimos temporales, subimos un poco el límite a 2000 para ser más agresivos,
// pero manteniendo la seguridad de que el archivo no pese 10MB.
$limit = $include_all ? 2000 : 1000;
$sql .= " ORDER BY id DESC LIMIT %d";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$results = $wpdb->get_col( $wpdb->prepare( $sql, $limit ) );
return $results ? $results : [];
}
public function generate_rules_content() {
$rules = [];
$options = $this->plugin->options;
// 1. Hardening Rules
$hardening_active = false;
if ( ! empty( $options['htaccess_protect_system_files'] ) ) {
$rules[] = 'RedirectMatch 403 (?i)\.(7z|bak|bz2|com|conf|dist|fla|git|env|inc|ini|log|old|psd|rar|tar|tgz|save|sh|sql|svn|swo|swp)$';
$rules[] = 'RedirectMatch 403 (?i)/\.ds_store$';
$hardening_active = true;
}
if ( ! empty( $options['htaccess_protect_wp_config'] ) ) {
$rules[] = 'RedirectMatch 403 (?i)/wp-config\.php$';
$rules[] = 'RedirectMatch 403 (?i)/wp-config-sample\.php$';
$hardening_active = true;
}
if ( ! empty( $options['htaccess_protect_readme'] ) ) {
$rules[] = 'RedirectMatch 403 (?i)/readme\.(html|txt)$';
$hardening_active = true;
}
if ( $hardening_active ) {
array_unshift( $rules, '' );
$rules[] = '';
$rules[] = '';
}
// 2. IP Blocking Rules
$ips_to_block = [];
if ( ! empty( $options['enable_htaccess_ip_blocking'] ) ) {
$ips_to_block = $this->get_ips_to_block();
}
if ( ! empty( $ips_to_block ) ) {
$rules[] = '# IP Blocking Rules';
// Generar reglas SetEnvIF SOLO para IPs únicas (sin CIDR)
foreach ( $ips_to_block as $ip ) {
if ( strpos( $ip, '/' ) === false ) {
$ip_regex = str_replace( '.', '\.', $ip );
$ip_regex = '^' . $ip_regex . '$';
$rules[] = 'SetEnvIF REMOTE_ADDR "' . $ip_regex . '" DenyAccess';
$rules[] = 'SetEnvIF X-FORWARDED-FOR "' . $ip_regex . '" DenyAccess';
$rules[] = 'SetEnvIF X-CLUSTER-CLIENT-IP "' . $ip_regex . '" DenyAccess';
}
}
$rules[] = '';
// Apache 2.4
$rules[] = '';
$rules[] = ' ';
$rules[] = ' Require all granted';
$rules[] = ' Require not env DenyAccess';
foreach ( $ips_to_block as $ip ) {
$rules[] = ' Require not ip ' . $ip;
}
$rules[] = ' ';
$rules[] = '';
// Apache 2.2
$rules[] = '';
$rules[] = ' Order allow,deny';
$rules[] = ' Allow from all';
$rules[] = ' Deny from env=DenyAccess';
foreach ( $ips_to_block as $ip ) {
$rules[] = ' Deny from ' . $ip;
}
$rules[] = '';
}
// 3. Security Headers
$security_headers = get_option('advaipbl_security_headers');
if ( is_array($security_headers) ) {
$header_rules = [];
foreach ($security_headers as $key => $header) {
if (!empty($header['enabled']) && !empty($header['name']) && !empty($header['value'])) {
// Escape double quotes to prevent breaking the Apache Header directive syntax
$safe_value = str_replace('"', '\"', trim($header['value']));
if ($key === 'hsts') {
$header_rules[] = ' Header set ' . trim($header['name']) . ' "' . $safe_value . '" env=HTTPS';
} elseif ($key === 'server_header' && trim($header['value']) === 'remove') {
$header_rules[] = ' Header unset Server';
} else {
$header_rules[] = ' Header set ' . trim($header['name']) . ' "' . $safe_value . '"';
}
}
}
if (!empty($header_rules)) {
$rules[] = '';
$rules[] = '# Security Headers';
$rules[] = '';
$rules = array_merge($rules, $header_rules);
$rules[] = '';
}
}
if ( empty($rules) ) {
return "# No active rules selected in Advanced IP Blocker settings.";
}
return implode( "\n", $rules );
}
public function update_htaccess() {
if ( ! $this->is_writable() ) {
return new WP_Error( 'file_not_writable', 'The .htaccess file is not writable.' );
}
$this->create_backup();
$rules_string = $this->generate_rules_content();
$rules_array = explode( "\n", $rules_string );
if ( ! function_exists( 'insert_with_markers' ) ) {
require_once ABSPATH . 'wp-admin/includes/misc.php';
}
$result = insert_with_markers( $this->get_htaccess_path(), $this->marker, $rules_array );
if ( ! $result ) {
return new WP_Error( 'write_failed', 'Failed to write to .htaccess file.' );
}
// FIM & Firewall Sync: Update baseline to prevent false positive alert
if ( isset( $this->plugin->file_verifier ) && ! empty( $this->plugin->options['enable_fim'] ) ) {
$this->plugin->file_verifier->update_file_hash( $this->get_htaccess_path() );
}
return true;
}
public function remove_rules() {
if ( ! function_exists( 'insert_with_markers' ) ) {
require_once ABSPATH . 'wp-admin/includes/misc.php';
}
return insert_with_markers( $this->get_htaccess_path(), $this->marker, [] );
}
}