PluginProbe
Auto Alt Text / trunk
Auto Alt Text vtrunk
3.0.3 2.8.2 1.3.1 1.3.2 2.0.0 2.1.0 2.1.1 2.2.0 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.4.2 2.5.0 2.5.1 2.5.2 2.5.3 2.6.0 2.6.1 2.7.0 2.8.0 2.8.1 All 28 releases
← All changes | src/App/Admin/PluginOptions.php +515 -17 2.3.4trunk View file →
@@ -2,10 +2,16 @@
2 2
3 3 namespace AATXT\App\Admin;
4 4
5 5 use AATXT\App\Logging\DBLogger;
6 +use AATXT\App\AIProviders\Anthropic\AnthropicModelsRegistry;
7 +use AATXT\App\AIProviders\Gemini\GeminiModelsRegistry;
8 +use AATXT\App\AIProviders\OpenAI\OpenAIModelsRegistry;
6 9 use AATXT\App\AIProviders\Azure\AzureTranslator;
10 +use AATXT\App\Configuration\AzureConfig;
11 +use AATXT\App\Core\Container;
7 12 use AATXT\App\Exceptions\Azure\AzureTranslateInstanceException;
13 +use AATXT\App\Infrastructure\Http\WordPressHttpClient;
8 14 use AATXT\App\Utilities\AssetsManager;
9 15 use AATXT\App\Utilities\Encryption;
10 16 use AATXT\Config\Constants;
11 17
@@ -30,21 +36,201 @@
30 36 }
31 37
32 38 self::$assetsManager = AssetsManager::make();
33 39
40 + add_action('admin_notices', [self::$instance, 'showEncryptionConstantsNotice']);
41 +
34 42 add_action('admin_enqueue_scripts', [self::$instance, 'enqueueAdminScripts'], 1);
35 43 add_action('admin_menu', [self::$instance, 'addOptionsPageToTheMenu']);
36 - add_action('admin_init', [self::$instance, 'setupPluginOptions']);
44 + add_action('admin_init', [self::$instance, 'setupPluginOptions'], 10);
45 + add_action('admin_init', [self::$instance, 'migrateEncryptionKeys'], 9);
37 46
38 47 // Encrypt API Keys on update
39 48 add_action('pre_update_option_' . Constants::AATXT_OPTION_FIELD_API_KEY_AZURE_COMPUTER_VISION, [self::$instance, 'encryptDataOnUpdate'], 10, 3);
40 49 add_action('pre_update_option_' . Constants::AATXT_OPTION_FIELD_API_KEY_AZURE_TRANSLATE_INSTANCE, [self::$instance, 'encryptDataOnUpdate'], 10, 3);
41 50 add_action('pre_update_option_' . Constants::AATXT_OPTION_FIELD_API_KEY_OPENAI, [self::$instance, 'encryptDataOnUpdate'], 10, 3);
51 + add_action('pre_update_option_' . Constants::AATXT_OPTION_FIELD_API_KEY_ANTHROPIC, [self::$instance, 'encryptDataOnUpdate'], 10, 3);
52 + add_action('pre_update_option_' . Constants::AATXT_OPTION_FIELD_API_KEY_GEMINI, [self::$instance, 'encryptDataOnUpdate'], 10, 3);
42 53
43 54 add_action('admin_notices', [self::$instance, 'encryptionErrorNotice']);
55 + add_action('admin_notices', [self::$instance, 'anthropicModelUnavailableNotice']);
56 + add_action('admin_notices', [self::$instance, 'openAiModelUnavailableNotice']);
57 + add_action('admin_notices', [self::$instance, 'geminiModelUnavailableNotice']);
58 +
59 + // Bust the cached Anthropic models list whenever the API key changes,
60 + // so the next admin page load fetches the model list with the new credentials.
61 + add_action('update_option_' . Constants::AATXT_OPTION_FIELD_API_KEY_ANTHROPIC, [self::$instance, 'flushAnthropicModelsCache']);
62 + add_action('add_option_' . Constants::AATXT_OPTION_FIELD_API_KEY_ANTHROPIC, [self::$instance, 'flushAnthropicModelsCache']);
63 +
64 + // Same for the cached OpenAI models list.
65 + add_action('update_option_' . Constants::AATXT_OPTION_FIELD_API_KEY_OPENAI, [self::$instance, 'flushOpenAiModelsCache']);
66 + add_action('add_option_' . Constants::AATXT_OPTION_FIELD_API_KEY_OPENAI, [self::$instance, 'flushOpenAiModelsCache']);
67 +
68 + // Same for the cached Gemini models list.
69 + add_action('update_option_' . Constants::AATXT_OPTION_FIELD_API_KEY_GEMINI, [self::$instance, 'flushGeminiModelsCache']);
70 + add_action('add_option_' . Constants::AATXT_OPTION_FIELD_API_KEY_GEMINI, [self::$instance, 'flushGeminiModelsCache']);
44 71 }
45 72
46 73 /**
74 + * Resolve the Anthropic models registry from the DI container.
75 + */
76 + private static function anthropicModelsRegistry(): AnthropicModelsRegistry
77 + {
78 + return Container::make()->get(AnthropicModelsRegistry::class);
79 + }
80 +
81 + public function flushAnthropicModelsCache(): void
82 + {
83 + self::anthropicModelsRegistry()->flushCache();
84 + }
85 +
86 + /**
87 + * Resolve the OpenAI models registry from the DI container.
88 + */
89 + private static function openAiModelsRegistry(): OpenAIModelsRegistry
90 + {
91 + return Container::make()->get(OpenAIModelsRegistry::class);
92 + }
93 +
94 + public function flushOpenAiModelsCache(): void
95 + {
96 + self::openAiModelsRegistry()->flushCache();
97 + }
98 +
99 + /**
100 + * Resolve the Gemini models registry from the DI container.
101 + */
102 + private static function geminiModelsRegistry(): GeminiModelsRegistry
103 + {
104 + return Container::make()->get(GeminiModelsRegistry::class);
105 + }
106 +
107 + public function flushGeminiModelsCache(): void
108 + {
109 + self::geminiModelsRegistry()->flushCache();
110 + }
111 +
112 + /**
113 + * Warn the admin if the OpenAI model currently saved in the options
114 + * is no longer returned by the registry (typically because OpenAI
115 + * has retired it). The user has to pick a replacement; in the meantime
116 + * OpenAIVision falls back to the least expensive available model so
117 + * generation keeps working.
118 + */
119 + public function openAiModelUnavailableNotice(): void
120 + {
121 + if (!current_user_can('manage_options')) {
122 + return;
123 + }
124 +
125 + $savedModel = get_option(Constants::AATXT_OPTION_FIELD_MODEL_OPENAI);
126 + if (!is_string($savedModel) || $savedModel === '') {
127 + return;
128 + }
129 +
130 + $registry = self::openAiModelsRegistry();
131 + if (!$registry->hasApiKey()) {
132 + return;
133 + }
134 +
135 + if ($registry->isAvailable($savedModel)) {
136 + return;
137 + }
138 +
139 + $settingsUrl = esc_url(menu_page_url(Constants::AATXT_PLUGIN_OPTIONS_PAGE_SLUG, false));
140 +
141 + echo '<div class="notice notice-warning is-dismissible">';
142 + echo '<p><strong>' . esc_html__('Auto Alt Text', 'auto-alt-text') . ':</strong> ';
143 + printf(
144 + /* translators: %s is the model id that is no longer available */
145 + esc_html__('the OpenAI model "%s" you previously selected is no longer available. Please choose a new model in the plugin settings.', 'auto-alt-text'),
146 + esc_html($savedModel)
147 + );
148 + echo ' <a href="' . $settingsUrl . '">' . esc_html__('Go to settings page', 'auto-alt-text') . '</a>.';
149 + echo '</p></div>';
150 + }
151 +
152 + /**
153 + * Warn the admin if the Anthropic model currently saved in the options
154 + * is no longer returned by the registry (typically because Anthropic
155 + * has retired it). The user has to pick a replacement; in the meantime
156 + * AnthropicResponse falls back to the most recent available model so
157 + * generation keeps working.
158 + */
159 + public function anthropicModelUnavailableNotice(): void
160 + {
161 + if (!current_user_can('manage_options')) {
162 + return;
163 + }
164 +
165 + $savedModel = get_option(Constants::AATXT_OPTION_FIELD_MODEL_ANTHROPIC);
166 + if (!is_string($savedModel) || $savedModel === '') {
167 + return;
168 + }
169 +
170 + $registry = self::anthropicModelsRegistry();
171 + if (!$registry->hasApiKey()) {
172 + return;
173 + }
174 +
175 + if ($registry->isAvailable($savedModel)) {
176 + return;
177 + }
178 +
179 + $settingsUrl = esc_url(menu_page_url(Constants::AATXT_PLUGIN_OPTIONS_PAGE_SLUG, false));
180 +
181 + echo '<div class="notice notice-warning is-dismissible">';
182 + echo '<p><strong>' . esc_html__('Auto Alt Text', 'auto-alt-text') . ':</strong> ';
183 + printf(
184 + /* translators: %s is the model id that is no longer available */
185 + esc_html__('the Anthropic model "%s" you previously selected is no longer available. Please choose a new model in the plugin settings.', 'auto-alt-text'),
186 + esc_html($savedModel)
187 + );
188 + echo ' <a href="' . $settingsUrl . '">' . esc_html__('Go to settings page', 'auto-alt-text') . '</a>.';
189 + echo '</p></div>';
190 + }
191 +
192 + /**
193 + * Warn the admin if the Gemini model currently saved in the options
194 + * is no longer returned by the registry (typically because Google
195 + * has retired it). The user has to pick a replacement; in the meantime
196 + * GeminiResponse falls back to the least expensive available model so
197 + * generation keeps working.
198 + */
199 + public function geminiModelUnavailableNotice(): void
200 + {
201 + if (!current_user_can('manage_options')) {
202 + return;
203 + }
204 +
205 + $savedModel = get_option(Constants::AATXT_OPTION_FIELD_MODEL_GEMINI);
206 + if (!is_string($savedModel) || $savedModel === '') {
207 + return;
208 + }
209 +
210 + $registry = self::geminiModelsRegistry();
211 + if (!$registry->hasApiKey()) {
212 + return;
213 + }
214 +
215 + if ($registry->isAvailable($savedModel)) {
216 + return;
217 + }
218 +
219 + $settingsUrl = esc_url(menu_page_url(Constants::AATXT_PLUGIN_OPTIONS_PAGE_SLUG, false));
220 +
221 + echo '<div class="notice notice-warning is-dismissible">';
222 + echo '<p><strong>' . esc_html__('Auto Alt Text', 'auto-alt-text') . ':</strong> ';
223 + printf(
224 + /* translators: %s is the model id that is no longer available */
225 + esc_html__('the Gemini model "%s" you previously selected is no longer available. Please choose a new model in the plugin settings.', 'auto-alt-text'),
226 + esc_html($savedModel)
227 + );
228 + echo ' <a href="' . $settingsUrl . '">' . esc_html__('Go to settings page', 'auto-alt-text') . '</a>.';
229 + echo '</p></div>';
230 + }
231 +
232 + /**
47 233 * Show an error notice if the encryption of the API Key fails
48 234 * @return void
49 235 */
50 236 public function encryptionErrorNotice(): void
@@ -60,16 +246,16 @@
60 246 $settingsUrl = esc_url( menu_page_url(Constants::AATXT_PLUGIN_OPTIONS_PAGE_SLUG, false) );
61 247
62 248 echo '<div class="notice notice-error is-dismissible">';
63 249 echo '<p>';
64 - // Titolo in grassetto
250 +
65 251 echo '<strong>' . esc_html__('Auto Alt Text', 'auto-alt-text') . ':</strong> ';
66 - // Messaggio di errore
252 +
67 253 echo esc_html__(
68 254 'There was a problem with the encryption of your API Key for alt text generation. Please re-enter the key and save.',
69 255 'auto-alt-text'
70 256 );
71 - // Link condizionale
257 +
72 258 if ($showLink) {
73 259 echo ' <a href="' . $settingsUrl . '">'
74 260 . esc_html__('Go to settings page', 'auto-alt-text')
75 261 . '</a>.';
@@ -78,16 +264,83 @@
78 264 echo '</div>';
79 265 }
80 266 }
81 267
268 + /**
269 + * Print an admin notice containing the defines to be added
270 + * to the wp-config.php.
271 + */
272 + public function showEncryptionConstantsNotice(): void
273 + {
274 + if ( ! is_admin() || ! current_user_can('manage_options') ) {
275 + return;
276 + }
82 277
278 + $screen = function_exists('get_current_screen') ? get_current_screen() : null;
279 + if ( ! $screen || $screen->id !== 'toplevel_page_' . Constants::AATXT_PLUGIN_OPTIONS_PAGE_SLUG ) {
280 + return;
281 + }
282 +
283 + if ( defined('AATXT_ENCRYPTION_KEY') && defined('AATXT_ENCRYPTION_SALT') ) {
284 + return;
285 + }
286 +
287 + $suggestedKey = bin2hex(random_bytes(32));
288 + $suggestedSalt = bin2hex(random_bytes(32));
289 + ?>
290 + <div class="notice notice-info is-dismissible">
291 + <p>
292 + <?php
293 + printf(
294 + /* translators: “Optional” label */
295 + __( '%s: you can add two lines to your wp-config.php to make your API key more resilient if WordPress salts ever change.', 'auto-alt-text' ),
296 + '<strong>' . esc_html__( 'Optional', 'auto-alt-text' ) . '</strong>'
297 + );
298 + ?>
299 + </p>
300 +<pre style="background:#f5f5f5; padding:10px; border-radius:4px;">
301 +define('AATXT_ENCRYPTION_KEY', '<?php echo esc_html( $suggestedKey ); ?>');
302 +define('AATXT_ENCRYPTION_SALT', '<?php echo esc_html( $suggestedSalt ); ?>');
303 +</pre>
304 + <p>
305 + <?php esc_html_e(
306 + 'Just paste them before the line “/* That\'s all, stop editing! Happy publishing. */” in wp-config.php.',
307 + 'auto-alt-text'
308 + ); ?>
309 + </p>
310 + <p>
311 + <?php esc_html_e(
312 + "If you don't have the ability to edit the wp-config.php, don't worry because the plugin will still work without this change. If your site's salting keys change in the future, you will simply need to resave your API Key in the options below.",
313 + "auto-alt-text"
314 + ); ?>
315 + </p>
316 + </div>
317 + <?php
318 + }
319 +
320 + public function migrateEncryptionKeys(): void
321 + {
322 + if ( ! defined('AATXT_ENCRYPTION_KEY') || ! defined('AATXT_ENCRYPTION_SALT') ) {
323 + return;
324 + }
325 +
326 + update_option(Constants::AATXT_LEGACY_ENCRYPTION_MIGRATION_DONE, '0');
327 +
328 + if ( get_option(Constants::AATXT_LEGACY_ENCRYPTION_MIGRATION_DONE) ) {
329 + return;
330 + }
331 +
332 + Encryption::make()->migrateLegacyApiKeys();
333 +
334 + update_option(Constants::AATXT_LEGACY_ENCRYPTION_MIGRATION_DONE, '1');
335 + }
336 +
83 337 /**
84 338 * Encrypt data
85 339 * @param ?string $newValue
86 - * @param ?string $oldValue
87 340 * @return ?string
88 341 */
89 - public function encryptDataOnUpdate(?string $newValue, ?string $oldValue): ?string
342 + public function encryptDataOnUpdate(?string $newValue): ?string
90 343 {
91 344 if (!empty($newValue)) {
92 345 $newValue = (Encryption::make())->encrypt($newValue);
93 346 }
@@ -141,9 +394,11 @@
141 394 <br>
142 395 <?php esc_html_e("Each line corresponds to an error generated by a call to the Azure or OpenAI API after uploading an image.", 'auto-alt-text'); ?>
143 396 </p>
144 397 <?php
145 - $logs = DBLogger::make()->getImageLog();
398 + $container = Container::make();
399 + $logger = $container->get(DBLogger::class);
400 + $logs = $logger->getImageLog();
146 401 if ($logs) {
147 402 echo '<textarea id="error-log" name="error-log" readonly>' . esc_textarea($logs) . '</textarea>';
148 403 } else {
149 404 echo '<p>' . esc_html__('There is no error log yet!', 'auto-alt-text') . '</p>';
@@ -175,8 +430,11 @@
175 430 <li>
176 431 <strong><?php esc_html_e("OpenAI's APIs", 'auto-alt-text'); ?></strong>: <?php esc_html_e("the image will be analyzed by the AI services provided by OpenAI and an alt text will be generated based on the prompt you set;", 'auto-alt-text'); ?>
177 432 </li>
178 433 <li>
434 + <strong><?php esc_html_e("Google Gemini's APIs", 'auto-alt-text'); ?></strong>: <?php esc_html_e("the image will be analyzed by the AI services provided by Google and an alt text will be generated based on the prompt you set;", 'auto-alt-text'); ?>
435 + </li>
436 + <li>
179 437 <strong><?php esc_html_e("Azure's APIs", 'auto-alt-text'); ?></strong>: <?php esc_html_e("the image will be analyzed by the AI services provided by Azure and an alt text will be generated in the language of your choice;", 'auto-alt-text'); ?>
180 438 </li>
181 439 <li>
182 440 <strong><?php esc_html_e("Title of the article (not AI)", 'auto-alt-text'); ?></strong>: <?php esc_html_e("if the image is uploaded within an article, the title of the article will be used as alt text;", 'auto-alt-text'); ?>
@@ -206,8 +464,10 @@
206 464 <select name="<?php echo esc_attr(Constants::AATXT_OPTION_FIELD_TYPOLOGY); ?>"
207 465 id="<?php echo esc_attr(Constants::AATXT_OPTION_FIELD_TYPOLOGY); ?>">
208 466 <option value="<?php echo esc_attr(Constants::AATXT_OPTION_TYPOLOGY_DEACTIVATED); ?>"<?php echo esc_attr(self::selected($typology, Constants::AATXT_OPTION_TYPOLOGY_DEACTIVATED)); ?>><?php esc_html_e("Deactivated", 'auto-alt-text'); ?></option>
209 467 <option value="<?php echo esc_attr(Constants::AATXT_OPTION_TYPOLOGY_CHOICE_OPENAI); ?>"<?php echo esc_attr(self::selected($typology, Constants::AATXT_OPTION_TYPOLOGY_CHOICE_OPENAI)); ?>><?php esc_html_e("OpenAI's APIs", 'auto-alt-text'); ?></option>
468 + <option value="<?php echo esc_attr(Constants::AATXT_OPTION_TYPOLOGY_CHOICE_ANTHROPIC); ?>"<?php echo esc_attr(self::selected($typology, Constants::AATXT_OPTION_TYPOLOGY_CHOICE_ANTHROPIC)); ?>><?php esc_html_e("Antrhopic's APIs", 'auto-alt-text'); ?></option>
469 + <option value="<?php echo esc_attr(Constants::AATXT_OPTION_TYPOLOGY_CHOICE_GEMINI); ?>"<?php echo esc_attr(self::selected($typology, Constants::AATXT_OPTION_TYPOLOGY_CHOICE_GEMINI)); ?>><?php esc_html_e("Google Gemini's APIs", 'auto-alt-text'); ?></option>
210 470 <option value="<?php echo esc_attr(Constants::AATXT_OPTION_TYPOLOGY_CHOICE_AZURE); ?>"<?php echo esc_attr(self::selected($typology, Constants::AATXT_OPTION_TYPOLOGY_CHOICE_AZURE)); ?>><?php esc_html_e("Azure's APIs", 'auto-alt-text'); ?></option>
211 471 <option value="<?php echo esc_attr(Constants::AATXT_OPTION_TYPOLOGY_CHOICE_ARTICLE_TITLE); ?>"<?php echo esc_attr(self::selected($typology, Constants::AATXT_OPTION_TYPOLOGY_CHOICE_ARTICLE_TITLE)); ?>><?php esc_html_e("Title of the article (not AI)", 'auto-alt-text'); ?></option>
212 472 <option value="<?php echo esc_attr(Constants::AATXT_OPTION_TYPOLOGY_CHOICE_ATTACHMENT_TITLE); ?>"<?php echo esc_attr(self::selected($typology, Constants::AATXT_OPTION_TYPOLOGY_CHOICE_ATTACHMENT_TITLE)); ?>><?php esc_html_e("Title of the attachment (not AI)", 'auto-alt-text'); ?></option>
213 473 </select>
@@ -219,22 +479,60 @@
219 479 esc_html__('Therefore, the alt text "Auto draft" will be inserted. To avoid this behavior, save the article draft first and then upload the image.', 'auto-alt-text') .
220 480 '</div>';
221 481
222 482 $openaiModel = self::openAiModel();
483 + $openaiRegistry = self::openAiModelsRegistry();
223 484
224 485 echo '<div class="plugin-option type-openai">';
225 486 echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_OPENAI) . '">' . esc_html__('Model', 'auto-alt-text') . '</label>';
226 487
227 - echo '<select name="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_OPENAI) . '" id="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_OPENAI) . '">';
228 - foreach(Constants::AATXT_OPTION_FIELD_MODEL_OPENAI_OPTIONS as $key => $value) {
229 - echo '<option value="' . esc_attr($key) . '" ' . esc_attr(self::selected($openaiModel, $key)) . '>' . esc_html($value) . '</option>';
488 + if (!$openaiRegistry->hasApiKey()) {
489 + echo '<p class="description">' . esc_html__('Enter and save your OpenAI API Key to load the list of available models.', 'auto-alt-text') . '</p>';
490 + // The <select> below is disabled and therefore not submitted with
491 + // the form; this hidden field re-submits the currently stored model
492 + // so saving the settings page does not wipe the user's choice.
493 + $rawSavedModel = get_option(Constants::AATXT_OPTION_FIELD_MODEL_OPENAI);
494 + $rawSavedModel = is_string($rawSavedModel) ? $rawSavedModel : '';
495 + echo '<input type="hidden" name="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_OPENAI) . '" value="' . esc_attr($rawSavedModel) . '">';
496 + echo '<select id="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_OPENAI) . '" disabled>';
497 + if ($openaiModel !== '') {
498 + echo '<option value="' . esc_attr($openaiModel) . '" selected>' . esc_html($openaiModel) . '</option>';
499 + } else {
500 + echo '<option value="" selected>' . esc_html__('— API Key required —', 'auto-alt-text') . '</option>';
501 + }
502 + echo '</select>';
503 + } else {
504 + $availableModels = $openaiRegistry->getAvailableModels();
505 +
506 + // Read the raw saved value (not self::openAiModel(), which falls
507 + // back to the static gpt-4o default). This lets us tell "nothing
508 + // saved yet" apart from "gpt-4o explicitly chosen": on a fresh
509 + // install we pre-select the registry's default (least expensive
510 + // available) model instead of flagging gpt-4o as no longer
511 + // available for a model the user never picked.
512 + $savedModel = get_option(Constants::AATXT_OPTION_FIELD_MODEL_OPENAI);
513 + $savedModel = is_string($savedModel) ? $savedModel : '';
514 + $selectedModel = $savedModel !== '' ? $savedModel : $openaiRegistry->getDefaultModel();
515 + $savedIsLegacy = $savedModel !== '' && !array_key_exists($savedModel, $availableModels);
516 +
517 + echo '<select name="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_OPENAI) . '" id="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_OPENAI) . '">';
518 + foreach ($availableModels as $key => $value) {
519 + echo '<option value="' . esc_attr($key) . '" ' . esc_attr(self::selected($selectedModel, $key)) . '>' . esc_html($value) . '</option>';
520 + }
521 + if ($savedIsLegacy) {
522 + $legacyLabel = sprintf(
523 + /* translators: %s is the saved model id */
524 + esc_html__('%s (no longer available)', 'auto-alt-text'),
525 + $savedModel
526 + );
527 + echo '<option value="' . esc_attr($savedModel) . '" selected>' . esc_html($legacyLabel) . '</option>';
528 + }
529 + echo '</select>';
230 530 }
231 - echo '</select>';
232 531 echo '</div>';
233 532
234 533 echo '<div class="plugin-option type-openai"><strong>' . esc_html__('Notice', 'auto-alt-text') . '</strong>: ' .
235 - esc_html__('Rarely it may happen that the chosen model fails to generate correct alt text for the image.', 'auto-alt-text') . ' ' .
236 - esc_html__('In these cases, the call to the api will be re-performed using the gpt-4o-mini model as fallback.', 'auto-alt-text') . '<br>' .
534 + esc_html__('If the model you selected is retired by OpenAI and is no longer available, the least expensive available model will be used as fallback until you choose a new one.', 'auto-alt-text') . '<br>' .
237 535 esc_html__('In case of errors, it is still possible to find the specific reason stated on the', 'auto-alt-text') . ' <a href="' . esc_url(menu_page_url(Constants::AATXT_PLUGIN_OPTION_LOG_PAGE_SLUG, false)) . '">' . esc_html__('error log page', 'auto-alt-text') . '</a>.' .
238 536 '</div>';
239 537
240 538 echo '<div class="plugin-option type-openai">';
@@ -301,9 +599,21 @@
301 599 echo '<p class="description">' . esc_html__("Select the language in which the alt text should be written.", 'auto-alt-text') . '</p>';
302 600 $currentLanguage = get_option(Constants::AATXT_OPTION_FIELD_LANGUAGE_AZURE_TRANSLATE_INSTANCE);
303 601
304 602 try {
305 - $supportedLanguages = (AzureTranslator::make())->supportedLanguages();
603 + $httpClient = new WordPressHttpClient();
604 + $azureConfig = new AzureConfig(
605 + self::apiKeyAzureComputerVision(),
606 + self::endpointAzureComputerVision(),
607 + '',
608 + '',
609 + self::apiKeyAzureTranslateInstance(),
610 + self::endpointAzureTranslateInstance(),
611 + self::regionAzureTranslateInstance(),
612 + self::languageAzureTranslateInstance()
613 + );
614 + $azureTranslator = new AzureTranslator($httpClient, $azureConfig);
615 + $supportedLanguages = $azureTranslator->supportedLanguages();
306 616 } catch (AzureTranslateInstanceException $e) {
307 617 $supportedLanguages = [
308 618 "en" => [
309 619 "name" => "English",
@@ -330,9 +640,126 @@
330 640
331 641 echo '</div>';
332 642 endif;
333 643
334 - echo '<div class="plugin-option type-article-title type-attachment-title type-openai type-azure">';
644 + echo '<div class="plugin-option type-anthropic">';
645 + echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_API_KEY_ANTHROPIC) . '">' . esc_html__('Antrhopic API Key', 'auto-alt-text') . '</label>';
646 + echo '<p class="description">' . esc_html__("Enter your API Key", 'auto-alt-text') . '</p>';
647 + $apiKey = get_option(Constants::AATXT_OPTION_FIELD_API_KEY_ANTHROPIC);
648 + echo '<input type="password" name="' . esc_attr(Constants::AATXT_OPTION_FIELD_API_KEY_ANTHROPIC) . '" value="' . esc_attr((Encryption::make())->decrypt($apiKey)) . '" />';
649 + echo '</div>';
650 +
651 + $anthropicModel = self::anthropicModel();
652 + $anthropicRegistry = self::anthropicModelsRegistry();
653 +
654 + echo '<div class="plugin-option type-anthropic">';
655 + echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_ANTHROPIC) . '">' . esc_html__('Model', 'auto-alt-text') . '</label>';
656 +
657 + if (!$anthropicRegistry->hasApiKey()) {
658 + echo '<p class="description">' . esc_html__('Enter and save your Anthropic API Key to load the list of available models.', 'auto-alt-text') . '</p>';
659 + // The <select> below is disabled and therefore not submitted with
660 + // the form; this hidden field re-submits the currently stored model
661 + // so saving the settings page does not wipe the user's choice.
662 + $rawSavedModel = get_option(Constants::AATXT_OPTION_FIELD_MODEL_ANTHROPIC);
663 + $rawSavedModel = is_string($rawSavedModel) ? $rawSavedModel : '';
664 + echo '<input type="hidden" name="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_ANTHROPIC) . '" value="' . esc_attr($rawSavedModel) . '">';
665 + echo '<select id="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_ANTHROPIC) . '" disabled>';
666 + if ($anthropicModel !== '') {
667 + echo '<option value="' . esc_attr($anthropicModel) . '" selected>' . esc_html($anthropicModel) . '</option>';
668 + } else {
669 + echo '<option value="" selected>' . esc_html__('— API Key required —', 'auto-alt-text') . '</option>';
670 + }
671 + echo '</select>';
672 + } else {
673 + $availableModels = $anthropicRegistry->getAvailableModels();
674 + $savedIsLegacy = $anthropicModel !== '' && !array_key_exists($anthropicModel, $availableModels);
675 +
676 + echo '<select name="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_ANTHROPIC) . '" id="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_ANTHROPIC) . '">';
677 + foreach ($availableModels as $key => $value) {
678 + echo '<option value="' . esc_attr($key) . '" ' . esc_attr(self::selected($anthropicModel, $key)) . '>' . esc_html($value) . '</option>';
679 + }
680 + if ($savedIsLegacy) {
681 + $legacyLabel = sprintf(
682 + /* translators: %s is the saved model id */
683 + esc_html__('%s (no longer available)', 'auto-alt-text'),
684 + $anthropicModel
685 + );
686 + echo '<option value="' . esc_attr($anthropicModel) . '" selected>' . esc_html($legacyLabel) . '</option>';
687 + }
688 + echo '</select>';
689 + }
690 + echo '</div>';
691 +
692 + echo '<div class="plugin-option type-anthropic">';
693 + echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_PROMPT_ANTHROPIC) . '">' . esc_html__('Prompt', 'auto-alt-text') . '</label>';
694 + echo '<p class="description">' . esc_html__("Enter a specific and detailed prompt according to your needs.", 'auto-alt-text') . '</p>';
695 + $defaultPrompt = sprintf(esc_html__("Act like an SEO expert and write an alt text of up to 125 characters for this image. Return only the complete alt text.", 'auto-alt-text'), Constants::AATXT_IMAGE_URL_TAG);
696 + $prompt = get_option(Constants::AATXT_OPTION_FIELD_PROMPT_ANTHROPIC) ?: $defaultPrompt;
697 + echo '<textarea name="' . esc_attr(Constants::AATXT_OPTION_FIELD_PROMPT_ANTHROPIC) . '" rows="5" cols="50">' . esc_textarea($prompt) . '</textarea>';
698 + echo '</div>';
699 +
700 + echo '<div class="plugin-option type-gemini">';
701 + echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_API_KEY_GEMINI) . '">' . esc_html__('Gemini API Key', 'auto-alt-text') . '</label>';
702 + echo '<p class="description">' . esc_html__("Enter your API Key", 'auto-alt-text') . '</p>';
703 + $apiKey = get_option(Constants::AATXT_OPTION_FIELD_API_KEY_GEMINI);
704 + echo '<input type="password" name="' . esc_attr(Constants::AATXT_OPTION_FIELD_API_KEY_GEMINI) . '" value="' . esc_attr((Encryption::make())->decrypt($apiKey)) . '" />';
705 + echo '</div>';
706 +
707 + $geminiModel = self::geminiModel();
708 + $geminiRegistry = self::geminiModelsRegistry();
709 +
710 + echo '<div class="plugin-option type-gemini">';
711 + echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_GEMINI) . '">' . esc_html__('Model', 'auto-alt-text') . '</label>';
712 +
713 + if (!$geminiRegistry->hasApiKey()) {
714 + echo '<p class="description">' . esc_html__('Enter and save your Gemini API Key to load the list of available models.', 'auto-alt-text') . '</p>';
715 + // The <select> below is disabled and therefore not submitted with
716 + // the form; this hidden field re-submits the currently stored model
717 + // so saving the settings page does not wipe the user's choice.
718 + $rawSavedModel = get_option(Constants::AATXT_OPTION_FIELD_MODEL_GEMINI);
719 + $rawSavedModel = is_string($rawSavedModel) ? $rawSavedModel : '';
720 + echo '<input type="hidden" name="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_GEMINI) . '" value="' . esc_attr($rawSavedModel) . '">';
721 + echo '<select id="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_GEMINI) . '" disabled>';
722 + if ($geminiModel !== '') {
723 + echo '<option value="' . esc_attr($geminiModel) . '" selected>' . esc_html($geminiModel) . '</option>';
724 + } else {
725 + echo '<option value="" selected>' . esc_html__('— API Key required —', 'auto-alt-text') . '</option>';
726 + }
727 + echo '</select>';
728 + } else {
729 + $availableModels = $geminiRegistry->getAvailableModels();
730 + $savedIsLegacy = $geminiModel !== '' && !array_key_exists($geminiModel, $availableModels);
731 +
732 + echo '<select name="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_GEMINI) . '" id="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_GEMINI) . '">';
733 + foreach ($availableModels as $key => $value) {
734 + echo '<option value="' . esc_attr($key) . '" ' . esc_attr(self::selected($geminiModel, $key)) . '>' . esc_html($value) . '</option>';
735 + }
736 + if ($savedIsLegacy) {
737 + $legacyLabel = sprintf(
738 + /* translators: %s is the saved model id */
739 + esc_html__('%s (no longer available)', 'auto-alt-text'),
740 + $geminiModel
741 + );
742 + echo '<option value="' . esc_attr($geminiModel) . '" selected>' . esc_html($legacyLabel) . '</option>';
743 + }
744 + echo '</select>';
745 + }
746 + echo '</div>';
747 +
748 + echo '<div class="plugin-option type-gemini"><strong>' . esc_html__('Notice', 'auto-alt-text') . '</strong>: ' .
749 + esc_html__('If the model you selected is retired by Google and is no longer available, the least expensive available model will be used as fallback until you choose a new one.', 'auto-alt-text') . '<br>' .
750 + esc_html__('In case of errors, it is still possible to find the specific reason stated on the', 'auto-alt-text') . ' <a href="' . esc_url(menu_page_url(Constants::AATXT_PLUGIN_OPTION_LOG_PAGE_SLUG, false)) . '">' . esc_html__('error log page', 'auto-alt-text') . '</a>.' .
751 + '</div>';
752 +
753 + echo '<div class="plugin-option type-gemini">';
754 + echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_PROMPT_GEMINI) . '">' . esc_html__('Prompt', 'auto-alt-text') . '</label>';
755 + echo '<p class="description">' . esc_html__("Enter a specific and detailed prompt according to your needs.", 'auto-alt-text') . '</p>';
756 + $defaultPrompt = sprintf(esc_html__("Act like an SEO expert and write an alt text of up to 125 characters for this image. Return only the complete alt text.", 'auto-alt-text'), Constants::AATXT_IMAGE_URL_TAG);
757 + $prompt = get_option(Constants::AATXT_OPTION_FIELD_PROMPT_GEMINI) ?: $defaultPrompt;
758 + echo '<textarea name="' . esc_attr(Constants::AATXT_OPTION_FIELD_PROMPT_GEMINI) . '" rows="5" cols="50">' . esc_textarea($prompt) . '</textarea>';
759 + echo '</div>';
760 +
761 + echo '<div class="plugin-option type-article-title type-attachment-title type-openai type-azure type-anthropic type-gemini">';
335 762 echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_PRESERVE_EXISTING_ALT_TEXT) . '">' . esc_html__('Keep existing alt text', 'auto-alt-text') . '</label>';
336 763 echo '<p class="description">' . esc_html__("If checked, the existing alt text of images will not be overwritten.", 'auto-alt-text') . '</p>';
337 764 $preserveAltText = get_option(Constants::AATXT_OPTION_FIELD_PRESERVE_EXISTING_ALT_TEXT);
338 765 echo '<input type="checkbox" name="' . esc_attr(Constants::AATXT_OPTION_FIELD_PRESERVE_EXISTING_ALT_TEXT) . '" value="1" class="notRequired" ' . checked(1, $preserveAltText, false) . ' />';
@@ -337,8 +764,18 @@
337 764 $preserveAltText = get_option(Constants::AATXT_OPTION_FIELD_PRESERVE_EXISTING_ALT_TEXT);
338 765 echo '<input type="checkbox" name="' . esc_attr(Constants::AATXT_OPTION_FIELD_PRESERVE_EXISTING_ALT_TEXT) . '" value="1" class="notRequired" ' . checked(1, $preserveAltText, false) . ' />';
339 766 echo '</div>';
340 767
768 + // Standalone option (no .plugin-option class, so it is always visible
769 + // regardless of the selected generation method): this feature works on
770 + // the front-end content and is independent of how alt texts are created.
771 + echo '<div>';
772 + echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_AUTO_RENDER_ALT_TEXT) . '">' . esc_html__('Automatically render alt text in content', 'auto-alt-text') . '</label>';
773 + echo '<p class="description">' . esc_html__("If checked, when a page is displayed, images in the content that have no alt text are automatically filled with the alt text stored in the media library (when available). It does not modify your saved content.", 'auto-alt-text') . '</p>';
774 + $autoRenderAltText = get_option(Constants::AATXT_OPTION_FIELD_AUTO_RENDER_ALT_TEXT);
775 + echo '<input type="checkbox" name="' . esc_attr(Constants::AATXT_OPTION_FIELD_AUTO_RENDER_ALT_TEXT) . '" value="1" class="notRequired" ' . checked(1, $autoRenderAltText, false) . ' />';
776 + echo '</div>';
777 +
341 778 submit_button();
342 779 ?>
343 780 </form>
344 781 </div>
@@ -376,8 +813,15 @@
376 813 register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_ENDPOINT_AZURE_TRANSLATE_INSTANCE, [self::class, 'sanitizeUrl']);
377 814 register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_REGION_AZURE_TRANSLATE_INSTANCE, [self::class, 'sanitizeText']);
378 815 register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_LANGUAGE_AZURE_TRANSLATE_INSTANCE, [self::class, 'sanitizeText']);
379 816 register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_PRESERVE_EXISTING_ALT_TEXT);
817 + register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_AUTO_RENDER_ALT_TEXT);
818 + register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_API_KEY_ANTHROPIC);
819 + register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_PROMPT_ANTHROPIC, [self::class, 'sanitizeTextArea']);
820 + register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_MODEL_ANTHROPIC, [self::class, 'sanitizeText']);
821 + register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_API_KEY_GEMINI);
822 + register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_PROMPT_GEMINI, [self::class, 'sanitizeTextArea']);
823 + register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_MODEL_GEMINI, [self::class, 'sanitizeText']);
380 824 }
381 825
382 826 /**
383 827 * @return string
@@ -391,12 +835,22 @@
391 835 {
392 836 return get_option(Constants::AATXT_OPTION_FIELD_MODEL_OPENAI) ?: Constants::AATXT_GPT4O;
393 837 }
394 838
839 + public static function anthropicModel(): string
840 + {
841 + return get_option(Constants::AATXT_OPTION_FIELD_MODEL_ANTHROPIC) ?: Constants::AATXT_CLAUDE_FALLBACK_MODEL;
842 + }
843 +
844 + public static function geminiModel(): string
845 + {
846 + return get_option(Constants::AATXT_OPTION_FIELD_MODEL_GEMINI) ?: Constants::AATXT_GEMINI_FALLBACK_MODEL;
847 + }
848 +
395 849 /**
396 850 * @return string
397 851 */
398 - public static function prompt(): string
852 + public static function openAiPrompt(): string
399 853 {
400 854 return get_option(Constants::AATXT_OPTION_FIELD_PROMPT_OPENAI);
401 855 }
402 856
@@ -402,8 +856,24 @@
402 856
403 857 /**
404 858 * @return string
405 859 */
860 + public static function anthropicPrompt(): string
861 + {
862 + return get_option(Constants::AATXT_OPTION_FIELD_PROMPT_ANTHROPIC);
863 + }
864 +
865 + /**
866 + * @return string
867 + */
868 + public static function geminiPrompt(): string
869 + {
870 + return get_option(Constants::AATXT_OPTION_FIELD_PROMPT_GEMINI);
871 + }
872 +
873 + /**
874 + * @return string
875 + */
406 876 public static function apiKeyOpenAI(): string
407 877 {
408 878 $apiKey = get_option(Constants::AATXT_OPTION_FIELD_API_KEY_OPENAI);
409 879 return (Encryption::make())->decrypt($apiKey);
@@ -411,8 +881,26 @@
411 881
412 882 /**
413 883 * @return string
414 884 */
885 + public static function apiKeyAnthropic(): string
886 + {
887 + $apiKey = get_option(Constants::AATXT_OPTION_FIELD_API_KEY_ANTHROPIC);
888 + return (Encryption::make())->decrypt($apiKey);
889 + }
890 +
891 + /**
892 + * @return string
893 + */
894 + public static function apiKeyGemini(): string
895 + {
896 + $apiKey = get_option(Constants::AATXT_OPTION_FIELD_API_KEY_GEMINI);
897 + return (Encryption::make())->decrypt($apiKey);
898 + }
899 +
900 + /**
901 + * @return string
902 + */
415 903 public static function apiKeyAzureComputerVision(): string
416 904 {
417 905 $apiKey = get_option(Constants::AATXT_OPTION_FIELD_API_KEY_AZURE_COMPUTER_VISION);
418 906 return (Encryption::make())->decrypt($apiKey);
@@ -466,8 +954,19 @@
466 954 {
467 955 return get_option(Constants::AATXT_OPTION_FIELD_PRESERVE_EXISTING_ALT_TEXT);
468 956 }
469 957
958 + /**
959 + * Whether missing alt text should be filled automatically when rendering
960 + * post content on the front-end. Unchecked (disabled) by default.
961 + *
962 + * @return bool
963 + */
964 + public static function autoRenderAltText(): bool
965 + {
966 + return (bool) get_option(Constants::AATXT_OPTION_FIELD_AUTO_RENDER_ALT_TEXT);
967 + }
968 +
470 969 public static function sanitizeUrl($input): string
471 970 {
472 971 return sanitize_url($input);
473 972 }
@@ -480,6 +979,5 @@
480 979 public static function sanitizeTextArea($input): string
481 980 {
482 981 return sanitize_textarea_field($input);
483 982 }
484 -
485 983 }