| 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 |
|