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 / Gzip / GzipModule.php

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

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