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 / class-gzip.php

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

186 lines 6.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * GZIP toggle.
4 *
5 * Apache + LiteSpeed: writes/removes a marker block in the site root
6 * .htaccess. Other servers (nginx, IIS): the toggle stores the user's
7 * preference but server config must be edited manually — the UI surfaces
8 * a copy-pasteable snippet via Gzip::manual_snippet().
9 *
10 * @package XSpeed
11 */
12
13 namespace XSpeed;
14
15 defined( 'ABSPATH' ) || exit;
16
17 class Gzip {
18
19 const MARKER = 'xSpeed GZIP';
20
21 public static function apply( $enabled ) {
22 if ( ! Server::supports_htaccess() ) {
23 return false;
24 }
25
26 $htaccess = ABSPATH . '.htaccess';
27 if ( ! file_exists( $htaccess ) ) {
28 return false;
29 }
30
31 if ( ! function_exists( 'insert_with_markers' ) ) {
32 require_once ABSPATH . 'wp-admin/includes/misc.php';
33 }
34
35 // Build the block from the GZIP base (only when GZIP is on) plus any
36 // rules add-ons append via the filter. An add-on (e.g. the Pro Brotli
37 // module) may want its own directives even when GZIP itself is off —
38 // so we run the filter regardless and write whatever it returns,
39 // emptying the marker block only when there is genuinely nothing.
40 $rules = self::apache_rules( (bool) $enabled );
41 return (bool) insert_with_markers( $htaccess, self::MARKER, $rules );
42 }
43
44 /**
45 * Compression rules written inside the marker block.
46 *
47 * @param bool $gzip_enabled Whether to include the GZIP (mod_deflate)
48 * base rules. Add-on filter contributions are
49 * applied either way, so Brotli (or another
50 * encoder) can be served even with GZIP off.
51 * @return string[]
52 */
53 public static function apache_rules( $gzip_enabled = true ) {
54 $rules = $gzip_enabled ? array(
55 '<IfModule mod_deflate.c>',
56 ' AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript',
57 ' AddOutputFilterByType DEFLATE application/javascript application/x-javascript application/json',
58 ' AddOutputFilterByType DEFLATE application/xml application/xhtml+xml application/rss+xml',
59 ' AddOutputFilterByType DEFLATE image/svg+xml font/ttf font/otf application/font-woff application/font-woff2',
60 '</IfModule>',
61 ) : array();
62
63 /**
64 * Filter the Apache compression rules written to .htaccess.
65 *
66 * The core engine emits GZIP (mod_deflate) when GZIP is enabled.
67 * Add-ons (xspeed-pro Brotli module) append their own
68 * <IfModule mod_brotli.c> block so Brotli is served where supported,
69 * GZIP otherwise. The base array is empty when GZIP is off, so an
70 * add-on can be the sole contributor. Listeners MUST return the full
71 * rules array (append, don't replace).
72 *
73 * @param string[] $rules Lines written inside the xSpeed GZIP marker block.
74 * @param bool $gzip_enabled Whether GZIP's own base rules are included.
75 */
76 return (array) apply_filters( 'xspeed_compression_apache_rules', $rules, $gzip_enabled );
77 }
78
79 /**
80 * nginx config snippet for users to paste into their server block.
81 */
82 public static function nginx_snippet() {
83 $snippet = implode(
84 "\n",
85 array(
86 'gzip on;',
87 'gzip_vary on;',
88 'gzip_min_length 1024;',
89 'gzip_proxied any;',
90 'gzip_comp_level 6;',
91 'gzip_types',
92 ' text/plain text/css text/xml text/javascript',
93 ' application/javascript application/json application/xml',
94 ' application/xml+rss application/xhtml+xml',
95 ' image/svg+xml font/ttf font/otf application/font-woff application/font-woff2;',
96 )
97 );
98
99 /**
100 * Filter the nginx compression snippet shown for manual config.
101 *
102 * Core emits the GZIP directives. Add-ons (xspeed-pro Brotli
103 * module) append an ngx_brotli block so users can paste Brotli +
104 * GZIP fallback in one go. Return the full snippet string.
105 *
106 * @param string $snippet The nginx directives block.
107 */
108 return (string) apply_filters( 'xspeed_compression_nginx_snippet', $snippet );
109 }
110
111 /**
112 * Does the origin actually serve a GZIP-encoded homepage?
113 *
114 * true — proven: the response was gzipped.
115 * false — proven otherwise: we reached the site and it wasn't.
116 * null — no verdict: the loopback never completed (firewalled,
117 * TLS failure, timeout, WAF answering instead of the origin).
118 *
119 * The third state is the point (issue #18). The old signature folded
120 * "couldn't ask" into "answer is no" and cached that for an hour, which
121 * pinned a permanent "GZIP enabled but not active on the server" warning
122 * onto sites whose gzip was demonstrably fine — the loopback was the
123 * only broken thing. Only a *proven* false may nag the user. Mirrors the
124 * inconclusive/active split already used by the rewrite probe.
125 *
126 * A verdict is cached for an hour because the probe costs a full HTTP
127 * request to home_url() — too slow to run on every /status hit. A
128 * non-verdict is cached for a minute only, so a transient blip resolves
129 * itself on the next page load instead of sticking around.
130 *
131 * @return bool|null
132 */
133 public static function probe_active() {
134 $cached = get_transient( 'xspeed_gzip_active' );
135 if ( '?' === $cached ) {
136 return null;
137 }
138 if ( false !== $cached ) {
139 return '1' === $cached;
140 }
141
142 $res = wp_remote_get(
143 home_url( '/' ),
144 array(
145 'headers' => array( 'Accept-Encoding' => 'gzip' ),
146 // 3s wasn't enough to pull a full homepage on a busy shared
147 // host, and every timeout used to read as "gzip is broken".
148 'timeout' => 5,
149 // Loopback to our own hostname; staging boxes routinely have
150 // a self-signed or mismatched cert. Same call the sibling
151 // probes make (Browser_Cache::probe_headers_present()).
152 'sslverify' => false,
153 // Keep the payload exactly as it came off the wire, so the
154 // gzip magic-byte fallback below still has something to
155 // look at if a transport strips Content-Encoding after
156 // decoding. See Cache_Benchmark::measure().
157 'decompress' => false,
158 )
159 );
160
161 if ( is_wp_error( $res ) || 200 !== (int) wp_remote_retrieve_response_code( $res ) ) {
162 set_transient( 'xspeed_gzip_active', '?', MINUTE_IN_SECONDS );
163 return null;
164 }
165
166 // wp_remote_retrieve_header() hands back a STRING for a single
167 // header but an ARRAY when it appears twice — routine behind a
168 // CDN or proxy. The old is_string() test threw those away and
169 // reported "not gzipped". (Same trap as FBS-82141.)
170 $raw = wp_remote_retrieve_header( $res, 'content-encoding' );
171 $enc = is_array( $raw ) ? implode( ', ', $raw ) : (string) $raw;
172
173 $active = false !== stripos( $enc, 'gzip' );
174 if ( ! $active ) {
175 // Header absent — a transport that decodes and drops it would
176 // look identical to a server that never compressed. The raw
177 // body settles it: a gzip stream starts with 0x1f 0x8b.
178 $body = wp_remote_retrieve_body( $res );
179 $active = is_string( $body ) && 0 === strncmp( $body, "\x1f\x8b", 2 );
180 }
181
182 set_transient( 'xspeed_gzip_active', $active ? '1' : '0', HOUR_IN_SECONDS );
183 return $active;
184 }
185 }
186