PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.1.4
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.1.4
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 / modules / ObjectCache / ObjectCacheModule.php

ObjectCacheModule.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.1.4, at includes/modules/ObjectCache/ObjectCacheModule.php

348 lines 11.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Object Cache module — status, settings, wp-config snippet, flush.
4 *
5 * Tier: Free per FEATURES.md "Object Cache" §1-11 (LiteSpeed parity).
6 *
7 * We don't ship our own object-cache.php drop-in in this Free release
8 * (see Object_Cache class docblock for rationale). The settings here
9 * are surfaced via the wp-config snippet; advanced consumers (Pro,
10 * external drop-ins like Redis Object Cache) can read them too.
11 *
12 * @package XSpeed
13 */
14
15 declare(strict_types=1);
16
17 namespace XSpeed\Modules\ObjectCache;
18
19 defined( 'ABSPATH' ) || exit;
20
21 use XSpeed\Module;
22 use XSpeed\Object_Cache;
23
24 final class ObjectCacheModule extends Module {
25
26 public const SLUG = 'object-cache';
27 public const TIER = self::TIER_FREE;
28 public const VERSION = '1.1.0';
29
30 public function ui_metadata(): array {
31 return array(
32 'label' => 'Object Cache',
33 'icon' => 'Server',
34 'description' => 'Configure a persistent object cache (Redis / Memcached) and generate a paste-ready wp-config.php snippet.',
35 'custom_panel' => 'ObjectCachePanel',
36 );
37 }
38
39 public function settings_schema(): array {
40 return array(
41 'backend' => array(
42 'type' => 'enum',
43 'default' => 'redis',
44 'options' => array( 'redis', 'memcached' ),
45 'option_labels' => array(
46 'redis' => 'Redis',
47 'memcached' => 'Memcached',
48 ),
49 'label' => 'Backend',
50 'description' => 'Which cache server you intend to use. Affects the generated wp-config snippet.',
51 ),
52 'redis_host' => array(
53 'type' => 'string',
54 'default' => '127.0.0.1',
55 'label' => 'Redis Host',
56 'description' => 'Hostname or IP of the Redis server. Use 127.0.0.1 for a local socket on the same machine as PHP.',
57 ),
58 'redis_port' => array(
59 'type' => 'int',
60 'default' => 6379,
61 'min' => 1,
62 'max' => 65535,
63 'label' => 'Redis Port',
64 'description' => 'Default Redis port is 6379.',
65 ),
66 'redis_user' => array(
67 'type' => 'string',
68 'default' => '',
69 'label' => 'Redis User',
70 'description' => 'Optional. Set this only if your host provisioned a dedicated Redis ACL user (Redis 6+) — e.g. some managed hosts issue a Redis User alongside the password. Leave blank to authenticate as the default user (legacy password-only Redis).',
71 ),
72 'redis_password' => array(
73 'type' => 'secret',
74 'default' => '',
75 'label' => 'Redis Password',
76 'description' => 'Leave blank if your Redis server runs without auth.',
77 ),
78 'redis_database' => array(
79 'type' => 'int',
80 'default' => 0,
81 'min' => 0,
82 'max' => 15,
83 'label' => 'Redis Database',
84 'description' => 'Redis logical DB number (0-15). Use a dedicated DB per site if Redis is shared.',
85 ),
86 'memcached_host' => array(
87 'type' => 'string',
88 'default' => '127.0.0.1',
89 'label' => 'Memcached Host',
90 'description' => 'Used when Backend = Memcached.',
91 ),
92 'memcached_port' => array(
93 'type' => 'int',
94 'default' => 11211,
95 'min' => 1,
96 'max' => 65535,
97 'label' => 'Memcached Port',
98 'description' => 'Default Memcached port is 11211.',
99 ),
100 'key_prefix' => array(
101 'type' => 'string',
102 'default' => '',
103 'label' => 'Cache Key Prefix',
104 'description' => 'Unique salt for this site\'s cache keys. Critical when multiple WP sites share one Redis/Memcached server. On ACL/namespaced Redis (e.g. xCloud), this MUST match the host\'s "Redis Object Cache Key" — otherwise cache writes are denied (NOPERM) and nothing persists.',
105 ),
106 'connection_timeout' => array(
107 'type' => 'int',
108 'default' => 1,
109 'min' => 0,
110 'max' => 60,
111 'label' => 'Connection Timeout (seconds)',
112 'description' => 'How long to wait for a connection. Keep low (1-2s) so a misconfigured cache never stalls the page.',
113 ),
114 'persistent' => array(
115 'type' => 'bool',
116 'default' => true,
117 'label' => 'Persistent Connections',
118 'description' => 'Reuse the connection across PHP requests when supported. Generally a win unless the cache server complains about idle connections.',
119 ),
120 );
121 }
122
123 /**
124 * Encrypt the pre-1.1.0 plaintext redis_password on upgrade — it became a
125 * `secret`-typed field (encrypted at rest). Idempotent. (#115)
126 */
127 public function migrations(): array {
128 return array(
129 '1.1.0' => static function ( array $opts ): array {
130 if ( isset( $opts['redis_password'] ) && is_string( $opts['redis_password'] ) && '' !== $opts['redis_password'] ) {
131 $opts['redis_password'] = \XSpeed\Settings_Manager::encrypt_for_storage( $opts['redis_password'] );
132 }
133 return $opts;
134 },
135 );
136 }
137
138 public function boot(): void {
139 // Keep the deployed drop-in in sync with the shipped template. It is
140 // copied into wp-content/object-cache.php on enable and then never
141 // touched again — so a fix shipped in a plugin update (e.g. the
142 // stale-alloptions eviction on failed backend writes, issue #41)
143 // would never reach existing installs. Version-gated so the file
144 // comparison runs once per plugin version, not on every admin load.
145 add_action(
146 'admin_init',
147 static function (): void {
148 if ( get_option( 'xspeed_oc_dropin_synced', '' ) === XSPEED_VERSION ) {
149 return;
150 }
151 if ( Object_Cache::is_our_dropin_present() ) {
152 Object_Cache::install_dropin();
153 }
154 update_option( 'xspeed_oc_dropin_synced', XSPEED_VERSION );
155 }
156 );
157 }
158
159 public function rest_routes(): array {
160 $default = parent::rest_routes();
161 return array_merge(
162 $default,
163 array(
164 array(
165 'path' => '/detect',
166 'methods' => 'GET',
167 'callback' => array( $this, 'rest_detect' ),
168 ),
169 array(
170 'path' => '/flush',
171 'methods' => 'POST',
172 'callback' => array( $this, 'rest_flush' ),
173 ),
174 array(
175 'path' => '/snippet',
176 'methods' => 'GET',
177 'callback' => array( $this, 'rest_snippet' ),
178 ),
179 array(
180 'path' => '/test-connection',
181 'methods' => 'POST',
182 'callback' => array( $this, 'rest_test_connection' ),
183 ),
184 array(
185 'path' => '/enable',
186 'methods' => 'POST',
187 'callback' => array( $this, 'rest_enable' ),
188 ),
189 array(
190 'path' => '/disable',
191 'methods' => 'POST',
192 'callback' => array( $this, 'rest_disable' ),
193 ),
194 )
195 );
196 }
197
198 public function rest_detect( \WP_REST_Request $request ) {
199 return rest_ensure_response( Object_Cache::detect() );
200 }
201
202 public function rest_flush( \WP_REST_Request $request ) {
203 $ok = Object_Cache::flush();
204 if ( $ok && class_exists( '\\XSpeed\\Activity_Log' ) ) {
205 \XSpeed\Activity_Log::record(
206 'object_cache_flushed',
207 'Object cache flushed.',
208 \XSpeed\Activity_Log::INFO
209 );
210 }
211 return rest_ensure_response( array( 'ok' => $ok ) );
212 }
213
214 public function rest_snippet( \WP_REST_Request $request ) {
215 return rest_ensure_response(
216 array( 'snippet' => Object_Cache::render_config_snippet( $this->get_settings() ) )
217 );
218 }
219
220 /**
221 * Merge any settings sent in the request body over the saved settings, so
222 * the UI can "Test connection" with unsaved values. Only known keys pass.
223 */
224 private function settings_with_overrides( \WP_REST_Request $request ): array {
225 $body = $request->get_json_params();
226 return self::merge_overrides(
227 $this->get_settings(),
228 is_array( $body ) ? $body : array(),
229 $this->settings_schema()
230 );
231 }
232
233 /**
234 * Overlay request-body values onto the stored settings for a one-off "Test
235 * connection" — but NEVER let a masked secret echoed from the panel overwrite
236 * the real stored value. The panel holds `Redi••••CRET`; without this guard,
237 * clicking Test connection authenticates Redis with the mask and a correct
238 * password reports as wrong. A genuinely new (typed) password still applies,
239 * and an explicit empty value still tests the no-auth case. Static + pure so
240 * it's unit-testable without a REST request. (QA B3)
241 *
242 * @param array<string,mixed> $settings Stored, decrypted settings.
243 * @param array<string,mixed> $body Request overrides.
244 * @param array<string,array> $schema The module schema (for secret detection).
245 * @return array<string,mixed>
246 */
247 public static function merge_overrides( array $settings, array $body, array $schema ): array {
248 foreach ( $settings as $key => $value ) {
249 if ( ! array_key_exists( $key, $body ) ) {
250 continue;
251 }
252 if ( isset( $schema[ $key ] )
253 && \XSpeed\Settings_Manager::is_secret_field( $key, $schema[ $key ] )
254 && \XSpeed\Settings_Manager::is_masked_secret( (string) $body[ $key ] ) ) {
255 continue;
256 }
257 $settings[ $key ] = $body[ $key ];
258 }
259 return $settings;
260 }
261
262 public function rest_test_connection( \WP_REST_Request $request ) {
263 return rest_ensure_response( Object_Cache::test_connection( $this->settings_with_overrides( $request ) ) );
264 }
265
266 public function rest_enable( \WP_REST_Request $request ) {
267 // Persist any settings sent with the enable call first, then act on them.
268 $body = $request->get_json_params();
269 if ( is_array( $body ) && ! empty( $body ) ) {
270 \XSpeed\Settings_Manager::update( self::SLUG, $body );
271 }
272 $result = Object_Cache::enable( $this->get_settings() );
273
274 if ( $result['ok'] && class_exists( '\\XSpeed\\Activity_Log' ) ) {
275 \XSpeed\Activity_Log::record(
276 'object_cache_enabled',
277 'Object cache enabled (' . ( $result['test']['backend'] ?? '' ) . ').',
278 \XSpeed\Activity_Log::INFO
279 );
280 }
281 return rest_ensure_response( $result );
282 }
283
284 public function rest_disable( \WP_REST_Request $request ) {
285 $result = Object_Cache::disable();
286 if ( $result['ok'] && class_exists( '\\XSpeed\\Activity_Log' ) ) {
287 \XSpeed\Activity_Log::record(
288 'object_cache_disabled',
289 'Object cache disabled.',
290 \XSpeed\Activity_Log::INFO
291 );
292 }
293 return rest_ensure_response( $result );
294 }
295
296 public function cli_commands(): array {
297 return array(
298 array(
299 'name' => 'xspeed objcache',
300 'callback' => array( $this, 'cli_handler' ),
301 'shortdesc' => 'Show object cache status, flush, or print the wp-config snippet.',
302 'synopsis' => array(
303 array(
304 'type' => 'positional',
305 'name' => 'action',
306 'options' => array( 'status', 'flush', 'snippet', 'enable', 'disable', 'test' ),
307 'optional' => true,
308 ),
309 ),
310 ),
311 );
312 }
313
314 public function cli_handler( array $args, array $assoc ): void {
315 $action = $args[0] ?? 'status';
316 switch ( $action ) {
317 case 'status':
318 $d = Object_Cache::detect();
319 \WP_CLI::log( 'drop-in installed: ' . ( $d['drop_in_installed'] ? 'yes' : 'no' ) );
320 \WP_CLI::log( 'label: ' . $d['drop_in_label'] );
321 \WP_CLI::log( 'backend: ' . $d['backend'] );
322 \WP_CLI::log( 'ext object cache: ' . ( $d['wp_cache_active'] ? 'yes' : 'no' ) );
323 return;
324 case 'flush':
325 $ok = Object_Cache::flush();
326 $ok ? \WP_CLI::success( 'Flushed.' ) : \WP_CLI::error( 'Flush failed.' );
327 return;
328 case 'snippet':
329 \WP_CLI::log( Object_Cache::render_config_snippet( $this->get_settings() ) );
330 return;
331 case 'test':
332 $t = Object_Cache::test_connection( $this->get_settings() );
333 $t['ok'] ? \WP_CLI::success( $t['message'] ) : \WP_CLI::error( $t['message'] );
334 return;
335 case 'enable':
336 $r = Object_Cache::enable( $this->get_settings() );
337 $r['ok'] ? \WP_CLI::success( $r['message'] ) : \WP_CLI::error( $r['message'] );
338 return;
339 case 'disable':
340 $r = Object_Cache::disable();
341 $r['ok'] ? \WP_CLI::success( $r['message'] ) : \WP_CLI::error( $r['message'] );
342 return;
343 default:
344 \WP_CLI::error( "Unknown action: $action" );
345 }
346 }
347 }
348