PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.1.6
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.1.6
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 1.2.0 All 28 releases
xspeed / includes / modules / ObjectCache / ObjectCacheModule.php

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

349 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 'unit' => 'seconds',
113 'description' => 'How long to wait for a connection. Keep low (1-2s) so a misconfigured cache never stalls the page.',
114 ),
115 'persistent' => array(
116 'type' => 'bool',
117 'default' => true,
118 'label' => 'Persistent Connections',
119 'description' => 'Reuse the connection across PHP requests when supported. Generally a win unless the cache server complains about idle connections.',
120 ),
121 );
122 }
123
124 /**
125 * Encrypt the pre-1.1.0 plaintext redis_password on upgrade — it became a
126 * `secret`-typed field (encrypted at rest). Idempotent. (#115)
127 */
128 public function migrations(): array {
129 return array(
130 '1.1.0' => static function ( array $opts ): array {
131 if ( isset( $opts['redis_password'] ) && is_string( $opts['redis_password'] ) && '' !== $opts['redis_password'] ) {
132 $opts['redis_password'] = \XSpeed\Settings_Manager::encrypt_for_storage( $opts['redis_password'] );
133 }
134 return $opts;
135 },
136 );
137 }
138
139 public function boot(): void {
140 // Keep the deployed drop-in in sync with the shipped template. It is
141 // copied into wp-content/object-cache.php on enable and then never
142 // touched again — so a fix shipped in a plugin update (e.g. the
143 // stale-alloptions eviction on failed backend writes, issue #41)
144 // would never reach existing installs. Version-gated so the file
145 // comparison runs once per plugin version, not on every admin load.
146 add_action(
147 'admin_init',
148 static function (): void {
149 if ( get_option( 'xspeed_oc_dropin_synced', '' ) === XSPEED_VERSION ) {
150 return;
151 }
152 if ( Object_Cache::is_our_dropin_present() ) {
153 Object_Cache::install_dropin();
154 }
155 update_option( 'xspeed_oc_dropin_synced', XSPEED_VERSION );
156 }
157 );
158 }
159
160 public function rest_routes(): array {
161 $default = parent::rest_routes();
162 return array_merge(
163 $default,
164 array(
165 array(
166 'path' => '/detect',
167 'methods' => 'GET',
168 'callback' => array( $this, 'rest_detect' ),
169 ),
170 array(
171 'path' => '/flush',
172 'methods' => 'POST',
173 'callback' => array( $this, 'rest_flush' ),
174 ),
175 array(
176 'path' => '/snippet',
177 'methods' => 'GET',
178 'callback' => array( $this, 'rest_snippet' ),
179 ),
180 array(
181 'path' => '/test-connection',
182 'methods' => 'POST',
183 'callback' => array( $this, 'rest_test_connection' ),
184 ),
185 array(
186 'path' => '/enable',
187 'methods' => 'POST',
188 'callback' => array( $this, 'rest_enable' ),
189 ),
190 array(
191 'path' => '/disable',
192 'methods' => 'POST',
193 'callback' => array( $this, 'rest_disable' ),
194 ),
195 )
196 );
197 }
198
199 public function rest_detect( \WP_REST_Request $request ) {
200 return rest_ensure_response( Object_Cache::detect() );
201 }
202
203 public function rest_flush( \WP_REST_Request $request ) {
204 $ok = Object_Cache::flush();
205 if ( $ok && class_exists( '\\XSpeed\\Activity_Log' ) ) {
206 \XSpeed\Activity_Log::record(
207 'object_cache_flushed',
208 'Object cache flushed.',
209 \XSpeed\Activity_Log::INFO
210 );
211 }
212 return rest_ensure_response( array( 'ok' => $ok ) );
213 }
214
215 public function rest_snippet( \WP_REST_Request $request ) {
216 return rest_ensure_response(
217 array( 'snippet' => Object_Cache::render_config_snippet( $this->get_settings() ) )
218 );
219 }
220
221 /**
222 * Merge any settings sent in the request body over the saved settings, so
223 * the UI can "Test connection" with unsaved values. Only known keys pass.
224 */
225 private function settings_with_overrides( \WP_REST_Request $request ): array {
226 $body = $request->get_json_params();
227 return self::merge_overrides(
228 $this->get_settings(),
229 is_array( $body ) ? $body : array(),
230 $this->settings_schema()
231 );
232 }
233
234 /**
235 * Overlay request-body values onto the stored settings for a one-off "Test
236 * connection" — but NEVER let a masked secret echoed from the panel overwrite
237 * the real stored value. The panel holds `Redi••••CRET`; without this guard,
238 * clicking Test connection authenticates Redis with the mask and a correct
239 * password reports as wrong. A genuinely new (typed) password still applies,
240 * and an explicit empty value still tests the no-auth case. Static + pure so
241 * it's unit-testable without a REST request. (QA B3)
242 *
243 * @param array<string,mixed> $settings Stored, decrypted settings.
244 * @param array<string,mixed> $body Request overrides.
245 * @param array<string,array> $schema The module schema (for secret detection).
246 * @return array<string,mixed>
247 */
248 public static function merge_overrides( array $settings, array $body, array $schema ): array {
249 foreach ( $settings as $key => $value ) {
250 if ( ! array_key_exists( $key, $body ) ) {
251 continue;
252 }
253 if ( isset( $schema[ $key ] )
254 && \XSpeed\Settings_Manager::is_secret_field( $key, $schema[ $key ] )
255 && \XSpeed\Settings_Manager::is_masked_secret( (string) $body[ $key ] ) ) {
256 continue;
257 }
258 $settings[ $key ] = $body[ $key ];
259 }
260 return $settings;
261 }
262
263 public function rest_test_connection( \WP_REST_Request $request ) {
264 return rest_ensure_response( Object_Cache::test_connection( $this->settings_with_overrides( $request ) ) );
265 }
266
267 public function rest_enable( \WP_REST_Request $request ) {
268 // Persist any settings sent with the enable call first, then act on them.
269 $body = $request->get_json_params();
270 if ( is_array( $body ) && ! empty( $body ) ) {
271 \XSpeed\Settings_Manager::update( self::SLUG, $body );
272 }
273 $result = Object_Cache::enable( $this->get_settings() );
274
275 if ( $result['ok'] && class_exists( '\\XSpeed\\Activity_Log' ) ) {
276 \XSpeed\Activity_Log::record(
277 'object_cache_enabled',
278 'Object cache enabled (' . ( $result['test']['backend'] ?? '' ) . ').',
279 \XSpeed\Activity_Log::INFO
280 );
281 }
282 return rest_ensure_response( $result );
283 }
284
285 public function rest_disable( \WP_REST_Request $request ) {
286 $result = Object_Cache::disable();
287 if ( $result['ok'] && class_exists( '\\XSpeed\\Activity_Log' ) ) {
288 \XSpeed\Activity_Log::record(
289 'object_cache_disabled',
290 'Object cache disabled.',
291 \XSpeed\Activity_Log::INFO
292 );
293 }
294 return rest_ensure_response( $result );
295 }
296
297 public function cli_commands(): array {
298 return array(
299 array(
300 'name' => 'xspeed objcache',
301 'callback' => array( $this, 'cli_handler' ),
302 'shortdesc' => 'Show object cache status, flush, or print the wp-config snippet.',
303 'synopsis' => array(
304 array(
305 'type' => 'positional',
306 'name' => 'action',
307 'options' => array( 'status', 'flush', 'snippet', 'enable', 'disable', 'test' ),
308 'optional' => true,
309 ),
310 ),
311 ),
312 );
313 }
314
315 public function cli_handler( array $args, array $assoc ): void {
316 $action = $args[0] ?? 'status';
317 switch ( $action ) {
318 case 'status':
319 $d = Object_Cache::detect();
320 \WP_CLI::log( 'drop-in installed: ' . ( $d['drop_in_installed'] ? 'yes' : 'no' ) );
321 \WP_CLI::log( 'label: ' . $d['drop_in_label'] );
322 \WP_CLI::log( 'backend: ' . $d['backend'] );
323 \WP_CLI::log( 'ext object cache: ' . ( $d['wp_cache_active'] ? 'yes' : 'no' ) );
324 return;
325 case 'flush':
326 $ok = Object_Cache::flush();
327 $ok ? \WP_CLI::success( 'Flushed.' ) : \WP_CLI::error( 'Flush failed.' );
328 return;
329 case 'snippet':
330 \WP_CLI::log( Object_Cache::render_config_snippet( $this->get_settings() ) );
331 return;
332 case 'test':
333 $t = Object_Cache::test_connection( $this->get_settings() );
334 $t['ok'] ? \WP_CLI::success( $t['message'] ) : \WP_CLI::error( $t['message'] );
335 return;
336 case 'enable':
337 $r = Object_Cache::enable( $this->get_settings() );
338 $r['ok'] ? \WP_CLI::success( $r['message'] ) : \WP_CLI::error( $r['message'] );
339 return;
340 case 'disable':
341 $r = Object_Cache::disable();
342 $r['ok'] ? \WP_CLI::success( $r['message'] ) : \WP_CLI::error( $r['message'] );
343 return;
344 default:
345 \WP_CLI::error( "Unknown action: $action" );
346 }
347 }
348 }
349