PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.7
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.7
1.3.7 1.3.6 1.3.5 1.3.4 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 All 33 releases
xspeed / includes / modules / Gzip / GzipModule.php

GzipModule.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.3.7, at includes/modules/Gzip/GzipModule.php

298 lines 11.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * GZIP module.
4 *
5 * Owns the gzip_enabled setting. On flip → writes / removes Apache /
6 * LiteSpeed `.htaccess` rules via the static XSpeed\Gzip helper. On
7 * nginx (or any server xSpeed can't auto-configure), the panel shows
8 * the manual snippet via ui_notices().
9 *
10 * Tier: Free. See SETTINGS.md for the contract this module satisfies.
11 *
12 * @package XSpeed
13 */
14
15 declare(strict_types=1);
16
17 namespace XSpeed\Modules\Gzip;
18
19 defined( 'ABSPATH' ) || exit;
20
21 use XSpeed\Deep_Link;
22 use XSpeed\Gzip as LegacyGzip;
23 use XSpeed\Module;
24 use XSpeed\Server;
25 use XSpeed\Settings_Manager;
26
27 final class GzipModule extends Module {
28
29 public const SLUG = 'gzip';
30 public const TIER = self::TIER_FREE;
31 public const VERSION = '1.1.0';
32
33 public function ui_metadata(): array {
34 return array(
35 'label' => __( 'Compression', 'xspeed' ),
36 'tab_label' => __( 'GZIP', 'xspeed' ), // its own tab on the Compression page
37 'icon' => 'Layers',
38 'description' => __( 'Compresses pages and files so they download faster.', 'xspeed' ),
39 'group' => 'performance',
40 // Host panel merges GZIP (this module) + Brotli (Pro) into one
41 // page — they are one decision with a fallback chain, not two
42 // sidebar rows (FBS-83633). The panel renders this module's own
43 // schema form plus a Brotli Pro section.
44 'custom_panel' => 'CompressionPanel',
45 );
46 }
47
48 public function settings_schema(): array {
49 return array(
50 'gzip_enabled' => array(
51 'type' => 'bool',
52 'default' => false,
53 'label' => __( 'Enable Gzip compression', 'xspeed' ),
54 // Server-conditional. The old copy said "On nginx the snippet
55 // below must be added to your server config" — unconditionally,
56 // and there is no snippet below: the Compression page is a tab
57 // strip plus this toggle, and NginxServerBlock only mounts under
58 // the Cache panel. So Apache/LiteSpeed users read dead text
59 // about a server they aren't on, and nginx users got a promise
60 // the page couldn't keep. The nginx half now lives in
61 // ui_notices(), which can link to where the snippet actually is.
62 'description' => self::gzip_description(),
63 ),
64 );
65 }
66
67 /**
68 * Copy for the GZIP toggle, matched to the server we're actually on.
69 *
70 * Schema descriptions have no server-conditional rendering, so the choice
71 * has to happen here rather than in the panel.
72 */
73 private static function gzip_description(): string {
74 if ( class_exists( '\\XSpeed\\Server' ) && Server::supports_htaccess() ) {
75 return __( 'Compresses pages and files before sending them. xSpeed adds the .htaccess rules for you on this server.', 'xspeed' );
76 }
77 if ( class_exists( '\\XSpeed\\Server' ) && Server::NGINX === Server::type() ) {
78 return __( 'Compresses pages and files before sending them. On nginx you add the rules to your server config yourself. See the notice below.', 'xspeed' );
79 }
80 return __( 'Compresses pages and files before sending them. On this server you add the rules to your server config yourself. See the notice below.', 'xspeed' );
81 }
82
83 /**
84 * 1.1.0: drain gzip_enabled from the legacy xspeed_options blob into
85 * this module's option. Idempotent — a re-run with the legacy key
86 * already gone is a no-op.
87 */
88 public function migrations(): array {
89 return array(
90 '1.1.0' => static function ( array $opts ): array {
91 $legacy = get_option( 'xspeed_options', array() );
92 if ( ! is_array( $legacy ) || ! array_key_exists( 'gzip_enabled', $legacy ) ) {
93 return $opts;
94 }
95 $opts['gzip_enabled'] = (bool) $legacy['gzip_enabled'];
96 unset( $legacy['gzip_enabled'] );
97 update_option( 'xspeed_options', $legacy );
98 return $opts;
99 },
100 );
101 }
102
103 public function cli_commands(): array {
104 return array(
105 array(
106 'name' => 'xspeed gzip',
107 'callback' => array( $this, 'cli_handler' ),
108 'shortdesc' => 'Show GZIP status (server type, active, mode).',
109 'ai_hint' => 'Is text compression (GZIP/Brotli) actually working on this site? Answers "why are my HTML/CSS/JS transfers so large" and whether the server is compressing at all. Reports the detected server, whether compression is active, and how it is applied.',
110 'synopsis' => array(),
111 ),
112 );
113 }
114
115 /**
116 * Conditional notice published to the module's panel via
117 * Module::ui_notices(). When gzip is enabled and the server can't
118 * auto-configure (nginx / unknown / IIS), we surface a copy-able
119 * snippet so the user can wire it up.
120 */
121 public function ui_notices(): array {
122 $opts = Settings_Manager::get( self::SLUG );
123 if ( empty( $opts['gzip_enabled'] ) ) {
124 return array();
125 }
126
127 // Probe the live response — works regardless of server type. If gzip
128 // is actually being served, nothing is wrong. Mirrors BrowserCache's
129 // probe_headers_present() pattern.
130 //
131 // Tri-state: true = proven serving, false = proven not serving,
132 // null = the loopback never reached the origin, which is not evidence
133 // of anything. Telling someone their server is misconfigured because
134 // *we* couldn't call it is the bug behind issue #18. (#18)
135 $serving = LegacyGzip::probe_active();
136
137 // nginx gets a notice EITHER WAY, because on nginx this notice is the
138 // only route from the Compression page to the snippet — the panel body
139 // is a tab strip plus one toggle, and NginxServerBlock mounts under
140 // the Cache panel alone. Suppressing it on a correctly-configured box
141 // left that user with no way to reach the directives at all (say, to
142 // re-paste them after an nginx upgrade). Working sites get a calm
143 // "here's where it lives"; broken ones get the warning. (#87)
144 //
145 // An unreachable probe (null) takes the calm wording too: we cannot
146 // prove gzip is missing, so the notice points at the snippet without
147 // claiming the server is misconfigured. Only a proven-false probe
148 // warns. (#18)
149 if ( Server::NGINX === Server::type() ) {
150 $proven_missing = ( false === $serving );
151 return array(
152 array(
153 'tone' => $proven_missing ? 'warn' : 'info',
154 'title' => $proven_missing
155 ? __( 'GZIP requires server config on nginx', 'xspeed' )
156 : __( 'GZIP is being served by nginx', 'xspeed' ),
157 'body' => $proven_missing
158 ? __( 'xSpeed can only auto-configure GZIP on Apache and LiteSpeed (via .htaccess). The directives are included in the unified server-block snippet on the Cache panel — copy + paste it once into your nginx vhost (or container nginx config) and reload nginx.', 'xspeed' )
159 : __( 'Responses are compressed. xSpeed cannot configure nginx from WordPress, so these directives live in the unified server-block snippet on the Cache panel — that is where to re-copy them if your server config is ever rebuilt.', 'xspeed' ),
160 // Lands on the snippet itself and auto-expands it, rather
161 // than on the Cache page where it is one collapsed section
162 // among several. Same call BrowserCacheModule makes.
163 'action' => Deep_Link::action(
164 __( 'Go to the snippet', 'xspeed' ),
165 'cache',
166 'nginx_snippet'
167 ),
168 ),
169 );
170 }
171
172 // Non-nginx: stay quiet unless the probe positively proved gzip is
173 // missing. `null` (unreachable) must not produce a notice. (#18)
174 if ( false !== $serving ) {
175 return array();
176 }
177 // Probe says NOT active and gzip is enabled — surface the right
178 // notice per server topology.
179 if ( 'auto' === Server::gzip_mode() ) {
180 // Apache/LiteSpeed but probe failed — .htaccess write must
181 // have been blocked, or another plugin is overriding. Tell
182 // the user something is wrong, no snippet (we can't fix it
183 // without their server access).
184 return array(
185 array(
186 'tone' => 'warn',
187 'title' => __( 'GZIP enabled but not active on the server', 'xspeed' ),
188 'body' => __( 'xSpeed wrote the .htaccess rules but the response still isn\'t gzipped. Your server may have AllowOverride disabled, another caching plugin overriding, or mod_deflate missing. Ask your host to enable GZIP on Apache.', 'xspeed' ),
189 ),
190 );
191 }
192 // IIS / unknown server fallback — keep the legacy "paste this"
193 // notice until a non-nginx unified-snippet surface ships. (nginx
194 // returned above, whether or not gzip is currently being served.)
195 return array(
196 array(
197 'tone' => 'warn',
198 'title' => __( 'GZIP requires server config on this server', 'xspeed' ),
199 'body' => __( 'xSpeed can only auto-configure GZIP on Apache and LiteSpeed (via .htaccess). Paste the snippet below into your server config and reload.', 'xspeed' ),
200 'snippet' => LegacyGzip::nginx_snippet(),
201 ),
202 );
203 }
204
205 /**
206 * Module booting: seed per-module option from legacy if needed,
207 * then register the change hook that flips Apache / LiteSpeed
208 * .htaccess rules whenever gzip_enabled is toggled. This handler
209 * fires on writes to xspeed_module_gzip (not the legacy blob).
210 */
211 public function boot(): void {
212 $this->seed_from_legacy_if_needed();
213 add_action( 'update_option_xspeed_module_gzip', array( __CLASS__, 'on_settings_change' ), 10, 2 );
214 add_action( 'add_option_xspeed_module_gzip', array( __CLASS__, 'on_settings_added' ), 10, 2 );
215 }
216
217 /**
218 * Detect the gzip_enabled flip on write and apply / remove the
219 * .htaccess rules. Idempotent — re-running with the same state is a
220 * no-op inside LegacyGzip::apply().
221 */
222 public static function on_settings_change( $old, $new ): void {
223 $old_gzip = is_array( $old ) ? ! empty( $old['gzip_enabled'] ) : false;
224 $new_gzip = is_array( $new ) ? ! empty( $new['gzip_enabled'] ) : false;
225 if ( $old_gzip !== $new_gzip ) {
226 LegacyGzip::apply( $new_gzip );
227 // Settings just changed — current "probe says active" answer
228 // is stale. Drop the transient so the next dashboard load
229 // re-checks the live response.
230 delete_transient( 'xspeed_gzip_active' );
231 }
232 }
233
234 /**
235 * The very first write (option doesn't exist yet) hits add_option
236 * instead of update_option. Treat it as old=false → new=current.
237 */
238 public static function on_settings_added( $name, $value ): void {
239 $enabled = is_array( $value ) ? ! empty( $value['gzip_enabled'] ) : false;
240 if ( $enabled ) {
241 LegacyGzip::apply( true );
242 }
243 }
244
245 public function activate(): void {
246 $this->seed_from_legacy_if_needed();
247 }
248
249 private function seed_from_legacy_if_needed(): void {
250 if ( null !== get_option( 'xspeed_module_gzip', null ) ) {
251 return;
252 }
253 $legacy = get_option( 'xspeed_options', array() );
254 if ( ! is_array( $legacy ) || ! array_key_exists( 'gzip_enabled', $legacy ) ) {
255 return;
256 }
257 update_option(
258 'xspeed_module_gzip',
259 array(
260 '_version' => self::VERSION,
261 'gzip_enabled' => (bool) $legacy['gzip_enabled'],
262 )
263 );
264 unset( $legacy['gzip_enabled'] );
265 update_option( 'xspeed_options', $legacy );
266 }
267
268 public function cli_handler( array $args, array $assoc ): void {
269 $opts = Settings_Manager::get( self::SLUG );
270 \WP_CLI::log( 'enabled ' . ( $opts['gzip_enabled'] ? 'true' : 'false' ) );
271 \WP_CLI::log( 'server ' . Server::type() );
272 \WP_CLI::log( 'mode ' . Server::gzip_mode() );
273 $active = LegacyGzip::probe_active();
274 \WP_CLI::log( 'active ' . ( null === $active ? 'unknown (loopback probe failed)' : ( $active ? 'true' : 'false' ) ) );
275 \WP_CLI::log( 'nginx_snippet ' . LegacyGzip::nginx_snippet() );
276 }
277
278 /**
279 * GZIP directives for the unified nginx server-block snippet. Null
280 * when the module is disabled.
281 */
282 public function nginx_directives(): ?string {
283 $opts = Settings_Manager::get( self::SLUG );
284 if ( empty( $opts['gzip_enabled'] ) ) {
285 return null;
286 }
287 $snippet = LegacyGzip::nginx_snippet();
288 return is_string( $snippet ) && '' !== $snippet ? $snippet : null;
289 }
290
291 /**
292 * Gzip stores its switch as `gzip_enabled`, not `enabled`. (#363)
293 */
294 public function is_active(): ?bool {
295 return $this->any_bool_flag_on();
296 }
297 }
298