$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, " $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; } }