PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.0.3
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.0.3
1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 All 29 releases
xspeed / includes / class-server.php

class-server.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.0.3, at includes/class-server.php

242 lines 9.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Server / SAPI detection.
4 *
5 * Used by Gzip and the UI to decide which optimizations are server-applied
6 * (Apache / LiteSpeed via .htaccess) vs. require manual config (nginx).
7 *
8 * @package XSpeed
9 */
10
11 namespace XSpeed;
12
13 defined( 'ABSPATH' ) || exit;
14
15 class Server {
16
17 const APACHE = 'apache';
18 const LITESPEED = 'litespeed';
19 const NGINX = 'nginx';
20 const IIS = 'iis';
21 const UNKNOWN = 'unknown';
22
23 const OPT_CACHED_TYPE = 'xspeed_server_type';
24
25 public static function type() {
26 $detected = self::detect();
27 if ( self::UNKNOWN !== $detected ) {
28 // Persist whenever we have a real answer so future CLI /
29 // cron / REST calls (where SERVER_SOFTWARE may be empty)
30 // inherit it. Non-autoloaded — only read when needed.
31 $cached = get_option( self::OPT_CACHED_TYPE, null );
32 if ( $cached !== $detected ) {
33 update_option( self::OPT_CACHED_TYPE, $detected, false );
34 }
35 return $detected;
36 }
37
38 // No definitive signal this request (typically WP-CLI, where
39 // SERVER_SOFTWARE is empty). Read whatever was cached the last
40 // time we ran from a real HTTP request.
41 $cached = get_option( self::OPT_CACHED_TYPE, null );
42 if ( is_string( $cached ) && '' !== $cached ) {
43 return $cached;
44 }
45
46 return self::UNKNOWN;
47 }
48
49 /**
50 * Live detection — never reads the cache. Used by type() and by
51 * any caller that explicitly wants the current-request answer
52 * (e.g. diagnostic UI showing "detected this request").
53 *
54 * We DO NOT fall back to "if .htaccess exists assume Apache" here:
55 * Cache::install_rewrite() writes .htaccess itself, so on nginx
56 * hosts the file appears after first cache toggle and a presence
57 * check then flips us to APACHE forever. Cached HTTP detection
58 * is the cleaner backstop.
59 */
60 public static function detect(): string {
61 global $is_apache, $is_nginx, $is_IIS, $is_iis7;
62
63 $signature = self::server_signature();
64
65 if ( false !== stripos( $signature, 'litespeed' ) ) {
66 return self::LITESPEED;
67 }
68 // apache_get_modules() exists only with mod_php (not FPM), so
69 // gate it behind SERVER_SOFTWARE first. Otherwise an
70 // "apache_get_modules exists" check would false-positive on a
71 // few PHP-builtin-server / mod_php-on-localhost dev edge cases.
72 if ( false !== stripos( $signature, 'apache' ) || ! empty( $is_apache ) ) {
73 return self::APACHE;
74 }
75 if ( false !== stripos( $signature, 'nginx' ) || ! empty( $is_nginx ) ) {
76 return self::NGINX;
77 }
78 if ( false !== stripos( $signature, 'microsoft-iis' ) || ! empty( $is_IIS ) || ! empty( $is_iis7 ) ) {
79 return self::IIS;
80 }
81 return self::UNKNOWN;
82 }
83
84 /**
85 * Whether the server respects .htaccess / web.config-style file-based config.
86 */
87 public static function supports_htaccess() {
88 $t = self::type();
89 return self::APACHE === $t || self::LITESPEED === $t;
90 }
91
92 /**
93 * GZIP support category for the UI:
94 * 'auto' — toggling writes server config (Apache / LiteSpeed)
95 * 'manual' — must be configured outside the plugin (nginx, IIS, unknown)
96 */
97 public static function gzip_mode() {
98 return self::supports_htaccess() ? 'auto' : 'manual';
99 }
100
101 /**
102 * Is WordPress running inside a container (Docker / Podman / k8s)?
103 *
104 * Three signals checked in cheapness order, OR'd together:
105 * 1. /.dockerenv exists — Docker's traditional marker; rare absence.
106 * 2. /proc/self/mountinfo references /var/lib/docker/overlay2 or
107 * containerd/podman storage drivers — works under cgroup v2.
108 * 3. /proc/1/cgroup names docker / kubepods / containerd / podman / lxc
109 * — the cgroup v1 signal, still present on older Docker installs.
110 *
111 * Any single positive returns true. On non-Linux hosts (Windows /
112 * macOS / WSL host process), all three quietly return false and we
113 * fall back to "not containerized."
114 */
115 public static function is_containerized(): bool {
116 // 1. Docker marker file — cheap to stat, almost always present.
117 if ( file_exists( '/.dockerenv' ) ) {
118 return true;
119 }
120 // 2. mountinfo overlay2 / containerd footprint — works under cgroup v2.
121 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- /proc/self/mountinfo is a virtual file; WP_Filesystem doesn't model /proc.
122 $mounts = @file_get_contents( '/proc/self/mountinfo' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- missing /proc on non-Linux hosts is the negative answer.
123 if ( is_string( $mounts ) && '' !== $mounts && preg_match( '#(docker/overlay2|/var/lib/containerd|/var/lib/podman)#i', $mounts ) ) {
124 return true;
125 }
126 // 3. cgroup v1 fallback.
127 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- See above.
128 $cgroup = @file_get_contents( '/proc/1/cgroup' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- See above.
129 if ( is_string( $cgroup ) && '' !== $cgroup && preg_match( '#(docker|kubepods|containerd|podman|lxc)#i', $cgroup ) ) {
130 return true;
131 }
132 return false;
133 }
134
135 /**
136 * Is WordPress likely behind a reverse proxy (host nginx → container
137 * php-fpm, host nginx → docker nginx, etc.)? Detection is a heuristic
138 * built from the headers WordPress hands to PHP: when a proxy forwards
139 * the request it almost always sets X-Forwarded-* or X-Real-IP.
140 *
141 * False positives (CDN-only forwarding without a local reverse proxy)
142 * are acceptable — the caller uses this signal only to soften messaging
143 * that would otherwise mislead container-host customers. False negatives
144 * (proxy that strips headers) just mean we keep showing the snippet
145 * paste UX, which is the safe default.
146 */
147 public static function is_behind_proxy(): bool {
148 $proxy_headers = array( 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED_HOST', 'HTTP_X_FORWARDED_PROTO', 'HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_SERVER' );
149 foreach ( $proxy_headers as $h ) {
150 if ( ! empty( $_SERVER[ $h ] ) ) {
151 return true;
152 }
153 }
154 return false;
155 }
156
157 /**
158 * High-level topology classifier driving the rewrite-alert UX.
159 *
160 * Decides WHERE the user's nginx snippet needs to be installed
161 * (or whether automatic install via .htaccess covers it). The
162 * dashboard banner uses the return value to render the right
163 * "paste this here" message — there is no topology that can't
164 * benefit from xSpeed's static-rewrite; the question is only
165 * which nginx is in the cache file's filesystem.
166 *
167 * Returns one of:
168 * 'htaccess' — Apache / LiteSpeed; .htaccess block is
169 * installed automatically, no user action.
170 * 'nginx-host' — self-managed nginx on the host (no
171 * container in the request path). User
172 * pastes the snippet into their vhost
173 * (typically /etc/nginx/sites-enabled/<site>).
174 * 'nginx-container' — nginx running inside the same container
175 * as PHP. User pastes the snippet into the
176 * container's nginx config (typically
177 * docker/nginx.conf in the site's
178 * docker-compose dir). Host nginx (if any)
179 * is a reverse-proxy that just forwards
180 * bytes — snippet does NOT go there.
181 * 'unknown' — IIS or undetected; treat as manual.
182 */
183 public static function rewrite_topology(): string {
184 $type = self::type();
185 if ( self::APACHE === $type || self::LITESPEED === $type ) {
186 return 'htaccess';
187 }
188 if ( self::NGINX === $type ) {
189 return self::is_containerized() ? 'nginx-container' : 'nginx-host';
190 }
191 return 'unknown';
192 }
193
194 private static function server_signature() {
195 return isset( $_SERVER['SERVER_SOFTWARE'] )
196 ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) )
197 : '';
198 }
199
200 /**
201 * Detect active caching plugins that would conflict with xSpeed. Returns
202 * a list of human-readable labels for any conflicting plugin currently
203 * active; empty array means the field is clear. Used by the onboarding
204 * wizard's Step 1 health check and (Phase 2.1) the main dashboard's
205 * Health card.
206 *
207 * The detection key is the plugin's main file path relative to the
208 * plugins directory — the same value WordPress uses internally in
209 * `active_plugins`. Folder-only checks (`is_plugin_active('foo/')`)
210 * would false-positive on disabled plugins still on disk.
211 */
212 public static function conflicts() {
213 if ( ! function_exists( 'is_plugin_active' ) ) {
214 require_once ABSPATH . 'wp-admin/includes/plugin.php';
215 }
216
217 $known = array(
218 'wp-rocket/wp-rocket.php' => 'WP Rocket',
219 'w3-total-cache/w3-total-cache.php' => 'W3 Total Cache',
220 'wp-super-cache/wp-cache.php' => 'WP Super Cache',
221 'wp-fastest-cache/wpFastestCache.php' => 'WP Fastest Cache',
222 'litespeed-cache/litespeed-cache.php' => 'LiteSpeed Cache',
223 'cache-enabler/cache-enabler.php' => 'Cache Enabler',
224 'comet-cache/comet-cache.php' => 'Comet Cache',
225 'hummingbird-performance/wp-hummingbird.php' => 'Hummingbird',
226 'sg-cachepress/sg-cachepress.php' => 'SG Optimizer',
227 'breeze/breeze.php' => 'Breeze',
228 'autoptimize/autoptimize.php' => 'Autoptimize',
229 'flying-press/flying-press.php' => 'FlyingPress',
230 'nitropack/main.php' => 'NitroPack',
231 );
232
233 $active = array();
234 foreach ( $known as $file => $label ) {
235 if ( is_plugin_active( $file ) ) {
236 $active[] = $label;
237 }
238 }
239 return $active;
240 }
241 }
242