| @@ -30,25 +30,135 @@ | ||
| 30 | 30 | } |
| 31 | 31 | |
| 32 | 32 | self::$assetsManager = AssetsManager::make(); |
| 33 | 33 | |
| 34 | + add_action('admin_notices', [self::$instance, 'showEncryptionConstantsNotice']); | |
| 35 | + | |
| 34 | 36 | add_action('admin_enqueue_scripts', [self::$instance, 'enqueueAdminScripts'], 1); |
| 35 | 37 | add_action('admin_menu', [self::$instance, 'addOptionsPageToTheMenu']); |
| 36 | - add_action('admin_init', [self::$instance, 'setupPluginOptions']); | |
| 38 | + add_action('admin_init', [self::$instance, 'setupPluginOptions'], 10); | |
| 39 | + add_action('admin_init', [self::$instance, 'migrateEncryptionKeys'], 9); | |
| 37 | 40 | |
| 38 | 41 | // Encrypt API Keys on update |
| 39 | 42 | add_action('pre_update_option_' . Constants::AATXT_OPTION_FIELD_API_KEY_AZURE_COMPUTER_VISION, [self::$instance, 'encryptDataOnUpdate'], 10, 3); |
| 40 | 43 | add_action('pre_update_option_' . Constants::AATXT_OPTION_FIELD_API_KEY_AZURE_TRANSLATE_INSTANCE, [self::$instance, 'encryptDataOnUpdate'], 10, 3); |
| 41 | 44 | add_action('pre_update_option_' . Constants::AATXT_OPTION_FIELD_API_KEY_OPENAI, [self::$instance, 'encryptDataOnUpdate'], 10, 3); |
| 45 | + add_action('pre_update_option_' . Constants::AATXT_OPTION_FIELD_API_KEY_ANTHROPIC, [self::$instance, 'encryptDataOnUpdate'], 10, 3); | |
| 46 | + | |
| 47 | + add_action('admin_notices', [self::$instance, 'encryptionErrorNotice']); | |
| 42 | 48 | } |
| 43 | 49 | |
| 44 | 50 | /** |
| 51 | + * Show an error notice if the encryption of the API Key fails | |
| 52 | + * @return void | |
| 53 | + */ | |
| 54 | + public function encryptionErrorNotice(): void | |
| 55 | + { | |
| 56 | + $raw = get_option(Constants::AATXT_OPTION_FIELD_API_KEY_OPENAI); | |
| 57 | + $decrypted = Encryption::make()->decrypt((string) $raw); | |
| 58 | + | |
| 59 | + if ($raw && $decrypted === '') { | |
| 60 | + $screen = get_current_screen(); | |
| 61 | + $settingsScreenId = 'toplevel_page_' . Constants::AATXT_PLUGIN_OPTIONS_PAGE_SLUG; | |
| 62 | + $showLink = ! ( $screen && $screen->id === $settingsScreenId ); | |
| 63 | + | |
| 64 | + $settingsUrl = esc_url( menu_page_url(Constants::AATXT_PLUGIN_OPTIONS_PAGE_SLUG, false) ); | |
| 65 | + | |
| 66 | + echo '<div class="notice notice-error is-dismissible">'; | |
| 67 | + echo '<p>'; | |
| 68 | + | |
| 69 | + echo '<strong>' . esc_html__('Auto Alt Text', 'auto-alt-text') . ':</strong> '; | |
| 70 | + | |
| 71 | + echo esc_html__( | |
| 72 | + 'There was a problem with the encryption of your API Key for alt text generation. Please re-enter the key and save.', | |
| 73 | + 'auto-alt-text' | |
| 74 | + ); | |
| 75 | + | |
| 76 | + if ($showLink) { | |
| 77 | + echo ' <a href="' . $settingsUrl . '">' | |
| 78 | + . esc_html__('Go to settings page', 'auto-alt-text') | |
| 79 | + . '</a>.'; | |
| 80 | + } | |
| 81 | + echo '</p>'; | |
| 82 | + echo '</div>'; | |
| 83 | + } | |
| 84 | + } | |
| 85 | + | |
| 86 | + /** | |
| 87 | + * Print an admin notice containing the defines to be added | |
| 88 | + * to the wp-config.php. | |
| 89 | + */ | |
| 90 | + public function showEncryptionConstantsNotice(): void | |
| 91 | + { | |
| 92 | + if ( ! is_admin() || ! current_user_can('manage_options') ) { | |
| 93 | + return; | |
| 94 | + } | |
| 95 | + | |
| 96 | + $screen = function_exists('get_current_screen') ? get_current_screen() : null; | |
| 97 | + if ( ! $screen || $screen->id !== 'toplevel_page_' . Constants::AATXT_PLUGIN_OPTIONS_PAGE_SLUG ) { | |
| 98 | + return; | |
| 99 | + } | |
| 100 | + | |
| 101 | + if ( defined('AATXT_ENCRYPTION_KEY') && defined('AATXT_ENCRYPTION_SALT') ) { | |
| 102 | + return; | |
| 103 | + } | |
| 104 | + | |
| 105 | + $suggestedKey = bin2hex(random_bytes(32)); | |
| 106 | + $suggestedSalt = bin2hex(random_bytes(32)); | |
| 107 | + ?> | |
| 108 | + <div class="notice notice-info is-dismissible"> | |
| 109 | + <p> | |
| 110 | + <?php | |
| 111 | + printf( | |
| 112 | + /* translators: “Optional” label */ | |
| 113 | + __( '%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' ), | |
| 114 | + '<strong>' . esc_html__( 'Optional', 'auto-alt-text' ) . '</strong>' | |
| 115 | + ); | |
| 116 | + ?> | |
| 117 | + </p> | |
| 118 | +<pre style="background:#f5f5f5; padding:10px; border-radius:4px;"> | |
| 119 | +define('AATXT_ENCRYPTION_KEY', '<?php echo esc_html( $suggestedKey ); ?>'); | |
| 120 | +define('AATXT_ENCRYPTION_SALT', '<?php echo esc_html( $suggestedSalt ); ?>'); | |
| 121 | +</pre> | |
| 122 | + <p> | |
| 123 | + <?php esc_html_e( | |
| 124 | + 'Just paste them before the line “/* That\'s all, stop editing! Happy publishing. */” in wp-config.php.', | |
| 125 | + 'auto-alt-text' | |
| 126 | + ); ?> | |
| 127 | + </p> | |
| 128 | + <p> | |
| 129 | + <?php esc_html_e( | |
| 130 | + "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.", | |
| 131 | + "auto-alt-text" | |
| 132 | + ); ?> | |
| 133 | + </p> | |
| 134 | + </div> | |
| 135 | + <?php | |
| 136 | + } | |
| 137 | + | |
| 138 | + public function migrateEncryptionKeys(): void | |
| 139 | + { | |
| 140 | + if ( ! defined('AATXT_ENCRYPTION_KEY') || ! defined('AATXT_ENCRYPTION_SALT') ) { | |
| 141 | + return; | |
| 142 | + } | |
| 143 | + | |
| 144 | + update_option(Constants::AATXT_LEGACY_ENCRYPTION_MIGRATION_DONE, '0'); | |
| 145 | + | |
| 146 | + if ( get_option(Constants::AATXT_LEGACY_ENCRYPTION_MIGRATION_DONE) ) { | |
| 147 | + return; | |
| 148 | + } | |
| 149 | + | |
| 150 | + Encryption::make()->migrateLegacyApiKeys(); | |
| 151 | + | |
| 152 | + update_option(Constants::AATXT_LEGACY_ENCRYPTION_MIGRATION_DONE, '1'); | |
| 153 | + } | |
| 154 | + | |
| 155 | + /** | |
| 45 | 156 | * Encrypt data |
| 46 | 157 | * @param ?string $newValue |
| 47 | - * @param ?string $oldValue | |
| 48 | 158 | * @return ?string |
| 49 | 159 | */ |
| 50 | - public function encryptDataOnUpdate(?string $newValue, ?string $oldValue): ?string | |
| 160 | + public function encryptDataOnUpdate(?string $newValue): ?string | |
| 51 | 161 | { |
| 52 | 162 | if (!empty($newValue)) { |
| 53 | 163 | $newValue = (Encryption::make())->encrypt($newValue); |
| 54 | 164 | } |
| @@ -167,8 +277,9 @@ | ||
| 167 | 277 | <select name="<?php echo esc_attr(Constants::AATXT_OPTION_FIELD_TYPOLOGY); ?>" |
| 168 | 278 | id="<?php echo esc_attr(Constants::AATXT_OPTION_FIELD_TYPOLOGY); ?>"> |
| 169 | 279 | <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> |
| 170 | 280 | <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> |
| 281 | + <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> | |
| 171 | 282 | <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> |
| 172 | 283 | <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> |
| 173 | 284 | <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> |
| 174 | 285 | </select> |
| @@ -291,9 +402,36 @@ | ||
| 291 | 402 | |
| 292 | 403 | echo '</div>'; |
| 293 | 404 | endif; |
| 294 | 405 | |
| 295 | - echo '<div class="plugin-option type-article-title type-attachment-title type-openai type-azure">'; | |
| 406 | + echo '<div class="plugin-option type-anthropic">'; | |
| 407 | + echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_API_KEY_ANTHROPIC) . '">' . esc_html__('Antrhopic API Key', 'auto-alt-text') . '</label>'; | |
| 408 | + echo '<p class="description">' . esc_html__("Enter your API Key", 'auto-alt-text') . '</p>'; | |
| 409 | + $apiKey = get_option(Constants::AATXT_OPTION_FIELD_API_KEY_ANTHROPIC); | |
| 410 | + echo '<input type="password" name="' . esc_attr(Constants::AATXT_OPTION_FIELD_API_KEY_ANTHROPIC) . '" value="' . esc_attr((Encryption::make())->decrypt($apiKey)) . '" />'; | |
| 411 | + echo '</div>'; | |
| 412 | + | |
| 413 | + $anthropicModel = self::anthropicModel(); | |
| 414 | + | |
| 415 | + echo '<div class="plugin-option type-anthropic">'; | |
| 416 | + echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_ANTHROPIC) . '">' . esc_html__('Model', 'auto-alt-text') . '</label>'; | |
| 417 | + | |
| 418 | + echo '<select name="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_ANTHROPIC) . '" id="' . esc_attr(Constants::AATXT_OPTION_FIELD_MODEL_ANTHROPIC) . '">'; | |
| 419 | + foreach(Constants::AATXT_OPTION_FIELD_MODEL_ANTHROPIC_OPTIONS as $key => $value) { | |
| 420 | + echo '<option value="' . esc_attr($key) . '" ' . esc_attr(self::selected($anthropicModel, $key)) . '>' . esc_html($value) . '</option>'; | |
| 421 | + } | |
| 422 | + echo '</select>'; | |
| 423 | + echo '</div>'; | |
| 424 | + | |
| 425 | + echo '<div class="plugin-option type-anthropic">'; | |
| 426 | + echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_PROMPT_ANTHROPIC) . '">' . esc_html__('Prompt', 'auto-alt-text') . '</label>'; | |
| 427 | + echo '<p class="description">' . esc_html__("Enter a specific and detailed prompt according to your needs.", 'auto-alt-text') . '</p>'; | |
| 428 | + $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); | |
| 429 | + $prompt = get_option(Constants::AATXT_OPTION_FIELD_PROMPT_ANTHROPIC) ?: $defaultPrompt; | |
| 430 | + echo '<textarea name="' . esc_attr(Constants::AATXT_OPTION_FIELD_PROMPT_ANTHROPIC) . '" rows="5" cols="50">' . esc_textarea($prompt) . '</textarea>'; | |
| 431 | + echo '</div>'; | |
| 432 | + | |
| 433 | + echo '<div class="plugin-option type-article-title type-attachment-title type-openai type-azure type-anthropic">'; | |
| 296 | 434 | echo '<label for="' . esc_attr(Constants::AATXT_OPTION_FIELD_PRESERVE_EXISTING_ALT_TEXT) . '">' . esc_html__('Keep existing alt text', 'auto-alt-text') . '</label>'; |
| 297 | 435 | echo '<p class="description">' . esc_html__("If checked, the existing alt text of images will not be overwritten.", 'auto-alt-text') . '</p>'; |
| 298 | 436 | $preserveAltText = get_option(Constants::AATXT_OPTION_FIELD_PRESERVE_EXISTING_ALT_TEXT); |
| 299 | 437 | 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 +475,11 @@ | ||
| 337 | 475 | register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_ENDPOINT_AZURE_TRANSLATE_INSTANCE, [self::class, 'sanitizeUrl']); |
| 338 | 476 | register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_REGION_AZURE_TRANSLATE_INSTANCE, [self::class, 'sanitizeText']); |
| 339 | 477 | register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_LANGUAGE_AZURE_TRANSLATE_INSTANCE, [self::class, 'sanitizeText']); |
| 340 | 478 | register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_PRESERVE_EXISTING_ALT_TEXT); |
| 479 | + register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_API_KEY_ANTHROPIC); | |
| 480 | + register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_PROMPT_ANTHROPIC, [self::class, 'sanitizeTextArea']); | |
| 481 | + register_setting('auto_alt_text_options', Constants::AATXT_OPTION_FIELD_MODEL_ANTHROPIC, [self::class, 'sanitizeText']); | |
| 341 | 482 | } |
| 342 | 483 | |
| 343 | 484 | /** |
| 344 | 485 | * @return string |
| @@ -352,12 +493,17 @@ | ||
| 352 | 493 | { |
| 353 | 494 | return get_option(Constants::AATXT_OPTION_FIELD_MODEL_OPENAI) ?: Constants::AATXT_GPT4O; |
| 354 | 495 | } |
| 355 | 496 | |
| 497 | + public static function anthropicModel(): string | |
| 498 | + { | |
| 499 | + return get_option(Constants::AATXT_OPTION_FIELD_MODEL_ANTHROPIC) ?: Constants::AATXT_CLAUDE_HAIKU_3_5; | |
| 500 | + } | |
| 501 | + | |
| 356 | 502 | /** |
| 357 | 503 | * @return string |
| 358 | 504 | */ |
| 359 | - public static function prompt(): string | |
| 505 | + public static function openAiPrompt(): string | |
| 360 | 506 | { |
| 361 | 507 | return get_option(Constants::AATXT_OPTION_FIELD_PROMPT_OPENAI); |
| 362 | 508 | } |
| 363 | 509 | |
| @@ -363,8 +509,16 @@ | ||
| 363 | 509 | |
| 364 | 510 | /** |
| 365 | 511 | * @return string |
| 366 | 512 | */ |
| 513 | + public static function anthropicPrompt(): string | |
| 514 | + { | |
| 515 | + return get_option(Constants::AATXT_OPTION_FIELD_PROMPT_ANTHROPIC); | |
| 516 | + } | |
| 517 | + | |
| 518 | + /** | |
| 519 | + * @return string | |
| 520 | + */ | |
| 367 | 521 | public static function apiKeyOpenAI(): string |
| 368 | 522 | { |
| 369 | 523 | $apiKey = get_option(Constants::AATXT_OPTION_FIELD_API_KEY_OPENAI); |
| 370 | 524 | return (Encryption::make())->decrypt($apiKey); |
| @@ -372,8 +526,17 @@ | ||
| 372 | 526 | |
| 373 | 527 | /** |
| 374 | 528 | * @return string |
| 375 | 529 | */ |
| 530 | + public static function apiKeyAnthropic(): string | |
| 531 | + { | |
| 532 | + $apiKey = get_option(Constants::AATXT_OPTION_FIELD_API_KEY_ANTHROPIC); | |
| 533 | + return (Encryption::make())->decrypt($apiKey); | |
| 534 | + } | |
| 535 | + | |
| 536 | + /** | |
| 537 | + * @return string | |
| 538 | + */ | |
| 376 | 539 | public static function apiKeyAzureComputerVision(): string |
| 377 | 540 | { |
| 378 | 541 | $apiKey = get_option(Constants::AATXT_OPTION_FIELD_API_KEY_AZURE_COMPUTER_VISION); |
| 379 | 542 | return (Encryption::make())->decrypt($apiKey); |
| @@ -441,6 +604,5 @@ | ||
| 441 | 604 | public static function sanitizeTextArea($input): string |
| 442 | 605 | { |
| 443 | 606 | return sanitize_textarea_field($input); |
| 444 | 607 | } |
| 445 | - | |
| 446 | 608 | } |