PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.3
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.3
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
← All changes | includes/class-gzip.php +100 -14 1.0.31.3.3 View file →
@@ -31,14 +31,28 @@
31 31 if ( ! function_exists( 'insert_with_markers' ) ) {
32 32 require_once ABSPATH . 'wp-admin/includes/misc.php';
33 33 }
34 34
35 - $rules = $enabled ? self::apache_rules() : array();
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 );
36 41 return (bool) insert_with_markers( $htaccess, self::MARKER, $rules );
37 42 }
38 43
39 - public static function apache_rules() {
40 - return array(
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(
41 55 '<IfModule mod_deflate.c>',
42 56 ' AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript',
43 57 ' AddOutputFilterByType DEFLATE application/javascript application/x-javascript application/json',
44 58 ' AddOutputFilterByType DEFLATE application/xml application/xhtml+xml application/rss+xml',
@@ -43,9 +57,24 @@
43 57 ' AddOutputFilterByType DEFLATE application/javascript application/x-javascript application/json',
44 58 ' AddOutputFilterByType DEFLATE application/xml application/xhtml+xml application/rss+xml',
45 59 ' AddOutputFilterByType DEFLATE image/svg+xml font/ttf font/otf application/font-woff application/font-woff2',
46 60 '</IfModule>',
47 - );
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 );
48 77 }
49 78
50 79 /**
51 80 * nginx config snippet for users to paste into their server block.
@@ -50,9 +79,9 @@
50 79 /**
51 80 * nginx config snippet for users to paste into their server block.
52 81 */
53 82 public static function nginx_snippet() {
54 - return implode(
83 + $snippet = implode(
55 84 "\n",
56 85 array(
57 86 'gzip on;',
58 87 'gzip_vary on;',
@@ -65,17 +94,48 @@
65 94 ' application/xml+rss application/xhtml+xml',
66 95 ' image/svg+xml font/ttf font/otf application/font-woff application/font-woff2;',
67 96 )
68 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 );
69 109 }
70 110
71 111 /**
72 - * Returns true if the response we just received looks GZIP-encoded.
73 - * Result is cached in a transient for 1 hour because the probe makes a
74 - * full HTTP request to home_url() — too slow to run on every /status hit.
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
75 132 */
76 133 public static function probe_active() {
77 134 $cached = get_transient( 'xspeed_gzip_active' );
135 + if ( '?' === $cached ) {
136 + return null;
137 + }
78 138 if ( false !== $cached ) {
79 139 return '1' === $cached;
80 140 }
81 141
@@ -81,17 +141,43 @@
81 141
82 142 $res = wp_remote_get(
83 143 home_url( '/' ),
84 144 array(
85 - 'headers' => array( 'Accept-Encoding' => 'gzip' ),
86 - 'timeout' => 3,
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,
87 158 )
88 159 );
89 160
90 - $active = false;
91 - if ( ! is_wp_error( $res ) ) {
92 - $enc = wp_remote_retrieve_header( $res, 'content-encoding' );
93 - $active = is_string( $enc ) && false !== stripos( $enc, 'gzip' );
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 );
94 180 }
95 181
96 182 set_transient( 'xspeed_gzip_active', $active ? '1' : '0', HOUR_IN_SECONDS );
97 183 return $active;