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

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

205 lines 6.6 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: Cache_Plugin_Catalog (which plugins exist, what they do,
7 * how they conflict) projected through IMPLEMENTATION.md §6.2's contract.
8 * Each entry says:
9 * - which plugin we're conflicting with (its main file path),
10 * - which "kind" of plugin it is (page-cache / minify / lazy-load / etc.),
11 * - per-feature strategy (refuse | warn | allow),
12 * - a human-readable reason rendered in UI + REST errors.
13 *
14 * The matrix is queried by:
15 * - Onboarding Step 1 (health rows).
16 * - Dashboard health card (Phase 2.1).
17 * - REST gates (refuse → 409, warn → response includes warnings[]).
18 * - CLI `wp xspeed conflicts`.
19 * - Cache toggle handler (refuses to enable when another page cache active).
20 *
21 * Detection results are cached for the request lifetime. The cache is
22 * invalidated on `activated_plugin` / `deactivated_plugin`.
23 *
24 * Modules can declare additional conflicts via Module::conflicts(); those
25 * are merged into the static matrix at runtime so Pro modules can extend
26 * the registry without modifying Free code.
27 *
28 * @package XSpeed
29 */
30
31 namespace XSpeed;
32
33 defined( 'ABSPATH' ) || exit;
34
35 final class Conflict_Registry {
36
37 public const STRATEGY_REFUSE = 'refuse';
38 public const STRATEGY_WARN = 'warn';
39 public const STRATEGY_ALLOW = 'allow';
40
41 /**
42 * @var array|null Memoized detection result for this request.
43 */
44 private static $cache = null;
45
46 /**
47 * Per-feature strategy matrix, projected from Cache_Plugin_Catalog.
48 *
49 * The catalog is the single description of every plugin we know about;
50 * this projection keeps the shape older callers expect — plugin file =>
51 * label / kind / strategy. `kind` is derived from the catalog's
52 * capabilities (see kind_for()) rather than stored twice.
53 *
54 * Feature keys are namespaced `<module-slug>.<sub-feature>` or just
55 * `<module-slug>` for whole-module conflicts. Modules extend the matrix
56 * through the `xspeed_conflict_matrix` filter, and a Pro module can
57 * describe a whole new plugin through `xspeed_cache_plugin_catalog`.
58 */
59 private static function matrix(): array {
60 $out = array();
61 foreach ( Cache_Plugin_Catalog::all() as $file => $entry ) {
62 if ( empty( $entry['strategy'] ) ) {
63 // Catalogued for detection only (e.g. an object-cache plugin);
64 // it conflicts with nothing, so it is not a matrix row.
65 continue;
66 }
67 $out[ $file ] = array(
68 'label' => $entry['label'],
69 'kind' => self::kind_for( $entry['capabilities'] ),
70 'strategy' => $entry['strategy'],
71 );
72 }
73 return $out;
74 }
75
76 /**
77 * Collapse a capability list into the single legacy `kind` label: the one
78 * capability when there is only one, 'mixed' when there are several.
79 *
80 * @param string[] $capabilities
81 */
82 private static function kind_for( array $capabilities ): string {
83 if ( empty( $capabilities ) ) {
84 return 'mixed';
85 }
86 return count( $capabilities ) === 1 ? (string) $capabilities[0] : 'mixed';
87 }
88
89 /**
90 * Active conflicts on this site. Returns one entry per active
91 * conflicting plugin: [
92 * 'plugin' => 'wp-rocket/wp-rocket.php',
93 * 'label' => 'WP Rocket',
94 * 'kind' => 'mixed',
95 * 'strategy' => [ feature => strategy ],
96 * ]
97 *
98 * @return array[]
99 */
100 public static function detected(): array {
101 if ( null !== self::$cache ) {
102 return self::$cache;
103 }
104 if ( ! function_exists( 'is_plugin_active' ) ) {
105 require_once ABSPATH . 'wp-admin/includes/plugin.php';
106 }
107
108 $matrix = apply_filters( 'xspeed_conflict_matrix', self::matrix() );
109 $out = array();
110 foreach ( $matrix as $plugin => $spec ) {
111 if ( is_plugin_active( $plugin ) ) {
112 $out[] = array(
113 'plugin' => $plugin,
114 'label' => $spec['label'],
115 'kind' => $spec['kind'],
116 'strategy' => $spec['strategy'],
117 );
118 }
119 }
120 self::$cache = $out;
121 return $out;
122 }
123
124 /**
125 * Strongest strategy across all detected conflicts for a given feature
126 * key. Returns 'refuse' > 'warn' > 'allow' (refuse wins if any
127 * conflict refuses; warn wins if any warns but none refuse).
128 */
129 public static function strategy_for( string $feature_key ): string {
130 $strongest = self::STRATEGY_ALLOW;
131 foreach ( self::detected() as $conflict ) {
132 $s = $conflict['strategy'][ $feature_key ] ?? self::STRATEGY_ALLOW;
133 if ( self::STRATEGY_REFUSE === $s ) {
134 return self::STRATEGY_REFUSE;
135 }
136 if ( self::STRATEGY_WARN === $s && self::STRATEGY_ALLOW === $strongest ) {
137 $strongest = self::STRATEGY_WARN;
138 }
139 }
140 return $strongest;
141 }
142
143 /**
144 * Human-readable reason a feature is blocked by a refuse-strategy
145 * conflict, or null if not blocked. Used by Rest_Manager (409) and UI
146 * tooltips.
147 *
148 * The optional $module_slug is currently informational only — the
149 * strategy lookup uses $feature_key alone. Reserved for future
150 * module-specific overrides.
151 */
152 public static function why_blocked( string $module_slug, string $feature_key ): ?string {
153 foreach ( self::detected() as $conflict ) {
154 if ( ( $conflict['strategy'][ $feature_key ] ?? null ) === self::STRATEGY_REFUSE ) {
155 return sprintf(
156 /* translators: %s: conflicting plugin name */
157 __( '%s is active and handles the same feature. Deactivate it before enabling this in xSpeed.', 'xspeed' ),
158 $conflict['label']
159 );
160 }
161 }
162 return null;
163 }
164
165 /**
166 * All warnings for a feature (one string per warning-strategy conflict).
167 * Returned in the response body of warn-gated REST endpoints.
168 *
169 * @return string[]
170 */
171 public static function warnings_for( string $feature_key ): array {
172 $out = array();
173 foreach ( self::detected() as $conflict ) {
174 if ( ( $conflict['strategy'][ $feature_key ] ?? null ) === self::STRATEGY_WARN ) {
175 $out[] = sprintf(
176 /* translators: %s: conflicting plugin name */
177 __( '%s is active and may interfere. Test carefully.', 'xspeed' ),
178 $conflict['label']
179 );
180 }
181 }
182 return $out;
183 }
184
185 /**
186 * Invalidate the per-request cache. Called on activated_plugin /
187 * deactivated_plugin so the next read reflects the new state.
188 */
189 public static function invalidate(): void {
190 self::$cache = null;
191 // The matrix is a projection of the catalog, and the catalog memoizes
192 // its own filter pass — flushing one without the other would serve a
193 // stale projection of fresh data.
194 Cache_Plugin_Catalog::invalidate();
195 }
196
197 /**
198 * Bootstrap hooks. Call once from Plugin::init().
199 */
200 public static function boot(): void {
201 add_action( 'activated_plugin', array( __CLASS__, 'invalidate' ) );
202 add_action( 'deactivated_plugin', array( __CLASS__, 'invalidate' ) );
203 }
204 }
205