PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.0
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.0
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 1.2.0 1.2.1 All 27 releases
xspeed / includes / class-host.php

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

227 lines 8.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Host — everything another WPDeveloper plugin needs from xSpeed.
4 *
5 * EmbedPress, Templately and Essential Addons can install and activate xSpeed
6 * for a user. They used to do it by copy-vendoring a detector and a settings
7 * writer out of this repo, which meant every one of them carried a second
8 * implementation of rules that live here, and a fix had to be re-copied into
9 * each before it reached anybody.
10 *
11 * They do not need any of that now: xSpeed always installs, and works out for
12 * itself how to come up (Settings::set_defaults()). What is left is this
13 * class — one place a host may call into, so nothing has to reach into Cache
14 * or Page_Cache_Detector and nothing has to be duplicated.
15 *
16 * The one thing NOT here is provenance, because a host records it before any
17 * of this code exists:
18 *
19 * update_option( 'xspeed_installed_by', 'essential-addons' );
20 * // then install + activate as normal
21 *
22 * See IMPLEMENTATION.md §6.2d.
23 *
24 * @package XSpeed
25 */
26
27 namespace XSpeed;
28
29 defined( 'ABSPATH' ) || exit;
30
31 final class Host {
32
33 /**
34 * Is xSpeed installed on this site at all?
35 *
36 * A host asks before offering to install it. Reads the plugin list rather
37 * than a constant, because the question is asked from a request where
38 * xSpeed may not be loaded.
39 */
40 public static function is_installed(): bool {
41 self::load_plugin_functions();
42 if ( ! function_exists( 'get_plugins' ) ) {
43 return false;
44 }
45 return array_key_exists( self::PLUGIN_FILE, (array) get_plugins() );
46 }
47
48 /** Is it active — on this site, or network-wide? */
49 public static function is_active(): bool {
50 self::load_plugin_functions();
51 return function_exists( 'is_plugin_active' ) && (bool) is_plugin_active( self::PLUGIN_FILE );
52 }
53
54 /**
55 * Does anything already own the page cache?
56 *
57 * A host does NOT have to consult this before installing — xSpeed installs
58 * beside anything and comes up with everything switched off. It is here
59 * for a host that wants to say what it found, or to decide whether to
60 * bother asking the user about page caching at all.
61 */
62 public static function page_cache_is_free(): bool {
63 return Page_Cache_Detector::is_field_clear();
64 }
65
66 /**
67 * Who owns the page cache, as a name to put in a sentence, or null.
68 *
69 * Prefers whoever owns the drop-in, since that is proof; falls back to the
70 * first active plugin that could be caching, which is the honest answer
71 * for something like LiteSpeed that caches at server level.
72 */
73 public static function page_cache_owner(): ?string {
74 $owner = Page_Cache_Detector::dropin_owner_label();
75 if ( is_string( $owner ) && '' !== $owner ) {
76 return $owner;
77 }
78 /*
79 * The catalog counts xSpeed among the page caches, correctly, so it
80 * turns up here on any site running us. It is not the answer a host
81 * is after: if we owned the drop-in the branch above would already
82 * have said so, and without it we are one more plugin that COULD be
83 * caching. Name a competitor first and fall back to ourselves.
84 */
85 $active = Page_Cache_Detector::active_page_caches();
86 $others = array_values( array_filter( $active, static fn ( $label ) => 'xSpeed Cache' !== $label ) );
87 if ( array() !== $others ) {
88 return (string) $others[0];
89 }
90 return empty( $active ) ? null : (string) reset( $active );
91 }
92
93 /** Is xSpeed's page cache actually serving right now? */
94 public static function page_cache_is_live(): bool {
95 return Cache::page_cache_operational();
96 }
97
98 /**
99 * Turn xSpeed's page cache on, for a host acting on a user's request.
100 *
101 * Null means the cache is SERVING. A string is the reason it is not,
102 * already worded and translated — render it, do not re-interpret it. The
103 * refusal is not an error: installing beside another cache plugin and being
104 * told so is the designed outcome, and the site is fine.
105 *
106 * Two ways to not be serving, and only one of them is a refusal. On a host
107 * that ships wp-config.php read-only the drop-in installs and WP_CACHE
108 * cannot be written, so toggle() reports success with a line for the user
109 * to paste — and a caller reading only `blocked` announced a working cache
110 * that answers every request uncached. status()['page_cache_manual_snippet']
111 * carries that line.
112 *
113 * Only ever call this because a user asked. Activation deliberately leaves
114 * page caching off, and a host that turns it on unprompted is making a
115 * decision about shared WordPress state on the user's behalf.
116 */
117 public static function enable_page_cache(): ?string {
118 /*
119 * Not consented, deliberately. A host plugin calling this is acting
120 * on its OWN user's click in its own onboarding — nobody has been
121 * shown whose advanced-cache.php is about to be replaced, which is
122 * the disclosure that makes a takeover legitimate in the dashboard.
123 *
124 * So a competitor's drop-in comes back as a reason the host can
125 * render, exactly as it did before, and the takeover stays something
126 * the site owner does knowingly in xSpeed's own UI.
127 */
128 $state = Cache::toggle( true, false );
129
130 if ( ! empty( $state['blocked'] ) ) {
131 return is_string( $state['blocked_reason'] ) && '' !== $state['blocked_reason']
132 ? $state['blocked_reason']
133 : __( 'xSpeed could not enable the page cache on this site.', 'xspeed' );
134 }
135
136 if ( ! empty( $state['manual_snippet'] ) ) {
137 return sprintf(
138 /* translators: %s: the PHP line to add to wp-config.php. */
139 __( 'xSpeed installed its page cache, but wp-config.php is not writable. Add this line to it to start serving: %s', 'xspeed' ),
140 (string) $state['manual_snippet']
141 );
142 }
143
144 return null;
145 }
146
147 /** The slug of whoever installed xSpeed, or '' when the user did. */
148 public static function installed_by(): string {
149 return Settings::installed_by();
150 }
151
152 /**
153 * Everything a host needs to say whether the install came up right.
154 *
155 * One call, because the alternative is every consumer assembling the same
156 * four reads and drawing its own conclusion from them — which is how three
157 * plugins ended up with three different sentences for one situation.
158 *
159 * `profile` is how a FRESH install came up and is written once, at
160 * activation: `recommended` on a clear site, `conflict-safe` when
161 * something else owned the page cache, `''` when xSpeed was already
162 * installed and nothing was decided. It does not change afterwards — it
163 * records a decision, not the current settings.
164 *
165 * `page_cache_blocked_reason` is non-null only when the cache is NOT live
166 * and there is a nameable reason. A live cache and a clear field both give
167 * null, so it is safe to render whenever it is there.
168 *
169 * `page_cache_manual_snippet` is the wp-config.php line to paste, non-null
170 * only on a host that ships that file read-only. A cache that is not live
171 * and has no blocked reason is almost always this.
172 *
173 * @return array{
174 * installed:bool,
175 * active:bool,
176 * installed_by:string,
177 * profile:string,
178 * page_cache_live:bool,
179 * page_cache_owner:?string,
180 * page_cache_blocked_reason:?string,
181 * page_cache_manual_snippet:?string
182 * }
183 */
184 public static function status(): array {
185 $live = self::page_cache_is_live();
186
187 return array(
188 'installed' => self::is_installed(),
189 'active' => self::is_active(),
190 'installed_by' => self::installed_by(),
191 'profile' => Settings::install_profile(),
192 'page_cache_live' => $live,
193 'page_cache_owner' => self::page_cache_owner(),
194 'page_cache_blocked_reason' => $live ? null : Cache::acquisition_blocker(),
195 /*
196 * Not gated on `$live`. Serving and "there is a line to paste"
197 * are independent: on a managed host with an unwritable
198 * wp-config.php the cache serves every hit from
199 * template_redirect while WP_CACHE never landed, so the pre-boot
200 * path is still missing and the user can still act on it. The
201 * helper returns null whenever nothing is needed.
202 */
203 'page_cache_manual_snippet' => Cache::manual_wp_cache_snippet(),
204 );
205 }
206
207 /**
208 * Pull in get_plugins()/is_plugin_active(), which are admin-only.
209 *
210 * A host can ask these from a front-end or REST request, where the file is
211 * not loaded; and the callers above degrade rather than fatal if it is not
212 * there at all, because a missing answer must not take the site down.
213 */
214 private static function load_plugin_functions(): void {
215 if ( function_exists( 'get_plugins' ) && function_exists( 'is_plugin_active' ) ) {
216 return;
217 }
218 $file = ABSPATH . 'wp-admin/includes/plugin.php';
219 if ( file_exists( $file ) ) {
220 require_once $file;
221 }
222 }
223
224 /** How xSpeed appears in the plugin list. */
225 private const PLUGIN_FILE = 'xspeed/xspeed.php';
226 }
227