PATH:
home
/
ajwellnessmassag
/
angelictravels.online
/
wp-content
/
plugins
/
really-simple-ssl
/
security
<?php /** * class-rsssl-htaccess-file-manager.php * * Responsible for reading, writing and versioning .htaccess * rules via WordPress’s insert_with_markers API. * * @package RSSSL\Pro\Security\WordPress\Firewall\Builders\Rules */ namespace { //Multiple requirements to support different WordPress versions and ensure the filesystem API is available. if ( ! function_exists( 'insert_with_markers' )) { require_once ABSPATH . 'wp-admin/includes/misc.php'; } if ( ! function_exists( 'get_home_path' )) { require_once ABSPATH . 'wp-admin/includes/file.php'; } } namespace RSSSL\Security { /** * Handles low-level .htaccess file operations: * – locating the file, * – reading/writing rules, * – recording history, * – cooperating with WP Rocket. * – will no longer auto-create a missing .htaccess (opt-in via `rsssl_allow_create_htaccess`). */ class RSSSL_Htaccess_File_Manager { /** * Singleton instance. * * @var self|null */ private static ?self $instance = null; /** * Return the shared instance of this class. * * @return self */ public static function get_instance(): self { if ( self::$instance === null ) { self::$instance = new self(); } return self::$instance; } /** * Is used for storing the path to the .htaccess file. */ public string $htaccess_file_path; /** * Constructor. * */ public function __construct() { $this->htaccess_file_path = $this->determineHtaccessFilePath(); $this->registerRocketHooks(); } /** * Determines the path to the .htaccess file based on various conditions. */ private function determineHtaccessFilePath(): string { // Prefer a custom home .htaccess if it exists $homePath = apply_filters('rsssl_home_htaccess_path', get_home_path() . '.htaccess'); if ($this->file_exists($homePath)) { return apply_filters('rsssl_htaccess_file_path', $homePath); } // Otherwise use the default .htaccess in ABSPATH $defaultPath = apply_filters('rsssl_default_htaccess_path', ABSPATH . '.htaccess'); if ($this->file_exists($defaultPath)) { return apply_filters('rsssl_htaccess_file_path', $defaultPath); } // Fallback to WP_CONTENT_DIR/.htaccess (path only; file will not be auto-created) $contentPath = apply_filters('rsssl_wp_content_htaccess_path', WP_CONTENT_DIR . '/.htaccess'); return apply_filters('rsssl_htaccess_file_path', $contentPath); } /** * Registers hooks for WP Rocket activation and deactivation. So we can record the history of changes made by WP Rocket. */ private function registerRocketHooks(): void { // Register hooks for WP Rocket activation and deactivation add_action('rocket_activation', [ $this, 'record_history_from_rocket' ]); add_action('rocket_deactivation', [ $this, 'record_history_from_rocket' ]); } /** * Sets or updates the path to the .htaccess file to be managed. */ public function set_htaccess_file_path(string $htaccess_file_path): void { $this->htaccess_file_path = $htaccess_file_path; } /** * Reads the content of the .htaccess file. */ public function get_htaccess_content():? string { if ( is_file($this->htaccess_file_path) && is_readable($this->htaccess_file_path)) { return file_get_contents($this->htaccess_file_path); } return null; } /** * Writes a rule block to the .htaccess file. */ public function write_rule(array $rule_definition, string $debugTest = 'unknown'): bool { if (! $this->validateRuleDefinition($rule_definition)) { return false; } if (! $this->ensure_htaccess_is_writable()) { return false; } return $this->applyMarkerBlock( $this->extract_name_from_marker($rule_definition['marker']), $this->prepareLines($rule_definition), $debugTest ); } /** * Validates the rule definition before writing. * * @param array $ruleDefinition * @return bool True if valid, false otherwise. */ private function validateRuleDefinition(array $ruleDefinition): bool { if (empty($ruleDefinition['marker'])) { $this->log_error('No marker provided for write_rule.'); return false; } return true; } /** * Prepares the lines to write, inserting a placeholder if needed. * * @param array $ruleDefinition * @return string[] Array of lines to write. */ private function prepareLines(array $ruleDefinition): array { $lines = $ruleDefinition['lines'] ?? []; $isBeingCleared = ! empty($ruleDefinition['clear_rule']); if (empty($lines) && ! $isBeingCleared) { return [ '', '# This feature has not been activated.', '', ]; } return $lines; } /** * Applies a marker block to the .htaccess file, supporting configurable top-priority markers. */ private function applyMarkerBlock(string $markerName, array $lines, string $debugTest = 'unknown'): bool { $oldContent = $this->get_htaccess_content() ?: ''; // Allow certain markers to be forced to the very top of .htaccess (right under any existing top block) $top_markers = apply_filters( 'rsssl_htaccess_top_markers', [ 'Really Simple Auto Prepend File', 'Really Simple Security Redirect' ] ); if ( in_array( $markerName, $top_markers, true ) ) { // first remove any existing marker block with the same name $result = $this->write_top_marker_block( $markerName, $lines ); } else { // WP core will preserve everything outside of your marker $probe = $this->get_htaccess_content(); if ( $this->is_effectively_empty( $probe ) ) { $result = false; } else { // WP core will preserve everything outside of your marker $result = insert_with_markers( $this->htaccess_file_path, $markerName, $lines ); } } if ( $result ) { $newContent = $this->get_htaccess_content() ?: ''; $this->record_history( $oldContent, $newContent, $markerName, $debugTest ); } return $result; } /** * Ensures that the .htaccess file exists and is writable. */ private function ensure_htaccess_is_writable(): bool { $dir = dirname( $this->htaccess_file_path ); // Ensure the directory exists (same as before) if ( ! is_dir( $dir ) && ! wp_mkdir_p( $dir ) ) { $this->log_error( 'Cannot create directory for .htaccess at: ' . esc_html( $dir ) ); return false; } // Do **not** create a new .htaccess automatically anymore. // This previously led to empty files overwriting existing rewrite rules in some environments. // If a site really wants us to create the file, they must opt in via the filter below. if ( ! is_file( $this->htaccess_file_path ) ) { $allow_create = apply_filters( 'rsssl_allow_create_htaccess', false, $this->htaccess_file_path ); if ( $allow_create ) { if ( @file_put_contents( $this->htaccess_file_path, '', LOCK_EX ) === false ) { $this->log_error( 'Could not create .htaccess file at: ' . esc_html( $this->htaccess_file_path ) ); return false; } else { $this->log_error( 'Created new .htaccess file at: ' . esc_html( $this->htaccess_file_path ) ); } } else { $this->log_error( '.htaccess file does not exist and automatic creation is disabled. Path: ' . esc_html( $this->htaccess_file_path ) ); return false; } } if ( ! is_writable( $this->htaccess_file_path ) ) { $this->log_error( '.htaccess file is not writable at: ' . esc_html( $this->htaccess_file_path ) ); return false; } return true; } /** * Writes a marker block that must live at the very top of .htaccess. * * Used for markers that must run before WordPress rewrite rules – e.g. * - "Really Simple Auto Prepend File" * - "Really Simple Security Redirect" (HTTP→HTTPS redirect) */ private function write_top_marker_block( string $markerName, array $linesToWrite ): bool { // Preserve original content for history $originalHtaccess = $this->get_htaccess_content() ?: ''; // SAFETY: if .htaccess is (effectively) empty or unreadable, do not write our markers if ( $this->is_effectively_empty( $originalHtaccess ) ) { return false; } // we remove the redirect marker block if it exists, so we can write a new one // this is needed because the redirect marker block is not removed by insert_with_markers // We added this function because not on every save we can determine when to remove options when the rule is not present. if ( $markerName !== 'Really Simple Security Redirect' && 'htaccess' !== rsssl_get_option('redirect')) { $originalHtaccess = $this->remove_marker_block( $originalHtaccess, 'Really Simple Security Redirect' ); } $htaccessWithoutMarker = $this->remove_marker_block( $originalHtaccess, $markerName ); if (empty($linesToWrite)) { return $this->save_htaccess_if_changed($originalHtaccess, $htaccessWithoutMarker, $markerName); } $newMarkerBlock = $this->build_marker_block( $markerName, $linesToWrite ); $updatedHtaccess = $this->insert_marker_in_correct_position($htaccessWithoutMarker, $markerName, $newMarkerBlock); $updatedHtaccess = $this->cleanupEmptyLines($updatedHtaccess); @file_put_contents( $this->htaccess_file_path, $updatedHtaccess, LOCK_EX ); $this->record_history( $originalHtaccess, $updatedHtaccess, $markerName ); return true; } /** * Inserts a marker block in the correct position in the .htaccess file. */ private function insert_marker_in_correct_position(string $htaccess, string $markerName, string $markerBlock): string { $autoPrependName = 'Really Simple Auto Prepend File'; if (strcasecmp($markerName, $autoPrependName) === 0) { return $markerBlock . $htaccess; } $escapedAutoPrependName = preg_quote($autoPrependName, '/'); $autoPrependPattern = $this->generate_marker_pattern($autoPrependName); if (preg_match($autoPrependPattern, $htaccess, $match, PREG_OFFSET_CAPTURE)) { $insertPosition = $match[1][1] + strlen($match[1][0]); return substr($htaccess, 0, $insertPosition) . $markerBlock . substr($htaccess, $insertPosition); } return $markerBlock . $htaccess; } /** * Generates a regex pattern to match a marker block in the .htaccess file. * * This pattern matches both # and ### markers, case-insensitive, and captures * the entire block including the BEGIN and END lines. */ public function generate_marker_pattern(string $markerName): string { $escaped = preg_quote($markerName, '/'); //return '/(^#+\s*BEGIN\s+' . $escaped . '[^\n]*\n.*?^#+\s*END\s+' . $escaped . '[^\n]*\n?)/ims'; return '/(^\s*#+\s*BEGIN\s+' . $escaped . '[^\n]*\n.*?^\s*#+\s*END\s+' . $escaped . '[^\n]*\n?)/ims'; } /** * Removes a marker block from the .htaccess file. */ private function remove_marker_block(string $htaccess, string $markerName): string { // Normalize line endings so regex behaves consistently // $htaccess = preg_replace("/\r\n? /", "\n", $htaccess); $htaccess = preg_replace("/\r\n?/", "\n", $htaccess); // Build a single, tolerant pattern matching any number of leading '#', optional trailing text on BEGIN/END lines, // and capturing across multiple lines. $pattern = $this->generate_marker_pattern($markerName); // Apply the replacement and capture match count for debugging $before = $htaccess; $htaccess = preg_replace($pattern, '', $htaccess, -1, $count); return ltrim($htaccess, "\n"); } /** * Saves the .htaccess file if it has changed, and record the history. */ private function save_htaccess_if_changed(string $original, string $modified, string $markerName): bool { if ( $modified === $original ) { return true; } // SAFETY: do not write when the current .htaccess is effectively empty if ( $this->is_effectively_empty( $original ) ) { return false; } $cleaned = $this->cleanupEmptyLines( $modified ); // Avoid writing an empty result if ( $this->is_effectively_empty( $cleaned ) ) { return true; } @file_put_contents( $this->htaccess_file_path, $cleaned, LOCK_EX ); $this->record_history( $original, $cleaned, $markerName ); return true; } private function build_marker_block(string $markerName, array $lines): string { return implode(PHP_EOL, array_merge( ["# BEGIN {$markerName}"], $lines, ["# END {$markerName}"] )) . PHP_EOL; } /** * Checks if a specific marker block exists in the .htaccess file. * * @param array $markers The start and end markers (e.g., ['#BEGIN rule', '#END rule']). * @return bool True if the block exists, false otherwise. */ public function are_markers_present(array $markers): bool { if (count($markers) !== 2) { return false; } $content = $this->get_htaccess_content(); if ($content === null) { return false; } $start_marker_escaped = preg_quote($markers[0], '/'); $end_marker_escaped = preg_quote($markers[1], '/'); return preg_match('/^\s*' . $start_marker_escaped . '.*?^\s*' . $end_marker_escaped . '/ms', $content) === 1; } /** * Extracts a usable name from the BEGIN marker for insert_with_markers. * E.g., "#BEGIN My Rule" becomes "My Rule". */ private function extract_name_from_marker(string $begin_marker): string { // Remove #, BEGIN, Begin, begin and then trim // also remove trailing ### $name = preg_replace( array( '/^#+\s*(BEGIN|Begin|begin)\s*/i', '/\s*#+$/' ), '', $begin_marker ); return trim($name); } /** * Records a change to the .htaccess history. * * @param string $old_content The previous content. * @param string $new_content The new content. */ private function record_history( string $old_content, string $new_content , string $marker = 'unknown', string $debugTest = 'unknown'): void { if ( ! $this->is_htaccess_tracking_enabled() ) { // we remove the option if the constant is not defined. delete_option( 'rsssl_htaccess_history' ); return; } if ( $old_content === $new_content ) { return; } $history = get_option( 'rsssl_htaccess_history', [] ); $history[] = [ 'timestamp' => time(), 'file_path' => $this->htaccess_file_path, 'old_content' => $old_content, 'new_content' => $new_content, 'user_id' => function_exists( 'get_current_user_id' ) ? get_current_user_id() : 0, 'marker' => $marker, // logging the current hook name for debugging purposes 'hook' => current_filter() ?: 'unknown', // logging the current action for debugging purposes 'action' => current_action()? : 'unknown', 'debug_test' => $debugTest, ]; if ( count( $history ) > 20 ) { $history = array_slice( $history, -20 ); } update_option( 'rsssl_htaccess_history', $history, false ); } /** * Clears a specific marker block from the .htaccess file. * * @param string|array $marker The marker name (string) or marker array (['#Begin ...', '#End ...']). * @return bool True on success, false on failure. */ public function clear_rule($marker, string $debugTest = 'unknown'): bool { // Accept either a string (marker name) or an array (markers) if (is_array($marker)) { $begin_marker = $marker[0] ?? ''; } else { $begin_marker = $marker; } $rule_definition = [ 'marker' => $begin_marker, 'lines' => [], 'clear_rule' => true, ]; return $this->write_rule($rule_definition, $debugTest); } /** * Clears a specific marker block from the .htaccess file without using * insert_with_markers. This method directly removes the block using raw * regex matching. This is needed for old markings that had capitalized * Begin and End markers. */ public function clear_legacy_rule(string $marker): bool { $content = $this->get_htaccess_content(); if ($content === null) { return false; } // SAFETY: if the file is effectively empty, do not attempt to rewrite it if ( $this->is_effectively_empty( $content ) ) { return false; } // Match and remove the block with the exact marker name // Use case-insensitive matching for BEGIN/END to handle both legacy and WordPress standard formats $escaped = preg_quote($marker, '/'); $pattern = '/^#+\s*BEGIN\s+' . $escaped . '.*?^#+\s*END\s+' . $escaped . '.*?$/msi'; $new_content = trim(preg_replace($pattern, '', $content)); // Regex error if ($new_content === null) { return false; } // Write the updated content back to the .htaccess file if ( $new_content !== $content && ! $this->is_effectively_empty( $new_content ) ) { return file_put_contents($this->htaccess_file_path, $new_content, LOCK_EX) !== false; } return true; // No changes needed } /** * Records the history of changes made by WP Rocket to the .htaccess file. */ public function record_history_from_rocket(): void { // We get the previous content from the history, if it exists. $history = get_option( 'rsssl_htaccess_history', [] ); $old_content = ''; if ( ! empty( $history ) ) { $last_entry = end( $history ); if ( isset( $last_entry['new_content'] ) ) { $old_content = $last_entry['new_content']; } } $new_content = file_get_contents( $this->htaccess_file_path ); if ( $new_content === false ) { return; } $this->record_history( $old_content, $new_content, 'wp-rocket' ); } /** * Checks if .htaccess history tracking is enabled via constant. * * @since 5.x.x * * @return bool True if .htaccess history tracking is enabled, false otherwise. */ private function is_htaccess_tracking_enabled(): bool { return defined( 'RSSSL_RECORDS_HISTORY_VERSION' ); } /** * Reads the content between a marker block in the .htaccess file and returns it as a string, including the marker lines. */ public function get_rule_content(string $markerName):? string { $content = $this->get_htaccess_content(); if ($content === null) { return null; } // Match both # and ### marker styles, case-insensitive, including the marker lines $escaped = preg_quote($markerName, '/'); $pattern = '/(#+\s*BEGIN\s+' . $escaped . '[^\n]*\n.*?#+\s*END\s+' . $escaped . '[^\n]*\n?)/is'; if (preg_match($pattern, $content, $matches)) { return trim($matches[1]); } return null; } /** * Writes an error message to the error log. */ public function log_error(string $message): void { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( 'RSSSL_Htaccess_File_Manager: ' . $message ); } } /** * Validates the .htaccess file path. If exists, writable and a valid string. */ public function validate_htaccess_file_path(): bool { // Check if the file path is a valid string and not empty if (empty( $this->htaccess_file_path ) ) { return false; } // Check if the file exists and is writable if ( ! is_file( $this->htaccess_file_path ) || ! is_writable( $this->htaccess_file_path ) ) { return false; } return true; } /** * Checks if the .htaccess file exists. */ public function file_exists( string $file_path ): bool { return is_file( $file_path ); } /** * Cleans up extra empty lines in .htaccess content. * * @param string $content The raw .htaccess content. * @return string The content with consecutive blank lines reduced. */ private function cleanupEmptyLines(string $content): string { // Normalize all line endings to "\n" // Collapse three or more consecutive newlines into two $content = preg_replace( array( "/\r\n?/", "/\n{3,}/" ), array( "\n", "\n\n" ), $content ); return $content; } /** * Checks if the given content is effectively empty (only whitespace). */ private function is_effectively_empty( $content ): bool { if ( $content === null || $content === false ) { return true; } return trim( (string) $content ) === ''; } } }
[-] index.php
[edit]
[-] security.php
[edit]
[-] notices.php
[edit]
[-] tests.php
[edit]
[+]
server
[-] integrations.php
[edit]
[+]
wordpress
[-] firewall-manager.php
[edit]
[-] deactivate-integration.php
[edit]
[-] class-rsssl-htaccess-file-manager.php
[edit]
[+]
tests
[+]
..
[-] sync-settings.php
[edit]
[+]
includes
[-] hardening.php
[edit]
[-] cron.php
[edit]
[-] functions.php
[edit]