diff --git a/includes/Classifai/Admin/Settings.php b/includes/Classifai/Admin/Settings.php index 4436ef26d..3334e04dd 100644 --- a/includes/Classifai/Admin/Settings.php +++ b/includes/Classifai/Admin/Settings.php @@ -7,6 +7,7 @@ use Classifai\Services\ServicesManager; use Classifai\Taxonomy\TaxonomyFactory; use Classifai\Helpers\CredentialReuse; +use Classifai\Providers\CredentialObfuscator; use function Classifai\get_asset_info; use function Classifai\get_plugin; @@ -226,14 +227,18 @@ public function get_nlu_taxonomies(): array { /** * Get the settings. * - * @return array The settings. + * Obfuscates sensitive credentials before returning to prevent + * exposure of API keys in the frontend. + * + * @return array The settings with credentials obfuscated. */ public function get_settings(): array { $features = $this->get_features( true ); $settings = []; foreach ( $features as $feature ) { - $settings[ $feature::ID ] = $feature->get_settings(); + $feature_settings = $feature->get_settings(); + $settings[ $feature::ID ] = CredentialObfuscator::obfuscate_feature_settings( $feature_settings ); } return $settings; @@ -472,6 +477,12 @@ public function update_settings_permissions_check(): bool { public function get_registration_settings_callback(): \WP_REST_Response { $service_manager = new ServicesManager(); $settings = $service_manager->get_settings(); + + // Obfuscate the license key before returning. + if ( isset( $settings['license_key'] ) ) { + $settings['license_key'] = CredentialObfuscator::obfuscate( $settings['license_key'] ); + } + return rest_ensure_response( $settings ); } @@ -487,12 +498,19 @@ public function update_registration_settings_callback( \WP_REST_Request $request require_once ABSPATH . 'wp-admin/includes/template.php'; } - $service_manager = new ServicesManager(); - $settings = $service_manager->get_settings(); - $new_settings = $service_manager->sanitize_settings( $request->get_json_params() ); + $service_manager = new ServicesManager(); + $current_settings = $service_manager->get_settings(); + $new_settings = $request->get_json_params(); + + // If the license key is obfuscated, use the current value. + if ( isset( $new_settings['license_key'] ) && CredentialObfuscator::is_obfuscated( $new_settings['license_key'] ) ) { + $new_settings['license_key'] = $current_settings['license_key'] ?? ''; + } + + $new_settings = $service_manager->sanitize_settings( $new_settings ); // Update the settings with the new values. - $new_settings = array_merge( $settings, $new_settings ); + $new_settings = array_merge( $current_settings, $new_settings ); update_option( 'classifai_settings', $new_settings ); $setting_errors = get_settings_errors(); diff --git a/includes/Classifai/Features/Feature.php b/includes/Classifai/Features/Feature.php index 784a3af04..fb2ed2e5f 100644 --- a/includes/Classifai/Features/Feature.php +++ b/includes/Classifai/Features/Feature.php @@ -4,6 +4,7 @@ use WP_REST_Request; use WP_Error; +use Classifai\Providers\CredentialObfuscator; use function Classifai\find_provider_class; use function Classifai\should_use_legacy_settings_panel; @@ -301,6 +302,13 @@ public function sanitize_settings( array $settings ): array { // Sanitize the feature specific settings. $new_settings = $this->sanitize_default_feature_settings( $new_settings ); + // Preserve obfuscated credentials for all Providers. + // This ensures switching Providers doesn't save obfuscated values for inactive Providers. + $new_settings = CredentialObfuscator::merge_all_provider_credentials( + $new_settings, + $current_settings + ); + // Sanitize the provider specific settings. $provider_instance = $this->get_feature_provider_instance( $new_settings['provider'] ); diff --git a/includes/Classifai/Providers/CredentialObfuscator.php b/includes/Classifai/Providers/CredentialObfuscator.php new file mode 100644 index 000000000..f92c588c6 --- /dev/null +++ b/includes/Classifai/Providers/CredentialObfuscator.php @@ -0,0 +1,251 @@ + $value ) { + if ( is_array( $value ) && in_array( $key, $all_provider_ids, true ) ) { + $settings[ $key ] = self::obfuscate_provider_settings( $key, $value ); + } + } + + return $settings; + } + + /** + * Merge new credentials with existing credentials, preserving originals when obfuscated. + * + * If a new value is obfuscated, use the existing value instead. + * This prevents obfuscated placeholder values from being saved to the database. + * + * @since x.x.x + * + * @param array $new_settings The new settings being saved. + * @param array $existing_settings The current saved settings. + * @param string $provider_id The Provider ID. + * @return array The merged settings. + */ + public static function merge_credentials( array $new_settings, array $existing_settings, string $provider_id ): array { + $profile_id = ProviderProfiles::get_profile_for_provider( $provider_id ); + + if ( ! $profile_id ) { + return $new_settings; + } + + $credential_fields = ProviderProfiles::get_credential_fields( $profile_id ); + + foreach ( $credential_fields as $field ) { + // Skip non-sensitive fields. + if ( ! self::should_obfuscate_field( $field, $profile_id ) ) { + continue; + } + + // If the new value is obfuscated, preserve the existing value. + if ( + isset( $new_settings[ $field ] ) && + is_string( $new_settings[ $field ] ) && + self::is_obfuscated( $new_settings[ $field ] ) && + isset( $existing_settings[ $field ] ) + ) { + $new_settings[ $field ] = $existing_settings[ $field ]; + } + } + + return $new_settings; + } + + /** + * Merge credentials for all Providers in Feature settings. + * + * Iterates through all Provider settings and preserves existing credentials + * when obfuscated values are submitted. This ensures switching Providers + * doesn't save obfuscated values for inactive Providers. + * + * @since x.x.x + * + * @param array $new_settings The new Feature settings being saved. + * @param array $existing_settings The current saved Feature settings. + * @return array The settings with all Provider credentials properly merged. + */ + public static function merge_all_provider_credentials( array $new_settings, array $existing_settings ): array { + $profiles = ProviderProfiles::get_all_profiles(); + + // Get all Provider IDs from profiles. + $all_provider_ids = []; + foreach ( $profiles as $profile ) { + $all_provider_ids = array_merge( $all_provider_ids, $profile['provider_ids'] ); + } + + // Merge credentials for each Provider that has settings. + foreach ( $new_settings as $key => $value ) { + if ( is_array( $value ) && in_array( $key, $all_provider_ids, true ) ) { + $new_settings[ $key ] = self::merge_credentials( + $value, + $existing_settings[ $key ] ?? [], + $key + ); + } + } + + return $new_settings; + } +} diff --git a/includes/Classifai/Providers/ProviderProfiles.php b/includes/Classifai/Providers/ProviderProfiles.php index e6b37e82e..6b60a1d25 100644 --- a/includes/Classifai/Providers/ProviderProfiles.php +++ b/includes/Classifai/Providers/ProviderProfiles.php @@ -39,71 +39,85 @@ class ProviderProfiles { 'openai_text_to_speech', ], 'credential_fields' => [ 'api_key', 'authenticated' ], + 'sensitive_fields' => [ 'api_key' ], 'label' => 'OpenAI', ], 'googleai' => [ 'provider_ids' => [ 'googleai_gemini_api', 'googleai_images' ], 'credential_fields' => [ 'api_key', 'authenticated' ], + 'sensitive_fields' => [ 'api_key' ], 'label' => 'Google AI', ], 'azure_openai' => [ 'provider_ids' => [ 'azure_openai' ], 'credential_fields' => [ 'endpoint_url', 'api_key', 'deployment', 'authenticated' ], + 'sensitive_fields' => [ 'api_key' ], 'label' => 'Azure OpenAI', ], 'azure_openai_embeddings' => [ 'provider_ids' => [ 'azure_openai_embeddings' ], 'credential_fields' => [ 'endpoint_url', 'api_key', 'deployment', 'authenticated' ], + 'sensitive_fields' => [ 'api_key' ], 'label' => 'Azure OpenAI Embeddings', ], 'ms_computer_vision' => [ 'provider_ids' => [ 'ms_computer_vision' ], 'credential_fields' => [ 'endpoint_url', 'api_key', 'authenticated' ], + 'sensitive_fields' => [ 'api_key' ], 'label' => 'Azure AI Vision', ], 'ms_azure_text_to_speech' => [ 'provider_ids' => [ 'ms_azure_text_to_speech' ], 'credential_fields' => [ 'endpoint_url', 'api_key', 'authenticated' ], + 'sensitive_fields' => [ 'api_key' ], 'label' => 'Azure Text to Speech', ], 'ibm_watson_nlu' => [ 'provider_ids' => [ 'ibm_watson_nlu' ], 'credential_fields' => [ 'endpoint_url', 'apikey', 'username', 'password', 'authenticated' ], + 'sensitive_fields' => [ 'apikey', 'password' ], 'label' => 'IBM Watson NLU', ], 'xai_grok' => [ 'provider_ids' => [ 'xai_grok' ], 'credential_fields' => [ 'api_key', 'authenticated' ], + 'sensitive_fields' => [ 'api_key' ], 'label' => 'XAI Grok', ], 'aws_polly' => [ 'provider_ids' => [ 'aws_polly' ], 'credential_fields' => [ 'access_key_id', 'secret_access_key', 'aws_region', 'authenticated' ], + 'sensitive_fields' => [ 'secret_access_key' ], 'label' => 'AWS Polly', ], 'elevenlabs' => [ 'provider_ids' => [ 'elevenlabs_text_to_speech', 'elevenlabs_speech_to_text' ], 'credential_fields' => [ 'api_key', 'authenticated' ], + 'sensitive_fields' => [ 'api_key' ], 'label' => 'ElevenLabs', ], 'togetherai_image' => [ 'provider_ids' => [ 'togetherai_image' ], 'credential_fields' => [ 'api_key', 'authenticated' ], + 'sensitive_fields' => [ 'api_key' ], 'label' => 'Together AI', ], 'stable_diffusion' => [ 'provider_ids' => [ 'stable_diffusion' ], 'credential_fields' => [ 'endpoint_url', 'authenticated' ], + 'sensitive_fields' => [], 'label' => 'Stable Diffusion', ], 'ollama' => [ 'provider_ids' => [ 'ollama', 'ollama_embeddings', 'ollama_multimodal' ], 'credential_fields' => [ 'endpoint_url', 'authenticated' ], + 'sensitive_fields' => [], 'label' => 'Ollama', ], 'chrome_ai' => [ 'provider_ids' => [ 'chrome_ai' ], 'credential_fields' => [], + 'sensitive_fields' => [], 'label' => 'Chrome AI', ], ]; @@ -166,4 +180,15 @@ public static function get_credential_fields( string $profile_id ): array { $profiles = self::get_all_profiles(); return $profiles[ $profile_id ]['credential_fields'] ?? []; } + + /** + * Get sensitive field names for a profile. + * + * @param string $profile_id Profile ID. + * @return array List of field names. + */ + public static function get_sensitive_fields( string $profile_id ): array { + $profiles = self::get_all_profiles(); + return $profiles[ $profile_id ]['sensitive_fields'] ?? []; + } }