# xspeed/1.0.0/includes/class-cache.php

xSpeed Cache: AI-Powered Performance Hub with MCP, Caching &amp; CDN, version 1.0.0. 457 lines.

- Page: https://pluginprobe.com/plugins/xspeed/1.0.0/code/includes/class-cache.php
- Raw: https://pluginprobe.com/plugins/xspeed/1.0.0/raw/includes/class-cache.php
- Modified: 2026-05-27T11:31:36+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/xspeed/1.0.0/code/includes/class-cache.php#L10-L20`.

```php
<?php
/**
 * Page cache engine.
 *
 * @package XSpeed
 */

namespace XSpeed;

defined( 'ABSPATH' ) || exit;

class Cache {

	/**
	 * Output-buffer nesting level at which we opened our cache buffer, so
	 * `close_buffer()` can flush ONLY our buffer and never disturb a buffer
	 * another plugin pushed on top of (or below) ours.
	 *
	 * @var int|null
	 */
	private static $buffer_level = null;

	public function __construct() {
		add_action( 'template_redirect', array( $this, 'maybe_start_cache' ), 0 );

		$invalidate_hooks = array( 'save_post', 'deleted_post', 'trashed_post', 'comment_post', 'wp_set_comment_status', 'switch_theme', 'activated_plugin', 'deactivated_plugin' );
		foreach ( $invalidate_hooks as $hook ) {
			add_action( $hook, array( __CLASS__, 'purge_all' ) );
			add_action( $hook, array( 'XSpeed\\Minifier', 'purge_minified' ) );
		}

		add_action( 'update_option_xspeed_options', array( __CLASS__, 'on_settings_change' ), 10, 2 );

		add_action( 'admin_bar_menu', array( $this, 'admin_bar_purge' ), 100 );
		add_action( 'admin_post_xspeed_purge', array( $this, 'handle_admin_bar_purge' ) );
	}

	public static function on_settings_change( $old, $new ) {
		// GZIP toggle: write or remove .htaccess rules.
		$old_gzip = ! empty( $old['gzip_enabled'] );
		$new_gzip = ! empty( $new['gzip_enabled'] );
		if ( $old_gzip !== $new_gzip ) {
			Gzip::apply( $new_gzip );
		}
		// Any settings change — purge caches so changes take effect.
		self::purge_all();
		Minifier::purge_minified();
	}

	public function maybe_start_cache() {
		if ( ! self::should_cache() ) {
			return;
		}

		$key  = self::cache_key();
		$file = self::cache_file_for( $key );

		if ( file_exists( $file ) && ! self::is_expired( $file ) ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- readfile is optimal for streaming a static cache file directly to the visitor; WP_Filesystem would buffer through PHP memory and is not appropriate for response streaming.
			readfile( $file );
			exit;
		}


		// WP < 6.9 fallback: ob_start() with a callback, paired with an
		// explicit shutdown close so the buffer lifecycle is visible to
		// reviewers and Plugin Check, instead of relying on PHP's implicit
		// request-end flush. We record our nesting level so close_buffer()
		// flushes ONLY the buffer we opened.
		ob_start( array( __CLASS__, 'finalize_buffer' ) );
		self::$buffer_level = ob_get_level();

		add_action( 'shutdown', array( __CLASS__, 'close_buffer' ), 0 );
	}

	/**
	 * Close the cache buffer opened by maybe_start_cache().
	 *
	 * Guarded by the recorded buffer level so we never flush a buffer that
	 * another plugin pushed on top of (or under) ours. If something else is
	 * currently on top, we leave the stack alone — PHP's shutdown sequence
	 * will unwind buffers in order and our finalize_buffer() callback will
	 * still run when our level becomes the topmost one.
	 */
	public static function close_buffer() {
		if ( null === self::$buffer_level ) {
			return;
		}
		if ( ob_get_level() === self::$buffer_level ) {
			ob_end_flush();
		}
		self::$buffer_level = null;
	}

	public static function should_cache() {
		$opts = Settings::get();
		if ( empty( $opts['cache_enabled'] ) ) {
			return false;
		}

		if ( is_user_logged_in() || is_admin() || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || ( defined( 'DOING_CRON' ) && DOING_CRON ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
			return false;
		}

		if ( defined( 'DONOTCACHEPAGE' ) && DONOTCACHEPAGE ) {
			return false;
		}

		$method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : '';
		if ( 'GET' !== $method ) {
			return false;
		}

		if ( ! empty( $_SERVER['QUERY_STRING'] ) ) {
			return false;
		}

		$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
		foreach ( $opts['excluded_urls'] as $excluded ) {
			if ( '' !== $excluded && false !== strpos( $request_uri, $excluded ) ) {
				return false;
			}
		}

		return true;
	}

	public static function cache_key() {
		$host = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : 'default';
		$uri  = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/';
		$uri  = strtok( $uri, '?' );
		return md5( $host . $uri );
	}

	public static function cache_file_for( $key ) {
		return XSPEED_CACHE_DIR . '/' . $key . '.html';
	}

	public static function is_expired( $file ) {
		$opts    = Settings::get();
		$max_age = (int) $opts['cache_expiry'] * HOUR_IN_SECONDS;
		return ( time() - filemtime( $file ) ) > $max_age;
	}

	/**
	 * Accumulator for the full response body across all output-handler phases.
	 *
	 * PHP invokes an ob_start() callback once per flush, and each invocation
	 * only receives the chunk produced *since the previous flush*. If anything
	 * during the render calls `ob_flush()` or `flush()` (some themes, lazy-
	 * load plugins, AMP, etc. do), the final-phase call would otherwise only
	 * see the tail of the page — and we'd cache a truncated response that
	 * gets served repeatedly until purge. We accumulate every chunk here so
	 * the cache file always reflects the complete page.
	 *
	 * @var string
	 */
	private static $accumulated = '';

	public static function finalize_buffer( $buffer, $phase = PHP_OUTPUT_HANDLER_FINAL ) {
		self::$accumulated .= $buffer;

		// On non-final phases (mid-request flushes), pass the current chunk
		// through to the client unmodified and keep collecting. The WP 6.9
		// filter path always passes the full body in one shot with the
		// default $phase, so it falls straight through to the final block.
		$is_final = ( $phase & ( PHP_OUTPUT_HANDLER_FINAL | PHP_OUTPUT_HANDLER_END ) ) !== 0;
		if ( ! $is_final ) {
			return $buffer;
		}

		$full              = self::$accumulated;
		self::$accumulated = '';

		if ( strlen( $full ) < 255 ) {
			return $buffer;
		}

		if ( function_exists( 'http_response_code' ) && 200 !== http_response_code() ) {
			return $buffer;
		}

		// If no mid-request flush happened, $buffer === $full and we can
		// safely minify the on-wire bytes too. Otherwise earlier chunks have
		// already been sent unminified, so we minify only what goes to disk —
		// the first visitor sees unminified HTML, every cache hit after that
		// is minified.
		$single_chunk = ( $buffer === $full );

		$opts = Settings::get();
		if ( ! empty( $opts['minify_html'] ) ) {
			$full = Minifier::minify_html( $full );
			if ( $single_chunk ) {
				$buffer = $full;
			}
		}

		if ( ! file_exists( XSPEED_CACHE_DIR ) ) {
			wp_mkdir_p( XSPEED_CACHE_DIR );
			self::write_silence( XSPEED_CACHE_DIR );
		}

		// Path safety: cache_file_for() builds `XSPEED_CACHE_DIR . '/' . $key . '.html'`
		// where $key comes from md5() — guaranteed to be exactly 32 lowercase
		// hex chars, so no traversal sequence ('..', '/', null byte, etc.)
		// can appear. The write is therefore always inside XSPEED_CACHE_DIR.
		$file = self::cache_file_for( self::cache_key() );
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_put_contents_file_put_contents -- WP_Filesystem requires admin context for credentials; cache writes happen on frontend requests where it's unavailable.
		file_put_contents( $file, $full, LOCK_EX );

		return $buffer;
	}

	public static function purge_all() {
		if ( ! is_dir( XSPEED_CACHE_DIR ) ) {
			return;
		}
		$files = glob( XSPEED_CACHE_DIR . '/*.html' );
		if ( $files ) {
			foreach ( $files as $f ) {
				wp_delete_file( $f );
			}
		}
		self::update_stats( array( 'last_purge' => time() ) );
	}

	/**
	 * Drop a "silence is golden" index.php into a directory so apaches/nginx
	 * with directory listing enabled don't expose cache contents.
	 */
	public static function write_silence( $dir ) {
		$file = trailingslashit( $dir ) . 'index.php';
		if ( ! file_exists( $file ) ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_put_contents_file_put_contents -- WP_Filesystem requires admin context for credentials; cache dir setup may run during a frontend page render.
			file_put_contents( $file, "<?php\n// Silence is golden.\n" );
		}
	}

	/**
	 * Persist stats with autoload disabled — stats are only read in admin
	 * contexts, so there is no reason to inflate every frontend request's
	 * `wp_load_alloptions()` payload.
	 */
	private static function update_stats( array $stats ) {
		if ( false === get_option( 'xspeed_stats' ) ) {
			add_option( 'xspeed_stats', $stats, '', 'no' );
			return;
		}
		update_option( 'xspeed_stats', $stats );
	}

	public static function get_stats() {
		$count = 0;
		$size  = 0;
		if ( is_dir( XSPEED_CACHE_DIR ) ) {
			$files = glob( XSPEED_CACHE_DIR . '/*.html' );
			if ( $files ) {
				$count = count( $files );
				foreach ( $files as $f ) {
					$size += filesize( $f );
				}
			}
		}
		$stats = get_option( 'xspeed_stats', array() );
		return array(
			'cached_pages' => $count,
			'cache_size'   => $size,
			'last_purge'   => isset( $stats['last_purge'] ) ? (int) $stats['last_purge'] : 0,
		);
	}

	/**
	 * Apply the user's enable/disable choice. Called only from the REST
	 * toggle endpoint, which is gated by current_user_can( 'manage_options' )
	 * and a verified REST nonce. This is the only place the drop-in and
	 * the WP_CACHE constant are written — they MUST NOT happen on
	 * register_activation_hook (WordPress.org review requirement).
	 *
	 * @param bool $enable User's choice.
	 * @return array{
	 *     enabled: bool,
	 *     dropin_installed: bool,
	 *     wp_cache_constant: bool,
	 *     wp_config_writable: bool,
	 *     manual_snippet: ?string
	 * }
	 */
	public static function toggle( $enable ) {
		$enable = (bool) $enable;

		if ( $enable ) {
			$dropin_ok    = self::install_dropin();
			$wp_config_ok = self::set_wp_cache_constant( true );
			$snippet      = $wp_config_ok ? null : "define( 'WP_CACHE', true );";

			return array(
				'enabled'            => true,
				'dropin_installed'   => (bool) $dropin_ok,
				'wp_cache_constant'  => (bool) $wp_config_ok,
				'wp_config_writable' => self::wp_config_writable(),
				'manual_snippet'     => $snippet,
			);
		}

		self::remove_dropin();
		self::set_wp_cache_constant( false );

		return array(
			'enabled'            => false,
			'dropin_installed'   => false,
			'wp_cache_constant'  => false,
			'wp_config_writable' => self::wp_config_writable(),
			'manual_snippet'     => null,
		);
	}

	/**
	 * Check wp-config.php writability via WP_Filesystem. Plugin Check flags
	 * direct is_writable() under WordPress.WP.AlternativeFunctions.
	 */
	private static function wp_config_writable() {
		global $wp_filesystem;
		if ( ! function_exists( 'WP_Filesystem' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}
		WP_Filesystem();

		return $wp_filesystem ? (bool) $wp_filesystem->is_writable( ABSPATH . 'wp-config.php' ) : false;
	}

	public static function install_dropin() {
		$source = XSPEED_DIR . 'includes/advanced-cache.php';
		$target = WP_CONTENT_DIR . '/advanced-cache.php';
		if ( ! file_exists( $source ) ) {
			return false;
		}

		global $wp_filesystem;
		if ( ! function_exists( 'WP_Filesystem' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}
		WP_Filesystem();
		if ( ! $wp_filesystem ) {
			return false;
		}

		$source_contents = $wp_filesystem->get_contents( $source );
		if ( ! is_string( $source_contents ) ) {
			return false;
		}

		if ( file_exists( $target ) ) {
			$existing = $wp_filesystem->get_contents( $target );
			$is_xspeed = is_string( $existing ) && false !== strpos( $existing, 'XSPEED_DROPIN' );

			if ( $is_xspeed ) {
				if ( $existing === $source_contents ) {
					return true;
				}
				return (bool) $wp_filesystem->put_contents( $target, $source_contents, FS_CHMOD_FILE );
			}

			// Foreign drop-in (e.g. left over from another cache plugin) — back it up
			// before overwriting so the user can recover if needed. Uploads dir
			// (not wp-content root) keeps the backup out of WordPress's reserved
			// drop-in location.
			$upload  = wp_upload_dir( null, false );
			$basedir = isset( $upload['basedir'] ) ? trailingslashit( $upload['basedir'] ) . 'xspeed-backups' : false;
			if ( $basedir ) {
				if ( ! file_exists( $basedir ) ) {
					wp_mkdir_p( $basedir );
					self::write_silence( $basedir );
				}
				$backup = $basedir . '/advanced-cache.foreign-' . gmdate( 'Ymd-His' ) . '.php.bak';
				$wp_filesystem->move( $target, $backup, true );
			} else {
				$wp_filesystem->delete( $target );
			}
		}

		return (bool) $wp_filesystem->put_contents( $target, $source_contents, FS_CHMOD_FILE );
	}

	public static function remove_dropin() {
		$target = WP_CONTENT_DIR . '/advanced-cache.php';
		if ( ! file_exists( $target ) ) {
			return;
		}

		global $wp_filesystem;
		if ( ! function_exists( 'WP_Filesystem' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}
		WP_Filesystem();
		if ( ! $wp_filesystem ) {
			return;
		}

		$contents = $wp_filesystem->get_contents( $target );
		if ( is_string( $contents ) && false !== strpos( $contents, 'XSPEED_DROPIN' ) ) {
			wp_delete_file( $target );
		}
	}

	public static function set_wp_cache_constant( $enable ) {
		$wp_config = ABSPATH . 'wp-config.php';
		if ( ! file_exists( $wp_config ) ) {
			return false;
		}

		global $wp_filesystem;
		if ( ! function_exists( 'WP_Filesystem' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}
		WP_Filesystem();
		if ( ! $wp_filesystem || ! $wp_filesystem->is_writable( $wp_config ) ) {
			return false;
		}

		$config = $wp_filesystem->get_contents( $wp_config );

		if ( $enable ) {
			if ( strpos( $config, "define( 'WP_CACHE'" ) !== false || strpos( $config, "define('WP_CACHE'" ) !== false ) {
				return true;
			}
			$config = preg_replace( '/(<\?php)/', "$1\ndefine( 'WP_CACHE', true );", $config, 1 );
		} else {
			$config = preg_replace( "/define\\(\\s*['\"]WP_CACHE['\"]\\s*,\\s*true\\s*\\);\\s*\\n?/", '', $config );
		}

		return (bool) $wp_filesystem->put_contents( $wp_config, $config, FS_CHMOD_FILE );
	}

	public function admin_bar_purge( $wp_admin_bar ) {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}
		$wp_admin_bar->add_node(
			array(
				'id'    => 'xspeed-purge',
				'title' => __( 'Purge xSpeed Cache', 'xspeed' ),
				'href'  => wp_nonce_url( admin_url( 'admin-post.php?action=xspeed_purge' ), 'xspeed_purge' ),
			)
		);
	}

	public function handle_admin_bar_purge() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Unauthorized.', 'xspeed' ), 403 );
		}
		check_admin_referer( 'xspeed_purge' );
		self::purge_all();
		wp_safe_redirect( wp_get_referer() ?: admin_url() );
		exit;
	}
}

```
