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

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

- Page: https://pluginprobe.com/plugins/xspeed/1.0.3/code/includes/class-server.php
- Raw: https://pluginprobe.com/plugins/xspeed/1.0.3/raw/includes/class-server.php
- Modified: 2026-06-09T06:20:14+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.3/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;
	}

	/**
	 * 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';
	}

	/**
	 * 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.
		// 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' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- missing /proc on non-Linux hosts is the negative answer.
		if ( is_string( $mounts ) && '' !== $mounts && preg_match( '#(docker/overlay2|/var/lib/containerd|/var/lib/podman)#i', $mounts ) ) {
			return true;
		}
		// 3. cgroup v1 fallback.
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- See above.
		$cgroup = @file_get_contents( '/proc/1/cgroup' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- See above.
		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;
	}
}

```
