@@ -93,8 +93,20 @@ protected function _renderContent() {
9393
9494 // Check if form was submitted
9595 if ( ! $ is_locked && isset ( $ _POST ['visualizer_ai_settings_nonce ' ] ) && wp_verify_nonce ( $ _POST ['visualizer_ai_settings_nonce ' ], 'visualizer_ai_settings ' ) ) {
96- $ this ->_saveSettings ();
97- echo '<div class="notice notice-success is-dismissible"><p> ' . esc_html__ ( 'Settings saved successfully. ' , 'visualizer ' ) . '</p></div> ' ;
96+ $ result = $ this ->_saveSettings ();
97+
98+ if ( $ result ['success ' ] && empty ( $ result ['errors ' ] ) ) {
99+ echo '<div class="notice notice-success is-dismissible"><p> ' . esc_html__ ( 'Settings saved successfully. ' , 'visualizer ' ) . '</p></div> ' ;
100+ } elseif ( ! empty ( $ result ['errors ' ] ) ) {
101+ echo '<div class="notice notice-error is-dismissible"> ' ;
102+ echo '<p><strong> ' . esc_html__ ( 'Some API keys could not be saved: ' , 'visualizer ' ) . '</strong></p> ' ;
103+ echo '<ul style="list-style: disc; padding-left: 20px;"> ' ;
104+ foreach ( $ result ['errors ' ] as $ provider => $ error ) {
105+ echo '<li><strong> ' . esc_html ( ucfirst ( $ provider ) ) . ':</strong> ' . esc_html ( $ error ) . '</li> ' ;
106+ }
107+ echo '</ul> ' ;
108+ echo '</div> ' ;
109+ }
98110 }
99111
100112 // Get saved API keys
@@ -117,7 +129,11 @@ protected function _renderContent() {
117129 echo '<th scope="row"><label for="visualizer_openai_api_key"> ' . esc_html__ ( 'OpenAI API Key (ChatGPT) ' , 'visualizer ' ) . '</label></th> ' ;
118130 echo '<td> ' ;
119131 echo '<input type="text" id="visualizer_openai_api_key" name="visualizer_openai_api_key" value=" ' . esc_attr ( $ openai_key_display ) . '" class="regular-text" placeholder=" ' . esc_attr__ ( 'Enter API key ' , 'visualizer ' ) . '" autocomplete="off" /> ' ;
120- echo '<p class="description"> ' . esc_html__ ( 'Enter your OpenAI API key to enable ChatGPT integration. ' , 'visualizer ' ) . ' <a href="https://platform.openai.com/api-keys" target="_blank"> ' . esc_html__ ( 'Get API Key ' , 'visualizer ' ) . '</a></p> ' ;
132+ echo '<p class="description"> ' . esc_html__ ( 'Enter your OpenAI API key to enable ChatGPT integration. ' , 'visualizer ' ) . ' <a href="https://platform.openai.com/api-keys" target="_blank"> ' . esc_html__ ( 'Get API Key ' , 'visualizer ' ) . '</a> ' ;
133+ if ( ! empty ( $ openai_key ) ) {
134+ echo '<br> ' . esc_html__ ( 'To remove this key, clear the field and save. ' , 'visualizer ' );
135+ }
136+ echo '</p> ' ;
121137 echo '</td> ' ;
122138 echo '</tr> ' ;
123139
@@ -126,7 +142,11 @@ protected function _renderContent() {
126142 echo '<th scope="row"><label for="visualizer_gemini_api_key"> ' . esc_html__ ( 'Google Gemini API Key ' , 'visualizer ' ) . '</label></th> ' ;
127143 echo '<td> ' ;
128144 echo '<input type="text" id="visualizer_gemini_api_key" name="visualizer_gemini_api_key" value=" ' . esc_attr ( $ gemini_key_display ) . '" class="regular-text" placeholder=" ' . esc_attr__ ( 'Enter API key ' , 'visualizer ' ) . '" autocomplete="off" /> ' ;
129- echo '<p class="description"> ' . esc_html__ ( 'Enter your Google Gemini API key. ' , 'visualizer ' ) . ' <a href="https://makersuite.google.com/app/apikey" target="_blank"> ' . esc_html__ ( 'Get API Key ' , 'visualizer ' ) . '</a></p> ' ;
145+ echo '<p class="description"> ' . esc_html__ ( 'Enter your Google Gemini API key. ' , 'visualizer ' ) . ' <a href="https://makersuite.google.com/app/apikey" target="_blank"> ' . esc_html__ ( 'Get API Key ' , 'visualizer ' ) . '</a> ' ;
146+ if ( ! empty ( $ gemini_key ) ) {
147+ echo '<br> ' . esc_html__ ( 'To remove this key, clear the field and save. ' , 'visualizer ' );
148+ }
149+ echo '</p> ' ;
130150 echo '</td> ' ;
131151 echo '</tr> ' ;
132152
@@ -135,7 +155,11 @@ protected function _renderContent() {
135155 echo '<th scope="row"><label for="visualizer_claude_api_key"> ' . esc_html__ ( 'Anthropic Claude API Key ' , 'visualizer ' ) . '</label></th> ' ;
136156 echo '<td> ' ;
137157 echo '<input type="text" id="visualizer_claude_api_key" name="visualizer_claude_api_key" value=" ' . esc_attr ( $ claude_key_display ) . '" class="regular-text" placeholder=" ' . esc_attr__ ( 'Enter API key ' , 'visualizer ' ) . '" autocomplete="off" /> ' ;
138- echo '<p class="description"> ' . esc_html__ ( 'Enter your Anthropic Claude API key. ' , 'visualizer ' ) . ' <a href="https://console.anthropic.com/account/keys" target="_blank"> ' . esc_html__ ( 'Get API Key ' , 'visualizer ' ) . '</a></p> ' ;
158+ echo '<p class="description"> ' . esc_html__ ( 'Enter your Anthropic Claude API key. ' , 'visualizer ' ) . ' <a href="https://console.anthropic.com/account/keys" target="_blank"> ' . esc_html__ ( 'Get API Key ' , 'visualizer ' ) . '</a> ' ;
159+ if ( ! empty ( $ claude_key ) ) {
160+ echo '<br> ' . esc_html__ ( 'To remove this key, clear the field and save. ' , 'visualizer ' );
161+ }
162+ echo '</p> ' ;
139163 echo '</td> ' ;
140164 echo '</tr> ' ;
141165
@@ -156,43 +180,266 @@ protected function _renderContent() {
156180 echo '</div> ' ; // End wrap
157181 }
158182
183+ /**
184+ * Validates an OpenAI API key.
185+ *
186+ * @since 3.12.0
187+ *
188+ * @access private
189+ * @param string $api_key The API key to validate.
190+ * @return array{valid: bool, message: string} Validation result.
191+ */
192+ private function _validateOpenAIKey ( $ api_key ) {
193+ $ response = wp_remote_post (
194+ 'https://api.openai.com/v1/chat/completions ' ,
195+ array (
196+ 'headers ' => array (
197+ 'Authorization ' => 'Bearer ' . $ api_key ,
198+ 'Content-Type ' => 'application/json ' ,
199+ ),
200+ 'body ' => wp_json_encode (
201+ array (
202+ 'model ' => 'gpt-4o-mini ' ,
203+ 'messages ' => array (
204+ array (
205+ 'role ' => 'user ' ,
206+ 'content ' => 'test ' ,
207+ ),
208+ ),
209+ 'max_tokens ' => 5 ,
210+ )
211+ ),
212+ 'timeout ' => 10 ,
213+ )
214+ );
215+
216+ if ( is_wp_error ( $ response ) ) {
217+ return array (
218+ 'valid ' => false ,
219+ 'message ' => 'Connection error: ' . $ response ->get_error_message (),
220+ );
221+ }
222+
223+ $ code = wp_remote_retrieve_response_code ( $ response );
224+ $ body = json_decode ( wp_remote_retrieve_body ( $ response ), true );
225+
226+ if ( $ code === 200 ) {
227+ return array (
228+ 'valid ' => true ,
229+ 'message ' => 'API key is valid and working. ' ,
230+ );
231+ } elseif ( isset ( $ body ['error ' ]['message ' ] ) ) {
232+ return array (
233+ 'valid ' => false ,
234+ 'message ' => 'OpenAI error: ' . $ body ['error ' ]['message ' ],
235+ );
236+ } else {
237+ return array (
238+ 'valid ' => false ,
239+ 'message ' => 'Invalid API key or authentication failed. ' ,
240+ );
241+ }
242+ }
243+
244+ /**
245+ * Validates a Gemini API key.
246+ *
247+ * @since 3.12.0
248+ *
249+ * @access private
250+ * @param string $api_key The API key to validate.
251+ * @return array{valid: bool, message: string} Validation result.
252+ */
253+ private function _validateGeminiKey ( $ api_key ) {
254+ $ response = wp_remote_post (
255+ 'https://generativelanguage.googleapis.com/v1/models/gemini-1.5-flash:generateContent?key= ' . $ api_key ,
256+ array (
257+ 'headers ' => array (
258+ 'Content-Type ' => 'application/json ' ,
259+ ),
260+ 'body ' => wp_json_encode (
261+ array (
262+ 'contents ' => array (
263+ array (
264+ 'parts ' => array (
265+ array ( 'text ' => 'test ' ),
266+ ),
267+ ),
268+ ),
269+ )
270+ ),
271+ 'timeout ' => 10 ,
272+ )
273+ );
274+
275+ if ( is_wp_error ( $ response ) ) {
276+ return array (
277+ 'valid ' => false ,
278+ 'message ' => 'Connection error: ' . $ response ->get_error_message (),
279+ );
280+ }
281+
282+ $ code = wp_remote_retrieve_response_code ( $ response );
283+ $ body = json_decode ( wp_remote_retrieve_body ( $ response ), true );
284+
285+ if ( $ code === 200 ) {
286+ return array (
287+ 'valid ' => true ,
288+ 'message ' => 'API key is valid and working. ' ,
289+ );
290+ } elseif ( isset ( $ body ['error ' ]['message ' ] ) ) {
291+ return array (
292+ 'valid ' => false ,
293+ 'message ' => 'Gemini error: ' . $ body ['error ' ]['message ' ],
294+ );
295+ } else {
296+ return array (
297+ 'valid ' => false ,
298+ 'message ' => 'Invalid API key or authentication failed. ' ,
299+ );
300+ }
301+ }
302+
303+ /**
304+ * Validates a Claude API key.
305+ *
306+ * @since 3.12.0
307+ *
308+ * @access private
309+ * @param string $api_key The API key to validate.
310+ * @return array{valid: bool, message: string} Validation result.
311+ */
312+ private function _validateClaudeKey ( $ api_key ) {
313+ $ response = wp_remote_post (
314+ 'https://api.anthropic.com/v1/messages ' ,
315+ array (
316+ 'headers ' => array (
317+ 'x-api-key ' => $ api_key ,
318+ 'anthropic-version ' => '2023-06-01 ' ,
319+ 'Content-Type ' => 'application/json ' ,
320+ ),
321+ 'body ' => wp_json_encode (
322+ array (
323+ 'model ' => 'claude-3-haiku-20240307 ' ,
324+ 'max_tokens ' => 10 ,
325+ 'messages ' => array (
326+ array (
327+ 'role ' => 'user ' ,
328+ 'content ' => 'test ' ,
329+ ),
330+ ),
331+ )
332+ ),
333+ 'timeout ' => 10 ,
334+ )
335+ );
336+
337+ if ( is_wp_error ( $ response ) ) {
338+ return array (
339+ 'valid ' => false ,
340+ 'message ' => 'Connection error: ' . $ response ->get_error_message (),
341+ );
342+ }
343+
344+ $ code = wp_remote_retrieve_response_code ( $ response );
345+ $ body = json_decode ( wp_remote_retrieve_body ( $ response ), true );
346+
347+ if ( $ code === 200 ) {
348+ return array (
349+ 'valid ' => true ,
350+ 'message ' => 'API key is valid and working. ' ,
351+ );
352+ } elseif ( isset ( $ body ['error ' ]['message ' ] ) ) {
353+ return array (
354+ 'valid ' => false ,
355+ 'message ' => 'Claude error: ' . $ body ['error ' ]['message ' ],
356+ );
357+ } else {
358+ return array (
359+ 'valid ' => false ,
360+ 'message ' => 'Invalid API key or authentication failed. ' ,
361+ );
362+ }
363+ }
364+
159365 /**
160366 * Saves AI settings.
161367 *
162368 * @since 3.12.0
163369 *
164370 * @access private
165- * @return void
371+ * @return array{success: bool, errors: array<string, string>} Save result with any validation errors.
166372 */
167373 private function _saveSettings () {
374+ $ errors = array ();
375+ $ success = true ;
376+
168377 // Get current keys
169378 $ current_openai = get_option ( 'visualizer_openai_api_key ' , '' );
170379 $ current_gemini = get_option ( 'visualizer_gemini_api_key ' , '' );
171380 $ current_claude = get_option ( 'visualizer_claude_api_key ' , '' );
172381
173- // Only update OpenAI key if a new value is provided and it's not the masked version
174- if ( isset ( $ _POST ['visualizer_openai_api_key ' ] ) && ! empty ( $ _POST [ ' visualizer_openai_api_key ' ] ) ) {
382+ // Handle OpenAI key
383+ if ( isset ( $ _POST ['visualizer_openai_api_key ' ] ) ) {
175384 $ new_key = sanitize_text_field ( $ _POST ['visualizer_openai_api_key ' ] );
176- if ( $ new_key !== $ this ->_maskAPIKey ( $ current_openai ) ) {
177- update_option ( 'visualizer_openai_api_key ' , $ new_key );
385+
386+ // If empty, delete the key
387+ if ( empty ( $ new_key ) ) {
388+ delete_option ( 'visualizer_openai_api_key ' );
389+ } elseif ( $ new_key !== $ this ->_maskAPIKey ( $ current_openai ) ) {
390+ // New key provided, validate it
391+ $ validation = $ this ->_validateOpenAIKey ( $ new_key );
392+ if ( $ validation ['valid ' ] ) {
393+ update_option ( 'visualizer_openai_api_key ' , $ new_key );
394+ } else {
395+ $ errors ['openai ' ] = $ validation ['message ' ];
396+ $ success = false ;
397+ }
178398 }
179399 }
180400
181- // Only update Gemini key if a new value is provided and it's not the masked version
182- if ( isset ( $ _POST ['visualizer_gemini_api_key ' ] ) && ! empty ( $ _POST [ ' visualizer_gemini_api_key ' ] ) ) {
401+ // Handle Gemini key
402+ if ( isset ( $ _POST ['visualizer_gemini_api_key ' ] ) ) {
183403 $ new_key = sanitize_text_field ( $ _POST ['visualizer_gemini_api_key ' ] );
184- if ( $ new_key !== $ this ->_maskAPIKey ( $ current_gemini ) ) {
185- update_option ( 'visualizer_gemini_api_key ' , $ new_key );
404+
405+ // If empty, delete the key
406+ if ( empty ( $ new_key ) ) {
407+ delete_option ( 'visualizer_gemini_api_key ' );
408+ } elseif ( $ new_key !== $ this ->_maskAPIKey ( $ current_gemini ) ) {
409+ // New key provided, validate it
410+ $ validation = $ this ->_validateGeminiKey ( $ new_key );
411+ if ( $ validation ['valid ' ] ) {
412+ update_option ( 'visualizer_gemini_api_key ' , $ new_key );
413+ } else {
414+ $ errors ['gemini ' ] = $ validation ['message ' ];
415+ $ success = false ;
416+ }
186417 }
187418 }
188419
189- // Only update Claude key if a new value is provided and it's not the masked version
190- if ( isset ( $ _POST ['visualizer_claude_api_key ' ] ) && ! empty ( $ _POST [ ' visualizer_claude_api_key ' ] ) ) {
420+ // Handle Claude key
421+ if ( isset ( $ _POST ['visualizer_claude_api_key ' ] ) ) {
191422 $ new_key = sanitize_text_field ( $ _POST ['visualizer_claude_api_key ' ] );
192- if ( $ new_key !== $ this ->_maskAPIKey ( $ current_claude ) ) {
193- update_option ( 'visualizer_claude_api_key ' , $ new_key );
423+
424+ // If empty, delete the key
425+ if ( empty ( $ new_key ) ) {
426+ delete_option ( 'visualizer_claude_api_key ' );
427+ } elseif ( $ new_key !== $ this ->_maskAPIKey ( $ current_claude ) ) {
428+ // New key provided, validate it
429+ $ validation = $ this ->_validateClaudeKey ( $ new_key );
430+ if ( $ validation ['valid ' ] ) {
431+ update_option ( 'visualizer_claude_api_key ' , $ new_key );
432+ } else {
433+ $ errors ['claude ' ] = $ validation ['message ' ];
434+ $ success = false ;
435+ }
194436 }
195437 }
438+
439+ return array (
440+ 'success ' => $ success ,
441+ 'errors ' => $ errors ,
442+ );
196443 }
197444
198445}
0 commit comments