# xspeed/1.0.7/includes/class-server.php

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

- Page: https://pluginprobe.com/plugins/xspeed/1.0.7/code/includes/class-server.php
- Raw: https://pluginprobe.com/plugins/xspeed/1.0.7/raw/includes/class-server.php
- Modified: 2026-07-05T11:41:46+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.7/code/includes/class-server.php#L10-L20`.

```php
<?php
/**
 * Server / SAPI detection.
 *
 * Used by Gzip and the UI to decide which optimizations are server-applied
 * (Apache / LiteSpeed via .htaccess) vs. require manual config (nginx).
 *
 * @package XSpeed
 */

namespace XSpeed;

defined( 'ABSPATH' ) || exit;

class Server {

	const APACHE    = 'apache';
	const LITESPEED = 'litespeed';
	const NGINX     = 'nginx';
	const IIS       = 'iis';
	const UNKNOWN   = 'unknown';

	const OPT_CACHED_TYPE = 'xspeed_server_type';

	public static function type() {
		$detected = self::detect();
		if ( self::UNKNOWN !== $detected ) {
			// Persist whenever we have a real answer so future CLI /
			// cron / REST calls (where SERVER_SOFTWARE may be empty)
			// inherit it. Non-autoloaded — only read when needed.
			$cached = get_option( self::OPT_CACHED_TYPE, null );
			if ( $cached !== $detected ) {
				update_option( self::OPT_CACHED_TYPE, $detected, false );
			}
			return $detected;
		}

		// No definitive signal this request (typically WP-CLI, where
		// SERVER_SOFTWARE is empty). Read whatever was cached the last
		// time we ran from a real HTTP request.
		$cached = get_option( self::OPT_CACHED_TYPE, null );
		if ( is_string( $cached ) && '' !== $cached ) {
			return $cached;
		}

		return self::UNKNOWN;
	}

	/**
	 * Live detection — never reads the cache. Used by type() and by
	 * any caller that explicitly wants the current-request answer
	 * (e.g. diagnostic UI showing "detected this request").
	 *
	 * We DO NOT fall back to "if .htaccess exists assume Apache" here:
	 * Cache::install_rewrite() writes .htaccess itself, so on nginx
	 * hosts the file appears after first cache toggle and a presence
	 * check then flips us to APACHE forever. Cached HTTP detection
	 * is the cleaner backstop.
	 */
	public static function detect(): string {
		global $is_apache, $is_nginx, $is_IIS, $is_iis7;

		$signature = self::server_signature();

		if ( false !== stripos( $signature, 'litespeed' ) ) {
			return self::LITESPEED;
		}
		// apache_get_modules() exists only with mod_php (not FPM), so
		// gate it behind SERVER_SOFTWARE first. Otherwise an
		// "apache_get_modules exists" check would false-positive on a
		// few PHP-builtin-server / mod_php-on-localhost dev edge cases.
		if ( false !== stripos( $signature, 'apache' ) || ! empty( $is_apache ) ) {
			return self::APACHE;
		}
		if ( false !== stripos( $signature, 'nginx' ) || ! empty( $is_nginx ) ) {
			return self::NGINX;
		}
		if ( false !== stripos( $signature, 'microsoft-iis' ) || ! empty( $is_IIS ) || ! empty( $is_iis7 ) ) {
			return self::IIS;
		}
		return self::UNKNOWN;
	}

	/**
	 * Whether the server respects .htaccess / web.config-style file-based config.
	 */
	public static function supports_htaccess() {
		$t = self::type();
		return self::APACHE === $t || self::LITESPEED === $t;
	}

	/**
	 * Best-effort path to the web server's access log, used to count
	 * static-rewrite HITs that bypass PHP on Apache/LiteSpeed (those
	 * requests are served straight from disk and never reach our
	 * Hit_Counter inline — see Hit_Counter::collect_server_log_hits()).
	 *
	 * Resolution order:
	 *   1. The `XSPEED_ACCESS_LOG` constant, if defined (explicit override
	 *      for hosts where the log lives somewhere non-standard).
	 *   2. The `xspeed_access_log_path` filter (programmatic override).
	 *   3. Auto-detection: a short list of the standard Apache/LiteSpeed
	 *      access-log locations, returning the first that exists AND is
	 *      readable by the PHP user.
	 *
	 * Returns '' when nothing readable is found — a very common case on
	 * managed/cPanel hosts where the PHP user can't read the server log.
	 * Callers MUST treat '' as "can't count static hits here" and fall
	 * back gracefully (the drop-in path still counts its own HITs).
	 *
	 * @return string Absolute path, or '' if none is readable.
	 */
	public static function access_log_path(): string {
		if ( defined( 'XSPEED_ACCESS_LOG' ) && is_string( XSPEED_ACCESS_LOG ) && '' !== XSPEED_ACCESS_LOG ) {
			$override = XSPEED_ACCESS_LOG;
			return is_readable( $override ) ? $override : '';
		}

		/**
		 * Filter: xspeed_access_log_path
		 *
		 * Override the auto-detected access-log path. Return '' to disable
		 * server-log hit counting entirely.
		 *
		 * @param string|null $path Null = use auto-detection below.
		 */
		$filtered = apply_filters( 'xspeed_access_log_path', null );
		if ( is_string( $filtered ) ) {
			return ( '' !== $filtered && is_readable( $filtered ) ) ? $filtered : '';
		}

		// Auto-detect: the standard Apache + OpenLiteSpeed/LiteSpeed
		// Enterprise access-log locations. First readable, NON-EMPTY file
		// wins — an empty global access.log (common on LiteSpeed, which
		// logs per-vhost instead) must not shadow the real per-vhost log we
		// discover below.
		$candidates = array(
			'/var/log/apache2/access.log',              // Debian/Ubuntu Apache
			'/var/log/httpd/access_log',                // RHEL/CentOS Apache
			'/var/log/apache2/other_vhosts_access.log', // Debian multi-vhost
			'/usr/local/lsws/logs/access.log',          // OpenLiteSpeed global
			'/var/log/lshttpd/access.log',              // LiteSpeed Enterprise
		);
		foreach ( $candidates as $path ) {
			if ( is_readable( $path ) && (int) @filesize( $path ) > 0 ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- racey stat, treated as "skip".
				return $path;
			}
		}

		// LiteSpeed (and some Apache vhost setups) write a per-vhost
		// `<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/<site>).
	 *   '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;
	}
}

```
