PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.0
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.0
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 1.2.0 All 28 releases
xspeed / includes / modules / BrowserCache / BrowserCacheModule.php

BrowserCacheModule.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.3.0, at includes/modules/BrowserCache/BrowserCacheModule.php

189 lines 6.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Browser cache headers module — writes Cache-Control + Expires
4 * directives to .htaccess (Apache/LiteSpeed) or surfaces an nginx
5 * snippet for manual paste on other servers.
6 *
7 * Tier: Free per FEATURES.md (LiteSpeed parity — Browser Cache
8 * settings are all in LS free).
9 *
10 * @package XSpeed
11 */
12
13 declare(strict_types=1);
14
15 namespace XSpeed\Modules\BrowserCache;
16
17 defined( 'ABSPATH' ) || exit;
18
19 use XSpeed\Browser_Cache;
20 use XSpeed\Deep_Link;
21 use XSpeed\Module;
22 use XSpeed\Server;
23
24 final class BrowserCacheModule extends Module {
25
26 public const SLUG = 'browser-cache';
27 public const TIER = self::TIER_FREE;
28 public const VERSION = '1.0.0';
29
30 public function ui_metadata(): array {
31 return array(
32 'label' => __( 'Browser Cache', 'xspeed' ),
33 'icon' => 'Clock',
34 'description' => __( 'Tell browsers (and intermediate CDNs) how long to cache static assets and HTML.', 'xspeed' ),
35 );
36 }
37
38 public function settings_schema(): array {
39 return array(
40 'enabled' => array(
41 'type' => 'bool',
42 'default' => false,
43 'label' => __( 'Enable browser cache headers', 'xspeed' ),
44 'description' => __( 'On Apache/LiteSpeed this writes Cache-Control + Expires rules into .htaccess. On nginx it just stores the settings — you paste the snippet into your server block manually.', 'xspeed' ),
45 ),
46 'asset_ttl' => array(
47 'type' => 'int',
48 'default' => Browser_Cache::DEFAULT_ASSET_TTL,
49 'min' => 0,
50 'max' => 31536000,
51 'label' => __( 'Static asset TTL (seconds)', 'xspeed' ),
52 'unit' => 'seconds',
53 'description' => __( 'Cache lifetime for CSS, JS, fonts, images. Defaults to 1 year + immutable (the industry-standard "fingerprinted assets never change" pattern).', 'xspeed' ),
54 'dependsOn' => array( 'field' => 'enabled' ),
55 ),
56 'html_ttl' => array(
57 'type' => 'int',
58 'default' => Browser_Cache::DEFAULT_HTML_TTL,
59 'min' => 0,
60 'max' => 31536000,
61 'label' => __( 'HTML TTL (seconds)', 'xspeed' ),
62 'unit' => 'seconds',
63 'description' => __( 'Cache lifetime for the HTML document itself. Keep short (default 1h) so post edits roll out same-day.', 'xspeed' ),
64 'dependsOn' => array( 'field' => 'enabled' ),
65 ),
66 );
67 }
68
69 public function boot(): void {
70 add_action( 'update_option_xspeed_module_browser-cache', array( $this, 'on_settings_change' ), 10, 2 );
71 add_action( 'add_option_xspeed_module_browser-cache', array( $this, 'on_settings_added' ), 10, 2 );
72 }
73
74 public function on_settings_change( $old, $new ): void {
75 if ( ! is_array( $new ) ) {
76 return;
77 }
78 Browser_Cache::apply( ! empty( $new['enabled'] ), $new );
79 // Settings just changed — TTL values likely differ, so the
80 // currently-cached "probe says headers active" answer is stale.
81 // Clear the transient so the next dashboard load re-probes.
82 delete_transient( 'xspeed_browser_cache_probe' );
83 }
84
85 public function on_settings_added( $name, $value ): void {
86 if ( ! is_array( $value ) ) {
87 return;
88 }
89 Browser_Cache::apply( ! empty( $value['enabled'] ), $value );
90 delete_transient( 'xspeed_browser_cache_probe' );
91 }
92
93 public function ui_notices(): array {
94 if ( ! class_exists( '\\XSpeed\\Server' ) || Server::supports_htaccess() ) {
95 return array();
96 }
97 $opts = $this->get_settings();
98 if ( empty( $opts['enabled'] ) ) {
99 return array();
100 }
101
102 // Probe the live response for caching headers on a known static
103 // asset. Suppress the notice unless the probe PROVES nothing is
104 // being sent:
105 //
106 // true → headers present, from our snippet or from the host's own
107 // vhost — either way the feature's job is done (issue #329)
108 // null → the loopback never completed, so we know nothing; a
109 // broken probe is not evidence of a broken server and must
110 // not raise a warning the operator cannot act on (issue #18)
111 // false → proven absent, fall through and show the notice
112 if ( false !== Browser_Cache::probe_headers_present() ) {
113 return array();
114 }
115
116 // nginx hosts can't auto-write Cache-Control / Expires headers, and the
117 // live probe just confirmed they are NOT being served — so the feature
118 // reads "enabled" in the dashboard while doing nothing. That is a warning,
119 // not a passive info note (a token that a user missed on this exact
120 // account, issue #117): escalate the tone and say plainly that the config
121 // is configured-but-not-live until the snippet is pasted + nginx reloaded.
122 // The directives go into the unified server-block snippet on the Cache
123 // panel — point users there instead of duplicating the snippet here.
124 return array(
125 array(
126 'tone' => 'warn',
127 'title' => __( 'Browser cache is enabled but not active on the server', 'xspeed' ),
128 'body' => __( 'Browser-cache headers need to live in your nginx config, and the live response shows they are not being sent yet — so this is on in settings but doing nothing. Your settings are included in the unified server-block snippet on the Cache panel: paste it once into your nginx vhost (or container nginx config) and reload nginx.', 'xspeed' ),
129 // Lands on the snippet itself rather than on the Cache panel,
130 // where it is one collapsed section among several (issue #49).
131 'action' => Deep_Link::action(
132 __( 'Go to the snippet', 'xspeed' ),
133 'cache',
134 'nginx_snippet'
135 ),
136 ),
137 );
138 }
139
140 public function deactivate(): void {
141 Browser_Cache::apply( false );
142 }
143
144 public function cli_commands(): array {
145 return array(
146 array(
147 'name' => 'xspeed browser-cache',
148 'callback' => array( $this, 'cli_handler' ),
149 'shortdesc' => 'Print the Apache or nginx browser-cache snippet.',
150 'ai_hint' => 'Get the server config snippet that sets browser cache-control headers for static assets. Use when PageSpeed reports "serve static assets with an efficient cache policy", or when the user needs the rules to paste into Apache/nginx.',
151 'synopsis' => array(
152 array(
153 'type' => 'positional',
154 'name' => 'flavor',
155 'options' => array( 'apache', 'nginx' ),
156 'optional' => true,
157 ),
158 ),
159 ),
160 );
161 }
162
163 public function cli_handler( array $args, array $assoc ): void {
164 $flavor = $args[0] ?? 'apache';
165 $opts = $this->get_settings();
166 if ( 'nginx' === $flavor ) {
167 \WP_CLI::log( Browser_Cache::nginx_snippet( $opts ) );
168 return;
169 }
170 foreach ( Browser_Cache::apache_rules( $opts ) as $line ) {
171 \WP_CLI::log( $line );
172 }
173 }
174
175 /**
176 * Cache-Control directives for the unified nginx server-block
177 * snippet. nginx sends no Expires — `expires off;` pins the block so
178 * only the explicit `add_header` speaks (#259). Null when the module is disabled — no
179 * directives to install.
180 */
181 public function nginx_directives(): ?string {
182 $opts = $this->get_settings();
183 if ( empty( $opts['enabled'] ) ) {
184 return null;
185 }
186 return Browser_Cache::nginx_snippet( $opts );
187 }
188 }
189