PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.1.4
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.1.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 1.1.5 1.1.6 1.1.7 1.1.8 All 29 releases
xspeed / includes / modules / Gzip / GzipModule.php

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

289 lines 11.1 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',
36 'tab_label' => 'GZIP', // its own tab on the Compression page
37 'icon' => 'Layers',
38 'description' => 'Compress responses to reduce transfer size.',
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',
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 'synopsis' => array(),
109 ),
110 );
111 }
112
113 /**
114 * Conditional notice published to the module's panel via
115 * Module::ui_notices(). When gzip is enabled and the server can't
116 * auto-configure (nginx / unknown / IIS), we surface a copy-able
117 * snippet so the user can wire it up.
118 */
119 public function ui_notices(): array {
120 $opts = Settings_Manager::get( self::SLUG );
121 if ( empty( $opts['gzip_enabled'] ) ) {
122 return array();
123 }
124
125 // Probe the live response — works regardless of server type. If gzip
126 // is actually being served, nothing is wrong. Mirrors BrowserCache's
127 // probe_headers_present() pattern.
128 //
129 // Tri-state: true = proven serving, false = proven not serving,
130 // null = the loopback never reached the origin, which is not evidence
131 // of anything. Telling someone their server is misconfigured because
132 // *we* couldn't call it is the bug behind issue #18. (#18)
133 $serving = LegacyGzip::probe_active();
134
135 // nginx gets a notice EITHER WAY, because on nginx this notice is the
136 // only route from the Compression page to the snippet — the panel body
137 // is a tab strip plus one toggle, and NginxServerBlock mounts under
138 // the Cache panel alone. Suppressing it on a correctly-configured box
139 // left that user with no way to reach the directives at all (say, to
140 // re-paste them after an nginx upgrade). Working sites get a calm
141 // "here's where it lives"; broken ones get the warning. (#87)
142 //
143 // An unreachable probe (null) takes the calm wording too: we cannot
144 // prove gzip is missing, so the notice points at the snippet without
145 // claiming the server is misconfigured. Only a proven-false probe
146 // warns. (#18)
147 if ( Server::NGINX === Server::type() ) {
148 $proven_missing = ( false === $serving );
149 return array(
150 array(
151 'tone' => $proven_missing ? 'warn' : 'info',
152 'title' => $proven_missing
153 ? __( 'GZIP requires server config on nginx', 'xspeed' )
154 : __( 'GZIP is being served by nginx', 'xspeed' ),
155 'body' => $proven_missing
156 ? __( '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' )
157 : __( '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' ),
158 // Lands on the snippet itself and auto-expands it, rather
159 // than on the Cache page where it is one collapsed section
160 // among several. Same call BrowserCacheModule makes.
161 'action' => Deep_Link::action(
162 __( 'Go to the snippet', 'xspeed' ),
163 'cache',
164 'nginx_snippet'
165 ),
166 ),
167 );
168 }
169
170 // Non-nginx: stay quiet unless the probe positively proved gzip is
171 // missing. `null` (unreachable) must not produce a notice. (#18)
172 if ( false !== $serving ) {
173 return array();
174 }
175 // Probe says NOT active and gzip is enabled — surface the right
176 // notice per server topology.
177 if ( 'auto' === Server::gzip_mode() ) {
178 // Apache/LiteSpeed but probe failed — .htaccess write must
179 // have been blocked, or another plugin is overriding. Tell
180 // the user something is wrong, no snippet (we can't fix it
181 // without their server access).
182 return array(
183 array(
184 'tone' => 'warn',
185 'title' => __( 'GZIP enabled but not active on the server', 'xspeed' ),
186 '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' ),
187 ),
188 );
189 }
190 // IIS / unknown server fallback — keep the legacy "paste this"
191 // notice until a non-nginx unified-snippet surface ships. (nginx
192 // returned above, whether or not gzip is currently being served.)
193 return array(
194 array(
195 'tone' => 'warn',
196 'title' => __( 'GZIP requires server config on this server', 'xspeed' ),
197 'body' => __( 'xSpeed can only auto-configure GZIP on Apache and LiteSpeed (via .htaccess). Paste the snippet below into your server config and reload.', 'xspeed' ),
198 'snippet' => LegacyGzip::nginx_snippet(),
199 ),
200 );
201 }
202
203 /**
204 * Module booting: seed per-module option from legacy if needed,
205 * then register the change hook that flips Apache / LiteSpeed
206 * .htaccess rules whenever gzip_enabled is toggled. This handler
207 * fires on writes to xspeed_module_gzip (not the legacy blob).
208 */
209 public function boot(): void {
210 $this->seed_from_legacy_if_needed();
211 add_action( 'update_option_xspeed_module_gzip', array( __CLASS__, 'on_settings_change' ), 10, 2 );
212 add_action( 'add_option_xspeed_module_gzip', array( __CLASS__, 'on_settings_added' ), 10, 2 );
213 }
214
215 /**
216 * Detect the gzip_enabled flip on write and apply / remove the
217 * .htaccess rules. Idempotent — re-running with the same state is a
218 * no-op inside LegacyGzip::apply().
219 */
220 public static function on_settings_change( $old, $new ): void {
221 $old_gzip = is_array( $old ) ? ! empty( $old['gzip_enabled'] ) : false;
222 $new_gzip = is_array( $new ) ? ! empty( $new['gzip_enabled'] ) : false;
223 if ( $old_gzip !== $new_gzip ) {
224 LegacyGzip::apply( $new_gzip );
225 // Settings just changed — current "probe says active" answer
226 // is stale. Drop the transient so the next dashboard load
227 // re-checks the live response.
228 delete_transient( 'xspeed_gzip_active' );
229 }
230 }
231
232 /**
233 * The very first write (option doesn't exist yet) hits add_option
234 * instead of update_option. Treat it as old=false → new=current.
235 */
236 public static function on_settings_added( $name, $value ): void {
237 $enabled = is_array( $value ) ? ! empty( $value['gzip_enabled'] ) : false;
238 if ( $enabled ) {
239 LegacyGzip::apply( true );
240 }
241 }
242
243 public function activate(): void {
244 $this->seed_from_legacy_if_needed();
245 }
246
247 private function seed_from_legacy_if_needed(): void {
248 if ( null !== get_option( 'xspeed_module_gzip', null ) ) {
249 return;
250 }
251 $legacy = get_option( 'xspeed_options', array() );
252 if ( ! is_array( $legacy ) || ! array_key_exists( 'gzip_enabled', $legacy ) ) {
253 return;
254 }
255 update_option(
256 'xspeed_module_gzip',
257 array(
258 '_version' => self::VERSION,
259 'gzip_enabled' => (bool) $legacy['gzip_enabled'],
260 )
261 );
262 unset( $legacy['gzip_enabled'] );
263 update_option( 'xspeed_options', $legacy );
264 }
265
266 public function cli_handler( array $args, array $assoc ): void {
267 $opts = Settings_Manager::get( self::SLUG );
268 \WP_CLI::log( 'enabled ' . ( $opts['gzip_enabled'] ? 'true' : 'false' ) );
269 \WP_CLI::log( 'server ' . Server::type() );
270 \WP_CLI::log( 'mode ' . Server::gzip_mode() );
271 $active = LegacyGzip::probe_active();
272 \WP_CLI::log( 'active ' . ( null === $active ? 'unknown (loopback probe failed)' : ( $active ? 'true' : 'false' ) ) );
273 \WP_CLI::log( 'nginx_snippet ' . LegacyGzip::nginx_snippet() );
274 }
275
276 /**
277 * GZIP directives for the unified nginx server-block snippet. Null
278 * when the module is disabled.
279 */
280 public function nginx_directives(): ?string {
281 $opts = Settings_Manager::get( self::SLUG );
282 if ( empty( $opts['gzip_enabled'] ) ) {
283 return null;
284 }
285 $snippet = LegacyGzip::nginx_snippet();
286 return is_string( $snippet ) && '' !== $snippet ? $snippet : null;
287 }
288 }
289