<?php
/**
* Settings repository.
*
* @package PoweredCache
*/
namespace PoweredCache;
use const PoweredCache\Constants\SETTING_OPTION;
/**
* Read and write Powered Cache settings through the current option storage.
*
* This class keeps the legacy `powered_cache_settings` option as the storage
* source while providing a schema-aware API for 4.0 services.
*
* @since 4.0.0
*/
class SettingsRepository {
const EXPORT_FORMAT = 'powered-cache-settings';
const EXPORT_FORMAT_VERSION = '1.0';
/**
* Whether settings should be read from network options.
*
* @var bool
*/
private $network_wide;
/**
* Runtime context for dynamic defaults.
*
* @var array
*/
private $context;
/**
* Constructor.
*
* @param bool|null $network_wide Whether to use network option storage.
* @param array $context Runtime context for dynamic defaults.
*/
public function __construct( $network_wide = null, array $context = array() ) {
$this->network_wide = null === $network_wide ? $this->detect_network_mode() : (bool) $network_wide;
$this->context = empty( $context ) ? $this->default_context() : $context;
}
/**
* Create a repository instance.
*
* @param bool|null $network_wide Whether to use network option storage.
* @param array $context Runtime context for dynamic defaults.
*
* @return SettingsRepository
*/
public static function factory( $network_wide = null, array $context = array() ) {
return new self( $network_wide, $context );
}
/**
* Read normalized settings.
*
* @return array
*/
public function all() {
return $this->normalize( self::migrate_legacy_settings( $this->read_raw() ), $this->defaults() );
}
/**
* Get one setting value.
*
* @param string $key Setting key.
* @param mixed $default Fallback value when the key is unknown.
*
* @return mixed
*/
public function get( $key, $default = null ) {
$settings = $this->all();
return array_key_exists( $key, $settings ) ? $settings[ $key ] : $default;
}
/**
* Save a full settings payload.
*
* @param array $settings Settings to save.
*
* @return bool
*/
public function save( array $settings ) {
$settings = $this->sanitize( self::migrate_legacy_settings( $this->normalize( $settings ) ) );
if ( $this->network_wide ) {
return (bool) update_site_option( SETTING_OPTION, $settings );
}
return (bool) update_option( SETTING_OPTION, $settings );
}
/**
* Merge a partial settings payload into the current settings.
*
* @param array $settings Settings patch.
*
* @return bool
*/
public function update( array $settings ) {
return $this->save( array_merge( $this->all(), $settings ) );
}
/**
* Delete the stored settings option.
*
* @return bool
*/
public function delete() {
if ( $this->network_wide ) {
return (bool) delete_site_option( SETTING_OPTION );
}
return (bool) delete_option( SETTING_OPTION );
}
/**
* Determine whether a raw settings payload needs 4.0 compatibility migration.
*
* @param array $settings Raw settings.
*
* @return bool
*/
public static function has_legacy_settings( array $settings ) {
foreach ( array( 'accepted_query_strings', 'js_execution_method' ) as $key ) {
if ( ! empty( $settings[ $key ] ) ) {
return true;
}
}
return false;
}
/**
* Migrate known legacy settings without dropping rollback-safe keys.
*
* @param array $settings Raw settings.
*
* @return array
*/
public static function migrate_legacy_settings( array $settings ) {
if ( ! empty( $settings['accepted_query_strings'] ) && empty( $settings['ignored_query_strings'] ) ) {
$settings['ignored_query_strings'] = $settings['accepted_query_strings'];
}
if ( ! empty( $settings['js_execution_method'] ) && in_array( $settings['js_execution_method'], array( 'async', 'defer' ), true ) ) {
$settings['js_defer'] = true;
}
if ( ! empty( $settings['js_execution_method'] ) && in_array( $settings['js_execution_method'], array( 'delay', 'delayed' ), true ) ) {
$settings['js_delay'] = true;
$settings['combine_js'] = false;
}
return $settings;
}
/**
* Return deprecated schema keys.
*
* @return array
*/
public static function deprecated_keys() {
$deprecated = array();
foreach ( SettingsSchema::fields() as $key => $field ) {
if ( ! empty( $field['deprecated'] ) ) {
$deprecated[] = $key;
}
}
return $deprecated;
}
/**
* Build a versioned settings export payload.
*
* @param array $settings Settings payload.
*
* @return array
*/
public static function export_payload( array $settings ) {
return array(
'format' => self::EXPORT_FORMAT,
'format_version' => self::EXPORT_FORMAT_VERSION,
'plugin_version' => defined( 'POWERED_CACHE_VERSION' ) ? POWERED_CACHE_VERSION : '',
'generated_at' => gmdate( 'c' ),
'settings' => $settings,
);
}
/**
* Extract settings from a versioned or legacy import payload.
*
* @param mixed $payload Decoded JSON payload.
*
* @return array
*/
public static function settings_from_payload( $payload ) {
if ( ! is_array( $payload ) ) {
return array();
}
if ( isset( $payload['settings'] ) && is_array( $payload['settings'] ) ) {
return $payload['settings'];
}
return $payload;
}
/**
* Remove sensitive values from settings before export or REST output.
*
* @param array $settings Settings payload.
*
* @return array
*/
public static function redact_sensitive( array $settings ) {
foreach ( self::sensitive_keys() as $key ) {
if ( isset( $settings[ $key ] ) ) {
$settings[ $key ] = '';
}
}
return $settings;
}
/**
* Report whether sensitive settings have stored values without exposing them.
*
* @param array $settings Settings payload.
*
* @return array
*/
public static function sensitive_value_status( array $settings ) {
$status = array();
foreach ( self::sensitive_keys() as $key ) {
$status[ $key ] = array_key_exists( $key, $settings ) && null !== $settings[ $key ] && '' !== $settings[ $key ];
}
return $status;
}
/**
* Return settings keys that should not be exported with values.
*
* @return array
*/
public static function sensitive_keys() {
return array(
'cloudflare_email',
'cloudflare_api_key',
'cloudflare_api_token',
'sucuri_api_key',
'sucuri_api_secret',
);
}
/**
* Return filtered settings defaults.
*
* @return array
*/
public function defaults() {
$defaults = SettingsSchema::defaults( $this->context );
/**
* Filter default settings.
*
* @hook powered_cache_default_settings
*
* @param {array} $defaults Default settings.
*
* @return {array} New value
* @since 2.0
*/
return apply_filters( 'powered_cache_default_settings', $defaults );
}
/**
* Normalize settings with schema defaults.
*
* Unknown keys are preserved for backward compatibility with older
* extensions, imports, or customer customizations.
*
* @param array $settings Raw settings.
* @param array|null $defaults Optional defaults.
*
* @return array
*/
public function normalize( array $settings, ?array $defaults = null ) {
if ( null === $defaults ) {
$defaults = SettingsSchema::defaults( $this->context );
}
return array_merge( $defaults, $settings );
}
/**
* Sanitize known settings using schema metadata.
*
* Unknown keys are preserved because the legacy option array is public and
* may contain values added by older versions or extensions.
*
* @param array $settings Raw settings.
*
* @return array
*/
public function sanitize( array $settings ) {
$fields = SettingsSchema::fields( $this->context );
$sanitized = array();
foreach ( $settings as $key => $value ) {
if ( ! isset( $fields[ $key ] ) ) {
$sanitized[ $key ] = $value;
continue;
}
$sanitized[ $key ] = $this->sanitize_value( $value, $fields[ $key ] );
}
return $sanitized;
}
/**
* Read raw option payload.
*
* @return array
*/
private function read_raw() {
$settings = $this->network_wide ? get_site_option( SETTING_OPTION, array() ) : get_option( SETTING_OPTION, array() );
return is_array( $settings ) ? $settings : array();
}
/**
* Sanitize one value.
*
* @param mixed $value Raw value.
* @param array $field Field metadata.
*
* @return mixed
*/
private function sanitize_value( $value, array $field ) {
switch ( $field['sanitizer'] ) {
case SettingsSchema::SANITIZE_BOOLEAN:
return ! empty( $value );
case SettingsSchema::SANITIZE_INTEGER:
return $this->absint( $value );
case SettingsSchema::SANITIZE_ARRAY:
return $this->sanitize_array( $value );
case SettingsSchema::SANITIZE_ENUM:
$value = $this->sanitize_text( $value );
if ( ! empty( $field['enum'] ) && ! in_array( $value, $field['enum'], true ) ) {
return $field['default'];
}
return $value;
case SettingsSchema::SANITIZE_TEXTAREA:
return $this->sanitize_textarea( $value );
case SettingsSchema::SANITIZE_CSS:
$value = is_scalar( $value ) ? (string) $value : '';
return \PoweredCache\Utils\sanitize_css( $value );
case SettingsSchema::SANITIZE_TEXT:
default:
return $this->sanitize_text( $value );
}
}
/**
* Sanitize an array as a list of text values.
*
* @param mixed $value Raw value.
*
* @return array
*/
private function sanitize_array( $value ) {
$values = is_array( $value ) ? $value : array( $value );
return array_map( array( $this, 'sanitize_text' ), array_values( $values ) );
}
/**
* Sanitize a text value.
*
* @param mixed $value Raw value.
*
* @return string
*/
private function sanitize_text( $value ) {
$value = is_scalar( $value ) ? (string) $value : '';
if ( function_exists( 'sanitize_text_field' ) ) {
return sanitize_text_field( $value );
}
if ( function_exists( 'wp_strip_all_tags' ) ) {
return trim( wp_strip_all_tags( $value ) );
}
return trim( strip_tags( $value ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags
}
/**
* Sanitize a textarea value.
*
* @param mixed $value Raw value.
*
* @return string
*/
private function sanitize_textarea( $value ) {
$value = is_scalar( $value ) ? (string) $value : '';
if ( function_exists( 'sanitize_textarea_field' ) ) {
return sanitize_textarea_field( $value );
}
if ( function_exists( 'wp_strip_all_tags' ) ) {
return trim( wp_strip_all_tags( $value ) );
}
return trim( strip_tags( $value ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags
}
/**
* Return an absolute integer.
*
* @param mixed $value Raw value.
*
* @return int
*/
private function absint( $value ) {
if ( function_exists( 'absint' ) ) {
return absint( $value );
}
return abs( (int) $value );
}
/**
* Detect network mode.
*
* @return bool
*/
private function detect_network_mode() {
return defined( 'POWERED_CACHE_IS_NETWORK' ) && POWERED_CACHE_IS_NETWORK;
}
/**
* Build default runtime context.
*
* @return array
*/
private function default_context() {
global $is_apache;
$context = array(
'is_apache' => (bool) $is_apache,
);
if ( function_exists( '\PoweredCache\Utils\get_available_object_caches' ) ) {
$context['object_cache_backends'] = \PoweredCache\Utils\get_available_object_caches();
}
return $context;
}
}