PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.1.6
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.1.6
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 / advanced-cache.php

advanced-cache.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.1.6, at includes/advanced-cache.php

281 lines 16.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * XSPEED_DROPIN
4 * XSPEED_DROPIN_VERSION: 4
5 * Drop-in cache loader. Serves cached HTML before WordPress fully boots.
6 *
7 * Bump XSPEED_DROPIN_VERSION whenever this file's serve logic changes so
8 * Cache::ensure_dropin_current() reinstalls it on existing sites (the
9 * "is it ours?" marker alone can't tell an old copy from a new one).
10 * v2: read .meta on the fast path — replay 404 status + feed Content-Type
11 * and honor per-content TTL (FBS-82406, FBS-82407).
12 * v3: conditional GET — emit Last-Modified + ETag, answer matching
13 * If-Modified-Since / If-None-Match with 304 (FBS-82407 #5).
14 * v4: bail when the `.maintenance-active` sentinel is present so a page
15 * cached while live isn't served during maintenance (FBS-82409 B1).
16 *
17 * IMPORTANT: This file is included by wp-settings.php BEFORE
18 * wp-includes/formatting.php and wp-includes/load.php are loaded, so NO
19 * WordPress functions (sanitize_text_field, wp_unslash, is_admin,
20 * HOUR_IN_SECONDS, etc.) are available here. Use raw PHP only.
21 *
22 * @package XSpeed
23 */
24
25 if ( ! defined( 'ABSPATH' ) ) {
26 exit;
27 }
28
29 // Only handle plain GET requests.
30 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Drop-in runs before wp-includes/formatting.php loads, so wp_unslash() and sanitize_text_field() are unavailable. Value is upper-cased and matched against the literal string 'GET'; never echoed, never executed.
31 $xspeed_method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( (string) $_SERVER['REQUEST_METHOD'] ) : '';
32 if ( 'GET' !== $xspeed_method ) {
33 return;
34 }
35
36 // Skip cached query-string requests (search, pagination via ?, etc.).
37 if ( ! empty( $_SERVER['QUERY_STRING'] ) ) {
38 return;
39 }
40
41 // Honor explicit bypass header. xSpeed's own benchmark REST endpoint
42 // sends `X-XSpeed-Bypass: 1` so we can measure uncached TTFB for the
43 // before/after comparison on the dashboard. Harmless if a third party
44 // sends it — they just get an uncached response.
45 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Drop-in runs before WP loads. Value is only used as an isset() check + literal string comparison, never echoed.
46 if ( ! empty( $_SERVER['HTTP_X_XSPEED_BYPASS'] ) ) {
47 return;
48 }
49
50 // Maintenance / coming-soon sentinel. The Pro Maintenance-Cache module writes
51 // `.maintenance-active` next to the cache files whenever the site enters
52 // maintenance / coming-soon mode, and removes it on recovery. The write-side
53 // veto alone can't stop a page cached while the site was live from being
54 // served here (this drop-in runs before WordPress loads), so we bail out and
55 // let WordPress render the maintenance / coming-soon screen instead of serving
56 // a stale real-site page. (FBS-82409 B1)
57 if ( file_exists( WP_CONTENT_DIR . '/cache/xspeed/.maintenance-active' ) ) {
58 return;
59 }
60
61 if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
62 return;
63 }
64
65 // Raw-PHP sanitization: strip null bytes only. This value is used for
66 // substring comparisons and as input to md5() — never echoed, never
67 // executed, never written to disk as data. Magic quotes was removed in
68 // PHP 5.4 and the plugin requires PHP 7.4+, so no unslashing is needed.
69 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Drop-in runs before wp_unslash()/sanitize_text_field() are loaded; null-byte strip is the strongest sanitizer available pre-WP-bootstrap. Value is only used for substring comparison and as md5() input.
70 $xspeed_request_uri = str_replace( "\0", '', (string) $_SERVER['REQUEST_URI'] );
71
72 // Skip admin / login requests.
73 if ( false !== strpos( $xspeed_request_uri, '/wp-admin' ) || false !== strpos( $xspeed_request_uri, '/wp-login' ) ) {
74 return;
75 }
76
77 // Skip logged-in users and comment authors — never serve a cached page to
78 // someone who has a session cookie. Reading raw cookies; we only inspect
79 // names, not values.
80 if ( ! empty( $_COOKIE ) ) {
81 foreach ( $_COOKIE as $xspeed_cookie_name => $xspeed_cookie_value ) {
82 unset( $xspeed_cookie_value );
83 $xspeed_cookie_name = (string) $xspeed_cookie_name;
84 if ( 0 === strpos( $xspeed_cookie_name, 'wordpress_logged_in' )
85 || 0 === strpos( $xspeed_cookie_name, 'comment_author_' )
86 || 0 === strpos( $xspeed_cookie_name, 'wp-postpass_' )
87 // The generic bypass cookie PHP sets whenever it decides a
88 // visitor must not be served from cache (Server_Rules::
89 // BYPASS_COOKIE). Covers repeat visitors even when the baked
90 // rules below are stale.
91 || 'wordpress_no_cache' === $xspeed_cookie_name ) {
92 return;
93 }
94 }
95 }
96
97 // The user's own excluded-cookie list, baked in at install time by
98 // Cache::install_dropin() (the token is replaced with an escaped regex
99 // built by Server_Rules). The drop-in runs before WordPress loads and so
100 // cannot read the settings itself; without this, every cart / membership
101 // / custom cookie rule applied only while a page was cold, and a warm
102 // page was served to exactly the visitors the settings excluded.
103 //
104 // An un-substituted token means the drop-in was copied straight from a
105 // source checkout — fall back to serving nothing from the fast path
106 // rather than treating the literal token as a pattern.
107 $xspeed_cookie_re = '@@XSPEED_COOKIE_RE@@';
108 if ( '@@' !== substr( $xspeed_cookie_re, 0, 2 ) && '' !== $xspeed_cookie_re && ! empty( $_COOKIE ) ) {
109 foreach ( array_keys( $_COOKIE ) as $xspeed_cookie_name ) {
110 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- a malformed baked pattern must degrade to "don't serve from cache", never warn on every request.
111 if ( 1 === @preg_match( '#(' . $xspeed_cookie_re . ')#i', (string) $xspeed_cookie_name ) ) {
112 return;
113 }
114 }
115 }
116
117 // Same for the user-agent bypass list. This is the rule the bypass cookie
118 // can never cover: a bot's very first request to a warm page never
119 // reaches PHP, so there is no earlier request in which to set a cookie.
120 $xspeed_ua_re = '@@XSPEED_UA_RE@@';
121 if ( '@@' !== substr( $xspeed_ua_re, 0, 2 ) && '' !== $xspeed_ua_re ) {
122 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Drop-in runs pre-WP. Value is only matched against a baked, pre-escaped regex; never echoed or executed.
123 $xspeed_ua_raw = isset( $_SERVER['HTTP_USER_AGENT'] ) ? (string) $_SERVER['HTTP_USER_AGENT'] : '';
124 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- see above; degrade to bypass rather than warn.
125 if ( '' !== $xspeed_ua_raw && 1 === @preg_match( '#(' . $xspeed_ua_re . ')#i', $xspeed_ua_raw ) ) {
126 return;
127 }
128 }
129
130 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Drop-in runs before wp_unslash()/sanitize_text_field() are loaded. Value is filtered through a strict allowlist regex below (letters, digits, dot, hyphen, colon) and only used as md5() input for the cache key.
131 $xspeed_host = isset( $_SERVER['HTTP_HOST'] ) ? (string) $_SERVER['HTTP_HOST'] : 'default';
132 $xspeed_host = str_replace( "\0", '', $xspeed_host );
133 // Restrict host to a safe charset (letters, digits, dot, hyphen, colon for port).
134 $xspeed_host = preg_replace( '/[^a-zA-Z0-9.\-:]/', '', $xspeed_host );
135
136 $xspeed_path_only = strtok( $xspeed_request_uri, '?' );
137
138 // Device bucket — MUST mirror XSpeed\Cache::cache_key() exactly, or the key
139 // the drop-in computes won't match the file Cache::store() wrote, the HIT
140 // branch below never fires, and every request falls through to a full
141 // WordPress boot (defeating the whole point of the pre-WP drop-in).
142 //
143 // Cache::cache_key() appends '|m' / '|d' when the cache module's
144 // `mobile_separate` setting is on. The drop-in can't read WP options
145 // (it runs before WordPress loads), so Cache writes a zero-byte sidecar
146 // flag — `.mobile-separate` next to the cache files — whenever that setting
147 // is on, and removes it when off (see Cache::sync_mobile_flag()). We mirror
148 // the same UA token list wp_is_mobile() uses, the same one Cache's inline
149 // fallback detector uses.
150 $xspeed_device = '';
151 if ( file_exists( WP_CONTENT_DIR . '/cache/xspeed/.mobile-separate' ) ) {
152 // Mirror core's wp_is_mobile() EXACTLY (which Cache::is_mobile_request()
153 // defers to): check the Sec-CH-UA-Mobile client hint first, then fall
154 // back to the same UA token list. Any divergence from the engine's
155 // detection re-introduces the key mismatch this whole flag exists to
156 // prevent.
157 $xspeed_is_mobile = false;
158 if ( isset( $_SERVER['HTTP_SEC_CH_UA_MOBILE'] ) ) {
159 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Drop-in runs pre-WP. Value is compared against the literal '?1', never echoed or executed.
160 $xspeed_is_mobile = ( '?1' === $_SERVER['HTTP_SEC_CH_UA_MOBILE'] );
161 } else {
162 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Drop-in runs before wp_unslash()/sanitize_text_field() load. Value is only matched against a literal token regex, never echoed or executed.
163 $xspeed_ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? (string) $_SERVER['HTTP_USER_AGENT'] : '';
164 $xspeed_is_mobile = (bool) preg_match( '/(Mobile|Android|Silk\/|Kindle|BlackBerry|Opera Mini|Opera Mobi)/i', $xspeed_ua );
165 }
166 $xspeed_device = $xspeed_is_mobile ? '|m' : '|d';
167 }
168
169 $xspeed_cache_key = md5( $xspeed_host . $xspeed_path_only . $xspeed_device );
170 $xspeed_cache_file = WP_CONTENT_DIR . '/cache/xspeed/' . $xspeed_cache_key . '.html';
171 $xspeed_meta_file = WP_CONTENT_DIR . '/cache/xspeed/' . $xspeed_cache_key . '.meta';
172
173 if ( file_exists( $xspeed_cache_file ) ) {
174 // Read the .meta sidecar (status / content_type / ttl) the same way the
175 // PHP HIT path does — the drop-in serves cached feeds and 404s too, so it
176 // must replay their Content-Type / status and honor their per-content TTL.
177 // Ordinary 200 text/html pages have no .meta (the common path stays fast).
178 // (FBS-82406 soft-404, FBS-82407 feed content-type + TTL)
179 $xspeed_meta = array();
180 if ( file_exists( $xspeed_meta_file ) ) {
181 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- pre-WP drop-in; one tiny JSON sidecar.
182 $xspeed_meta_raw = file_get_contents( $xspeed_meta_file );
183 if ( false !== $xspeed_meta_raw ) {
184 $xspeed_decoded = json_decode( $xspeed_meta_raw, true );
185 if ( is_array( $xspeed_decoded ) ) {
186 $xspeed_meta = $xspeed_decoded;
187 }
188 }
189 }
190
191 // Per-content TTL from meta (e.g. feeds) falls back to the 24h page
192 // default. HOUR_IN_SECONDS isn't defined yet (pre-WP), so use a literal.
193 $xspeed_ttl = ( isset( $xspeed_meta['ttl'] ) && (int) $xspeed_meta['ttl'] > 0 ) ? (int) $xspeed_meta['ttl'] : 86400;
194 $xspeed_age = time() - filemtime( $xspeed_cache_file );
195 if ( $xspeed_age < $xspeed_ttl ) {
196 // PHP-served cache hit (the ~85ms fallback path). The nginx static
197 // rewrite sends "HIT (nginx)" for the fast 5-15ms path; same header,
198 // distinct value so you can tell which layer served the page.
199 header( 'X-XSpeed-Cache: HIT (php)' );
200
201 // Record the HIT for the dashboard hit-ratio. The drop-in runs
202 // BEFORE WordPress loads, so it can't call Hit_Counter — instead
203 // it appends one line to the same hits.log the nginx static path
204 // uses, and Hit_Counter::collect_nginx_log_hits() drains + counts
205 // both on the next dashboard load. Without this, every drop-in HIT
206 // was served but never counted, so the hit ratio sat at 0.
207 // Best-effort: a failed append must never break serving the page.
208 //
209 // Path is baked in at install time by Cache::install_dropin(), which
210 // replaces the @@XSPEED_HITS_LOG@@ token on the next line with the
211 // resolved absolute path (uploads/xspeed/hits.log — NOT the cache dir,
212 // which gets deleted on purge/uninstall and would take nginx down,
213 // FBS-82478). The default below is the fallback for an un-substituted
214 // drop-in (e.g. run straight from a dev source checkout); the installed
215 // copy always carries the absolute uploads path.
216 $xspeed_hits_log = '@@XSPEED_HITS_LOG@@'; // replaced at install
217 if ( '@@' === substr( $xspeed_hits_log, 0, 2 ) ) {
218 $xspeed_hits_log = WP_CONTENT_DIR . '/uploads/xspeed/hits.log';
219 }
220 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- pre-WP drop-in; WP_Filesystem isn't loaded. One short line, append + lock; failures are non-fatal (the ratio just under-counts).
221 @file_put_contents( $xspeed_hits_log, "hit\n", FILE_APPEND | LOCK_EX );
222
223 // Replay the cached response's status + content-type from .meta, so a
224 // cached 404 serves 404 (not a soft-404 200) and a cached feed serves
225 // application/rss+xml (not text/html). (FBS-82406, FBS-82407)
226 if ( ! empty( $xspeed_meta['status'] ) && function_exists( 'http_response_code' ) ) {
227 http_response_code( (int) $xspeed_meta['status'] );
228 }
229 if ( ! empty( $xspeed_meta['content_type'] ) && is_string( $xspeed_meta['content_type'] ) ) {
230 header( 'Content-Type: ' . $xspeed_meta['content_type'] );
231 }
232
233 // Conditional GET: Last-Modified + ETag from the cache file's mtime,
234 // answer a matching If-Modified-Since / If-None-Match with 304 so
235 // aggregators skip re-downloading an unchanged cached feed/page.
236 // (FBS-82407 #5)
237 $xspeed_mtime = (int) filemtime( $xspeed_cache_file );
238 if ( $xspeed_mtime > 0 ) {
239 $xspeed_lastmod = gmdate( 'D, d M Y H:i:s', $xspeed_mtime ) . ' GMT';
240 $xspeed_etag = '"' . md5( $xspeed_cache_file . '|' . $xspeed_mtime ) . '"';
241 header( 'Last-Modified: ' . $xspeed_lastmod );
242 header( 'ETag: ' . $xspeed_etag );
243 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- pre-WP drop-in; values only compared to a server-generated etag / parsed as a date, never echoed or executed.
244 $xspeed_inm = isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ? trim( (string) $_SERVER['HTTP_IF_NONE_MATCH'] ) : '';
245 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- as above.
246 $xspeed_ims = isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ? trim( (string) $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) : '';
247 if ( ( '' !== $xspeed_inm && false !== strpos( $xspeed_inm, $xspeed_etag ) )
248 || ( '' !== $xspeed_ims && false !== ( $xspeed_ims_ts = strtotime( $xspeed_ims ) ) && $xspeed_ims_ts >= $xspeed_mtime ) ) {
249 if ( function_exists( 'http_response_code' ) ) {
250 http_response_code( 304 );
251 }
252 exit;
253 }
254 }
255
256 // Serve the precompressed Brotli sibling when the client accepts it
257 // and the Pro Brotli module wrote <file>.br. MUST mirror
258 // XSpeed\Cache::maybe_serve_brotli() on the non-drop-in serve path —
259 // both decide on the same Accept-Encoding token match + sibling
260 // existence, so the response is identical whichever path serves.
261 // pre-WP: no sanitize_text_field()/wp_unslash(); the value is only
262 // lowercased + regex-matched, never echoed.
263 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- pre-WP drop-in; value is only lowercased + token-matched, never echoed or executed.
264 $xspeed_accept_enc = isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ? strtolower( str_replace( "\0", '', (string) $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) : '';
265 $xspeed_br_file = $xspeed_cache_file . '.br';
266 if ( preg_match( '/(^|[\s,])br([\s,;]|$)/', $xspeed_accept_enc )
267 && is_readable( $xspeed_br_file ) ) {
268 header( 'Content-Encoding: br' );
269 header( 'Vary: Accept-Encoding', false );
270 header_remove( 'Content-Length' );
271 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- Drop-in runs before WP_Filesystem is available; readfile streams the precompressed sibling directly.
272 readfile( $xspeed_br_file );
273 exit;
274 }
275
276 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- Drop-in runs before WP_Filesystem is available; readfile is optimal for streaming a static cache file to the visitor.
277 readfile( $xspeed_cache_file );
278 exit;
279 }
280 }
281