| @@ -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 ); |