', '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 );
}
}
}