PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.1.7
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.1.7
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-conflict-registry.php

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

366 lines 12.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Conflict_Registry — the strategy matrix for coexisting (or refusing to
4 * coexist) with other caching / optimization plugins.
5 *
6 * Source of truth: IMPLEMENTATION.md §6.2. Each entry says:
7 * - which plugin we're conflicting with (its main file path),
8 * - which "kind" of plugin it is (page-cache / minify / lazy-load / etc.),
9 * - per-feature strategy (refuse | warn | allow),
10 * - a human-readable reason rendered in UI + REST errors.
11 *
12 * The matrix is queried by:
13 * - Onboarding Step 1 (health rows).
14 * - Dashboard health card (Phase 2.1).
15 * - REST gates (refuse → 409, warn → response includes warnings[]).
16 * - CLI `wp xspeed conflicts`.
17 * - Cache toggle handler (refuses to enable when another page cache active).
18 *
19 * Detection results are cached for the request lifetime. The cache is
20 * invalidated on `activated_plugin` / `deactivated_plugin`.
21 *
22 * Modules can declare additional conflicts via Module::conflicts(); those
23 * are merged into the static matrix at runtime so Pro modules can extend
24 * the registry without modifying Free code.
25 *
26 * @package XSpeed
27 */
28
29 namespace XSpeed;
30
31 defined( 'ABSPATH' ) || exit;
32
33 final class Conflict_Registry {
34
35 public const STRATEGY_REFUSE = 'refuse';
36 public const STRATEGY_WARN = 'warn';
37 public const STRATEGY_ALLOW = 'allow';
38
39 /**
40 * @var array|null Memoized detection result for this request.
41 */
42 private static $cache = null;
43
44 /**
45 * Static strategy matrix. Keys are plugin file paths as they appear in
46 * the `active_plugins` option. Values declare:
47 * - label : human-readable plugin name
48 * - kind : page-cache | minify | lazy-load | image-opt | cdn | preload | mixed
49 * - strategy : map of feature_key → strategy (refuse | warn | allow)
50 *
51 * Feature keys are namespaced `<module-slug>.<sub-feature>` or just
52 * `<module-slug>` for whole-module conflicts. Modules can extend this
53 * matrix via their conflicts() method.
54 */
55 private static function matrix(): array {
56 return array(
57 'wp-rocket/wp-rocket.php' => array(
58 'label' => 'WP Rocket',
59 'kind' => 'mixed',
60 'strategy' => array(
61 'cache.page' => self::STRATEGY_REFUSE,
62 'minify.html' => self::STRATEGY_REFUSE,
63 'minify.css' => self::STRATEGY_REFUSE,
64 'minify.js' => self::STRATEGY_REFUSE,
65 'preload.crawler' => self::STRATEGY_REFUSE,
66 'cdn.rewrite' => self::STRATEGY_REFUSE,
67 'images.lazyload' => self::STRATEGY_REFUSE,
68 ),
69 ),
70 'litespeed-cache/litespeed-cache.php' => array(
71 'label' => 'LiteSpeed Cache',
72 'kind' => 'mixed',
73 'strategy' => array(
74 'cache.page' => self::STRATEGY_REFUSE,
75 'minify.html' => self::STRATEGY_REFUSE,
76 'minify.css' => self::STRATEGY_REFUSE,
77 'minify.js' => self::STRATEGY_REFUSE,
78 'preload.crawler' => self::STRATEGY_REFUSE,
79 'cdn.rewrite' => self::STRATEGY_WARN,
80 'images.lazyload' => self::STRATEGY_REFUSE,
81 ),
82 ),
83 'w3-total-cache/w3-total-cache.php' => array(
84 'label' => 'W3 Total Cache',
85 'kind' => 'mixed',
86 'strategy' => array(
87 'cache.page' => self::STRATEGY_REFUSE,
88 'minify.html' => self::STRATEGY_REFUSE,
89 'minify.css' => self::STRATEGY_REFUSE,
90 'minify.js' => self::STRATEGY_REFUSE,
91 'preload.crawler' => self::STRATEGY_REFUSE,
92 'cdn.rewrite' => self::STRATEGY_WARN,
93 ),
94 ),
95 'wp-super-cache/wp-cache.php' => array(
96 'label' => 'WP Super Cache',
97 'kind' => 'page-cache',
98 'strategy' => array(
99 'cache.page' => self::STRATEGY_REFUSE,
100 'preload.crawler' => self::STRATEGY_REFUSE,
101 ),
102 ),
103 'wp-fastest-cache/wpFastestCache.php' => array(
104 'label' => 'WP Fastest Cache',
105 'kind' => 'mixed',
106 'strategy' => array(
107 'cache.page' => self::STRATEGY_REFUSE,
108 'minify.html' => self::STRATEGY_REFUSE,
109 'minify.css' => self::STRATEGY_REFUSE,
110 'minify.js' => self::STRATEGY_REFUSE,
111 'preload.crawler' => self::STRATEGY_REFUSE,
112 'images.lazyload' => self::STRATEGY_REFUSE,
113 ),
114 ),
115 'cache-enabler/cache-enabler.php' => array(
116 'label' => 'Cache Enabler',
117 'kind' => 'page-cache',
118 'strategy' => array(
119 'cache.page' => self::STRATEGY_REFUSE,
120 'preload.crawler' => self::STRATEGY_REFUSE,
121 ),
122 ),
123 'comet-cache/comet-cache.php' => array(
124 'label' => 'Comet Cache',
125 'kind' => 'page-cache',
126 'strategy' => array(
127 'cache.page' => self::STRATEGY_REFUSE,
128 ),
129 ),
130 'hummingbird-performance/wp-hummingbird.php' => array(
131 'label' => 'Hummingbird',
132 'kind' => 'mixed',
133 'strategy' => array(
134 'cache.page' => self::STRATEGY_REFUSE,
135 'minify.html' => self::STRATEGY_REFUSE,
136 'minify.css' => self::STRATEGY_REFUSE,
137 'minify.js' => self::STRATEGY_REFUSE,
138 'images.lazyload' => self::STRATEGY_REFUSE,
139 'preload.crawler' => self::STRATEGY_WARN,
140 'cdn.rewrite' => self::STRATEGY_WARN,
141 ),
142 ),
143 'sg-cachepress/sg-cachepress.php' => array(
144 'label' => 'SG Optimizer',
145 'kind' => 'mixed',
146 'strategy' => array(
147 'cache.page' => self::STRATEGY_REFUSE,
148 'minify.html' => self::STRATEGY_REFUSE,
149 'minify.css' => self::STRATEGY_REFUSE,
150 'minify.js' => self::STRATEGY_REFUSE,
151 'preload.crawler' => self::STRATEGY_REFUSE,
152 'images.lazyload' => self::STRATEGY_WARN,
153 ),
154 ),
155 'breeze/breeze.php' => array(
156 'label' => 'Breeze',
157 'kind' => 'mixed',
158 'strategy' => array(
159 'cache.page' => self::STRATEGY_REFUSE,
160 'minify.html' => self::STRATEGY_REFUSE,
161 'minify.css' => self::STRATEGY_REFUSE,
162 'minify.js' => self::STRATEGY_REFUSE,
163 'preload.crawler' => self::STRATEGY_REFUSE,
164 'cdn.rewrite' => self::STRATEGY_WARN,
165 ),
166 ),
167 'autoptimize/autoptimize.php' => array(
168 'label' => 'Autoptimize',
169 'kind' => 'minify',
170 'strategy' => array(
171 'minify.html' => self::STRATEGY_REFUSE,
172 'minify.css' => self::STRATEGY_REFUSE,
173 'minify.js' => self::STRATEGY_REFUSE,
174 ),
175 ),
176 'flying-press/flying-press.php' => array(
177 'label' => 'FlyingPress',
178 'kind' => 'mixed',
179 'strategy' => array(
180 'cache.page' => self::STRATEGY_REFUSE,
181 'minify.html' => self::STRATEGY_REFUSE,
182 'minify.css' => self::STRATEGY_REFUSE,
183 'minify.js' => self::STRATEGY_REFUSE,
184 'preload.crawler' => self::STRATEGY_REFUSE,
185 'cdn.rewrite' => self::STRATEGY_REFUSE,
186 'images.lazyload' => self::STRATEGY_REFUSE,
187 ),
188 ),
189 'nitropack/main.php' => array(
190 'label' => 'NitroPack',
191 'kind' => 'mixed',
192 'strategy' => array(
193 'cache.page' => self::STRATEGY_REFUSE,
194 'minify.html' => self::STRATEGY_REFUSE,
195 'minify.css' => self::STRATEGY_REFUSE,
196 'minify.js' => self::STRATEGY_REFUSE,
197 'preload.crawler' => self::STRATEGY_REFUSE,
198 'cdn.rewrite' => self::STRATEGY_REFUSE,
199 'images.lazyload' => self::STRATEGY_REFUSE,
200 ),
201 ),
202 'wp-optimize/wp-optimize.php' => array(
203 'label' => 'WP-Optimize',
204 'kind' => 'mixed',
205 'strategy' => array(
206 'cache.page' => self::STRATEGY_WARN,
207 'minify.html' => self::STRATEGY_WARN,
208 'minify.css' => self::STRATEGY_WARN,
209 'minify.js' => self::STRATEGY_WARN,
210 ),
211 ),
212 'jetpack-boost/jetpack-boost.php' => array(
213 'label' => 'Jetpack Boost',
214 'kind' => 'mixed',
215 'strategy' => array(
216 'minify.css' => self::STRATEGY_WARN,
217 'minify.js' => self::STRATEGY_WARN,
218 'images.lazyload' => self::STRATEGY_WARN,
219 ),
220 ),
221 'wp-asset-clean-up/wpacu.php' => array(
222 'label' => 'Asset CleanUp',
223 'kind' => 'minify',
224 'strategy' => array(
225 'minify.css' => self::STRATEGY_WARN,
226 'minify.js' => self::STRATEGY_WARN,
227 ),
228 ),
229 'ewww-image-optimizer/ewww-image-optimizer.php' => array(
230 'label' => 'EWWW Image Optimizer',
231 'kind' => 'image-opt',
232 'strategy' => array(
233 'images.optimize' => self::STRATEGY_REFUSE,
234 ),
235 ),
236 'shortpixel-image-optimiser/wp-shortpixel.php' => array(
237 'label' => 'ShortPixel',
238 'kind' => 'image-opt',
239 'strategy' => array(
240 'images.optimize' => self::STRATEGY_REFUSE,
241 ),
242 ),
243 'wp-smushit/wp-smush.php' => array(
244 'label' => 'Smush',
245 'kind' => 'image-opt',
246 'strategy' => array(
247 'images.optimize' => self::STRATEGY_REFUSE,
248 'images.lazyload' => self::STRATEGY_WARN,
249 ),
250 ),
251 );
252 }
253
254 /**
255 * Active conflicts on this site. Returns one entry per active
256 * conflicting plugin: [
257 * 'plugin' => 'wp-rocket/wp-rocket.php',
258 * 'label' => 'WP Rocket',
259 * 'kind' => 'mixed',
260 * 'strategy' => [ feature => strategy ],
261 * ]
262 *
263 * @return array[]
264 */
265 public static function detected(): array {
266 if ( null !== self::$cache ) {
267 return self::$cache;
268 }
269 if ( ! function_exists( 'is_plugin_active' ) ) {
270 require_once ABSPATH . 'wp-admin/includes/plugin.php';
271 }
272
273 $matrix = apply_filters( 'xspeed_conflict_matrix', self::matrix() );
274 $out = array();
275 foreach ( $matrix as $plugin => $spec ) {
276 if ( is_plugin_active( $plugin ) ) {
277 $out[] = array(
278 'plugin' => $plugin,
279 'label' => $spec['label'],
280 'kind' => $spec['kind'],
281 'strategy' => $spec['strategy'],
282 );
283 }
284 }
285 self::$cache = $out;
286 return $out;
287 }
288
289 /**
290 * Strongest strategy across all detected conflicts for a given feature
291 * key. Returns 'refuse' > 'warn' > 'allow' (refuse wins if any
292 * conflict refuses; warn wins if any warns but none refuse).
293 */
294 public static function strategy_for( string $feature_key ): string {
295 $strongest = self::STRATEGY_ALLOW;
296 foreach ( self::detected() as $conflict ) {
297 $s = $conflict['strategy'][ $feature_key ] ?? self::STRATEGY_ALLOW;
298 if ( self::STRATEGY_REFUSE === $s ) {
299 return self::STRATEGY_REFUSE;
300 }
301 if ( self::STRATEGY_WARN === $s && self::STRATEGY_ALLOW === $strongest ) {
302 $strongest = self::STRATEGY_WARN;
303 }
304 }
305 return $strongest;
306 }
307
308 /**
309 * Human-readable reason a feature is blocked by a refuse-strategy
310 * conflict, or null if not blocked. Used by Rest_Manager (409) and UI
311 * tooltips.
312 *
313 * The optional $module_slug is currently informational only — the
314 * strategy lookup uses $feature_key alone. Reserved for future
315 * module-specific overrides.
316 */
317 public static function why_blocked( string $module_slug, string $feature_key ): ?string {
318 foreach ( self::detected() as $conflict ) {
319 if ( ( $conflict['strategy'][ $feature_key ] ?? null ) === self::STRATEGY_REFUSE ) {
320 return sprintf(
321 /* translators: %s: conflicting plugin name */
322 __( '%s is active and handles the same feature. Deactivate it before enabling this in xSpeed.', 'xspeed' ),
323 $conflict['label']
324 );
325 }
326 }
327 return null;
328 }
329
330 /**
331 * All warnings for a feature (one string per warning-strategy conflict).
332 * Returned in the response body of warn-gated REST endpoints.
333 *
334 * @return string[]
335 */
336 public static function warnings_for( string $feature_key ): array {
337 $out = array();
338 foreach ( self::detected() as $conflict ) {
339 if ( ( $conflict['strategy'][ $feature_key ] ?? null ) === self::STRATEGY_WARN ) {
340 $out[] = sprintf(
341 /* translators: %s: conflicting plugin name */
342 __( '%s is active and may interfere. Test carefully.', 'xspeed' ),
343 $conflict['label']
344 );
345 }
346 }
347 return $out;
348 }
349
350 /**
351 * Invalidate the per-request cache. Called on activated_plugin /
352 * deactivated_plugin so the next read reflects the new state.
353 */
354 public static function invalidate(): void {
355 self::$cache = null;
356 }
357
358 /**
359 * Bootstrap hooks. Call once from Plugin::init().
360 */
361 public static function boot(): void {
362 add_action( 'activated_plugin', array( __CLASS__, 'invalidate' ) );
363 add_action( 'deactivated_plugin', array( __CLASS__, 'invalidate' ) );
364 }
365 }
366