PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / trunk
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN vtrunk
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 1.2.1 1.2.2 1.2.3
xspeed / xspeed.php

xspeed.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN trunk, at xspeed.php

298 lines 11.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: xSpeed Cache
4 * Description: Minimal, ultra-fast caching plugin for WordPress.
5 * Version: 1.2.4
6 * Requires at least: 6.0
7 * Tested up to: 7.1
8 * Requires PHP: 7.4
9 * Author: WPDeveloper
10 * Author URI: https://wpdeveloper.com
11 * License: GPLv2 or later
12 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
13 * Text Domain: xspeed
14 *
15 * @package XSpeed
16 */
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit;
20 }
21
22 define( 'XSPEED_VERSION', '1.2.4' );
23 define( 'XSPEED_FILE', __FILE__ );
24 define( 'XSPEED_DIR', plugin_dir_path( __FILE__ ) );
25 define( 'XSPEED_URL', plugin_dir_url( __FILE__ ) );
26 define( 'XSPEED_CACHE_DIR', WP_CONTENT_DIR . '/cache/xspeed' );
27
28 /**
29 * Static-cache tree. Files written here are served directly by the
30 * web server via the rewrite block in .htaccess, bypassing PHP for
31 * every cache hit. Layout: `xspeed-static/{host}{request_uri}/index.html`.
32 * Separate from XSPEED_CACHE_DIR so the PHP drop-in's flat-hash cache
33 * stays intact as a fallback for cookied / query-string / nginx hosts.
34 */
35 define( 'XSPEED_CACHE_STATIC_DIR', WP_CONTENT_DIR . '/cache/xspeed-static' );
36
37 /**
38 * Free-API version. xspeed-pro reads this to decide if it speaks the
39 * current Module / Module_Registry / Settings_Manager / Rest_Manager
40 * contract. Bump on any breaking change to those interfaces — never on
41 * additive change. See IMPLEMENTATION.md §1 and class-tier-registry.php.
42 */
43 define( 'XSPEED_API_VERSION', 1 );
44
45 /**
46 * WP Insights project token for opt-in usage analytics (Usage_Tracker). Routes
47 * this plugin's anonymous diagnostics to its project on send.wpinsight.com.
48 * Overridable via wp-config.php for staging/QA. Nothing is ever sent unless the
49 * admin explicitly opts in during the setup wizard — see class-usage-tracker.php.
50 *
51 * The WP Insights item_id for xSpeed — the project hash send.wpinsight.com
52 * keys this plugin's telemetry on. The tracker only sends after explicit
53 * user opt-in (require_optin is forced true); this just identifies the
54 * project on the initial registration handshake.
55 */
56 if ( ! defined( 'XSPEED_INSIGHTS_ITEM_ID' ) ) {
57 define( 'XSPEED_INSIGHTS_ITEM_ID', 'd2268aeacaa69d9f6d2f' );
58 }
59
60 /**
61 * Report an autoload path that is not on disk.
62 *
63 * PHP's own message names the CLASS and not the PATH, so a missing include
64 * reads as a code bug in a file that shipped intact — which is exactly how
65 * `Class "XSpeed\Migration" not found` was reported against 1.1.8 while the
66 * published zip was byte-identical to the tag. Name the file instead.
67 *
68 * Logged without a WP_DEBUG gate, because the sites this has to reach are the
69 * production ones that run with it off. Deduped per class per request: a miss
70 * usually precedes a fatal, but not always — `class_exists()` on a missing
71 * class is a graceful probe that returns false and carries on, and repeating
72 * the line for it would fill the log rather than explain anything.
73 *
74 * @param string $class Class that could not be autoloaded.
75 * @param string $path Path the autoloader resolved it to.
76 */
77 /**
78 * Delete everything inside a cache tree, without loading a single class.
79 *
80 * Only ever called with XSPEED_CACHE_DIR / XSPEED_CACHE_STATIC_DIR, and it
81 * refuses anything that is not a real directory under WP_CONTENT_DIR, so a
82 * mangled constant cannot turn this into a recursive delete somewhere else.
83 * Symlinks are unlinked, never followed.
84 *
85 * Leaves the root directory itself in place — same contract as
86 * Cache::purge_all(), which this stands in for when the classes are gone.
87 *
88 * @param string $root Absolute path to a cache tree.
89 */
90 if ( ! function_exists( 'xspeed_empty_cache_tree' ) ) {
91 function xspeed_empty_cache_tree( $root ) {
92 $real = realpath( $root );
93 $content = realpath( WP_CONTENT_DIR );
94 if ( false === $real || false === $content || ! is_dir( $real ) ) {
95 return;
96 }
97 // Must live under wp-content, and must not BE wp-content.
98 if ( $real === $content || 0 !== strpos( $real, $content . DIRECTORY_SEPARATOR ) ) {
99 return;
100 }
101
102 $items = new RecursiveIteratorIterator(
103 new RecursiveDirectoryIterator( $real, FilesystemIterator::SKIP_DOTS ),
104 RecursiveIteratorIterator::CHILD_FIRST
105 );
106 foreach ( $items as $item ) {
107 if ( $item->isLink() || ! $item->isDir() ) {
108 @unlink( $item->getPathname() );
109 continue;
110 }
111 @rmdir( $item->getPathname() );
112 }
113 }
114 }
115
116 if ( ! function_exists( 'xspeed_log_autoload_miss' ) ) {
117 function xspeed_log_autoload_miss( $class, $path ) {
118 static $seen = array();
119 if ( isset( $seen[ $class ] ) ) {
120 return;
121 }
122 $seen[ $class ] = true;
123
124 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Names the missing file behind an unresolvable class.
125 error_log( sprintf( '[xspeed] cannot autoload %1$s: %2$s is missing from disk', $class, $path ) );
126 }
127 }
128
129 spl_autoload_register(
130 function ( $class ) {
131 if ( strpos( $class, 'XSpeed\\' ) !== 0 ) {
132 return;
133 }
134 // substr, not str_replace — str_replace strips EVERY occurrence of
135 // the prefix, so a class whose sub-namespace repeats it resolves to
136 // the wrong path instead of missing outright.
137 $relative = substr( $class, strlen( 'XSpeed\\' ) );
138
139 /*
140 * Module classes are composer PSR-4 (XSpeed\Modules\Cache\CacheModule
141 * → includes/modules/Cache/CacheModule.php), so they resolve against
142 * that layout rather than the class-<kebab>.php one below.
143 *
144 * Reaching this branch at all means composer already failed: it
145 * registers with `$loader->register( true )`, which PREPENDS, so it
146 * gets every class before this autoloader does. A miss here is
147 * therefore just as genuine as an engine miss, and just as worth
148 * naming — a quarantined includes/modules/Cache/CacheModule.php
149 * otherwise reproduces the original incident exactly, with a bare
150 * class name and no path.
151 */
152 if ( strpos( $relative, 'Modules\\' ) === 0 ) {
153 $module_file = XSPEED_DIR . 'includes/modules/'
154 . str_replace( '\\', '/', substr( $relative, strlen( 'Modules\\' ) ) ) . '.php';
155 if ( ! file_exists( $module_file ) ) {
156 xspeed_log_autoload_miss( $class, $module_file );
157 }
158 return;
159 }
160
161 $file = XSPEED_DIR . 'includes/class-' . strtolower( str_replace( '_', '-', $relative ) ) . '.php';
162 if ( file_exists( $file ) ) {
163 require_once $file;
164 return;
165 }
166
167 // The class is one of ours and its file is gone from disk — a
168 // half-applied update, a stale opcache or realpath cache, a security
169 // plugin quarantining the file.
170 xspeed_log_autoload_miss( $class, $file );
171 }
172 );
173
174 if ( ! file_exists( XSPEED_DIR . 'vendor/autoload.php' ) ) {
175 /*
176 * Composer's autoloader is what resolves every XSpeed\Modules\* class.
177 * Without it there is no version of this plugin that works: booting on
178 * would register hooks and then fatal on the first module touched, from
179 * whichever request got there first.
180 *
181 * So stop here. Log the path, tell an admin who can act on it, and
182 * return without registering anything. The plugin stays "active" and
183 * does nothing, which is recoverable by reinstalling; a fatal on a
184 * front-end request is not.
185 */
186 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Names the missing file that makes the plugin unloadable.
187 error_log( '[xspeed] vendor/autoload.php is missing from disk; xSpeed cannot load and has stopped before registering hooks' );
188
189 /*
190 * Deactivation still has to empty the caches, even in this state.
191 *
192 * Normally Plugin::deactivate() runs Cache::purge_all(). Bailing here
193 * skips that registration, and the consequence is not "no cleanup" — it
194 * is stale pages that never expire. The .htaccess block rewrites straight
195 * to xspeed-static/{host}{path}/index.html on a bare file-exists test,
196 * with no TTL available at the rewrite layer, so a deactivated plugin
197 * would keep serving frozen HTML indefinitely. Deactivating is the first
198 * thing an admin does when a plugin complains, so it has to work.
199 *
200 * Raw PHP on purpose: no class here is loadable, which is the whole
201 * reason we are in this branch.
202 */
203 register_deactivation_hook(
204 __FILE__,
205 static function () {
206 foreach ( array( XSPEED_CACHE_DIR, XSPEED_CACHE_STATIC_DIR ) as $root ) {
207 xspeed_empty_cache_tree( $root );
208 }
209 }
210 );
211
212 add_action(
213 'admin_notices',
214 static function () {
215 if ( ! current_user_can( 'activate_plugins' ) ) {
216 return;
217 }
218 echo '<div class="notice notice-error"><p><strong>xSpeed</strong> — '
219 . esc_html__( 'is missing files and could not start, so no new pages are being cached. Pages already in the cache are still being served, and while the plugin is in this state nothing clears them automatically. Deactivating xSpeed empties the cache; reinstalling the plugin restores it. Deactivating alone will not repair the installation.', 'xspeed' )
220 . '</p></div>';
221 }
222 );
223 return;
224 }
225
226 require_once XSPEED_DIR . 'vendor/autoload.php';
227 require_once XSPEED_DIR . 'includes/wp-cache-constant.php';
228
229 register_activation_hook( __FILE__, array( 'XSpeed\\Plugin', 'activate' ) );
230 register_deactivation_hook( __FILE__, array( 'XSpeed\\Plugin', 'deactivate' ) );
231
232 /**
233 * Core classes that boot touches unconditionally. If any is missing the
234 * plugin directory is incomplete, and booting would fatal — on the front end
235 * too, since Plugin::init() runs on `plugins_loaded` for every request.
236 *
237 * Guarding each call site individually does not scale and misses the next
238 * one, so the integrity check lives here: refuse to boot, say why, and leave
239 * WordPress usable so the site owner can actually fix it. A cached page can
240 * mask this on the homepage while every uncached request 500s, which is
241 * exactly the failure that is hardest to diagnose from the outside.
242 *
243 * @return string[] Class names that could not be loaded.
244 */
245 function xspeed_missing_core_classes() {
246 $missing = array();
247 foreach ( array( 'Plugin', 'Cache', 'Cache_GC', 'Settings', 'Settings_Manager', 'Module_Registry' ) as $name ) {
248 if ( ! class_exists( '\\XSpeed\\' . $name ) ) {
249 $missing[] = 'XSpeed\\' . $name;
250 }
251 }
252
253 /*
254 * Module classes matter just as much: register_free_modules() does an
255 * unconditional `new` on every one, from this same `plugins_loaded`
256 * hook, so a single missing file under includes/modules/ fatals every
257 * request exactly like a missing engine class does.
258 *
259 * Read the list out of class-plugin.php's own `new \XSpeed\Modules\…()`
260 * calls rather than hand-keeping it here, so a module added or removed
261 * there cannot leave this check silently out of date.
262 */
263 $plugin_file = XSPEED_DIR . 'includes/class-plugin.php';
264 $source = is_readable( $plugin_file ) ? @file_get_contents( $plugin_file ) : false; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Runs before WP_Filesystem exists, and reads a plugin file by absolute path.
265 if ( false !== $source && preg_match_all( '/new\s+(\\\\XSpeed\\\\Modules\\\\[A-Za-z0-9_\\\\]+)\s*\(/', $source, $m ) ) {
266 foreach ( array_unique( $m[1] ) as $class ) {
267 if ( ! class_exists( $class ) ) {
268 $missing[] = ltrim( $class, '\\' );
269 }
270 }
271 }
272
273 return $missing;
274 }
275
276 add_action(
277 'plugins_loaded',
278 function () {
279 $missing = xspeed_missing_core_classes();
280 if ( ! empty( $missing ) ) {
281 add_action(
282 'admin_notices',
283 function () use ( $missing ) {
284 if ( ! current_user_can( 'activate_plugins' ) ) {
285 return;
286 }
287 echo '<div class="notice notice-error"><p><strong>' .
288 esc_html__( 'xSpeed Cache could not start.', 'xspeed' ) . '</strong> ' .
289 esc_html__( 'Some of its files are missing, so it stopped rather than take the site down with it. No new pages are being cached, but pages already in the cache are still being served and nothing clears them while the plugin is in this state. Re-install or re-upload the plugin to restore it, then purge the cache.', 'xspeed' ) .
290 '</p><p><code>' . esc_html( implode( ', ', $missing ) ) . '</code></p></div>';
291 }
292 );
293 return;
294 }
295 \XSpeed\Plugin::instance()->init();
296 }
297 );
298