', 'RewriteEngine On', 'RewriteBase /' ); // Self-test for the plugin's check: /zc-ping.txt answers with a token from the cache folder only // when these rules really run. Needs no other module. (.txt is never treated as a page.) $lines[] = 'RewriteCond %{DOCUMENT_ROOT}' . $cache . '/ping.txt -f'; $lines[] = 'RewriteRule ^zc-ping\.txt$ ' . $cache . '/ping.txt [L,T=text/plain]'; foreach ( array( 'm' => '', 'd' => '!' ) as $dev => $neg ) { foreach ( array( true, false ) as $gz ) { $file = $cache . '/%{HTTP_HOST}%{REQUEST_URI}index-' . $dev . '.html' . ( $gz ? '.gz' : '' ); $block = $common; $block[] = 'RewriteCond %{HTTP_USER_AGENT} ' . $neg . '(Mobile|Android|iPhone|iPad|iPod|Opera\ Mini|IEMobile) [NC]'; if ( $gz ) { $block[] = 'RewriteCond %{HTTP:Accept-Encoding} gzip'; } $block[] = 'RewriteCond %{DOCUMENT_ROOT}' . $file . ' -f'; // gz: force the HTML type and stop mod_deflate from compressing it a second time. $block[] = 'RewriteRule .* ' . $file . ( $gz ? ' [L,T=text/html,E=no-gzip:1]' : ' [L]' ); if ( $gz ) { // Only serve the .gz variant where Apache can label it as gzip (mod_mime ships enabled everywhere; // mod_headers often does not). Without the label browsers would show compressed bytes. $block = array_merge( array( '' ), $block, array( '' ) ); } $lines = array_merge( $lines, $block ); } } $lines[] = ''; // Scoped to our cache file names only: never touches other .gz files on the site (backups, downloads). $lines[] = ''; $lines[] = 'ForceType text/html'; $lines[] = 'AddDefaultCharset UTF-8'; $lines[] = ''; $lines[] = 'AddEncoding gzip .gz'; $lines[] = ''; $lines[] = ''; $lines[] = ''; $lines[] = 'AddDefaultCharset UTF-8'; $lines[] = ''; $lines[] = ''; $lines[] = ''; $lines[] = 'Header set X-ZeroConfig-Cache "HIT-HTACCESS"'; $lines[] = 'Header set Cache-Control "no-cache, must-revalidate, max-age=0"'; $lines[] = 'Header append Vary "Accept-Encoding, Cookie"'; $lines[] = ''; $lines[] = ''; $lines[] = ''; $lines[] = 'ExpiresActive On'; // Without mod_headers the block above is skipped and cached HTML would carry no Cache-Control at // all; browsers would then keep it by guesswork. Expire it at once, as the PHP paths do. $lines[] = ''; $lines[] = 'ExpiresDefault A0'; $lines[] = ''; $lines[] = 'ExpiresByType image/webp "access plus 1 year"'; $lines[] = 'ExpiresByType image/avif "access plus 1 year"'; $lines[] = 'ExpiresByType image/jpeg "access plus 1 year"'; $lines[] = 'ExpiresByType image/png "access plus 1 year"'; $lines[] = 'ExpiresByType image/svg+xml "access plus 1 year"'; $lines[] = 'ExpiresByType text/css "access plus 1 year"'; $lines[] = 'ExpiresByType application/javascript "access plus 1 year"'; $lines[] = 'ExpiresByType font/woff2 "access plus 1 year"'; $lines[] = ''; $lines[] = self::END; return implode( "\n", $lines ) . "\n"; } private static function host_re() { $re = array(); foreach ( ZeroConfig_Page_Cache::hosts() as $h ) { if ( ! preg_match( '/^[a-z0-9.-]+(:\d+)?$/', $h ) ) { continue; // anything else in a host name cannot be a real address of this site } $re[] = str_replace( '\:', ':', preg_quote( $h, '#' ) ); // preg_quote also escapes the port's colon } return $re ? implode( '|', $re ) : '__never__'; } public static function install() { if ( ! self::supported() ) { return false; } self::remove_stale(); return self::write( self::rules() ); } /** Older versions wrote into ABSPATH/.htaccess even when the site root is the folder above: take that block out. */ private static function remove_stale() { $old = trailingslashit( wp_normalize_path( ABSPATH ) ) . '.htaccess'; if ( $old !== self::file() && is_file( $old ) && false !== strpos( (string) file_get_contents( $old ), self::BEGIN ) ) { self::write( '', $old ); } } /** * Hourly from the heartbeat: rewrite our block when it is already there, even when this run cannot * tell the server is Apache (system cron runs PHP from the command line). A write that failed once * (file locked or read-only for a moment) must not leave old rules in place for good. A no-op when * the text is unchanged. */ public static function refresh() { if ( ! self::installed() ) { return false; } self::remove_stale(); return self::write( self::rules() ); } public static function remove() { self::remove_stale(); return self::write( '' ); } /** Our block is in the site root .htaccess, or still in the old place (ABSPATH) from an older version. */ public static function installed() { foreach ( array_unique( array( self::file(), trailingslashit( wp_normalize_path( ABSPATH ) ) . '.htaccess' ) ) as $f ) { if ( is_file( $f ) && false !== strpos( (string) file_get_contents( $f ), self::BEGIN ) ) { return true; } } return false; } /** * Replace (or remove, when $block is '') our marker block at the top of * .htaccess. Locked, atomic, and a no-op when the content would not change. */ private static function write( $block, $file = null ) { $file = null === $file ? self::file() : $file; if ( ! is_file( $file ) ) { if ( '' === $block ) { return true; } if ( ! is_writable( dirname( $file ) ) ) { return new WP_Error( 'zc_htaccess', 'root is not writable' ); } @file_put_contents( $file, '' ); // phpcs:ignore WordPress.WP.AlternativeFunctions } if ( ! is_writable( $file ) ) { return new WP_Error( 'zc_htaccess', '.htaccess is not writable' ); } $fh = @fopen( $file, 'c+' ); // phpcs:ignore WordPress.WP.AlternativeFunctions if ( ! $fh ) { return new WP_Error( 'zc_htaccess', 'cannot open .htaccess' ); } try { if ( ! flock( $fh, LOCK_EX ) ) { return new WP_Error( 'zc_htaccess', 'cannot lock .htaccess' ); } $src = stream_get_contents( $fh ); if ( false === $src ) { return new WP_Error( 'zc_htaccess', 'cannot read .htaccess' ); } $re = '/' . preg_quote( self::BEGIN, '/' ) . '.*?' . preg_quote( self::END, '/' ) . '\R?/s'; $without = preg_replace( $re, '', $src ); if ( null === $without ) { $without = $src; } $new = '' === $block ? ltrim( $without, "\n" ) : $block . "\n" . ltrim( $without, "\n" ); if ( $new === $src ) { return true; // unchanged: don't touch mtime, don't trigger opcache/watchers } $tmp = $file . '.' . uniqid( 'zc', true ) . '.tmp'; if ( false === @file_put_contents( $tmp, $new ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions return new WP_Error( 'zc_htaccess', 'cannot write temp file' ); } @chmod( $tmp, fileperms( $file ) & 0777 ); if ( @rename( $tmp, $file ) ) { return true; } wp_delete_file( $tmp ); // Windows will not replace a file that is open (we hold it locked): write through the handle. // Write first, cut the tail after: a short write (disk full) never leaves an empty file behind. if ( rewind( $fh ) && strlen( $new ) === fwrite( $fh, $new ) && ftruncate( $fh, strlen( $new ) ) && fflush( $fh ) ) { return true; } return new WP_Error( 'zc_htaccess', 'cannot replace .htaccess' ); } finally { flock( $fh, LOCK_UN ); fclose( $fh ); } } }