load_options(); } public function reload() { $this->load_options(); } private function load_options() { $defaults = $this->get_defaults(); $saved = get_option( $this->option_name, array() ); $this->options = wp_parse_args( $saved, $defaults ); } private function get_defaults() { return array( 'enabled' => false, 'domain_key' => '', 'endpoint' => AIADVISORS_API_ENDPOINT, 'batch_size' => 500, 'max_queue_size' => 100000, 'retention_days' => 7, 'circuit_breaker_threshold' => 3, 'circuit_breaker_cooldown' => 1800, 'redact_query_keys' => 'password,pass,token,auth,authorization,apikey,api_key,secret,card,cc,ssn', ); } /** * Get a setting value. * * @param string|null $key Setting key (null returns all). * @param mixed $fallback Default value if key not found. * @return mixed */ public function get( $key = null, $fallback = null ) { if ( $key === null ) { return $this->options; } if ( $key === 'domain_key' ) { return $this->get_domain_key(); } return isset( $this->options[ $key ] ) ? $this->options[ $key ] : $fallback; } /** * Set a single setting. */ public function set( $key, $value ) { $this->options[ $key ] = $value; return update_option( $this->option_name, $this->options ); } /** * Update multiple settings at once. */ public function update( $settings ) { if ( ! is_array( $settings ) ) { return false; } $settings = $this->sanitize_settings( $settings ); $this->options = wp_parse_args( $settings, $this->get_defaults() ); return update_option( $this->option_name, $this->options ); } // --- Domain Key --- /** * Get the domain key, checking wp-config constants first. */ public function get_domain_key() { // Allow wp-config.php constant override. if ( defined( 'AIADVISORS_DOMAIN_KEY' ) ) { return AIADVISORS_DOMAIN_KEY; } $stored = $this->options['domain_key'] ?? ''; if ( $stored && $this->is_encrypted( $stored ) ) { return $this->decrypt( $stored ); } return $stored; } /** * Store the domain key (encrypted if Sodium available). */ public function set_domain_key( $key ) { if ( empty( $key ) ) { $this->options['domain_key'] = ''; } else { $this->options['domain_key'] = $this->encrypt( $key ); } return update_option( $this->option_name, $this->options ); } /** * Encrypt a value for storage. Public so the sanitize callback can * encrypt without calling set_domain_key (which triggers update_option). * * @param string $value Raw value to encrypt. * @return string Encrypted value (or plaintext if Sodium unavailable). */ public function encrypt_value( $value ) { return $this->encrypt( $value ); } /** * Check if a domain key is configured. */ public function has_domain_key() { return ! empty( $this->get_domain_key() ); } /** * Get masked domain key for display. */ public function get_masked_domain_key() { $key = $this->get_domain_key(); if ( empty( $key ) ) { return ''; } if ( strlen( $key ) <= 8 ) { return str_repeat( '*', strlen( $key ) ); } return substr( $key, 0, 4 ) . str_repeat( '*', strlen( $key ) - 8 ) . substr( $key, -4 ); } // --- Encryption --- private function encrypt( $value ) { if ( ! function_exists( 'sodium_crypto_secretbox' ) || ! defined( 'SODIUM_CRYPTO_SECRETBOX_NONCEBYTES' ) ) { return $value; // Store plaintext if Sodium unavailable. } try { $nonce = random_bytes( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ); $secret = $this->get_encryption_key(); $encrypted = sodium_crypto_secretbox( $value, $nonce, $secret ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Encoding binary Sodium ciphertext for storage, not obfuscation. return 'enc:' . base64_encode( $nonce . $encrypted ); } catch ( \Exception $e ) { return $value; } } private function decrypt( $encrypted ) { if ( ! function_exists( 'sodium_crypto_secretbox_open' ) || ! $this->is_encrypted( $encrypted ) ) { return $encrypted; } try { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Decoding stored Sodium ciphertext, not obfuscation. $decoded = base64_decode( substr( $encrypted, 4 ) ); $nonce = substr( $decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ); $ciphertext = substr( $decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ); $secret = $this->get_encryption_key(); $decrypted = sodium_crypto_secretbox_open( $ciphertext, $nonce, $secret ); return $decrypted !== false ? $decrypted : ''; } catch ( \Exception $e ) { return ''; } } private function is_encrypted( $value ) { return is_string( $value ) && strpos( $value, 'enc:' ) === 0; } private function get_encryption_key() { $key = ''; if ( defined( 'AUTH_KEY' ) ) { $key .= AUTH_KEY; } if ( defined( 'SECURE_AUTH_KEY' ) ) { $key .= SECURE_AUTH_KEY; } if ( empty( $key ) ) { $key = get_option( 'aiadvisors_install_time', 'fallback-key' ); } $key_length = defined( 'SODIUM_CRYPTO_SECRETBOX_KEYBYTES' ) ? SODIUM_CRYPTO_SECRETBOX_KEYBYTES : 32; return substr( hash( 'sha256', $key, true ), 0, $key_length ); } // --- Sanitization --- private function sanitize_settings( $settings ) { $sanitized = array(); $sanitized['enabled'] = ! empty( $settings['enabled'] ); if ( isset( $settings['domain_key'] ) ) { $sanitized['domain_key'] = sanitize_text_field( wp_unslash( $settings['domain_key'] ) ); } if ( isset( $settings['endpoint'] ) ) { $endpoint = esc_url_raw( wp_unslash( $settings['endpoint'] ) ); $sanitized['endpoint'] = ( $endpoint && wp_parse_url( $endpoint, PHP_URL_SCHEME ) === 'https' ) ? $endpoint : AIADVISORS_API_ENDPOINT; } $sanitized['batch_size'] = isset( $settings['batch_size'] ) ? min( 1000, max( 50, (int) $settings['batch_size'] ) ) : 500; $sanitized['max_queue_size'] = isset( $settings['max_queue_size'] ) ? min( 1000000, max( 1000, (int) $settings['max_queue_size'] ) ) : 100000; $sanitized['retention_days'] = isset( $settings['retention_days'] ) ? min( 30, max( 1, (int) $settings['retention_days'] ) ) : 7; $sanitized['circuit_breaker_threshold'] = isset( $settings['circuit_breaker_threshold'] ) ? min( 10, max( 1, (int) $settings['circuit_breaker_threshold'] ) ) : 3; $sanitized['circuit_breaker_cooldown'] = isset( $settings['circuit_breaker_cooldown'] ) ? min( 7200, max( 60, (int) $settings['circuit_breaker_cooldown'] ) ) : 1800; if ( isset( $settings['redact_query_keys'] ) ) { $sanitized['redact_query_keys'] = sanitize_text_field( wp_unslash( $settings['redact_query_keys'] ) ); } return $sanitized; } }