plugin = $plugin_instance;
}
/**
* Gets the full path to the .htaccess file.
*
* @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';
}
/**
* Verifies if the .htaccess file exists and is writable.
* Uses wp_is_writable() to comply with WP standards.
*
* @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)));
}
/**
* Creates a backup of the current .htaccess file in a secure folder.
* Keeps only the last 30 backups.
*
* @return bool|string Backup path if successful, false on failure.
*/
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;
}
$backup_filename = 'htaccess_backup_' . gmdate('Y-m-d_H-i-s') . '_' . bin2hex(random_bytes(8)) . '.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');
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) {
wp_delete_file($file);
}
}
return $backup_path;
} else {
$this->plugin->log_event('Backup failed: Could not copy .htaccess to ' . $backup_path, 'error');
}
return false;
}
/**
* Gets IPs to block.
* Excludes hyphenated ranges (-) since Apache doesn't natively support them in Require ip.
*/
private function get_ips_to_block()
{
global $wpdb;
$table_name = $wpdb->prefix . 'advaipbl_blocked_ips';
$include_all = !empty($this->plugin->options['enable_htaccess_all_ips']);
$sql = "SELECT ip_range FROM {$table_name} WHERE ip_range NOT LIKE '%-%'";
if (! $include_all) {
$sql .= " AND (block_type IN ('manual', 'bulk_import') OR expires_at = 0)";
}
$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;
$hardening_files = [];
if (! empty($options['htaccess_protect_system_files'])) {
$hardening_files[] = '\.(7z|bak|bz2|com|conf|dist|fla|git|env|inc|ini|log|old|psd|rar|tar|tgz|save|sh|sql|svn|swo|swp)$';
$hardening_files[] = '^\.ds_store$';
}
if (! empty($options['htaccess_protect_wp_config'])) {
$hardening_files[] = '^wp-config(-sample)?\.php$';
}
if (! empty($options['htaccess_protect_readme'])) {
$hardening_files[] = '^(readme\.(html|txt)|license\.txt)$';
}
if (! empty($hardening_files)) {
$regex = '(?i)(' . implode('|', $hardening_files) . ')';
$rules[] = '';
$rules[] = ' ';
$rules[] = ' Require all denied';
$rules[] = ' ';
$rules[] = ' ';
$rules[] = ' Order allow,deny';
$rules[] = ' Deny from all';
$rules[] = ' ';
$rules[] = '';
$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';
$setenvif_rules = [];
foreach ($ips_to_block as $ip) {
if (strpos($ip, '/') === false) {
$ip_regex = str_replace('.', '\.', $ip);
$ip_regex = '^' . $ip_regex . '$';
$setenvif_rules[] = ' SetEnvIF REMOTE_ADDR "' . $ip_regex . '" DenyAccess';
$setenvif_rules[] = ' SetEnvIF X-FORWARDED-FOR "' . $ip_regex . '" DenyAccess';
$setenvif_rules[] = ' SetEnvIF X-CLUSTER-CLIENT-IP "' . $ip_regex . '" DenyAccess';
}
}
if (!empty($setenvif_rules)) {
$rules[] = '';
$rules = array_merge($rules, $setenvif_rules);
$rules[] = '';
}
$rules[] = '';
$rules[] = '';
$rules[] = ' ';
$rules[] = ' Require all granted';
$rules[] = ' Require not env DenyAccess';
foreach ($ips_to_block as $ip) {
if (empty(trim($ip))) {
continue;
}
$rules[] = ' Require not ip ' . $ip;
}
$rules[] = ' ';
$rules[] = '';
$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'])) {
$safe_value = str_replace('"', '\"', trim($header['value']));
if ($key === 'hsts') {
$header_rules[] = ' Header set ' . trim($header['name']) . ' "' . $safe_value . '" env=HTTPS';
} 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.');
}
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, []);
}
}