PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.0.3
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.0.3
1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 All 29 releases
xspeed / includes / class-browser-cache.php

class-browser-cache.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.0.3, at includes/class-browser-cache.php

158 lines 5.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 /**
101 * Probe whether the server is actually emitting the Cache-Control
102 * headers our nginx snippet is supposed to add. Picks a recognisable
103 * static asset (anything in `wp-includes/css/` is always served on
104 * a WP install) and HEADs it, looking for `immutable` in the response
105 * — the unique fingerprint our snippet adds that WP core's defaults
106 * never set. Cached in a 5-minute transient so this never adds
107 * latency to the dashboard.
108 *
109 * Returns:
110 * true → headers present, no notice needed
111 * false → headers absent, snippet hasn't been pasted yet (or hasn't
112 * been reloaded into nginx), keep the notice up
113 */
114 public static function probe_headers_present(): bool {
115 $cached = get_transient( 'xspeed_browser_cache_probe' );
116 if ( null !== $cached && false !== $cached ) {
117 return (bool) $cached;
118 }
119
120 $asset_url = includes_url( 'css/dashicons.min.css' );
121 $resp = wp_remote_head(
122 $asset_url,
123 array(
124 'timeout' => 3,
125 'sslverify' => false,
126 'redirection' => 0,
127 'headers' => array( 'Cache-Control' => 'no-cache' ),
128 )
129 );
130 if ( is_wp_error( $resp ) ) {
131 set_transient( 'xspeed_browser_cache_probe', 0, MINUTE_IN_SECONDS );
132 return false;
133 }
134 $cache_control = (string) wp_remote_retrieve_header( $resp, 'cache-control' );
135 $active = false !== stripos( $cache_control, 'immutable' );
136 set_transient( 'xspeed_browser_cache_probe', $active ? 1 : 0, 5 * MINUTE_IN_SECONDS );
137 return $active;
138 }
139
140 public static function nginx_snippet( array $opts = array() ): string {
141 $asset = (int) ( $opts['asset_ttl'] ?? self::DEFAULT_ASSET_TTL );
142 $html = (int) ( $opts['html_ttl'] ?? self::DEFAULT_HTML_TTL );
143 return implode(
144 "\n",
145 array(
146 'location ~* \.(css|js|jpg|jpeg|png|gif|webp|avif|svg|ico|woff2|woff|ttf|otf|eot|mp4|webm|mp3|ogg)$ {',
147 ' expires ' . $asset . 's;',
148 ' add_header Cache-Control "public, max-age=' . $asset . ', immutable";',
149 '}',
150 'location ~* \.html$ {',
151 ' expires ' . $html . 's;',
152 ' add_header Cache-Control "public, max-age=' . $html . '";',
153 '}',
154 )
155 );
156 }
157 }
158