* @copyright 2026 Oleg Klenitsky * @license GPL-2.0-or-later * @version 1.0.0 */ namespace sitewatchman\swm7; if ( ! defined( 'ABSPATH' ) ) { exit(); } if ( ! defined( 'FS_CHMOD_FILE' ) ) { define( 'FS_CHMOD_FILE', ( 0644 & ~ umask() ) ); } /** * Performs atomic bulk removal of multiple IP addresses and corresponding temporal ban comment vectors from the server configuration file. * * Initializes the native WordPress abstraction layers to handle underlying file input/output transactions, evaluates structural * write compliance metrics against the `.htaccess` infrastructure, processes a targeted line-by-line reduction pass to isolate * exact directive string patterns, purges adjacent architectural timestamp markers, and synchronously commits the modified configuration stream. * * @since 1.0.0 * @see WP_Filesystem * @see get_home_path * @see \WP_Filesystem_Base::exists() * @see \WP_Filesystem_Base::is_writable() * @see \WP_Filesystem_Base::get_contents_array() * @see \WP_Filesystem_Base::put_contents() * * @param array $ips A collection of validated target IPv4/IPv6 addresses designated for removal. * @return bool True if the configuration state is unmodified or rewritten successfully, false during permission or system failures. */ function swm7_batch_ips_del_from_file( $ips ) { if ( empty( $ips ) || ! is_array( $ips ) ) { return true; } if ( ! function_exists( 'WP_Filesystem' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } if ( ! WP_Filesystem() ) { return false; } global $wp_filesystem; $filename = get_home_path() . '.htaccess'; if ( ! $wp_filesystem->exists( $filename ) || ! $wp_filesystem->is_writable( $filename ) ) { return false; } $arr = $wp_filesystem->get_contents_array( $filename ); if ( ! is_array( $arr ) ) { return false; } $target_lines = array_map( function ( $ip ) { return 'Deny from ' . $ip; }, $ips ); $is_changed = false; foreach ( $arr as $key => $value ) { $clean_line = trim( $value ); if ( in_array( $clean_line, $target_lines, true ) ) { unset( $arr[ $key ] ); $is_changed = true; if ( isset( $arr[ $key - 1 ] ) ) { $prev_line = trim( $arr[ $key - 1 ] ); if ( 0 === strpos( $prev_line, '# Banned at' ) ) { unset( $arr[ $key - 1 ] ); } } } } if ( $is_changed ) { $arr = array_filter( array_map( 'trim', $arr ) ); $content = implode( PHP_EOL, $arr ) . PHP_EOL; return $wp_filesystem->put_contents( $filename, $content, 0644 ); } return true; } /** * Removes a single specific IP blocking directive from the server configuration file. * * Validates the network token using raw semantic filters to prevent malformed pattern matching, * interfaces with the central WordPress filesystem abstraction layer to verify file integrity, * performs a clean line-by-line inspection loop of `.htaccess` rows, and conditionally executes * a structural file flush only if structural changes are caught. * * @since 1.0.0 * @see filter_var * @see WP_Filesystem * @see get_home_path * @see \WP_Filesystem_Base::exists() * @see \WP_Filesystem_Base::is_writable() * @see \WP_Filesystem_Base::get_contents_array() * @see \WP_Filesystem_Base::put_contents() * * @param string $user_ip The target host IP address string designated for blocking rule removal. * @return bool True if the directive is cleanly omitted or missing, false on validation or permission failures. */ function swm7_ip_del_from_file( $user_ip ) { // 1. Validation of the incoming IP address. $user_ip = filter_var( $user_ip, FILTER_VALIDATE_IP ); if ( ! $user_ip ) { return false; } // 2. Initialize the WordPress file system. if ( ! function_exists( 'WP_Filesystem' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } if ( ! WP_Filesystem() ) { return false; } global $wp_filesystem; $filename = get_home_path() . '.htaccess'; if ( ! $wp_filesystem->exists( $filename ) || ! $wp_filesystem->is_writable( $filename ) ) { return false; } // 3. We retrieve the file contents line by line. $arr = $wp_filesystem->get_contents_array( $filename ); if ( ! is_array( $arr ) ) { return false; } $target_line = 'Deny from ' . $user_ip; $is_changed = false; foreach ( $arr as $key => $value ) { // We strip the string of extra spaces and line breaks to ensure an exact match. $clean_line = trim( $value ); // We check for an exact match of the blocking rule to avoid partial IP matches. if ( $clean_line === $target_line ) { unset( $arr[ $key ] ); $is_changed = true; } } // 4. We write the data back only if the file has actually changed. if ( $is_changed ) { $arr = array_filter( array_map( 'trim', $arr ) ); // We concatenate strings using the explicit newline constant PHP_EOL. $content = implode( PHP_EOL, $arr ) . PHP_EOL; return $wp_filesystem->put_contents( $filename, $content, 0644 ); } return true; } /** * Appends a single specific IP blocking directive to the server configuration file. * * Validates the incoming network token using semantic structural filters, establishes a secure connection * via the WordPress filesystem abstraction layer, verifies that the identical target deny directive does * not already exist within the file space to prevent redundant collisions, and commits the appended rule * configuration utilizing safe 0644 operational system permissions. * * @since 1.0.0 * @see filter_var * @see WP_Filesystem * @see get_home_path * @see \WP_Filesystem_Base::exists() * @see \WP_Filesystem_Base::is_writable() * @see \WP_Filesystem_Base::get_contents() * @see \WP_Filesystem_Base::put_contents() * * @param string $user_ip The validated target host IP address string designated for blocking rule insertion. * @return bool True if the directive already exists or is successfully committed, false on security validation or permission failures. */ function swm7_ip_ins_to_file( $user_ip ) { // 1. Validation of the incoming IP address. $user_ip = filter_var( $user_ip, FILTER_VALIDATE_IP ); if ( ! $user_ip ) { return false; } // 2. Initialize the WordPress file system. if ( ! function_exists( 'WP_Filesystem' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } if ( ! WP_Filesystem() ) { return false; } global $wp_filesystem; $filename = get_home_path() . '.htaccess'; if ( ! $wp_filesystem->exists( $filename ) || ! $wp_filesystem->is_writable( $filename ) ) { return false; } // 3. Reading the current content. $content = $wp_filesystem->get_contents( $filename ); // Blocking rule. $deny_rule = 'Deny from ' . $user_ip; // We are looking for an exact match for the rule. if ( false !== strpos( $content, $deny_rule ) ) { return true; } // 4. Add the rule on a new line, paying attention to formatting. $content = rtrim( $content ); $content .= PHP_EOL . $deny_rule . PHP_EOL; // 5. We write with permissions 0644 (read/write for the owner, read for everyone). return $wp_filesystem->put_contents( $filename, $content, 0644 ); } /** * Resolves or establishes an isolated, secure directory space inside the central uploads infrastructure for storing security log backups. * * Interrogates core system paths to evaluate deployment environment settings, constructs a standardized structural location trace * for backup artifacts, checks the state of the target folder via the filesystem abstraction layer, and automatically provisions * missing subdirectories with restricted 0755 operational parameters while injecting an empty index file to suppress public index scanning. * * @since 1.0.0 * @see wp_upload_dir * @see wp_normalize_path * @see \WP_Filesystem_Base::is_dir() * @see \WP_Filesystem_Base::mkdir() * @see \WP_Filesystem_Base::put_contents() * * @return string|bool The absolute canonical path mapping the isolated backup directory, or false during configuration or folder instantiation failures. */ function swm7_get_backup_dir() { if ( ! function_exists( 'wp_upload_dir' ) ) { return false; } // We obtain the official path to the downloads directory. $upload_dir = wp_upload_dir(); if ( ! empty( $upload_dir['error'] ) ) { return false; } // An isolated folder inside wp-content/uploads/. $backup_dir = wp_normalize_path( $upload_dir['basedir'] . '/sitewatchman-backups' ); global $wp_filesystem; if ( $wp_filesystem && ! $wp_filesystem->is_dir( $backup_dir ) ) { $wp_filesystem->mkdir( $backup_dir, 0755 ); // Create an empty index.php file to secure the directory. $wp_filesystem->put_contents( $backup_dir . '/index.php', 'exists( $filename ) ) { return; } // Safe file permissions: 0644. $wp_filesystem->chmod( $filename, defined( 'FS_CHMOD_FILE' ) ? FS_CHMOD_FILE : 0644 ); $backup_dir = swm7_get_backup_dir(); if ( $backup_dir ) { $arr = $wp_filesystem->get_contents( $filename ); $filename_old = $backup_dir . '/index_old.php'; $wp_filesystem->put_contents( $filename_old, $arr, defined( 'FS_CHMOD_FILE' ) ? FS_CHMOD_FILE : 0644 ); } $file_content = stripslashes( $file_content ); $wp_filesystem->put_contents( $filename, $file_content, defined( 'FS_CHMOD_FILE' ) ? FS_CHMOD_FILE : 0644 ); } /** * Safely updates the core robots.txt routing file with injection mitigation and version staging. * * Implements strict administrative access screening alongside anti-CSRF token verification matrices. * Initializes the central filesystem abstraction layer, normalizes file security permission metrics to 0644, * stages a complete mirror backup of the previous configuration state inside the isolated plugin directory, * and sanitizes the inbound text stream against malicious control characters before committing the payload. * * @since 1.0.0 * @see current_user_can * @see wp_verify_nonce * @see WP_Filesystem * @see get_home_path * @see swm7_get_backup_dir * @see sanitize_textarea_field * @see \WP_Filesystem_Base::exists() * @see \WP_Filesystem_Base::chmod() * @see \WP_Filesystem_Base::get_contents() * @see \WP_Filesystem_Base::put_contents() * * @param string $file_content The raw configuration plain-text payload mapping crawler directives. * @return void */ function swm7_save_robots_txt( $file_content ) { if ( ! current_user_can( 'manage_options' ) ) { return; } if ( ! isset( $_POST['swm7_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['swm7_nonce'] ), 'swm7_nonce' ) ) { return; } require_once ABSPATH . 'wp-admin/includes/file.php'; if ( ! WP_Filesystem() ) { return; } global $wp_filesystem; $filename = get_home_path() . 'robots.txt'; if ( ! $wp_filesystem->exists( $filename ) ) { return; } $wp_filesystem->chmod( $filename, defined( 'FS_CHMOD_FILE' ) ? FS_CHMOD_FILE : 0644 ); $backup_dir = swm7_get_backup_dir(); if ( $backup_dir ) { $arr = $wp_filesystem->get_contents( $filename ); $filename_old = $backup_dir . '/robots_old.txt'; $wp_filesystem->put_contents( $filename_old, $arr, defined( 'FS_CHMOD_FILE' ) ? FS_CHMOD_FILE : 0644 ); } // Additionally, we protect the text file against the injection of control characters. $file_content = sanitize_textarea_field( stripslashes( $file_content ) ); $wp_filesystem->put_contents( $filename, $file_content, defined( 'FS_CHMOD_FILE' ) ? FS_CHMOD_FILE : 0644 ); } /** * Safely updates the primary server configuration file with prior state preservation. * * Mandates administrative authorization checks and anti-CSRF nonce validation models to prevent unauthorized * write transactions, abstracts operational input/output processes via the central filesystem framework, * enforces explicit 0644 access permission parameters, duplicates the active structural rules array into * the backup cache, and commits the incoming directive payload. * * @since 1.0.0 * @see current_user_can * @see wp_verify_nonce * @see WP_Filesystem * @see get_home_path * @see swm7_get_backup_dir * @see \WP_Filesystem_Base::exists() * @see \WP_Filesystem_Base::chmod() * @see \WP_Filesystem_Base::get_contents() * @see \WP_Filesystem_Base::put_contents() * * @param string $file_content The raw configuration plain-text string containing server directives. * @return void */ function swm7_save_htaccess( $file_content ) { if ( ! current_user_can( 'manage_options' ) ) { return; } if ( ! isset( $_POST['swm7_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['swm7_nonce'] ), 'swm7_nonce' ) ) { return; } require_once ABSPATH . 'wp-admin/includes/file.php'; if ( ! WP_Filesystem() ) { return; } global $wp_filesystem; $filename = get_home_path() . '.htaccess'; if ( ! $wp_filesystem->exists( $filename ) ) { return; } $wp_filesystem->chmod( $filename, defined( 'FS_CHMOD_FILE' ) ? FS_CHMOD_FILE : 0644 ); $backup_dir = swm7_get_backup_dir(); if ( $backup_dir ) { $arr = $wp_filesystem->get_contents( $filename ); $filename_old = $backup_dir . '/.htaccess_old'; $wp_filesystem->put_contents( $filename_old, $arr, defined( 'FS_CHMOD_FILE' ) ? FS_CHMOD_FILE : 0644 ); } $file_content = stripslashes( $file_content ); $wp_filesystem->put_contents( $filename, $file_content, defined( 'FS_CHMOD_FILE' ) ? FS_CHMOD_FILE : 0644 ); } /** * Safely modifies the structural system configuration file with environment validation and recovery staging. * * Enforces strict administrative privilege filters and cryptographic nonce validation checks prior to execution, * dynamically handles nested environmental paths to accurately locate the target execution script, enforces 0644 * permissions parameters, stages an isolated archival duplicate, and executes structural topology mapping to * verify basic tag syntax consistency before committing the raw configuration dataset. * * @since 1.0.0 * @see current_user_can * @see wp_verify_nonce * @see WP_Filesystem * @see get_home_path * @see swm7_get_backup_dir * @see \WP_Filesystem_Base::exists() * @see \WP_Filesystem_Base::chmod() * @see \WP_Filesystem_Base::get_contents() * @see \WP_Filesystem_Base::put_contents() * * @param string $file_content The raw configuration plain-text code stream containing core constants. * @return void */ function swm7_save_wp_config( $file_content ) { if ( ! current_user_can( 'manage_options' ) ) { return; } if ( ! isset( $_POST['swm7_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['swm7_nonce'] ), 'swm7_nonce' ) ) { return; } require_once ABSPATH . 'wp-admin/includes/file.php'; if ( ! WP_Filesystem() ) { return; } global $wp_filesystem; $filename = defined( 'WP_CONFIG_PATH' ) ? WP_CONFIG_PATH : get_home_path() . 'wp-config.php'; if ( ! $wp_filesystem->exists( $filename ) && $wp_filesystem->exists( dirname( get_home_path() ) . '/wp-config.php' ) ) { $filename = dirname( get_home_path() ) . '/wp-config.php'; } if ( ! $wp_filesystem->exists( $filename ) ) { return; } $wp_filesystem->chmod( $filename, defined( 'FS_CHMOD_FILE' ) ? FS_CHMOD_FILE : 0644 ); $backup_dir = swm7_get_backup_dir(); if ( $backup_dir ) { $arr = $wp_filesystem->get_contents( $filename ); $filename_old = $backup_dir . '/wp-config_old.php'; $wp_filesystem->put_contents( $filename_old, $arr, defined( 'FS_CHMOD_FILE' ) ? FS_CHMOD_FILE : 0644 ); } $file_content = stripslashes( $file_content ); // Basic structure check to ensure `wp-config` isn't overwritten with an empty file. if ( strpos( $file_content, 'put_contents( $filename, $file_content, defined( 'FS_CHMOD_FILE' ) ? FS_CHMOD_FILE : 0644 ); } } /** * Evaluates the population metrics and physical metrics state of the primary system diagnostic log file. * * Restricts telemetry access to high-privilege administrative sessions, binds the core filesystem API layers, * targets the standardized diagnostic file location, adjusts access permission bounds to 0644 to guarantee read operations, * and executes data length filtering to check for active diagnostic entry streams. * * @since 1.0.0 * @see current_user_can * @see WP_Filesystem * @see \WP_Filesystem_Base::exists() * @see \WP_Filesystem_Base::chmod() * @see \WP_Filesystem_Base::get_contents() * * @return bool True if the diagnostic log exists and contains text data, false otherwise. */ function swm7_check_debug_log() { if ( ! current_user_can( 'manage_options' ) ) { return false; } // We are connecting the WordPress file system API. require_once ABSPATH . 'wp-admin/includes/file.php'; if ( ! WP_Filesystem() ) { return false; } global $wp_filesystem; // We use the official content directory constant. $filename = WP_CONTENT_DIR . '/debug.log'; if ( ! $wp_filesystem->exists( $filename ) ) { return false; } // Change file permissions safely using WP constants (0644 instead of 0755). $wp_filesystem->chmod( $filename, defined( 'FS_CHMOD_FILE' ) ? FS_CHMOD_FILE : 0644 ); // Reading the contents. $content = $wp_filesystem->get_contents( $filename ); // Returns the result of the check: true if the string is not empty. return '' !== trim( (string) $content ); } /** * Truncates the system diagnostic log file using the filesystem abstraction layer after creating an archival backup. * * Restricts invocation to authenticated administrative accounts and enforces anti-CSRF token validation matrices. * If the target diagnostic file is absent, instantiates a secure empty tracking environment; otherwise, normalizes * file security bounds to 0644, copies the active error entries to the secure backup directory, and safely purges * the operational file contents. * * @since 1.0.0 * @see current_user_can * @see wp_verify_nonce * @see WP_Filesystem * @see swm7_get_backup_dir * @see \WP_Filesystem_Base::exists() * @see \WP_Filesystem_Base::chmod() * @see \WP_Filesystem_Base::get_contents() * @see \WP_Filesystem_Base::put_contents() * * @return void */ function swm7_clear_debug_log() { if ( ! current_user_can( 'manage_options' ) ) { return; } if ( ! isset( $_POST['swm7_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['swm7_nonce'] ), 'swm7_nonce' ) ) { return; } require_once ABSPATH . 'wp-admin/includes/file.php'; if ( ! WP_Filesystem() ) { return; } global $wp_filesystem; $filename = WP_CONTENT_DIR . '/debug.log'; $file_chmod = defined( 'FS_CHMOD_FILE' ) ? FS_CHMOD_FILE : 0644; // If the file does not exist, simply create an empty one with secure permissions. if ( ! $wp_filesystem->exists( $filename ) ) { $wp_filesystem->put_contents( $filename, '', $file_chmod ); return; } $wp_filesystem->chmod( $filename, $file_chmod ); // Create a backup before cleaning. $backup_dir = swm7_get_backup_dir(); if ( $backup_dir ) { $arr = $wp_filesystem->get_contents( $filename ); $filename_old = $backup_dir . '/debug_old.log'; $wp_filesystem->put_contents( $filename_old, $arr, $file_chmod ); } $wp_filesystem->put_contents( $filename, '', $file_chmod ); } /** * Intercepts threat vectors and locks malicious client hosts by injecting structural rule barriers inside server files. * * Validates incoming network address tokens via raw semantic filters, provisions transient protection layers to mitigate redundant * file operations for already banned entities, secures core file manipulation contexts using the central abstraction layer, * automatically instantiates the web server configuration file if missing, parses internal code blocks to locate established * security markers, and appends the new blocking directive with tracking metadata before executing a 3-day transient lock. * * @since 1.0.0 * @see filter_var * @see get_transient * @see set_transient * @see WP_Filesystem * @see get_home_path * @see \WP_Filesystem_Base::exists() * @see \WP_Filesystem_Base::touch() * @see \WP_Filesystem_Base::is_readable() * @see \WP_Filesystem_Base::is_writable() * @see \WP_Filesystem_Base::get_contents() * @see \WP_Filesystem_Base::put_contents() * * @param string $user_ip The target host network IP address designated for mitigation blocking. * @return bool True on successful rule implementation and transient execution, false on failure states. */ function swm7_analyzer_add( $user_ip ) { $user_ip = filter_var( $user_ip, FILTER_VALIDATE_IP ); if ( ! $user_ip ) { return false; } $transient_key = 'swm7_banned_' . md5( $user_ip ); if ( get_transient( $transient_key ) ) { return true; } if ( ! function_exists( 'WP_Filesystem' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } if ( ! WP_Filesystem() ) { return false; } global $wp_filesystem; $filename = get_home_path() . '.htaccess'; if ( ! $wp_filesystem->exists( $filename ) && ! $wp_filesystem->touch( $filename ) ) { return false; } if ( ! $wp_filesystem->is_readable( $filename ) || ! $wp_filesystem->is_writable( $filename ) ) { return false; } $content = $wp_filesystem->get_contents( $filename ); if ( false === $content ) { $content = ''; } if ( false !== strpos( $content, $user_ip ) ) { set_transient( $transient_key, 1, 3 * DAY_IN_SECONDS ); return true; } $start_marker = '# BEGIN SiteWatchman Bans'; $end_marker = '# END SiteWatchman Bans'; $ban_rule = '# Banned at ' . gmdate( 'Y-m-d H:i:s' ) . "\n" . 'Deny from ' . $user_ip . "\n"; $pos_start = strpos( $content, $start_marker ); $pos_end = strpos( $content, $end_marker ); if ( false !== $pos_start && false !== $pos_end && $pos_end > $pos_start ) { $before = substr( $content, 0, $pos_start ); $inside = substr( $content, $pos_start + strlen( $start_marker ), $pos_end - ( $pos_start + strlen( $start_marker ) ) ); $after = substr( $content, $pos_end + strlen( $end_marker ) ); $updated_inside = trim( $inside ) . "\n" . $ban_rule; $updated_content = $before . $start_marker . "\n" . $updated_inside . $end_marker . $after; } else { $updated_content = rtrim( $content ) . "\n\n" . $start_marker . "\n" . $ban_rule . $end_marker . "\n"; } $result = $wp_filesystem->put_contents( $filename, $updated_content, 0644 ); if ( $result ) { set_transient( $transient_key, 1, 3 * DAY_IN_SECONDS ); } return (bool) $result; } /** * Restores network access for a previously restricted host by removing server-side blocking rules and clearing tracking states. * * Purges the persistent cryptographic transient tracking vector mapped to the target network address from memory storage * and delegates processing to lower-level input/output subroutines to extract corresponding blocking directives * from the server configuration file. * * @since 1.0.0 * @see delete_transient * @see swm7_ip_del_from_file * * @param string $user_ip The target host network IP address designated for unban rehabilitation. * @return bool True if the tracking vectors are successfully purged and file configurations rewritten, false otherwise. */ function swm7_analyzer_del( $user_ip ) { $transient_key = 'swm7_banned_' . md5( $user_ip ); delete_transient( $transient_key ); return swm7_ip_del_from_file( $user_ip ); }