" and "# END ": // the same two lines every release before 1.6.0 wrote, so a site that // still has the old block gets it replaced where it stands. const MARKER = 'GuardForge'; const BEGIN = '# BEGIN ' . self::MARKER; const END = '# END ' . self::MARKER; private static $instance = null; public static function instance() { if ( self::$instance === null ) { self::$instance = new self(); } return self::$instance; } private function __construct() { add_action( 'update_option_' . GUARDFORGE_Options::OPTION_KEY, [ __CLASS__, 'write_htaccess_rules' ] ); } public static function htaccess_path() { return ABSPATH . '.htaccess'; } public static function is_writable_target() { $path = self::htaccess_path(); if ( file_exists( $path ) ) { return wp_is_writable( $path ); } return wp_is_writable( dirname( $path ) ); } public static function build_rules() { $rules = [ self::BEGIN ]; if ( GUARDFORGE_Options::get( 'protect_sensitive_files' ) ) { $rules[] = ''; $rules[] = ' '; $rules[] = ' Require all denied'; $rules[] = ' '; $rules[] = ' '; $rules[] = ' Order allow,deny'; $rules[] = ' Deny from all'; $rules[] = ' '; $rules[] = ''; } if ( GUARDFORGE_Options::get( 'disable_directory_listing' ) ) { $rules[] = 'Options -Indexes'; } $rules[] = self::END; return implode( "\n", $rules ) . "\n"; } /** * Re-write the .htaccess fragment. Returns true on success, false on * permission failure (deliberately silent — surfaced as a UI warning). * * insert_with_markers holds an exclusive flock from the read to the * truncate, so a second writer cannot leave Apache a half-written * .htaccess, which is a 500 on every page of the site. */ public static function write_htaccess_rules() { if ( ! self::is_writable_target() ) { return false; } $path = self::htaccess_path(); if ( file_exists( $path ) && ! self::markers_paired( (string) file_get_contents( $path ) ) ) { return false; } self::load_insert_with_markers(); return insert_with_markers( $path, self::MARKER, self::rule_lines() ); } /** * Take our rules out on deactivation. The markers stay, empty — that is * how insert_with_markers removes, and WordPress leaves its own block the * same way. An .htaccess that held nothing but our rules ends up with * nothing but the two markers, and that is success. */ public static function remove_htaccess_rules() { $path = self::htaccess_path(); if ( ! file_exists( $path ) || ! wp_is_writable( $path ) ) { return false; } $current = (string) file_get_contents( $path ); // Nothing of ours in the file: writing would add an empty block of ours. if ( strpos( $current, self::BEGIN ) === false ) { return true; } if ( ! self::markers_paired( $current ) ) { return false; } self::load_insert_with_markers(); return insert_with_markers( $path, self::MARKER, [] ); } /** * Whether our block holds at least one directive. Removal and "both * switches off" leave the markers with nothing between them but, at most, * WordPress's explanatory comment — that protects nothing. */ public static function has_rules( $content ) { $inside = false; foreach ( explode( "\n", (string) $content ) as $line ) { $line = trim( $line ); if ( ! $inside ) { $inside = ( $line === self::BEGIN ); continue; } if ( $line === self::END ) { return false; } if ( $line !== '' && $line[0] !== '#' ) { return true; } } return false; } /** The lines between our markers: build_rules without its first and last line. */ private static function rule_lines() { return array_slice( explode( "\n", rtrim( self::build_rules(), "\n" ) ), 1, -1 ); } /** * insert_with_markers takes every line after a BEGIN with no END as the * block's own and replaces them all; on a hand-edited file those are the * operator's rules. A file like that is not written at all. strpos, not * a regex: a long .htaccess is where PCRE runs out of backtracking. */ private static function markers_paired( $content ) { $begin = strpos( $content, self::BEGIN ); return $begin === false || strpos( $content, self::END, $begin ) !== false; } /** The function lives in wp-admin; settings saved over REST or cron run without it. */ private static function load_insert_with_markers() { if ( ! function_exists( 'insert_with_markers' ) ) { require_once ABSPATH . 'wp-admin/includes/misc.php'; } } }