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.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 1.2.1 All 27 releases
xspeed / xspeed.php

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

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