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

243 lines 13.8 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 return;
88 }
89 }
90 }
91
92 // 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.
93 $xspeed_host = isset( $_SERVER['HTTP_HOST'] ) ? (string) $_SERVER['HTTP_HOST'] : 'default';
94 $xspeed_host = str_replace( "\0", '', $xspeed_host );
95 // Restrict host to a safe charset (letters, digits, dot, hyphen, colon for port).
96 $xspeed_host = preg_replace( '/[^a-zA-Z0-9.\-:]/', '', $xspeed_host );
97
98 $xspeed_path_only = strtok( $xspeed_request_uri, '?' );
99
100 // Device bucket — MUST mirror XSpeed\Cache::cache_key() exactly, or the key
101 // the drop-in computes won't match the file Cache::store() wrote, the HIT
102 // branch below never fires, and every request falls through to a full
103 // WordPress boot (defeating the whole point of the pre-WP drop-in).
104 //
105 // Cache::cache_key() appends '|m' / '|d' when the cache module's
106 // `mobile_separate` setting is on. The drop-in can't read WP options
107 // (it runs before WordPress loads), so Cache writes a zero-byte sidecar
108 // flag — `.mobile-separate` next to the cache files — whenever that setting
109 // is on, and removes it when off (see Cache::sync_mobile_flag()). We mirror
110 // the same UA token list wp_is_mobile() uses, the same one Cache's inline
111 // fallback detector uses.
112 $xspeed_device = '';
113 if ( file_exists( WP_CONTENT_DIR . '/cache/xspeed/.mobile-separate' ) ) {
114 // Mirror core's wp_is_mobile() EXACTLY (which Cache::is_mobile_request()
115 // defers to): check the Sec-CH-UA-Mobile client hint first, then fall
116 // back to the same UA token list. Any divergence from the engine's
117 // detection re-introduces the key mismatch this whole flag exists to
118 // prevent.
119 $xspeed_is_mobile = false;
120 if ( isset( $_SERVER['HTTP_SEC_CH_UA_MOBILE'] ) ) {
121 // 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.
122 $xspeed_is_mobile = ( '?1' === $_SERVER['HTTP_SEC_CH_UA_MOBILE'] );
123 } else {
124 // 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.
125 $xspeed_ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? (string) $_SERVER['HTTP_USER_AGENT'] : '';
126 $xspeed_is_mobile = (bool) preg_match( '/(Mobile|Android|Silk\/|Kindle|BlackBerry|Opera Mini|Opera Mobi)/i', $xspeed_ua );
127 }
128 $xspeed_device = $xspeed_is_mobile ? '|m' : '|d';
129 }
130
131 $xspeed_cache_key = md5( $xspeed_host . $xspeed_path_only . $xspeed_device );
132 $xspeed_cache_file = WP_CONTENT_DIR . '/cache/xspeed/' . $xspeed_cache_key . '.html';
133 $xspeed_meta_file = WP_CONTENT_DIR . '/cache/xspeed/' . $xspeed_cache_key . '.meta';
134
135 if ( file_exists( $xspeed_cache_file ) ) {
136 // Read the .meta sidecar (status / content_type / ttl) the same way the
137 // PHP HIT path does — the drop-in serves cached feeds and 404s too, so it
138 // must replay their Content-Type / status and honor their per-content TTL.
139 // Ordinary 200 text/html pages have no .meta (the common path stays fast).
140 // (FBS-82406 soft-404, FBS-82407 feed content-type + TTL)
141 $xspeed_meta = array();
142 if ( file_exists( $xspeed_meta_file ) ) {
143 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- pre-WP drop-in; one tiny JSON sidecar.
144 $xspeed_meta_raw = file_get_contents( $xspeed_meta_file );
145 if ( false !== $xspeed_meta_raw ) {
146 $xspeed_decoded = json_decode( $xspeed_meta_raw, true );
147 if ( is_array( $xspeed_decoded ) ) {
148 $xspeed_meta = $xspeed_decoded;
149 }
150 }
151 }
152
153 // Per-content TTL from meta (e.g. feeds) falls back to the 24h page
154 // default. HOUR_IN_SECONDS isn't defined yet (pre-WP), so use a literal.
155 $xspeed_ttl = ( isset( $xspeed_meta['ttl'] ) && (int) $xspeed_meta['ttl'] > 0 ) ? (int) $xspeed_meta['ttl'] : 86400;
156 $xspeed_age = time() - filemtime( $xspeed_cache_file );
157 if ( $xspeed_age < $xspeed_ttl ) {
158 // PHP-served cache hit (the ~85ms fallback path). The nginx static
159 // rewrite sends "HIT (nginx)" for the fast 5-15ms path; same header,
160 // distinct value so you can tell which layer served the page.
161 header( 'X-XSpeed-Cache: HIT (php)' );
162
163 // Record the HIT for the dashboard hit-ratio. The drop-in runs
164 // BEFORE WordPress loads, so it can't call Hit_Counter — instead
165 // it appends one line to the same hits.log the nginx static path
166 // uses, and Hit_Counter::collect_nginx_log_hits() drains + counts
167 // both on the next dashboard load. Without this, every drop-in HIT
168 // was served but never counted, so the hit ratio sat at 0.
169 // Best-effort: a failed append must never break serving the page.
170 //
171 // Path is baked in at install time by Cache::install_dropin(), which
172 // replaces the @@XSPEED_HITS_LOG@@ token on the next line with the
173 // resolved absolute path (uploads/xspeed/hits.log — NOT the cache dir,
174 // which gets deleted on purge/uninstall and would take nginx down,
175 // FBS-82478). The default below is the fallback for an un-substituted
176 // drop-in (e.g. run straight from a dev source checkout); the installed
177 // copy always carries the absolute uploads path.
178 $xspeed_hits_log = '@@XSPEED_HITS_LOG@@'; // replaced at install
179 if ( '@@' === substr( $xspeed_hits_log, 0, 2 ) ) {
180 $xspeed_hits_log = WP_CONTENT_DIR . '/uploads/xspeed/hits.log';
181 }
182 // 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).
183 @file_put_contents( $xspeed_hits_log, "hit\n", FILE_APPEND | LOCK_EX );
184
185 // Replay the cached response's status + content-type from .meta, so a
186 // cached 404 serves 404 (not a soft-404 200) and a cached feed serves
187 // application/rss+xml (not text/html). (FBS-82406, FBS-82407)
188 if ( ! empty( $xspeed_meta['status'] ) && function_exists( 'http_response_code' ) ) {
189 http_response_code( (int) $xspeed_meta['status'] );
190 }
191 if ( ! empty( $xspeed_meta['content_type'] ) && is_string( $xspeed_meta['content_type'] ) ) {
192 header( 'Content-Type: ' . $xspeed_meta['content_type'] );
193 }
194
195 // Conditional GET: Last-Modified + ETag from the cache file's mtime,
196 // answer a matching If-Modified-Since / If-None-Match with 304 so
197 // aggregators skip re-downloading an unchanged cached feed/page.
198 // (FBS-82407 #5)
199 $xspeed_mtime = (int) filemtime( $xspeed_cache_file );
200 if ( $xspeed_mtime > 0 ) {
201 $xspeed_lastmod = gmdate( 'D, d M Y H:i:s', $xspeed_mtime ) . ' GMT';
202 $xspeed_etag = '"' . md5( $xspeed_cache_file . '|' . $xspeed_mtime ) . '"';
203 header( 'Last-Modified: ' . $xspeed_lastmod );
204 header( 'ETag: ' . $xspeed_etag );
205 // 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.
206 $xspeed_inm = isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ? trim( (string) $_SERVER['HTTP_IF_NONE_MATCH'] ) : '';
207 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- as above.
208 $xspeed_ims = isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ? trim( (string) $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) : '';
209 if ( ( '' !== $xspeed_inm && false !== strpos( $xspeed_inm, $xspeed_etag ) )
210 || ( '' !== $xspeed_ims && false !== ( $xspeed_ims_ts = strtotime( $xspeed_ims ) ) && $xspeed_ims_ts >= $xspeed_mtime ) ) {
211 if ( function_exists( 'http_response_code' ) ) {
212 http_response_code( 304 );
213 }
214 exit;
215 }
216 }
217
218 // Serve the precompressed Brotli sibling when the client accepts it
219 // and the Pro Brotli module wrote <file>.br. MUST mirror
220 // XSpeed\Cache::maybe_serve_brotli() on the non-drop-in serve path —
221 // both decide on the same Accept-Encoding token match + sibling
222 // existence, so the response is identical whichever path serves.
223 // pre-WP: no sanitize_text_field()/wp_unslash(); the value is only
224 // lowercased + regex-matched, never echoed.
225 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- pre-WP drop-in; value is only lowercased + token-matched, never echoed or executed.
226 $xspeed_accept_enc = isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ? strtolower( str_replace( "\0", '', (string) $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) : '';
227 $xspeed_br_file = $xspeed_cache_file . '.br';
228 if ( preg_match( '/(^|[\s,])br([\s,;]|$)/', $xspeed_accept_enc )
229 && is_readable( $xspeed_br_file ) ) {
230 header( 'Content-Encoding: br' );
231 header( 'Vary: Accept-Encoding', false );
232 header_remove( 'Content-Length' );
233 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- Drop-in runs before WP_Filesystem is available; readfile streams the precompressed sibling directly.
234 readfile( $xspeed_br_file );
235 exit;
236 }
237
238 // 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.
239 readfile( $xspeed_cache_file );
240 exit;
241 }
242 }
243