, so a server missing a module ignores
* the rule instead of returning a 500 error. The block is clearly marked and is
* the only part of .htaccess this plugin ever edits.
*/
class Htaccess {
const BEGIN = '# BEGIN Cache Shark';
const END = '# END Cache Shark';
public static function path(): string {
return ABSPATH . '.htaccess';
}
/** True on Apache and LiteSpeed (WordPress sets $is_apache for both). */
public static function is_supported(): bool {
global $is_apache;
return ! empty( $is_apache );
}
/** The marked block, ready to write. */
public static function block(): string {
$rules = array(
self::BEGIN,
'# Rules between BEGIN and END are generated by Cache Shark. Do not edit by hand.',
'',
' AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript',
' AddOutputFilterByType DEFLATE application/javascript application/x-javascript application/json',
' AddOutputFilterByType DEFLATE application/xml application/xhtml+xml application/rss+xml application/atom+xml',
' AddOutputFilterByType DEFLATE image/svg+xml font/ttf font/otf application/vnd.ms-fontobject',
'',
'',
' ExpiresActive On',
' ExpiresByType image/jpeg "access plus 1 year"',
' ExpiresByType image/png "access plus 1 year"',
' ExpiresByType image/gif "access plus 1 year"',
' ExpiresByType image/webp "access plus 1 year"',
' ExpiresByType image/avif "access plus 1 year"',
' ExpiresByType image/svg+xml "access plus 1 year"',
' ExpiresByType image/x-icon "access plus 1 year"',
' ExpiresByType font/woff2 "access plus 1 year"',
' ExpiresByType font/woff "access plus 1 year"',
' ExpiresByType font/ttf "access plus 1 year"',
' ExpiresByType font/otf "access plus 1 year"',
' ExpiresByType text/css "access plus 1 month"',
' ExpiresByType text/javascript "access plus 1 month"',
' ExpiresByType application/javascript "access plus 1 month"',
'',
self::END,
);
return implode( "\n", $rules ) . "\n";
}
/** Pure: return $src with our block (re)placed at the top. */
public static function with_block( string $src, string $block ): string {
$src = self::without_block( $src );
return $block . ( '' === trim( $src ) ? '' : "\n" . $src );
}
/** Pure: return $src with our block removed. */
public static function without_block( string $src ): string {
$pattern = '/^' . preg_quote( self::BEGIN, '/' ) . '.*?^' . preg_quote( self::END, '/' ) . '[ \t]*\R?\R?/ms';
return (string) preg_replace( $pattern, '', $src );
}
public static function insert(): bool {
if ( ! self::is_supported() ) {
return false;
}
$path = self::path();
$src = file_exists( $path ) ? (string) file_get_contents( $path ) : ''; // phpcs:ignore WordPress.WP.AlternativeFunctions
return self::write( $path, self::with_block( $src, self::block() ) );
}
public static function remove(): bool {
$path = self::path();
if ( ! file_exists( $path ) ) {
return true;
}
$src = (string) file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions
if ( false === strpos( $src, self::BEGIN ) ) {
return true;
}
$new = self::without_block( $src );
if ( '' === trim( $new ) ) {
return Fs::delete( $path ); // The file held only our block.
}
return self::write( $path, $new );
}
/** Atomic write that keeps the existing permissions. */
private static function write( string $path, string $contents ): bool {
$exists = file_exists( $path );
if ( $exists ? ! is_writable( $path ) : ! is_writable( dirname( $path ) ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions
return false;
}
if ( $exists && ! is_writable( dirname( $path ) ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions
return false; // rename() needs a writable directory.
}
$tmp = $path . '.vc-tmp';
if ( false === file_put_contents( $tmp, $contents, LOCK_EX ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions
return false;
}
Fs::chmod( $tmp, $exists ? ( fileperms( $path ) & 0777 ) : 0644 );
if ( ! Fs::move( $tmp, $path ) ) {
Fs::delete( $tmp );
return false;
}
return true;
}
}