'wp_version', * 'tone' => 'ok' | 'warn' | 'fail' | 'info', * 'label' => 'WordPress 6.6', * 'detail' => 'Meets the 6.0+ minimum.', * ] * * Pure reads — never writes to disk, never makes outbound HTTP calls. * Safe to call from any request including the loading dashboard. * * @package XSpeed */ declare(strict_types=1); namespace XSpeed; defined( 'ABSPATH' ) || exit; final class Health { public const OK = 'ok'; public const WARN = 'warn'; public const FAIL = 'fail'; public const INFO = 'info'; private const SERVER_LABELS = array( 'apache' => 'Apache', 'litespeed' => 'LiteSpeed', 'nginx' => 'nginx', 'iis' => 'IIS', 'unknown' => 'Unknown', ); /** * Full check list used by the Health module's dashboard panel. * * @return array */ public static function checks(): array { global $wp_version; $server_type = Server::type(); $gzip_mode = Server::gzip_mode(); $conflicts = Server::conflicts(); $cache_dir = defined( 'XSPEED_CACHE_DIR' ) ? XSPEED_CACHE_DIR : ( WP_CONTENT_DIR . '/cache/xspeed' ); $out = array(); // WordPress version $wp_ok = version_compare( (string) $wp_version, '6.0', '>=' ); $out[] = array( 'id' => 'wp_version', 'tone' => $wp_ok ? self::OK : self::FAIL, 'label' => sprintf( 'WordPress %s', (string) $wp_version ), 'detail' => $wp_ok ? 'Meets the 6.0+ minimum.' : 'Upgrade to WordPress 6.0 or higher.', ); // PHP version $php_ok = version_compare( PHP_VERSION, '7.4', '>=' ); $php_modern = version_compare( PHP_VERSION, '8.1', '>=' ); $out[] = array( 'id' => 'php_version', 'tone' => $php_modern ? self::OK : ( $php_ok ? self::WARN : self::FAIL ), 'label' => sprintf( 'PHP %s', PHP_VERSION ), 'detail' => $php_modern ? 'Modern PHP — full speed.' : ( $php_ok ? 'Works, but 8.1+ is recommended for best performance.' : 'Upgrade to PHP 7.4 or higher.' ), ); // Server $out[] = array( 'id' => 'server', 'tone' => self::INFO, 'label' => sprintf( 'Server: %s', self::SERVER_LABELS[ $server_type ] ?? 'Unknown' ), 'detail' => 'auto' === $gzip_mode ? 'GZIP can be auto-configured via .htaccess.' : 'GZIP requires a manual server-config snippet (shown in the GZIP module).', ); // Cache directory writable $dir_writable = wp_mkdir_p( $cache_dir ) && wp_is_writable( $cache_dir ); $out[] = array( 'id' => 'cache_dir', 'tone' => $dir_writable ? self::OK : self::FAIL, 'label' => 'Cache directory writable', 'detail' => $dir_writable ? $cache_dir : sprintf( 'Cannot write to %s. Adjust file permissions before enabling cache.', $cache_dir ), ); // Drop-in installed (only when cache is enabled — otherwise N/A) $cache_enabled = (bool) Settings::get()['cache_enabled']; if ( $cache_enabled ) { $dropin_path = WP_CONTENT_DIR . '/advanced-cache.php'; $dropin_match = file_exists( $dropin_path ) && false !== strpos( (string) file_get_contents( $dropin_path ), 'xspeed' ); $out[] = array( 'id' => 'dropin', 'tone' => $dropin_match ? self::OK : self::FAIL, 'label' => 'advanced-cache.php drop-in', 'detail' => $dropin_match ? 'Installed and owned by xSpeed.' : 'Drop-in missing or owned by another plugin. Toggle Enable Cache off and on to reinstall.', ); } // WP_CACHE constant $wp_cache_const = defined( 'WP_CACHE' ) && WP_CACHE; if ( $cache_enabled ) { $out[] = array( 'id' => 'wp_cache_constant', 'tone' => $wp_cache_const ? self::OK : self::WARN, 'label' => 'WP_CACHE constant', 'detail' => $wp_cache_const ? 'Defined and truthy in wp-config.php.' : 'Not set. Cache is configured but WordPress will not load the drop-in until WP_CACHE = true is added to wp-config.php.', ); } // Static-rewrite probe. Active end-to-end check: writes a probe // file under the static-cache dir, fetches it over HTTP, and // confirms the web server (nginx OR Apache/LiteSpeed) served // the raw file. Result is throttled to a 5-minute transient // inside Cache::probe_static_rewrite so we never hit the // network per-paint. if ( $cache_enabled ) { $server_type = Server::type(); // The live loopback probe only matters where a server-level static // rewrite is actually used (nginx snippet / Apache .htaccess). // LiteSpeed serves hits via the drop-in (see below), so skip the // probe there entirely — no needless self-request. Health is the // right place to pay for the probe when we DO run it (the admin // bootstrap reads cache-only so it never blocks); the 5-minute // transient still throttles repeat runs. (FBS-82142) $probe = ( Server::LITESPEED === $server_type ) ? array( 'active' => false ) : Cache::probe_static_rewrite( true ); $is_active = (bool) ( $probe['active'] ?? false ); if ( Server::NGINX === $server_type ) { $out[] = array( 'id' => 'static_rewrite_nginx', 'tone' => $is_active ? self::OK : self::WARN, 'label' => 'Static-file rewrite (nginx server config)', 'detail' => $is_active ? 'nginx is serving cache hits directly — PHP bypassed (~5-15ms TTFB).' : 'nginx detected but not yet routing to the cache. Paste the snippet below into your site\'s server { } block, then reload nginx.', // Always ship the snippet — even when active, so the // admin has it handy for re-pasting after a server // rebuild without having to find it elsewhere. Mirror // the SAME unified block the "Server config" panel // renders (cache + gzip + browser-cache directives), // not the cache-only snippet — otherwise Health and // the Cache panel disagree on what to paste. 'snippet' => Cache::full_nginx_server_block(), ); } elseif ( Server::LITESPEED === $server_type ) { // LiteSpeed intentionally does NOT use the .htaccess static // rewrite: OpenLiteSpeed's .htaccess engine ignores // mod_headers (so we can't stamp X-XSpeed-Cache: HIT) and has // no per-rule access_log (so a static hit can't be counted). // We route LiteSpeed hits through the PHP drop-in instead, so // every hit is both visible (X-XSpeed-Cache: HIT) and counted // in the hit-ratio — see Cache::static_rewrite_allowed(). This // is the healthy, expected state on LiteSpeed, not a fallback. $out[] = array( 'id' => 'static_rewrite_litespeed', 'tone' => self::OK, 'label' => 'Cache serving (LiteSpeed)', 'detail' => 'Cache hits are served by xSpeed\'s drop-in and tagged X-XSpeed-Cache: HIT — so every hit is visible and counted in your hit-ratio. (LiteSpeed\'s .htaccess can\'t add that header or log static hits, so xSpeed serves them itself for accurate reporting.)', ); } elseif ( Server::APACHE === $server_type ) { $installed = Cache::rewrite_installed(); if ( $is_active ) { $tone = self::OK; $detail = 'Block installed and serving cache hits directly — PHP bypassed.'; } elseif ( ! $installed ) { $tone = self::WARN; $detail = 'Block missing from .htaccess. Toggle Enable Cache off and on to reinstall it.'; } else { $tone = self::WARN; $detail = sprintf( 'Block installed but probe failed (%s). Confirm the .htaccess block is at the TOP of the file, and that AllowOverride is enabled for your site so mod_rewrite reads it.', (string) ( $probe['reason'] ?? 'unknown' ) ); } $out[] = array( 'id' => 'static_rewrite', 'tone' => $tone, 'label' => 'Static-file rewrite (.htaccess)', 'detail' => $detail, ); } } // Permalinks $permalinks_ok = (bool) get_option( 'permalink_structure' ); $out[] = array( 'id' => 'permalinks', 'tone' => $permalinks_ok ? self::OK : self::WARN, 'label' => 'Permalinks', 'detail' => $permalinks_ok ? 'Pretty permalinks active.' : 'Set permalinks to anything other than "Plain" — page caching needs URL paths to key on.', ); // Conflicting plugins $out[] = array( 'id' => 'conflicts', 'tone' => empty( $conflicts ) ? self::OK : self::WARN, 'label' => 'Caching plugin conflicts', 'detail' => empty( $conflicts ) ? 'No other caching plugins detected.' : sprintf( 'Active: %s. Deactivate before enabling xSpeed cache to avoid double-caching.', implode( ', ', $conflicts ) ), ); return $out; } /** * Lightweight environment payload for the onboarding wizard. Keeps * the legacy shape Onboarding::env_payload returned so the Welcome * step's HealthRow rendering doesn't change. */ public static function env_payload(): array { global $wp_version; $cache_dir = defined( 'XSPEED_CACHE_DIR' ) ? XSPEED_CACHE_DIR : ( WP_CONTENT_DIR . '/cache/xspeed' ); return array( 'wp' => array( 'version' => (string) $wp_version, 'ok' => version_compare( (string) $wp_version, '6.0', '>=' ), ), 'php' => array( 'version' => PHP_VERSION, 'ok' => version_compare( PHP_VERSION, '7.4', '>=' ), 'modern' => version_compare( PHP_VERSION, '8.1', '>=' ), ), 'server' => array( 'type' => Server::type(), 'gzip_mode' => Server::gzip_mode(), ), 'cache_dir' => array( 'path' => $cache_dir, 'writable' => wp_mkdir_p( $cache_dir ) && wp_is_writable( $cache_dir ), ), 'wp_config' => array( 'writable' => self::wp_config_writable(), ), 'permalinks_ok' => (bool) get_option( 'permalink_structure' ), 'conflicts' => Server::conflicts(), ); } private static function wp_config_writable(): bool { $path = ABSPATH . 'wp-config.php'; if ( ! file_exists( $path ) ) { $path = dirname( ABSPATH ) . '/wp-config.php'; } return file_exists( $path ) && wp_is_writable( $path ); } }