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
← All changes | includes/class-module.php +211 -0 1.2.41.3.3 View file →
@@ -48,8 +48,24 @@
48 48 * supported `type` values (bool, int, string, enum, list, url). Each
49 49 * field declares `default` and optional `min` / `max` / `options` /
50 50 * `item_type`. Storage key is `xspeed_module_<slug>`.
51 51 *
52 + * A field may also declare `constants` -- an ordered list of wp-config.php
53 + * constant names that pin its value, most specific first:
54 + *
55 + * 'redis_host' => array(
56 + * 'type' => 'string',
57 + * 'default' => '127.0.0.1',
58 + * 'constants' => array( 'XSPEED_OC_HOST', 'WP_REDIS_HOST' ),
59 + * ),
60 + *
61 + * Resolution order is then: first DEFINED constant -> stored option ->
62 + * `default`. A constant defined as an empty string still wins -- it is an
63 + * answer, not an absence. A pinned field is never persisted, and writes
64 + * targeting it are refused rather than silently dropped; see
65 + * Settings_Manager::origins() / locked_in_input(). This lets a host
66 + * (xCloud provisioning Redis) configure a module with no admin visit. (#398)
67 + *
52 68 * @return array<string,array>
53 69 */
54 70 public function settings_schema(): array {
55 71 return array();
@@ -132,12 +148,133 @@
132 148 'methods' => 'POST',
133 149 'callback' => array( $this, 'rest_update_settings' ),
134 150 'feature' => static::SLUG,
135 151 ),
152 + // Take a constant-pinned field back, or hand it to wp-config again.
153 + // On every module, because any field can declare `constants`. (#398)
154 + array(
155 + 'path' => '/override',
156 + 'methods' => 'POST',
157 + 'callback' => array( $this, 'rest_set_override' ),
158 + 'feature' => static::SLUG,
159 + ),
160 + // Per-field provenance on its own, for a panel that needs to
161 + // re-read it after an action that changes which fields are pinned
162 + // (enabling the object cache writes constants) without re-fetching
163 + // every module descriptor. (#398)
164 + array(
165 + 'path' => '/origins',
166 + 'methods' => 'GET',
167 + 'callback' => array( $this, 'rest_get_origins' ),
168 + ),
136 169 );
137 170 }
138 171
139 172 /**
173 + * Where each of this module's settings currently comes from.
174 + */
175 + public function rest_get_origins( \WP_REST_Request $request ) {
176 + return rest_ensure_response( Settings_Manager::origins( static::SLUG ) );
177 + }
178 +
179 + /**
180 + * Toggle a deliberate override of a constant-pinned field.
181 + *
182 + * Body: `{ "field": "redis_host", "override": true }`. Overriding does not
183 + * itself set a value -- it unlocks the field, and the admin's next save
184 + * writes it like any other setting. Reverting hands the field back to the
185 + * constant, which resumes winning immediately.
186 + */
187 + public function rest_set_override( \WP_REST_Request $request ) {
188 + if ( $this->is_license_locked() ) {
189 + return new \WP_Error(
190 + 'xspeed_license_required',
191 + sprintf(
192 + /* translators: %s: module slug. */
193 + __( '"%s" is a Pro module and this site has no valid license, so its settings cannot be changed.', 'xspeed' ),
194 + static::SLUG
195 + ),
196 + array( 'status' => 403 )
197 + );
198 + }
199 +
200 + $body = (array) $request->get_json_params();
201 + $field = isset( $body['field'] ) ? (string) $body['field'] : '';
202 + $on = ! empty( $body['override'] );
203 +
204 + $spec = $this->settings_schema()[ $field ] ?? null;
205 + if ( ! is_array( $spec ) ) {
206 + return new \WP_Error(
207 + 'xspeed_unknown_setting',
208 + sprintf(
209 + /* translators: %s: setting key. */
210 + __( 'Unknown setting: %s', 'xspeed' ),
211 + $field
212 + ),
213 + array( 'status' => 400 )
214 + );
215 + }
216 +
217 + // Overriding a field no constant pins is meaningless -- it is already
218 + // editable -- and would leave a stale entry that silently disables the
219 + // lock if a constant appeared later.
220 + if ( $on && null === Settings_Manager::constant_source( $spec ) ) {
221 + return new \WP_Error(
222 + 'xspeed_setting_not_pinned',
223 + sprintf(
224 + /* translators: %s: setting key. */
225 + __( '"%s" is not defined in wp-config.php, so there is nothing to override.', 'xspeed' ),
226 + $field
227 + ),
228 + array( 'status' => 409 )
229 + );
230 + }
231 +
232 + if ( $on ) {
233 + Settings_Manager::set_override( static::SLUG, $field, true );
234 + } else {
235 + /*
236 + * The full hand-back, not just dropping the override entry. Once we
237 + * have written our own define it outranks the host's, so clearing
238 + * the entry alone left the field on our stale value with no route
239 + * back -- the panel button did nothing while `wp xspeed objcache
240 + * revert`, which did the extra work inline, worked. Both now go
241 + * through Settings_Manager::revert(). (#398)
242 + */
243 + $reverted = Settings_Manager::revert( static::SLUG, $field );
244 + if ( is_wp_error( $reverted ) ) {
245 + return $reverted;
246 + }
247 + }
248 +
249 + if ( class_exists( '\\XSpeed\\Activity_Log' ) ) {
250 + Activity_Log::record(
251 + $on ? 'setting_override_taken' : 'setting_override_reverted',
252 + sprintf(
253 + $on
254 + /* translators: 1: setting key, 2: module slug. */
255 + ? __( 'Overrode "%1$s" on "%2$s" — the wp-config.php value no longer applies.', 'xspeed' )
256 + /* translators: 1: setting key, 2: module slug. */
257 + : __( 'Reverted "%1$s" on "%2$s" to the value set in wp-config.php.', 'xspeed' ),
258 + $field,
259 + static::SLUG
260 + ),
261 + Activity_Log::INFO
262 + );
263 + }
264 +
265 + return rest_ensure_response(
266 + array(
267 + 'ok' => true,
268 + 'field' => $field,
269 + 'override' => $on,
270 + 'settings' => Settings_Manager::get_public( static::SLUG ),
271 + 'origins' => Settings_Manager::origins( static::SLUG ),
272 + )
273 + );
274 + }
275 +
276 + /**
140 277 * Default GET handler — returns all settings (defaults + stored)
141 278 * coerced against the schema, with secret fields masked. Uses the public
142 279 * view (not get_settings()) so a credential never leaves in a REST payload;
143 280 * the engine reads real values through get_settings()/get_setting(). (#115)
@@ -172,8 +309,31 @@
172 309 array( 'status' => 403 )
173 310 );
174 311 }
175 312
313 + // A field pinned by a wp-config.php constant cannot be written. Saying
314 + // so beats a 200 over a write that silently did nothing -- and naming
315 + // the constant tells the caller where to go and change it. (#398)
316 + $locked = Settings_Manager::locked_in_input( static::SLUG, is_array( $params ) ? $params : array() );
317 + if ( ! empty( $locked ) ) {
318 + $pairs = array();
319 + foreach ( $locked as $field => $constant ) {
320 + $pairs[] = $field . ' (' . $constant . ')';
321 + }
322 + return new \WP_Error(
323 + 'xspeed_setting_defined_in_wp_config',
324 + sprintf(
325 + /* translators: %s: comma-separated list of "field (CONSTANT_NAME)" pairs. */
326 + __( 'These settings are defined in wp-config.php and cannot be changed here: %s. Edit the constant, or remove it to manage the setting from this screen.', 'xspeed' ),
327 + implode( ', ', $pairs )
328 + ),
329 + array(
330 + 'status' => 409,
331 + 'fields' => $locked,
332 + )
333 + );
334 + }
335 +
176 336 return rest_ensure_response( $this->update_settings( $params ) );
177 337 }
178 338
179 339 /**
@@ -456,8 +616,59 @@
456 616 */
457 617 final public function get_setting( string $key, $default = null ) {
458 618 $opts = Settings_Manager::get( static::SLUG );
459 619 return array_key_exists( $key, $opts ) ? $opts[ $key ] : $default;
620 + }
621 +
622 + /**
623 + * Read one boolean flag WITHOUT building the settings schema.
624 + *
625 + * `get_setting()` routes through `Settings_Manager::get()`, which calls
626 + * `settings_schema()` to know the defaults and types. That schema
627 + * declares its `label` / `description` through `__()` — correct, they are
628 + * UI copy a translator has to reach. But modules that read their own
629 + * settings from `boot()` do so on `plugins_loaded`, before `init`, where
630 + * text domains load. Building the schema there translates every label too
631 + * early: WordPress 6.7+ emits `_load_textdomain_just_in_time` on each
632 + * request, and the labels resolve against a domain that is not loaded
633 + * yet, which silently defeats the translation.
634 + *
635 + * A boot-time gate only ever asks "is this feature switched on", so it
636 + * needs the stored value, not the schema. This reads the module's option
637 + * directly and casts. `$default` is what applies when the key was never
638 + * written — pass the same value the schema declares as that field's
639 + * default, or the two disagree on a fresh install.
640 + *
641 + * Use ONLY for a boot-time on/off check. Anything that needs coercion,
642 + * schema defaults, or a non-boolean value must keep using
643 + * `get_setting()` / `get_settings()`.
644 + *
645 + * @param string $key Field name in this module's settings.
646 + * @param bool $default Value when the key has never been stored.
647 + */
648 + final protected function flag_at_boot( string $key, bool $default = false ): bool {
649 + return (bool) $this->setting_at_boot( $key, $default );
650 + }
651 +
652 + /**
653 + * Raw stored value for one setting, WITHOUT building the schema. The
654 + * general form of `flag_at_boot()` — see that method for why boot-time
655 + * reads must not touch `settings_schema()`.
656 + *
657 + * No type coercion is applied, so pass a `$default` of the type the
658 + * caller expects and cast the result at the call site. Same rule: use
659 + * ONLY from code that runs before `init`.
660 + *
661 + * @param string $key Field name in this module's settings.
662 + * @param mixed $default Value when the key has never been stored.
663 + * @return mixed
664 + */
665 + final protected function setting_at_boot( string $key, $default = null ) {
666 + $stored = get_option( Settings_Manager::OPTION_PREFIX . static::SLUG, array() );
667 + if ( ! is_array( $stored ) || ! array_key_exists( $key, $stored ) ) {
668 + return $default;
669 + }
670 + return $stored[ $key ];
460 671 }
461 672
462 673 final public function get_settings(): array {
463 674 return Settings_Manager::get( static::SLUG );