Skip to content

Commit 5a4ee44

Browse files
Add API key deletion and validation on save
Implemented the ability to delete API keys and validate them when saving: API Key Deletion: - Users can now clear an API key field and save to remove it completely - Empty values are treated as deletion requests using delete_option() - Added helpful hint "To remove this key, clear the field and save" under each existing key - Keys return to empty state just like fresh plugin installation API Key Validation: - Added validation methods for OpenAI, Gemini, and Claude API keys - Each key is tested with a minimal API call when saved - Validation uses appropriate test endpoints: * OpenAI: gpt-4o-mini model with 5 max tokens * Gemini: gemini-1.5-flash model * Claude: claude-3-haiku model with 10 max tokens - Empty keys skip validation (treated as deletion) - Masked keys skip validation (unchanged) User Feedback: - Success message shown when all keys save successfully - Error message displays validation failures with specific error details - Each provider error shown separately with clear messaging - Failed keys are not saved to database Error Handling: - Connection errors caught and reported - API errors from providers displayed to user - Invalid authentication clearly indicated - Timeout set to 10 seconds for validation requests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent fdec9e7 commit 5a4ee44

1 file changed

Lines changed: 265 additions & 18 deletions

File tree

classes/Visualizer/Render/Page/AISettings.php

Lines changed: 265 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)