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 | xspeed.php +247 -8 1.0.11.3.3 View file →
@@ -1,11 +1,11 @@
1 1 <?php
2 2 /**
3 - * Plugin Name: xSpeed
3 + * Plugin Name: xSpeed Cache
4 4 * Description: Minimal, ultra-fast caching plugin for WordPress.
5 - * Version: 1.0.1
5 + * Version: 1.3.3
6 6 * Requires at least: 6.0
7 - * Tested up to: 7.0
7 + * Tested up to: 7.1
8 8 * Requires PHP: 7.4
9 9 * Author: WPDeveloper
10 10 * Author URI: https://wpdeveloper.com
11 11 * License: GPLv2 or later
@@ -10,8 +10,9 @@
10 10 * Author URI: https://wpdeveloper.com
11 11 * License: GPLv2 or later
12 12 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
13 13 * Text Domain: xspeed
14 + * Domain Path: /languages
14 15 *
15 16 * @package XSpeed
16 17 */
17 18
@@ -18,9 +19,9 @@
18 19 if ( ! defined( 'ABSPATH' ) ) {
19 20 exit;
20 21 }
21 22
22 -define( 'XSPEED_VERSION', '1.0.1' );
23 +define( 'XSPEED_VERSION', '1.3.3' );
23 24 define( 'XSPEED_FILE', __FILE__ );
24 25 define( 'XSPEED_DIR', plugin_dir_path( __FILE__ ) );
25 26 define( 'XSPEED_URL', plugin_dir_url( __FILE__ ) );
26 27 define( 'XSPEED_CACHE_DIR', WP_CONTENT_DIR . '/cache/xspeed' );
@@ -41,30 +42,268 @@
41 42 * additive change. See IMPLEMENTATION.md §1 and class-tier-registry.php.
42 43 */
43 44 define( 'XSPEED_API_VERSION', 1 );
44 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 +
45 130 spl_autoload_register(
46 131 function ( $class ) {
47 132 if ( strpos( $class, 'XSpeed\\' ) !== 0 ) {
48 133 return;
49 134 }
50 - $relative = str_replace( 'XSpeed\\', '', $class );
51 - $file = XSPEED_DIR . 'includes/class-' . strtolower( str_replace( '_', '-', $relative ) ) . '.php';
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';
52 163 if ( file_exists( $file ) ) {
53 164 require_once $file;
165 + return;
54 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 );
55 172 }
56 173 );
57 174
58 -if ( file_exists( XSPEED_DIR . 'vendor/autoload.php' ) ) {
59 - require_once XSPEED_DIR . 'vendor/autoload.php';
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;
60 225 }
61 226
227 +require_once XSPEED_DIR . 'vendor/autoload.php';
228 +require_once XSPEED_DIR . 'includes/wp-cache-constant.php';
229 +
62 230 register_activation_hook( __FILE__, array( 'XSpeed\\Plugin', 'activate' ) );
63 231 register_deactivation_hook( __FILE__, array( 'XSpeed\\Plugin', 'deactivate' ) );
64 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 +
65 277 add_action(
66 278 'plugins_loaded',
67 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 + }
68 307 \XSpeed\Plugin::instance()->init();
69 308 }
70 309 );