| 1 |
<?php |
| 2 |
/** |
| 3 |
* Browser_Cache — writes/removes browser-cache directives in the site |
| 4 |
* root .htaccess so static assets get long Cache-Control + Expires |
| 5 |
* headers, and serves an nginx snippet for non-Apache hosts. |
| 6 |
* |
| 7 |
* Same shape as Gzip: marker block, insert_with_markers, snippet |
| 8 |
* fallback. Independent toggle so users can enable browser caching |
| 9 |
* without compression and vice versa. |
| 10 |
* |
| 11 |
* The default TTLs follow LiteSpeed/WP Rocket conventions: |
| 12 |
* - Static assets (CSS/JS/fonts/images): 1 year + immutable. |
| 13 |
* - HTML: 1 hour (so post edits go live the same day even if a CDN |
| 14 |
* has cached the document). |
| 15 |
* |
| 16 |
* @package XSpeed |
| 17 |
*/ |
| 18 |
|
| 19 |
declare(strict_types=1); |
| 20 |
|
| 21 |
namespace XSpeed; |
| 22 |
|
| 23 |
defined( 'ABSPATH' ) || exit; |
| 24 |
|
| 25 |
final class Browser_Cache { |
| 26 |
|
| 27 |
public const MARKER = 'xSpeed Browser Cache'; |
| 28 |
|
| 29 |
public const DEFAULT_ASSET_TTL = 31536000; // 1 year |
| 30 |
public const DEFAULT_HTML_TTL = 3600; // 1 hour |
| 31 |
|
| 32 |
public static function apply( bool $enabled, array $opts = array() ): bool { |
| 33 |
if ( ! function_exists( 'XSpeed\\Server::supports_htaccess' ) && class_exists( '\\XSpeed\\Server' ) ) { |
| 34 |
if ( ! Server::supports_htaccess() ) { |
| 35 |
return false; |
| 36 |
} |
| 37 |
} |
| 38 |
$htaccess = ABSPATH . '.htaccess'; |
| 39 |
if ( ! file_exists( $htaccess ) ) { |
| 40 |
return false; |
| 41 |
} |
| 42 |
if ( ! function_exists( 'insert_with_markers' ) ) { |
| 43 |
require_once ABSPATH . 'wp-admin/includes/misc.php'; |
| 44 |
} |
| 45 |
$rules = $enabled ? self::apache_rules( $opts ) : array(); |
| 46 |
return (bool) insert_with_markers( $htaccess, self::MARKER, $rules ); |
| 47 |
} |
| 48 |
|
| 49 |
/** |
| 50 |
* Apache rule lines. mod_expires handles the Expires header; an |
| 51 |
* inline mod_headers Cache-Control mirror lets us include |
| 52 |
* `immutable` (mod_expires doesn't emit it). |
| 53 |
* |
| 54 |
* @return string[] |
| 55 |
*/ |
| 56 |
public static function apache_rules( array $opts = array() ): array { |
| 57 |
$asset = (int) ( $opts['asset_ttl'] ?? self::DEFAULT_ASSET_TTL ); |
| 58 |
$html = (int) ( $opts['html_ttl'] ?? self::DEFAULT_HTML_TTL ); |
| 59 |
if ( $asset < 0 ) { |
| 60 |
$asset = self::DEFAULT_ASSET_TTL; |
| 61 |
} |
| 62 |
if ( $html < 0 ) { |
| 63 |
$html = self::DEFAULT_HTML_TTL; |
| 64 |
} |
| 65 |
return array( |
| 66 |
'<IfModule mod_expires.c>', |
| 67 |
' ExpiresActive On', |
| 68 |
' ExpiresByType text/html "access plus ' . $html . ' seconds"', |
| 69 |
' ExpiresByType text/css "access plus ' . $asset . ' seconds"', |
| 70 |
' ExpiresByType text/javascript "access plus ' . $asset . ' seconds"', |
| 71 |
' ExpiresByType application/javascript "access plus ' . $asset . ' seconds"', |
| 72 |
' ExpiresByType application/json "access plus ' . $html . ' seconds"', |
| 73 |
' ExpiresByType image/jpeg "access plus ' . $asset . ' seconds"', |
| 74 |
' ExpiresByType image/png "access plus ' . $asset . ' seconds"', |
| 75 |
' ExpiresByType image/webp "access plus ' . $asset . ' seconds"', |
| 76 |
' ExpiresByType image/avif "access plus ' . $asset . ' seconds"', |
| 77 |
' ExpiresByType image/gif "access plus ' . $asset . ' seconds"', |
| 78 |
' ExpiresByType image/svg+xml "access plus ' . $asset . ' seconds"', |
| 79 |
' ExpiresByType image/x-icon "access plus ' . $asset . ' seconds"', |
| 80 |
' ExpiresByType font/woff2 "access plus ' . $asset . ' seconds"', |
| 81 |
' ExpiresByType font/woff "access plus ' . $asset . ' seconds"', |
| 82 |
' ExpiresByType font/ttf "access plus ' . $asset . ' seconds"', |
| 83 |
' ExpiresByType font/otf "access plus ' . $asset . ' seconds"', |
| 84 |
'</IfModule>', |
| 85 |
'<IfModule mod_headers.c>', |
| 86 |
' <FilesMatch "\.(css|js|jpg|jpeg|png|gif|webp|avif|svg|ico|woff2|woff|ttf|otf|eot|mp4|webm|mp3|ogg)$">', |
| 87 |
' Header set Cache-Control "public, max-age=' . $asset . ', immutable"', |
| 88 |
' </FilesMatch>', |
| 89 |
' <FilesMatch "\.html$">', |
| 90 |
' Header set Cache-Control "public, max-age=' . $html . '"', |
| 91 |
' </FilesMatch>', |
| 92 |
'</IfModule>', |
| 93 |
); |
| 94 |
} |
| 95 |
|
| 96 |
/** |
| 97 |
* nginx snippet — uses `expires` directive (the canonical nginx way) |
| 98 |
* plus an `add_header` line for the immutable flag. |
| 99 |
*/ |
| 100 |
public static function nginx_snippet( array $opts = array() ): string { |
| 101 |
$asset = (int) ( $opts['asset_ttl'] ?? self::DEFAULT_ASSET_TTL ); |
| 102 |
$html = (int) ( $opts['html_ttl'] ?? self::DEFAULT_HTML_TTL ); |
| 103 |
return implode( |
| 104 |
"\n", |
| 105 |
array( |
| 106 |
'location ~* \.(css|js|jpg|jpeg|png|gif|webp|avif|svg|ico|woff2|woff|ttf|otf|eot|mp4|webm|mp3|ogg)$ {', |
| 107 |
' expires ' . $asset . 's;', |
| 108 |
' add_header Cache-Control "public, max-age=' . $asset . ', immutable";', |
| 109 |
'}', |
| 110 |
'location ~* \.html$ {', |
| 111 |
' expires ' . $html . 's;', |
| 112 |
' add_header Cache-Control "public, max-age=' . $html . '";', |
| 113 |
'}', |
| 114 |
) |
| 115 |
); |
| 116 |
} |
| 117 |
} |
| 118 |
|