| 1 |
<?php |
| 2 |
|
| 3 |
namespace AATXT\App\Admin; |
| 4 |
|
| 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; |
| 9 |
use AATXT\App\AIProviders\Azure\AzureTranslator; |
| 10 |
use AATXT\App\Configuration\AzureConfig; |
| 11 |
use AATXT\App\Core\Container; |
| 12 |
use AATXT\App\Exceptions\Azure\AzureTranslateInstanceException; |
| 13 |
use AATXT\App\Infrastructure\Http\WordPressHttpClient; |
| 14 |
use AATXT\App\Utilities\AssetsManager; |
| 15 |
use AATXT\App\Utilities\Encryption; |
| 16 |
use AATXT\Config\Constants; |
| 17 |
|
| 18 |
class PluginOptions |
| 19 |
{ |
| 20 |
private static ?self $instance = null; |
| 21 |
private static AssetsManager $assetsManager; |
| 22 |
|
| 23 |
private function __construct() |
| 24 |
{ |
| 25 |
// |
| 26 |
} |
| 27 |
|
| 28 |
/** |
| 29 |
* Manage the necessary hooks to implement plugin options and their pages |
| 30 |
* @return void |
| 31 |
*/ |
| 32 |
public static function register(): void |
| 33 |
{ |
| 34 |
if (is_null(self::$instance)) { |
| 35 |
self::$instance = new self(); |
| 36 |
} |
| 37 |
|
| 38 |
self::$assetsManager = AssetsManager::make(); |
| 39 |
|
| 40 |
add_action('admin_notices', [self::$instance, 'showEncryptionConstantsNotice']); |
| 41 |
|
| 42 |
add_action('admin_enqueue_scripts', [self::$instance, 'enqueueAdminScripts'], 1); |
| 43 |
add_action('admin_menu', [self::$instance, 'addOptionsPageToTheMenu']); |
| 44 |
add_action('admin_init', [self::$instance, 'setupPluginOptions'], 10); |
| 45 |
add_action('admin_init', [self::$instance, 'migrateEncryptionKeys'], 9); |
| 46 |
|
| 47 |
// Encrypt API Keys on update |
| 48 |
add_action('pre_update_option_' . Constants::AATXT_OPTION_FIELD_API_KEY_AZURE_COMPUTER_VISION, [self::$instance, 'encryptDataOnUpdate'], 10, 3); |
| 49 |
add_action('pre_update_option_' . Constants::AATXT_OPTION_FIELD_API_KEY_AZURE_TRANSLATE_INSTANCE, [self::$instance, 'encryptDataOnUpdate'], 10, 3); |
| 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); |
| 53 |
|
| 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']); |
| 71 |
} |
| 72 |
|
| 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 |
/** |
| 233 |
* Show an error notice if the encryption of the API Key fails |
| 234 |
* @return void |
| 235 |
*/ |
| 236 |
public function encryptionErrorNotice(): void |
| 237 |
{ |
| 238 |
$raw = get_option(Constants::AATXT_OPTION_FIELD_API_KEY_OPENAI); |
| 239 |
$decrypted = Encryption::make()->decrypt((string) $raw); |
| 240 |
|
| 241 |
if ($raw && $decrypted === '') { |
| 242 |
$screen = get_current_screen(); |
| 243 |
$settingsScreenId = 'toplevel_page_' . Constants::AATXT_PLUGIN_OPTIONS_PAGE_SLUG; |
| 244 |
$showLink = ! ( $screen && $screen->id === $settingsScreenId ); |
| 245 |
|
| 246 |
$settingsUrl = esc_url( menu_page_url(Constants::AATXT_PLUGIN_OPTIONS_PAGE_SLUG, false) ); |
| 247 |
|
| 248 |
echo '<div class="notice notice-error is-dismissible">'; |
| 249 |
echo '<p>'; |
| 250 |
|
| 251 |
echo '<strong>' . esc_html__('Auto Alt Text', 'auto-alt-text') . ':</strong> '; |
| 252 |
|
| 253 |
echo esc_html__( |
| 254 |
'There was a problem with the encryption of your API Key for alt text generation. Please re-enter the key and save.', |
| 255 |
'auto-alt-text' |
| 256 |
); |
| 257 |
|
| 258 |
if ($showLink) { |
| 259 |
echo ' <a href="' . $settingsUrl . '">' |
| 260 |
. esc_html__('Go to settings page', 'auto-alt-text') |
| 261 |
. '</a>.'; |
| 262 |
} |
| 263 |
echo '</p>'; |
| 264 |
echo '</div>'; |
| 265 |
} |
| 266 |
} |
| 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 |
} |
| 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 |
|
| 337 |
/** |
| 338 |
* Encrypt data |
| 339 |
* @param ?string $newValue |
| 340 |
* @return ?string |
| 341 |
*/ |
| 342 |
public function encryptDataOnUpdate(?string $newValue): ?string |
| 343 |
{ |
| 344 |
if (!empty($newValue)) { |
| 345 |
$newValue = (Encryption::make())->encrypt($newValue); |
| 346 |
} |
| 347 |
return $newValue; |
| 348 |
} |
| 349 |
|
| 350 |
/** |
| 351 |
* @return void |
| 352 |
*/ |
| 353 |
public static function enqueueAdminScripts(): void |
| 354 |
{ |
| 355 |
$screen = get_current_screen(); |
| 356 |
$isMainOptionsPage = $screen->id === 'toplevel_page_' . Constants::AATXT_PLUGIN_OPTIONS_PAGE_SLUG; |
| 357 |
|
| 358 |
if ($isMainOptionsPage || strpos($screen->id, Constants::AATXT_PLUGIN_SLUG . '_') !== false) { |
| 359 |
|
| 360 |
$adminCss = self::$assetsManager->getAssetUrl('resources/js/admin.js', true); |
| 361 |
|
| 362 |
wp_enqueue_style(Constants::AATXT_PLUGIN_ASSETS_HANDLE, $adminCss, [], false); |
| 363 |
|
| 364 |
if ($isMainOptionsPage) { |
| 365 |
$adminJs = self::$assetsManager->getAssetUrl('resources/js/admin.js', false); |
| 366 |
wp_enqueue_script(Constants::AATXT_PLUGIN_ASSETS_HANDLE, $adminJs, [], false); |
| 367 |
} |
| 368 |
|
| 369 |
} |
| 370 |
} |
| 371 |
|
| 372 |
/** |
| 373 |
* Create options pages |
| 374 |
* @return void |
| 375 |
*/ |
| 376 |
public static function addOptionsPageToTheMenu(): void |
| 377 |
{ |
| 378 |
add_menu_page('Auto Alt Text Options', 'Auto Alt Text', 'manage_options', Constants::AATXT_PLUGIN_OPTIONS_PAGE_SLUG, [self::$instance, 'optionsMainPage'], null, 99); |
| 379 |
add_submenu_page(Constants::AATXT_PLUGIN_OPTIONS_PAGE_SLUG, 'Error Log', 'Error log', 'manage_options', Constants::AATXT_PLUGIN_OPTION_LOG_PAGE_SLUG, [self::$instance, 'logOptionsPage']); |
| 380 |
} |
| 381 |
|
| 382 |
/** |
| 383 |
* Implement the page showing error log |
| 384 |
* @return void |
| 385 |
*/ |
| 386 |
public static function logOptionsPage(): void |
| 387 |
{ |
| 388 |
?> |
| 389 |
<div class="wrap"> |
| 390 |
<h1><?php esc_html_e('Auto Alt Text Error Log', 'auto-alt-text') ?></h1> |
| 391 |
<div class="aat-options plugin-description"> |
| 392 |
<p> |
| 393 |
<?php esc_html_e("If the alt text of your images was not generated correctly, you can find the reason in this log.", 'auto-alt-text'); ?> |
| 394 |
<br> |
| 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'); ?> |
| 396 |
</p> |
| 397 |
<?php |
| 398 |
$container = Container::make(); |
| 399 |
$logger = $container->get(DBLogger::class); |
| 400 |
$logs = $logger->getImageLog(); |
| 401 |
if ($logs) { |
| 402 |
echo '<textarea id="error-log" name="error-log" readonly>' . esc_textarea($logs) . '</textarea>'; |
| 403 |
} else { |
| 404 |
echo '<p>' . esc_html__('There is no error log yet!', 'auto-alt-text') . '</p>'; |
| 405 |
} |
| 406 |
|
| 407 |
?> |
| 408 |
</div> |
| 409 |
</div> |
| 410 |
<?php |
| 411 |
} |
| 412 |
|
| 413 |
/** |
| 414 |
* Implement the main option page |
| 415 |
* @return void |
| 416 |
*/ |
| 417 |
public static function optionsMainPage(): void |
| 418 |
{ |
| 419 |
?> |
| 420 |
<div class="wrap"> |
| 421 |
<h1><?php esc_html_e('Auto Alt Text Options', 'auto-alt-text') ?></h1> |
| 422 |
|
| 423 |
<div class="aat-options plugin-description"> |
| 424 |
<p> |
| 425 |
<?php esc_html_e("This plugin allows you to automatically generate Alt Text for the images that are uploaded to the site's media library.", 'auto-alt-text'); ?> |
| 426 |
<br> |
| 427 |
<?php esc_html_e("The following methods are available to generate the alt text:", 'auto-alt-text'); ?> |
| 428 |
</p> |
| 429 |
<ul> |
| 430 |
<li> |
| 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'); ?> |
| 432 |
</li> |
| 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> |
| 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'); ?> |
| 438 |
</li> |
| 439 |
<li> |
| 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'); ?> |
| 441 |
</li> |
| 442 |
<li> |
| 443 |
<strong><?php esc_html_e("Title of the attachment (not AI)", 'auto-alt-text'); ?></strong>: <?php esc_html_e("the title of the attachment will be copied into the alt text;", 'auto-alt-text'); ?> |
| 444 |
</li> |
| 445 |
</ul> |
| 446 |
<p><?php esc_html_e("Once all the necessary data for the chosen generation method has been entered, the alt texts will be created automatically upon uploading each image.", 'auto-alt-text'); ?></p> |
| 447 |
<p><strong>New</strong>: <?php esc_html_e("For images already in the media library, you can create bulk alt texts. Open the Media Library in the \"list\" view, select the images for which to generate the alt text, and choose the \"Generate alt text\" bulk action. (Depending on the number of images chosen and their weight, this may take some time.)", 'auto-alt-text'); ?></p> |
| 448 |
<p> |
| 449 |
<strong><?php esc_html_e('Pay attention please:', 'auto-alt-text') ?></strong> <?php esc_html_e("if the alt text for an image is not generated, check the logs on the", 'auto-alt-text'); ?> |
| 450 |
<a href="<?php esc_url(menu_page_url(Constants::AATXT_PLUGIN_OPTION_LOG_PAGE_SLUG, true)); ?>"><?php esc_html_e('designated page.', 'auto-alt-text') ?></a> |
| 451 |
</p> |
| 452 |
</div> |
| 453 |
<form method="post" action="options.php" class="aat-options"> |
| 454 |
<?php |
| 455 |
settings_fields('auto_alt_text_options'); |
| 456 |
|
| 457 |
echo '<div>'; |
| 458 |
echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_TYPOLOGY) . '">' . esc_html__('Generation method', 'auto-alt-text') . '</label>'; |
| 459 |
echo '<p class="description">' . esc_html__("Which method do you want to use to generate the alt text for the images?", 'auto-alt-text') . '</p>'; |
| 460 |
$typology = self::typology(); |
| 461 |
|
| 462 |
?> |
| 463 |
|
| 464 |
<select name="<?php echo esc_attr(Constants::AATXT_OPTION_FIELD_TYPOLOGY); ?>" |
| 465 |
id="<?php echo esc_attr(Constants::AATXT_OPTION_FIELD_TYPOLOGY); ?>"> |
| 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> |
| 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> |
| 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> |
| 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> |
| 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> |
| 473 |
</select> |
| 474 |
<?php |
| 475 |
echo '</div>'; |
| 476 |
|
| 477 |
echo '<div class="plugin-option type-article-title"><strong>' . esc_html__('Notice', 'auto-alt-text') . '</strong>: ' . |
| 478 |
esc_html__('If you try to insert an image into a post that has not yet been saved as a draft or published, the plugin cannot generate an alt text based on the post\'s title since the title itself has not yet been saved.', 'auto-alt-text') . ' ' . |
| 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') . |
| 480 |
'</div>'; |
| 481 |
|
| 482 |
$openaiModel = self::openAiModel(); |
| 483 |
$openaiRegistry = self::openAiModelsRegistry(); |
| 484 |
|
| 485 |
echo '<div class="plugin-option type-openai">'; |
| 486 |
echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_OPENAI) . '">' . esc_html__('Model', 'auto-alt-text') . '</label>'; |
| 487 |
|
| 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>'; |
| 530 |
} |
| 531 |
echo '</div>'; |
| 532 |
|
| 533 |
echo '<div class="plugin-option type-openai"><strong>' . esc_html__('Notice', 'auto-alt-text') . '</strong>: ' . |
| 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>' . |
| 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>.' . |
| 536 |
'</div>'; |
| 537 |
|
| 538 |
echo '<div class="plugin-option type-openai">'; |
| 539 |
echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_API_KEY_OPENAI) . '">' . esc_html__('OpenAI API Key', 'auto-alt-text') . '</label>'; |
| 540 |
echo '<p class="description">' . esc_html__("Enter your API Key", 'auto-alt-text') . '</p>'; |
| 541 |
$apiKey = get_option(Constants::AATXT_OPTION_FIELD_API_KEY_OPENAI); |
| 542 |
echo '<input type="password" name="' . esc_attr(Constants::AATXT_OPTION_FIELD_API_KEY_OPENAI) . '" value="' . esc_attr((Encryption::make())->decrypt($apiKey)) . '" />'; |
| 543 |
echo '</div>'; |
| 544 |
|
| 545 |
echo '<div class="plugin-option type-openai">'; |
| 546 |
echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_PROMPT_OPENAI) . '">' . esc_html__('Prompt', 'auto-alt-text') . '</label>'; |
| 547 |
echo '<p class="description">' . esc_html__("Enter a specific and detailed prompt according to your needs.", 'auto-alt-text') . '</p>'; |
| 548 |
$defaultPrompt = sprintf(esc_html__("Act like an SEO expert and write an alt text of up to 125 characters for this image.", 'auto-alt-text'), Constants::AATXT_IMAGE_URL_TAG); |
| 549 |
$prompt = get_option(Constants::AATXT_OPTION_FIELD_PROMPT_OPENAI) ?: $defaultPrompt; |
| 550 |
echo '<textarea name="' . esc_attr(Constants::AATXT_OPTION_FIELD_PROMPT_OPENAI) . '" rows="5" cols="50">' . esc_textarea($prompt) . '</textarea>'; |
| 551 |
echo '</div>'; |
| 552 |
|
| 553 |
echo '<div class="plugin-option type-azure">' . esc_html__("Fill out the following fields to leverage Azure's computer vision services to generate the Alt texts.", 'auto-alt-text') . '</div>'; |
| 554 |
|
| 555 |
echo '<div class="plugin-option type-azure">'; |
| 556 |
echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_API_KEY_AZURE_COMPUTER_VISION) . '">' . esc_html__('Azure Computer Vision API Key', 'auto-alt-text') . '</label>'; |
| 557 |
echo '<p class="description">' . esc_html__("Enter the API key for the Computer Vision service of your Azure account.", 'auto-alt-text') . '</p>'; |
| 558 |
$apiKey = get_option(Constants::AATXT_OPTION_FIELD_API_KEY_AZURE_COMPUTER_VISION); |
| 559 |
echo '<input type="password" name="' . esc_attr(Constants::AATXT_OPTION_FIELD_API_KEY_AZURE_COMPUTER_VISION) . '" value="' . esc_attr((Encryption::make())->decrypt($apiKey)) . '" />'; |
| 560 |
echo '</div>'; |
| 561 |
|
| 562 |
echo '<div class="plugin-option type-azure">'; |
| 563 |
echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_ENDPOINT_AZURE_COMPUTER_VISION) . '">' . esc_html__('Azure Computer Vision Endpoint', 'auto-alt-text') . '</label>'; |
| 564 |
echo '<p class="description">' . esc_html__("Enter the endpoint of the Computer Vision service.", 'auto-alt-text') . ' (es. https://computer-vision-france-central.cognitiveservices.azure.com/)</p>'; |
| 565 |
$endpoint = get_option(Constants::AATXT_OPTION_FIELD_ENDPOINT_AZURE_COMPUTER_VISION); |
| 566 |
echo '<input type="text" name="' . esc_attr(Constants::AATXT_OPTION_FIELD_ENDPOINT_AZURE_COMPUTER_VISION) . '" value="' . esc_attr($endpoint) . '" />'; |
| 567 |
echo '</div>'; |
| 568 |
|
| 569 |
echo '<div class="plugin-option type-azure">' . |
| 570 |
'<strong>' . esc_html__('The default alt text language is English.', 'auto-alt-text') . '</strong><br>' . |
| 571 |
esc_html__('If you want to translate into another language, enter the following data necessary for the translation API to work.', 'auto-alt-text') . ' ' . |
| 572 |
esc_html__('After saving the changes you can select the desired language.', 'auto-alt-text') . |
| 573 |
'</div>'; |
| 574 |
|
| 575 |
echo '<div class="plugin-option type-azure">'; |
| 576 |
echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_API_KEY_AZURE_TRANSLATE_INSTANCE) . '">' . esc_html__('Azure Translate Instance API Key', 'auto-alt-text') . '</label>'; |
| 577 |
echo '<p class="description">' . esc_html__("Enter your API key for the Azure Translate Instance service.", 'auto-alt-text') . '</p>'; |
| 578 |
$translationApiKey = get_option(Constants::AATXT_OPTION_FIELD_API_KEY_AZURE_TRANSLATE_INSTANCE); |
| 579 |
echo '<input type="password" name="' . esc_attr(Constants::AATXT_OPTION_FIELD_API_KEY_AZURE_TRANSLATE_INSTANCE) . '" value="' . esc_attr((Encryption::make())->decrypt($translationApiKey)) . '" class="notRequired" />'; |
| 580 |
echo '</div>'; |
| 581 |
|
| 582 |
echo '<div class="plugin-option type-azure">'; |
| 583 |
echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_ENDPOINT_AZURE_TRANSLATE_INSTANCE) . '">' . esc_html__('Azure Translate Instance Endpoint', 'auto-alt-text') . '</label>'; |
| 584 |
echo '<p class="description">' . esc_html__("Enter the endpoint of the Translate Instance service", 'auto-alt-text') . ' (es. https://api.cognitive.microsofttranslator.com/)</p>'; |
| 585 |
$translationEndpoint = get_option(Constants::AATXT_OPTION_FIELD_ENDPOINT_AZURE_TRANSLATE_INSTANCE); |
| 586 |
echo '<input type="text" name="' . esc_attr(Constants::AATXT_OPTION_FIELD_ENDPOINT_AZURE_TRANSLATE_INSTANCE) . '" value="' . esc_attr($translationEndpoint) . '" class="notRequired" />'; |
| 587 |
echo '</div>'; |
| 588 |
|
| 589 |
echo '<div class="plugin-option type-azure">'; |
| 590 |
echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_REGION_AZURE_TRANSLATE_INSTANCE) . '">' . esc_html__('Azure Translate Instance Region', 'auto-alt-text') . '</label>'; |
| 591 |
echo '<p class="description">' . esc_html__("Enter the region of the Azure Translate Instance service.", 'auto-alt-text') . ' (es. westeurope)</p>'; |
| 592 |
$translationRegion = get_option(Constants::AATXT_OPTION_FIELD_REGION_AZURE_TRANSLATE_INSTANCE); |
| 593 |
echo '<input type="text" name="' . esc_attr(Constants::AATXT_OPTION_FIELD_REGION_AZURE_TRANSLATE_INSTANCE) . '" value="' . esc_attr($translationRegion) . '" class="notRequired" />'; |
| 594 |
echo '</div>'; |
| 595 |
|
| 596 |
if ($translationApiKey && $translationEndpoint && $translationRegion): |
| 597 |
echo '<div class="plugin-option type-azure">'; |
| 598 |
echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_LANGUAGE_AZURE_TRANSLATE_INSTANCE) . '">' . esc_html__('Alt Text Language', 'auto-alt-text') . '</label>'; |
| 599 |
echo '<p class="description">' . esc_html__("Select the language in which the alt text should be written.", 'auto-alt-text') . '</p>'; |
| 600 |
$currentLanguage = get_option(Constants::AATXT_OPTION_FIELD_LANGUAGE_AZURE_TRANSLATE_INSTANCE); |
| 601 |
|
| 602 |
try { |
| 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(); |
| 616 |
} catch (AzureTranslateInstanceException $e) { |
| 617 |
$supportedLanguages = [ |
| 618 |
"en" => [ |
| 619 |
"name" => "English", |
| 620 |
"nativeName" => "English", |
| 621 |
"dir" => "ltr", |
| 622 |
] |
| 623 |
]; |
| 624 |
echo '<p style="color:red"><strong>' . esc_html($e->getMessage()) . '</strong></p>'; |
| 625 |
} |
| 626 |
|
| 627 |
?> |
| 628 |
<select name="<?php echo esc_attr(Constants::AATXT_OPTION_FIELD_LANGUAGE_AZURE_TRANSLATE_INSTANCE); ?>" |
| 629 |
id="<?php echo esc_attr(Constants::AATXT_OPTION_FIELD_LANGUAGE_AZURE_TRANSLATE_INSTANCE); ?>"> |
| 630 |
<?php |
| 631 |
foreach ($supportedLanguages as $key => $language): |
| 632 |
?> |
| 633 |
<option value="<?php echo esc_attr($key) ?>" <?php echo esc_attr(self::selected($currentLanguage, $key)); ?>><?php echo esc_html($language['name']); ?></option> |
| 634 |
<?php |
| 635 |
endforeach; |
| 636 |
?> |
| 637 |
</select> |
| 638 |
|
| 639 |
<?php |
| 640 |
|
| 641 |
echo '</div>'; |
| 642 |
endif; |
| 643 |
|
| 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">'; |
| 762 |
echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_PRESERVE_EXISTING_ALT_TEXT) . '">' . esc_html__('Keep existing alt text', 'auto-alt-text') . '</label>'; |
| 763 |
echo '<p class="description">' . esc_html__("If checked, the existing alt text of images will not be overwritten.", 'auto-alt-text') . '</p>'; |
| 764 |
$preserveAltText = get_option(Constants::AATXT_OPTION_FIELD_PRESERVE_EXISTING_ALT_TEXT); |
| 765 |
echo '<input type="checkbox" name="' . esc_attr(Constants::AATXT_OPTION_FIELD_PRESERVE_EXISTING_ALT_TEXT) . '" value="1" class="notRequired" ' . checked(1, $preserveAltText, false) . ' />'; |
| 766 |
echo '</div>'; |
| 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 |
|
| 778 |
submit_button(); |
| 779 |
?> |
| 780 |
</form> |
| 781 |
</div> |
| 782 |
<?php |
| 783 |
} |
| 784 |
|
| 785 |
/** |
| 786 |
* If option is selected return the selected attribute |
| 787 |
* @param string $selectedValue |
| 788 |
* @param string $inputValue |
| 789 |
* @return string |
| 790 |
*/ |
| 791 |
public static function selected(string $selectedValue, string $inputValue): string |
| 792 |
{ |
| 793 |
if (empty($selectedValue) && 'en' == $inputValue) { |
| 794 |
return ' selected'; |
| 795 |
} |
| 796 |
return $selectedValue == $inputValue ? ' selected' : ''; |
| 797 |
} |
| 798 |
|
| 799 |
|
| 800 |
/** |
| 801 |
* Register input fields and option settings |
| 802 |
* @return void |
| 803 |
*/ |
| 804 |
public static function setupPluginOptions(): void |
| 805 |
{ |
| 806 |
register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_API_KEY_OPENAI); |
| 807 |
register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_PROMPT_OPENAI, [self::class, 'sanitizeTextArea']); |
| 808 |
register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_TYPOLOGY, [self::class, 'sanitizeText']); |
| 809 |
register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_MODEL_OPENAI, [self::class, 'sanitizeText']); |
| 810 |
register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_API_KEY_AZURE_COMPUTER_VISION); |
| 811 |
register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_ENDPOINT_AZURE_COMPUTER_VISION, [self::class, 'sanitizeUrl']); |
| 812 |
register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_API_KEY_AZURE_TRANSLATE_INSTANCE); |
| 813 |
register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_ENDPOINT_AZURE_TRANSLATE_INSTANCE, [self::class, 'sanitizeUrl']); |
| 814 |
register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_REGION_AZURE_TRANSLATE_INSTANCE, [self::class, 'sanitizeText']); |
| 815 |
register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_LANGUAGE_AZURE_TRANSLATE_INSTANCE, [self::class, 'sanitizeText']); |
| 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']); |
| 824 |
} |
| 825 |
|
| 826 |
/** |
| 827 |
* @return string |
| 828 |
*/ |
| 829 |
public static function typology(): string |
| 830 |
{ |
| 831 |
return get_option(Constants::AATXT_OPTION_FIELD_TYPOLOGY); |
| 832 |
} |
| 833 |
|
| 834 |
public static function openAiModel(): string |
| 835 |
{ |
| 836 |
return get_option(Constants::AATXT_OPTION_FIELD_MODEL_OPENAI) ?: Constants::AATXT_GPT4O; |
| 837 |
} |
| 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 |
|
| 849 |
/** |
| 850 |
* @return string |
| 851 |
*/ |
| 852 |
public static function openAiPrompt(): string |
| 853 |
{ |
| 854 |
return get_option(Constants::AATXT_OPTION_FIELD_PROMPT_OPENAI); |
| 855 |
} |
| 856 |
|
| 857 |
/** |
| 858 |
* @return string |
| 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 |
*/ |
| 876 |
public static function apiKeyOpenAI(): string |
| 877 |
{ |
| 878 |
$apiKey = get_option(Constants::AATXT_OPTION_FIELD_API_KEY_OPENAI); |
| 879 |
return (Encryption::make())->decrypt($apiKey); |
| 880 |
} |
| 881 |
|
| 882 |
/** |
| 883 |
* @return string |
| 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 |
*/ |
| 903 |
public static function apiKeyAzureComputerVision(): string |
| 904 |
{ |
| 905 |
$apiKey = get_option(Constants::AATXT_OPTION_FIELD_API_KEY_AZURE_COMPUTER_VISION); |
| 906 |
return (Encryption::make())->decrypt($apiKey); |
| 907 |
} |
| 908 |
|
| 909 |
/** |
| 910 |
* @return string |
| 911 |
*/ |
| 912 |
public static function endpointAzureComputerVision(): string |
| 913 |
{ |
| 914 |
return get_option(Constants::AATXT_OPTION_FIELD_ENDPOINT_AZURE_COMPUTER_VISION); |
| 915 |
} |
| 916 |
|
| 917 |
/** |
| 918 |
* @return string |
| 919 |
*/ |
| 920 |
public static function apiKeyAzureTranslateInstance(): string |
| 921 |
{ |
| 922 |
$apiKey = get_option(Constants::AATXT_OPTION_FIELD_API_KEY_AZURE_TRANSLATE_INSTANCE); |
| 923 |
return (Encryption::make())->decrypt($apiKey); |
| 924 |
} |
| 925 |
|
| 926 |
/** |
| 927 |
* @return string |
| 928 |
*/ |
| 929 |
public static function endpointAzureTranslateInstance(): string |
| 930 |
{ |
| 931 |
return get_option(Constants::AATXT_OPTION_FIELD_ENDPOINT_AZURE_TRANSLATE_INSTANCE); |
| 932 |
} |
| 933 |
|
| 934 |
/** |
| 935 |
* @return string |
| 936 |
*/ |
| 937 |
public static function regionAzureTranslateInstance(): string |
| 938 |
{ |
| 939 |
return get_option(Constants::AATXT_OPTION_FIELD_REGION_AZURE_TRANSLATE_INSTANCE); |
| 940 |
} |
| 941 |
|
| 942 |
/** |
| 943 |
* @return string |
| 944 |
*/ |
| 945 |
public static function languageAzureTranslateInstance(): string |
| 946 |
{ |
| 947 |
return get_option(Constants::AATXT_OPTION_FIELD_LANGUAGE_AZURE_TRANSLATE_INSTANCE) ?: 'en'; |
| 948 |
} |
| 949 |
|
| 950 |
/** |
| 951 |
* @return bool |
| 952 |
*/ |
| 953 |
public static function preserveExistingAltText(): bool |
| 954 |
{ |
| 955 |
return get_option(Constants::AATXT_OPTION_FIELD_PRESERVE_EXISTING_ALT_TEXT); |
| 956 |
} |
| 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 |
|
| 969 |
public static function sanitizeUrl($input): string |
| 970 |
{ |
| 971 |
return sanitize_url($input); |
| 972 |
} |
| 973 |
|
| 974 |
public static function sanitizeText($input): string |
| 975 |
{ |
| 976 |
return sanitize_text_field($input); |
| 977 |
} |
| 978 |
|
| 979 |
public static function sanitizeTextArea($input): string |
| 980 |
{ |
| 981 |
return sanitize_textarea_field($input); |
| 982 |
} |
| 983 |
} |
| 984 |
|