0 ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- racey stat, treated as "skip". return $path; } } // LiteSpeed (and some Apache vhost setups) write a per-vhost // `.access.log` rather than a single global file. Scan the // known log dirs for the most-recently-written, readable, non-empty // *.access.log and use that. Auto-tracks whichever vhost is serving // this site without the admin having to set a path. $dirs = array( '/usr/local/lsws/logs', '/var/log/lshttpd', '/var/log/apache2', '/var/log/httpd' ); $best = ''; $best_mtime = 0; foreach ( $dirs as $dir ) { if ( ! is_dir( $dir ) ) { continue; } $globbed = glob( $dir . '/*access*log*' ); if ( ! is_array( $globbed ) ) { continue; } foreach ( $globbed as $path ) { if ( ! is_readable( $path ) || (int) @filesize( $path ) === 0 ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged continue; } $mtime = (int) @filemtime( $path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if ( $mtime > $best_mtime ) { $best_mtime = $mtime; $best = $path; } } } return $best; } /** * GZIP support category for the UI: * 'auto' — toggling writes server config (Apache / LiteSpeed) * 'manual' — must be configured outside the plugin (nginx, IIS, unknown) */ public static function gzip_mode() { return self::supports_htaccess() ? 'auto' : 'manual'; } /** * Whether the server can serve Brotli-compressed responses. * * Brotli is an optional server module (mod_brotli on Apache, * ngx_brotli on nginx, built in on LiteSpeed/OpenLiteSpeed) — unlike * GZIP it is NOT guaranteed present. We report availability so the UI * and any add-on (xspeed-pro Brotli module) can decide whether to * emit Brotli rules or fall back to GZIP only. * * Detection, cheapest signal first: * 1. LiteSpeed — Brotli is part of the core server, always available. * 2. Apache mod_php — apache_get_modules() lists 'mod_brotli'. * 3. PHP `brotli` extension (kjdev/php-ext-brotli) — lets us at least * pre-compress static files even when the web server can't. * Anything else (nginx/FPM, IIS, unknown) is reported as not detected; * the user can still wire ngx_brotli manually and the UI surfaces a * snippet, mirroring how GZIP behaves on nginx. * * Result is filterable so a host with a known-good but undetectable * setup (e.g. nginx + ngx_brotli) can force-enable. */ public static function brotli_available(): bool { $available = false; if ( self::LITESPEED === self::type() ) { $available = true; } elseif ( function_exists( 'apache_get_modules' ) && in_array( 'mod_brotli', apache_get_modules(), true ) ) { $available = true; } elseif ( function_exists( 'brotli_compress' ) ) { $available = true; } /** * Filter detected Brotli availability. * * @param bool $available Whether Brotli serving was detected. */ return (bool) apply_filters( 'xspeed_brotli_available', $available ); } /** * Is WordPress running inside a container (Docker / Podman / k8s)? * * Three signals checked in cheapness order, OR'd together: * 1. /.dockerenv exists — Docker's traditional marker; rare absence. * 2. /proc/self/mountinfo references /var/lib/docker/overlay2 or * containerd/podman storage drivers — works under cgroup v2. * 3. /proc/1/cgroup names docker / kubepods / containerd / podman / lxc * — the cgroup v1 signal, still present on older Docker installs. * * Any single positive returns true. On non-Linux hosts (Windows / * macOS / WSL host process), all three quietly return false and we * fall back to "not containerized." */ public static function is_containerized(): bool { // 1. Docker marker file — cheap to stat, almost always present. if ( file_exists( '/.dockerenv' ) ) { return true; } // 2. mountinfo overlay2 / containerd footprint — works under cgroup v2. // Gate on is_readable() first: on non-Linux hosts (macOS/Windows) or // hosts that hide /proc (open_basedir, hardened Apache), the file is // absent and reading it would emit a warning. Query Monitor surfaces // even @-suppressed warnings, so guard rather than silence. (FBS-83114) if ( is_readable( '/proc/self/mountinfo' ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- /proc/self/mountinfo is a virtual file; WP_Filesystem doesn't model /proc. $mounts = file_get_contents( '/proc/self/mountinfo' ); if ( is_string( $mounts ) && '' !== $mounts && preg_match( '#(docker/overlay2|/var/lib/containerd|/var/lib/podman)#i', $mounts ) ) { return true; } } // 3. cgroup v1 fallback. if ( is_readable( '/proc/1/cgroup' ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- See above. $cgroup = file_get_contents( '/proc/1/cgroup' ); if ( is_string( $cgroup ) && '' !== $cgroup && preg_match( '#(docker|kubepods|containerd|podman|lxc)#i', $cgroup ) ) { return true; } } return false; } /** * Is WordPress likely behind a reverse proxy (host nginx → container * php-fpm, host nginx → docker nginx, etc.)? Detection is a heuristic * built from the headers WordPress hands to PHP: when a proxy forwards * the request it almost always sets X-Forwarded-* or X-Real-IP. * * False positives (CDN-only forwarding without a local reverse proxy) * are acceptable — the caller uses this signal only to soften messaging * that would otherwise mislead container-host customers. False negatives * (proxy that strips headers) just mean we keep showing the snippet * paste UX, which is the safe default. */ public static function is_behind_proxy(): bool { $proxy_headers = array( 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED_HOST', 'HTTP_X_FORWARDED_PROTO', 'HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_SERVER' ); foreach ( $proxy_headers as $h ) { if ( ! empty( $_SERVER[ $h ] ) ) { return true; } } return false; } /** * High-level topology classifier driving the rewrite-alert UX. * * Decides WHERE the user's nginx snippet needs to be installed * (or whether automatic install via .htaccess covers it). The * dashboard banner uses the return value to render the right * "paste this here" message — there is no topology that can't * benefit from xSpeed's static-rewrite; the question is only * which nginx is in the cache file's filesystem. * * Returns one of: * 'htaccess' — Apache / LiteSpeed; .htaccess block is * installed automatically, no user action. * 'nginx-host' — self-managed nginx on the host (no * container in the request path). User * pastes the snippet into their vhost * (typically /etc/nginx/sites-enabled/). * 'nginx-container' — nginx running inside the same container * as PHP. User pastes the snippet into the * container's nginx config (typically * docker/nginx.conf in the site's * docker-compose dir). Host nginx (if any) * is a reverse-proxy that just forwards * bytes — snippet does NOT go there. * 'unknown' — IIS or undetected; treat as manual. */ public static function rewrite_topology(): string { $type = self::type(); if ( self::APACHE === $type || self::LITESPEED === $type ) { return 'htaccess'; } if ( self::NGINX === $type ) { return self::is_containerized() ? 'nginx-container' : 'nginx-host'; } return 'unknown'; } private static function server_signature() { return isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : ''; } /** * Detect active caching plugins that would conflict with xSpeed. Returns * a list of human-readable labels for any conflicting plugin currently * active; empty array means the field is clear. Used by the onboarding * wizard's Step 1 health check and (Phase 2.1) the main dashboard's * Health card. * * The detection key is the plugin's main file path relative to the * plugins directory — the same value WordPress uses internally in * `active_plugins`. Folder-only checks (`is_plugin_active('foo/')`) * would false-positive on disabled plugins still on disk. */ public static function conflicts() { if ( ! function_exists( 'is_plugin_active' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $known = array( 'wp-rocket/wp-rocket.php' => 'WP Rocket', 'w3-total-cache/w3-total-cache.php' => 'W3 Total Cache', 'wp-super-cache/wp-cache.php' => 'WP Super Cache', 'wp-fastest-cache/wpFastestCache.php' => 'WP Fastest Cache', 'litespeed-cache/litespeed-cache.php' => 'LiteSpeed Cache', 'cache-enabler/cache-enabler.php' => 'Cache Enabler', 'comet-cache/comet-cache.php' => 'Comet Cache', 'hummingbird-performance/wp-hummingbird.php' => 'Hummingbird', 'sg-cachepress/sg-cachepress.php' => 'SG Optimizer', 'breeze/breeze.php' => 'Breeze', 'autoptimize/autoptimize.php' => 'Autoptimize', 'flying-press/flying-press.php' => 'FlyingPress', 'nitropack/main.php' => 'NitroPack', ); $active = array(); foreach ( $known as $file => $label ) { if ( is_plugin_active( $file ) ) { $active[] = $label; } } return $active; } }