in the configuration. * @param string $default_value The default string value to return if the key is not found. * @param bool $trim Whether to trim the value. * * @return string The configuration value as a string. */ public function get_string( $w3tc_key, $default_value = '', $trim = true ) { $w3tc_value = (string) $this->get( $w3tc_key, $default_value ); return $trim ? trim( $w3tc_value ) : $w3tc_value; } /** * Retrieves an integer configuration value for a given key. * * @param string $w3tc_key The key to look up in the configuration. * @param int $default_value The default integer value to return if the key is not found. * * @return int The configuration value as an integer. */ public function get_integer( $w3tc_key, $default_value = 0 ) { return (int) $this->get( $w3tc_key, $default_value ); } /** * Retrieves a boolean configuration value for a given key. * * @param string $w3tc_key The key to look up in the configuration. * @param bool $default_value The default boolean value to return if the key is not found. * * @return bool The configuration value as a boolean. */ public function get_boolean( $w3tc_key, $default_value = false ) { return (bool) $this->get( $w3tc_key, $default_value ); } /** * Retrieves an array configuration value for a given key. * * @param string $w3tc_key The key to look up in the configuration. * @param array $default_value The default array value to return if the key is not found. * * @return array The configuration value as an array. */ public function get_array( $w3tc_key, $default_value = array() ) { return (array) $this->get( $w3tc_key, $default_value ); } /** * Retrieves a filtered configuration value for a given key. * * @param string $w3tc_key The key to look up in the configuration. * @param mixed $default_value The default value to return if the key is not found. * * @return mixed The configuration value, potentially filtered. */ public function getf( $w3tc_key, $default_value = null ) { $v = $this->get( $w3tc_key, $default_value ); return apply_filters( 'w3tc_config_item_' . $w3tc_key, $v ); } /** * Retrieves a filtered string configuration value for a given key. * * @param string $w3tc_key The key to look up in the configuration. * @param string $default_value The default string value to return if the key is not found. * @param bool $trim Whether to trim the value. * * @return string The filtered configuration value as a string. */ public function getf_string( $w3tc_key, $default_value = '', $trim = true ) { $w3tc_value = (string) $this->getf( $w3tc_key, $default_value ); return $trim ? trim( $w3tc_value ) : $w3tc_value; } /** * Retrieves a filtered integer configuration value for a given key. * * @param string $w3tc_key The key to look up in the configuration. * @param int $default_value The default integer value to return if the key is not found. * * @return int The filtered configuration value as an integer. */ public function getf_integer( $w3tc_key, $default_value = 0 ) { return (int) $this->getf( $w3tc_key, $default_value ); } /** * Retrieves a filtered boolean configuration value for a given key. * * @param string $w3tc_key The key to look up in the configuration. * @param bool $default_value The default boolean value to return if the key is not found. * * @return bool The filtered configuration value as a boolean. */ public function getf_boolean( $w3tc_key, $default_value = false ) { return (bool) $this->getf( $w3tc_key, $default_value ); } /** * Retrieves a filtered array configuration value for a given key. * * @param string $w3tc_key The key to look up in the configuration. * @param array $default_value The default array value to return if the key is not found. * * @return array The filtered configuration value as an array. */ public function getf_array( $w3tc_key, $default_value = array() ) { return (array) $this->getf( $w3tc_key, $default_value ); } /** * Checks if a specific extension is active in the configuration. * * @param string $w3tc_extension The extension to check. * * @return bool True if the extension is active, false otherwise. */ public function is_extension_active( $w3tc_extension ) { $extensions = $this->get_array( 'extensions.active' ); return isset( $extensions[ $w3tc_extension ] ); } /** * Checks if a specific extension is active on the frontend. * * @param string $w3tc_extension The extension to check. * * @return bool True if the extension is active on the frontend, false otherwise. */ public function is_extension_active_frontend( $w3tc_extension ) { $extensions = $this->get_array( 'extensions.active_frontend' ); return isset( $extensions[ $w3tc_extension ] ); } /** * Sets the active frontend extension. * * @param string $w3tc_extension The extension key to be set as active. * @param bool $is_active_frontend Whether the extension should be active on the frontend. * * @return void */ public function set_extension_active_frontend( $w3tc_extension, $is_active_frontend ) { $w3tc_a = $this->get_array( 'extensions.active_frontend' ); if ( ! $is_active_frontend ) { unset( $w3tc_a[ $w3tc_extension ] ); } else { $w3tc_a[ $w3tc_extension ] = '*'; } $this->set( 'extensions.active_frontend', $w3tc_a ); } /** * Sets the active dropin extension. * * @param string $w3tc_extension The extension key to be set as active dropin. * @param bool $is_active_dropin Whether the extension should be active as a dropin. * * @return void */ public function set_extension_active_dropin( $w3tc_extension, $is_active_dropin ) { $w3tc_a = $this->get_array( 'extensions.active_dropin' ); if ( ! $is_active_dropin ) { unset( $w3tc_a[ $w3tc_extension ] ); } else { $w3tc_a[ $w3tc_extension ] = '*'; } $this->set( 'extensions.active_dropin', $w3tc_a ); } /** * Sets a key-value pair in the configuration data. * * @param string|array $w3tc_key The key or array of keys to set. * @param mixed $w3tc_value The value to set. * * @return mixed The value that was set. */ public function set( $w3tc_key, $w3tc_value ) { /** * Strip directive-terminating bytes from values bound for keys whose * stored string (or stored array of strings) is later concatenated * into a `.htaccess` / `nginx.conf` directive. See * `Util_Rule::sanitize_directive_value()` for the strip set and * rationale. Routed through the shared helper rather than * duplicating the regex so a future strip-set extension lands in * one place and stored/rendered values stay in lockstep. * * The same characters are stripped at the renderer boundary too; * this is the upstream defence-in-depth half so the bad bytes * never enter master.php in the first place. */ if ( ! is_array( $w3tc_key ) && '' !== $w3tc_value ) { $desc = self::directive_string_descriptor( $w3tc_key ); if ( null !== $desc ) { if ( is_string( $w3tc_value ) ) { $w3tc_value = Util_Rule::sanitize_directive_value( $w3tc_value ); } elseif ( is_array( $w3tc_value ) ) { /** * Directive-bound array keys (`pgcache.reject.cookie`, * `pgcache.reject.ua`, `mobile.rgroups`, etc.) each * element of which is later concatenated into a rule * alternation. Sanitise every scalar entry and drop * any non-scalar (the helper would return '' anyway, * and array_filter strips the empty result so a * fully-stripped entry doesn't widen an `implode( '|', ... )` * regex into a match-everything alternative). */ $w3tc_value = array_values( array_filter( array_map( function ( $v ) { return is_scalar( $v ) ? Util_Rule::sanitize_directive_value( (string) $v ) : ''; }, $w3tc_value ), function ( $v ) { return '' !== $v; } ) ); } } } $w3tc_value = self::enforce_enum( $w3tc_key, $w3tc_value, $this ); if ( ! is_array( $w3tc_key ) ) { $this->_data[ $w3tc_key ] = $w3tc_value; } else { // set extension's key. $key0 = $w3tc_key[0]; $key1 = $w3tc_key[1]; if ( ! isset( $this->_data[ $key0 ] ) || ! is_array( $this->_data[ $key0 ] ) ) { $this->_data[ $key0 ] = array(); } $this->_data[ $key0 ][ $key1 ] = $w3tc_value; } return $w3tc_value; } /** * Removes a configuration key from the in-memory data set. * * Intended for one-time migrations that retire keys whose feature * has been removed from the plugin (e.g. a discontinued CDN engine), * so the orphaned key — and any stored secret under it — stops being * written back to `master.php`. The key is dropped from * `$this->_data`; call {@see save()} afterwards to persist the * removal. Removing a key that is not present is a no-op. * * Only top-level string keys are supported; compound * `array( 'extension', 'sub' )` keys are rejected. * * @since 2.10.0 * * @param string $w3tc_key Config key to remove. * * @return bool True if the key was present and removed; false otherwise. */ public function unset_key( $w3tc_key ) { if ( ! is_string( $w3tc_key ) || ! isset( $this->_data[ $w3tc_key ] ) ) { return false; } unset( $this->_data[ $w3tc_key ] ); return true; } /** * Returns the descriptor for a `directive_string`-flagged key, or * null if the key isn't in the schema or doesn't carry the flag. * * Loads `ConfigKeys.php` once per request and caches the slimmed * directive-string map (keyed by config-key name → true). Kept * local to `Config` rather than promoted to a shared schema * accessor because the `directive_string` flag is the only piece * of `ConfigKeys.php` that `Config::set()` consults; promoting * would widen this file's `ConfigKeys.php` surface without a * second consumer to justify it. * * @since 2.10.0 * * @param string $w3tc_key Single-string config key. * * @return true|null `true` when the key is flagged; `null` otherwise. */ private static function directive_string_descriptor( $w3tc_key ) { static $set = null; if ( null === $set ) { $set = array(); $w3tc_keys = array(); include W3TC_DIR . '/ConfigKeys.php'; if ( is_array( $w3tc_keys ) ) { foreach ( $w3tc_keys as $w3tc_name => $w3tc_descriptor ) { if ( is_array( $w3tc_descriptor ) && isset( $w3tc_descriptor['flags'] ) && is_array( $w3tc_descriptor['flags'] ) && ! empty( $w3tc_descriptor['flags']['directive_string'] ) ) { $set[ $w3tc_name ] = true; } } } } return isset( $set[ $w3tc_key ] ) ? true : null; } /** * Constrains scalar `$w3tc_value` to the enum declared in * {@see ConfigKeys.php} for `$w3tc_key`, when present. * * Schema declares an enum like: * * 'cdn.engine' => array( * 'type' => 'string', * 'default' => '', * 'enum' => array( 'ftp', 's3', ... ), * ), * * Behavior: * * * If the key has no `enum` entry, return `$w3tc_value` unchanged. * * If `$w3tc_value` is in the enum, return it unchanged. * * Otherwise, retain the value already stored under that key * (or the schema `default`) and emit an audit-log entry. The * invalid value never reaches `$this->_data`, so downstream * callers — including the `header()` emitters that * interpolate `cdn.engine` — only ever see an allowlisted * slug. * * Top-level keys only (compound `array( 'extension', 'sub' )` * keys skip enforcement); the schema doesn't currently declare * enums on extension subkeys. * * @since 2.10.0 * * @param string|array $w3tc_key Config key (string for top-level, array for extension subkey). * @param mixed $w3tc_value The candidate value. * @param Config $w3tc_config Config instance, used to look up the prior value as fallback. * * @return mixed The original value if allowed, otherwise the prior stored value (or schema default). */ private static function enforce_enum( $w3tc_key, $w3tc_value, Config $w3tc_config ) { if ( ! \is_string( $w3tc_key ) ) { return $w3tc_value; } $schema = self::config_keys_schema(); if ( ! isset( $schema[ $w3tc_key ]['enum'] ) || ! \is_array( $schema[ $w3tc_key ]['enum'] ) ) { return $w3tc_value; } $enum = $schema[ $w3tc_key ]['enum']; /** * Non-scalar values are an immediate reject path. The earlier * shape coerced them to `''` and then ran the enum match — but * `''` is itself a valid enum member for `cdn.engine`, so an * array / object write (e.g. from a malformed import) would * have been *accepted* by the enum check and overwritten the * prior value. Route non-scalars straight to the fallback * branch below so they never normalise to an allowed slug. */ if ( \is_scalar( $w3tc_value ) ) { $value_string = (string) $w3tc_value; if ( \in_array( $value_string, $enum, true ) ) { /** * Return the normalised string form so the stored * type matches the schema's declared `string` type * even when a caller passes a non-string scalar * (e.g. `true` → `'1'`, `42` → `'42'`) that happens * to string-cast into an allowed enum member. */ return $value_string; } } /** * Reject. Retain whatever was previously stored (or fall * back to the schema default). Audit-log the rejection so * operators can see attempted out-of-enum writes (a CRLF- * bearing value, or a buggy filter widening the surface). */ $fallback = ''; if ( isset( $w3tc_config->_data[ $w3tc_key ] ) ) { $fallback = $w3tc_config->_data[ $w3tc_key ]; } elseif ( isset( $schema[ $w3tc_key ]['default'] ) ) { $fallback = $schema[ $w3tc_key ]['default']; } /** * Allow the autoloader to load `Util_Debug` if it isn't * already in memory (the earlier `class_exists(..., false)` * would skip the audit-log in any request path where * `Util_Debug` hadn't been touched yet — i.e. most enum-write * flows — and silently drop the diagnostic operators rely on). */ if ( \class_exists( __NAMESPACE__ . '\\Util_Debug' ) ) { Util_Debug::log( 'config', \sprintf( 'Rejected out-of-enum write to %s; retained prior value. Allowed: %s', $w3tc_key, \implode( ',', $enum ) ) ); } return $fallback; } /** * Lazy-load the {@see ConfigKeys.php} schema once per request. * * `ConfigKeys.php` populates a local `$w3tc_keys` variable; we * import the file inside an isolated scope and cache the * result so per-write enum lookups don't re-include the file. * * @since 2.10.0 * * @return array> */ private static function config_keys_schema() { static $schema = null; if ( null === $schema ) { $schema = array(); $loader = static function () { $w3tc_keys = array(); include W3TC_DIR . '/ConfigKeys.php'; return $w3tc_keys; }; $schema = (array) $loader(); } return $schema; } /** * Checks if the current configuration is a preview. * * @return bool True if preview mode is enabled, false otherwise. */ public function is_preview() { return $this->_preview; } /** * Checks if the current configuration is the master configuration. * * @return bool True if the configuration is the master, false otherwise. */ public function is_master() { return $this->_is_master; } /** * Checks if the configuration is compiled. * * @return bool True if the configuration is compiled, false otherwise. */ public function is_compiled() { return $this->_compiled; } /** * Sets the default configuration values. * * @return void */ public function set_defaults() { $w3tc_c = new ConfigCompiler( $this->_blog_id, $this->_preview ); $this->_data = $w3tc_c->get_data(); } /** * Saves the current configuration. * * @return void */ public function save() { if ( function_exists( 'do_action' ) ) { do_action( 'w3tc_config_save', $this ); } $w3tc_c = new ConfigCompiler( $this->_blog_id, $this->_preview ); $w3tc_c->apply_data( $this->_data ); $w3tc_c->save(); } /** * Checks if a configuration key is sealed (immutable). * * @param string $w3tc_key The configuration key to check. * * @return bool True if the key is sealed, false otherwise. */ public function is_sealed( $w3tc_key ) { if ( $this->is_master() ) { return false; } // better to use master config data here, but its faster and preciese enough for UI. return ConfigCompiler::child_key_sealed( $w3tc_key, $this->_data, $this->_data ); } /** * Exports the current configuration as a JSON string. * * @return string The configuration data as a JSON string. */ public function export() { $export_data = $this->_data; // Export files are often copied between environments; omit stored credential values. foreach ( \array_keys( self::secret_keys() ) as $w3tc_key ) { if ( isset( $export_data[ $w3tc_key ] ) && '' !== $export_data[ $w3tc_key ] ) { $export_data[ $w3tc_key ] = ''; } } /** * JSON_HEX_TAG / HEX_AMP / HEX_APOS / HEX_QUOT escape `<` `&` `'` * `"` to their `\uXXXX` forms. The export endpoint serves this * body to admins, and even with `Content-Type: application/json` * some clients can render the response as HTML (history walks, * view-source, intermediary proxies that ignore the content- * type). Hex-escaping every HTML-significant character means * the body cannot contain a literal `