optionStore = $optionStore; } /** * Load the current settings snapshot. * * Returns {@see PluginSettings::defaults()} when the option * is unset, when the stored value is not an array, or when * {@see PluginSettings::fromArray()} rejects it as * malformed. See the file-level docblock for the rationale * for treating malformed payloads as a silent recovery * branch in Phase 8. */ public function load(): PluginSettings { $stored = $this->optionStore->get( self::OPTION_KEY, null ); if ( ! is_array( $stored ) ) { return PluginSettings::defaults(); } $secrets = SettingsSecretBox::decryptArray( $this->optionStore->get( self::SECRETS_OPTION_KEY, array() ) ); if ( is_array( $secrets ) ) { $stored = self::mergeSecrets( $stored, $secrets ); } try { return PluginSettings::fromArray( $stored ); } catch ( InvalidArgumentException $unused ) { unset( $unused ); return PluginSettings::defaults(); } } /** * Persist the supplied settings snapshot. * * The blob is serialized via {@see PluginSettings::toArray()} * and written via {@see OptionStore::update()}. Errors * from the underlying store propagate. * * @param PluginSettings $settings Snapshot to persist. */ public function save( PluginSettings $settings ): void { $data = $settings->toArray(); $secrets = self::extractSecrets( $data ); $this->optionStore->update( self::OPTION_KEY, $data ); $this->optionStore->updateNonAutoload( self::SECRETS_OPTION_KEY, SettingsSecretBox::encryptArray( $secrets ) ); } /** * Delete the stored settings blob. * * After a successful call, the next {@see self::load()} * returns {@see PluginSettings::defaults()}. */ public function reset(): void { $this->optionStore->delete( self::OPTION_KEY ); $this->optionStore->delete( self::SECRETS_OPTION_KEY ); } /** * Extract secret-bearing leaves from the persisted settings payload, replacing * them in-place with empty placeholders in the non-secret blob. * * @param array $data Settings payload, mutated in-place. * * @return array Secret payload for non-autoload storage. */ private static function extractSecrets( array &$data ): array { $secrets = array( 'provider_credentials' => array(), 'openai' => array(), 'cloud' => array(), ); if ( isset( $data['provider_credentials'] ) && is_array( $data['provider_credentials'] ) ) { foreach ( $data['provider_credentials'] as $providerKey => &$entry ) { if ( is_array( $entry ) && isset( $entry['secrets'] ) && is_array( $entry['secrets'] ) ) { $secrets['provider_credentials'][ $providerKey ] = $entry['secrets']; $entry['secrets'] = array(); } } unset( $entry ); } if ( isset( $data['openai'] ) && is_array( $data['openai'] ) && isset( $data['openai']['api_key'] ) ) { $secrets['openai']['api_key'] = $data['openai']['api_key']; $data['openai']['api_key'] = ''; } if ( isset( $data['cloud'] ) && is_array( $data['cloud'] ) && isset( $data['cloud']['api_key'] ) ) { $secrets['cloud']['api_key'] = $data['cloud']['api_key']; $data['cloud']['api_key'] = ''; } return $secrets; } /** * Merge non-autoload secret payload back into a settings payload. * * @param array $data Non-secret settings payload. * @param array $secrets Secret payload. * * @return array */ private static function mergeSecrets( array $data, array $secrets ): array { $providerSecrets = $secrets['provider_credentials'] ?? array(); if ( is_array( $providerSecrets ) && isset( $data['provider_credentials'] ) && is_array( $data['provider_credentials'] ) ) { foreach ( $providerSecrets as $providerKey => $secretMap ) { if ( isset( $data['provider_credentials'][ $providerKey ] ) && is_array( $data['provider_credentials'][ $providerKey ] ) && is_array( $secretMap ) ) { $data['provider_credentials'][ $providerKey ]['secrets'] = $secretMap; } } } if ( isset( $data['openai'] ) && is_array( $data['openai'] ) && isset( $secrets['openai'] ) && is_array( $secrets['openai'] ) && array_key_exists( 'api_key', $secrets['openai'] ) ) { $data['openai']['api_key'] = $secrets['openai']['api_key']; } if ( isset( $data['cloud'] ) && is_array( $data['cloud'] ) && isset( $secrets['cloud'] ) && is_array( $secrets['cloud'] ) && array_key_exists( 'api_key', $secrets['cloud'] ) ) { $data['cloud']['api_key'] = $secrets['cloud']['api_key']; } return $data; } }