*/ private static array $cache = []; /** Canonical values, including empty/false, win over exact historic plugin-owned options. */ public static function read_owned_option( string $name, $default = false ) { $missing = new \stdClass(); $value = get_option( $name, $missing ); if ( $missing !== $value ) { return $value; } foreach ( \WooOrderCart\Lifecycle\CartDataManifest::legacy_options() as $legacy => $canonical ) { if ( $canonical === $name ) { $value = get_option( $legacy, $missing ); if ( $missing !== $value ) { return $value; } } } return $default; } /** Read old section arrays only until their prefixed replacement exists. */ public static function read_section( string $section ): array { $storage_key = CartOptionMap::storage_key( $section ); $values = get_option( $storage_key, null ); if ( null === $values && $storage_key !== $section ) { $values = get_option( $section, [] ); } return is_array( $values ) ? $values : []; } /** Site-local normalized settings must not survive switch_to_blog(). */ public static function clear_cache(): void { self::$cache = []; } /** * @return array> */ public static function schema(): array { return CartOptionMap::sections(); } /** * Read normalized admin values for all known cart settings. * * @return array */ public static function all(): array { $values = []; foreach ( CartOptionMap::fields() as $key => $field ) { $values[ $key ] = self::get( $key, $field['default'] ?? null ); } $store = self::get_react_store(); foreach ( $store as $key => $value ) { if ( ! array_key_exists( $key, $values ) ) { $values[ $key ] = $value; } } return $values; } /** * @param mixed $default * @return mixed */ public static function get( string $key, $default = null ) { if ( array_key_exists( $key, self::$cache ) ) { return self::$cache[ $key ]; } $field = CartOptionMap::field( $key ); if ( ! $field ) { $store = self::get_react_store(); return self::$cache[ $key ] = $store[ $key ] ?? $default; } $legacy = self::read_section( $field['section'] ); if ( is_array( $legacy ) && array_key_exists( $key, $legacy ) ) { return self::$cache[ $key ] = self::normalize_for_client( $field, $legacy[ $key ] ); } $store = self::get_react_store(); if ( array_key_exists( $key, $store ) ) { return self::$cache[ $key ] = self::normalize_for_client( $field, $store[ $key ] ); } return self::$cache[ $key ] = self::normalize_for_client( $field, $field['default'] ?? $default ); } /** * Persist a partial payload to prefixed sections and to the React store. * * @param array $partial * @return array */ public static function merge_and_persist( array $partial ): array { $react_store = self::get_react_store(); $section_updates = []; foreach ( $partial as $key => $value ) { if ( self::is_client_blocked_key( (string) $key ) ) { continue; } $field = CartOptionMap::field( (string) $key ); if ( ! $field ) { $react_store[ $key ] = $value; continue; } $client_value = self::normalize_for_client( $field, $value ); $legacy_value = self::normalize_for_storage( $field, $value ); $section = $field['section']; if ( ! isset( $section_updates[ $section ] ) ) { $current = self::read_section( $section ); $section_updates[ $section ] = is_array( $current ) ? $current : []; } $section_updates[ $section ][ $key ] = $legacy_value; $react_store[ $key ] = $client_value; self::$cache[ $key ] = $client_value; } foreach ( $section_updates as $section => $data ) { update_option( CartOptionMap::storage_key( $section ), $data ); } update_option( self::SECTION_REACT, $react_store ); CartLegacyOptionCache::flush_sections( $section_updates ); self::$cache = []; return self::all(); } /** * Seed missing legacy defaults and mirror them into the React store. * * @return array */ public static function ensure_legacy_defaults( bool $write_missing_defaults = true ): array { $stored_react_store = self::read_owned_option( self::SECTION_REACT, [] ); $stored_react_store = is_array( $stored_react_store ) ? $stored_react_store : []; $react_store = self::without_client_blocked_keys( $stored_react_store ); $react_changed = $stored_react_store !== $react_store; $sections_seen = []; $legacy_defaults_written = []; foreach ( CartOptionMap::fields() as $key => $field ) { $section = $field['section']; if ( ! array_key_exists( $section, $sections_seen ) ) { $current = self::read_section( $section ); $sections_seen[ $section ] = is_array( $current ) ? $current : []; } if ( ! array_key_exists( $key, $sections_seen[ $section ] ) && $write_missing_defaults ) { $sections_seen[ $section ][ $key ] = self::normalize_for_storage( $field, $field['default'] ?? null ); $legacy_defaults_written[ $section ][ $key ] = true; } if ( array_key_exists( $key, $sections_seen[ $section ] ) ) { $normalized = self::normalize_for_client( $field, $sections_seen[ $section ][ $key ] ); } else { $normalized = self::normalize_for_client( $field, $field['default'] ?? null ); } if ( ! array_key_exists( $key, $react_store ) || $react_store[ $key ] !== $normalized ) { $react_store[ $key ] = $normalized; $react_changed = true; } } if ( $write_missing_defaults ) { $preserved_defaults = Cart_Defaults_Service::instance()->build_preserved_legacy_defaults(); foreach ( $preserved_defaults as $section => $defaults ) { if ( ! array_key_exists( $section, $sections_seen ) ) { $current = self::read_section( $section ); $sections_seen[ $section ] = is_array( $current ) ? $current : []; } foreach ( $defaults as $key => $default ) { if ( array_key_exists( $key, $sections_seen[ $section ] ) ) { continue; } $sections_seen[ $section ][ $key ] = $default; $legacy_defaults_written[ $section ][ $key ] = true; } } } if ( $write_missing_defaults ) { foreach ( $sections_seen as $section => $data ) { update_option( CartOptionMap::storage_key( $section ), $data ); } } if ( $react_changed ) { update_option( self::SECTION_REACT, $react_store ); } CartLegacyOptionCache::flush_sections( $legacy_defaults_written ); self::$cache = []; return self::all(); } /** * Reset all known cart settings back to their declared defaults. * * @return array */ public static function reset_to_defaults(): array { // Core must retain settings owned by the absent Pro package. $react_store = \IPSCMI\Order\Cart\Config\PackageProfile::from_runtime()->includes_pro() ? [] : self::get_react_store(); $sections = []; $payload = Cart_Defaults_Service::instance()->build_payload(); foreach ( $payload as $key => $default ) { $field = CartOptionMap::field( (string) $key ); if ( ! $field ) { continue; } $client_value = self::normalize_for_client( $field, $default ); $legacy_value = self::normalize_for_storage( $field, $default ); if ( ! isset( $sections[ $field['section'] ] ) ) { $current = self::read_section( $field['section'] ); $sections[ $field['section'] ] = is_array( $current ) ? $current : []; } $sections[ $field['section'] ][ $key ] = $legacy_value; $react_store[ $key ] = $client_value; } foreach ( $sections as $section => $values ) { update_option( CartOptionMap::storage_key( $section ), $values ); } $hidden_sections = Cart_Defaults_Service::instance()->build_hidden_section_defaults(); foreach ( $hidden_sections as $section => $defaults ) { $current = self::read_section( $section ); $merged = is_array( $current ) ? $current : []; foreach ( $defaults as $key => $value ) { $merged[ $key ] = $value; } update_option( CartOptionMap::storage_key( $section ), $merged ); } $preserved_sections = Cart_Defaults_Service::instance()->build_preserved_legacy_defaults(); $preserved_defaults_written = []; foreach ( $preserved_sections as $section => $defaults ) { $current = self::read_section( $section ); $merged = is_array( $current ) ? $current : []; foreach ( $defaults as $key => $value ) { if ( array_key_exists( $key, $merged ) ) { continue; } $merged[ $key ] = $value; $preserved_defaults_written[ $section ][ $key ] = true; } if ( isset( $preserved_defaults_written[ $section ] ) || null === get_option( CartOptionMap::storage_key( $section ), null ) ) { update_option( CartOptionMap::storage_key( $section ), $merged ); } } update_option( self::SECTION_REACT, $react_store ); CartLegacyOptionCache::flush_sections( $sections ); CartLegacyOptionCache::flush_sections( $hidden_sections ); CartLegacyOptionCache::flush_sections( $preserved_defaults_written ); self::$cache = []; return self::all(); } /** * @return array */ public static function get_react_store(): array { $store = self::read_owned_option( self::SECTION_REACT, [] ); if ( ! is_array( $store ) ) { return []; } return self::without_client_blocked_keys( $store ); } /** * @param array $store * @return array */ private static function without_client_blocked_keys( array $store ): array { foreach ( self::CLIENT_BLOCKED_KEYS as $key ) { unset( $store[ $key ] ); } return $store; } private static function is_client_blocked_key( string $key ): bool { return in_array( $key, self::CLIENT_BLOCKED_KEYS, true ); } /** * @param array $field * @param mixed $value * @return mixed */ private static function normalize_for_client( array $field, $value ) { $type = $field['type'] ?? 'text'; if ( 'toggle' === $type ) { return in_array( $value, [ true, 1, '1', 'on', 'yes' ], true ); } if ( 'number' === $type ) { if ( '' === $value || null === $value ) { return $field['default'] ?? null; } return is_numeric( $value ) ? 0 + $value : $field['default'] ?? null; } if ( 'multiselect' === $type ) { if ( is_array( $value ) ) { return array_values( array_filter( array_map( static function( $item ) { return is_scalar( $item ) ? (string) $item : ''; }, $value ), static function( string $item ) { return '' !== $item; } ) ); } return []; } if ( 'html' === $type ) { return is_scalar( $value ) ? wp_kses_post( (string) $value ) : ''; } if ( 'select' === $type || 'icon-radio-grid' === $type || 'layout-preview-grid' === $type || 'media-image' === $type || 'text' === $type || 'textarea' === $type || 'color' === $type ) { return is_scalar( $value ) ? (string) $value : ( $field['default'] ?? '' ); } return $value; } /** * @param array $field * @param mixed $value * @return mixed */ private static function normalize_for_storage( array $field, $value ) { $type = $field['type'] ?? 'text'; if ( 'toggle' === $type ) { return in_array( $value, [ true, 1, '1', 'on', 'yes' ], true ) ? 'on' : 'off'; } if ( 'number' === $type ) { if ( '' === $value || null === $value ) { return $field['default'] ?? ''; } return is_numeric( $value ) ? 0 + $value : ( $field['default'] ?? '' ); } if ( 'html' === $type ) { return is_scalar( $value ) ? wp_kses_post( (string) $value ) : ''; } if ( 'textarea' === $type ) { return is_scalar( $value ) ? sanitize_textarea_field( (string) $value ) : ''; } if ( 'multiselect' === $type ) { if ( ! is_array( $value ) ) { return []; } return array_values( array_filter( array_map( static function( $item ) { return is_scalar( $item ) ? sanitize_text_field( (string) $item ) : ''; }, $value ), static function( string $item ) { return '' !== $item; } ) ); } if ( 'select' === $type || 'icon-radio-grid' === $type || 'layout-preview-grid' === $type || 'media-image' === $type || 'text' === $type || 'color' === $type ) { return is_scalar( $value ) ? sanitize_text_field( (string) $value ) : ''; } return $value; } }