--- a/alttext-ai/admin/class-atai-admin.php
+++ b/alttext-ai/admin/class-atai-admin.php
@@ -77,10 +77,12 @@
'security_update_toggle' => wp_create_nonce( 'atai_update_toggle' ),
'security_check_attachment_eligibility' => wp_create_nonce( 'atai_check_attachment_eligibility' ),
'security_update_public_setting' => wp_create_nonce( 'atai_update_public_setting' ),
+ 'security_preview_csv' => wp_create_nonce( 'atai_preview_csv' ),
+ 'security_url_generate' => wp_create_nonce( 'atai_url_generate' ),
'can_user_upload_files' => current_user_can( 'upload_files' ),
- 'should_update_title' => get_option( 'atai_update_title' ),
- 'should_update_caption' => get_option( 'atai_update_caption' ),
- 'should_update_description' => get_option( 'atai_update_description' ),
+ 'should_update_title' => ATAI_Utility::get_setting( 'atai_update_title' ),
+ 'should_update_caption' => ATAI_Utility::get_setting( 'atai_update_caption' ),
+ 'should_update_description' => ATAI_Utility::get_setting( 'atai_update_description' ),
'icon_button_generate' => plugin_dir_url( ATAI_PLUGIN_FILE ) . 'admin/img/icon-button-generate.svg',
'has_api_key' => ATAI_Utility::get_api_key() ? true : false,
'settings_page_url' => admin_url( 'admin.php?page=atai' ),
--- a/alttext-ai/admin/class-atai-settings.php
+++ b/alttext-ai/admin/class-atai-settings.php
@@ -82,7 +82,7 @@
* @access public
*/
public function register_settings_pages() {
- $capability = get_option( 'atai_admin_capability', 'manage_options' );
+ $capability = ATAI_Utility::get_setting( 'atai_admin_capability', 'manage_options' );
// Main page
add_menu_page(
__( 'AltText.ai WordPress Settings', 'alttext-ai' ),
@@ -147,6 +147,42 @@
}
/**
+ * Register the network settings page.
+ *
+ * @since 1.10.16
+ * @access public
+ */
+ public function register_network_settings_page() {
+ if ( ! is_multisite() ) {
+ return;
+ }
+
+ $hook_suffix = add_submenu_page(
+ 'settings.php', // Parent slug (network admin settings)
+ __( 'AltText.ai Network Settings', 'alttext-ai' ),
+ __( 'AltText.ai', 'alttext-ai' ),
+ 'manage_network_options',
+ 'atai-network',
+ array( $this, 'render_network_settings_page' )
+ );
+
+ // Enqueue styles for the network settings page
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_network_styles' ) );
+ }
+
+ /**
+ * Enqueue styles for the network settings page.
+ *
+ * @since 1.10.16
+ */
+ public function enqueue_network_styles( $hook ) {
+ // Debug the current hook to see what it is
+ if ( strpos( $hook, 'atai-network' ) !== false ) {
+ wp_enqueue_style( 'atai-admin', plugin_dir_url( __FILE__ ) . 'css/admin.css', array(), $this->version, 'all' );
+ }
+ }
+
+ /**
* Render the settings page.
*
* @since 1.0.0
@@ -162,6 +198,16 @@
}
/**
+ * Render the network settings page.
+ *
+ * @since 1.10.16
+ * @access public
+ */
+ public function render_network_settings_page() {
+ require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/partials/network-settings.php';
+ }
+
+ /**
* Render the bulk generate page.
*
* @since 1.0.0
@@ -203,7 +249,7 @@
* @return string The configured capability.
*/
public function filter_settings_capability( $capability ) {
- return get_option( 'atai_admin_capability', 'manage_options' );
+ return ATAI_Utility::get_setting( 'atai_admin_capability', 'manage_options' );
}
/**
@@ -221,6 +267,27 @@
)
);
+ // Network API key option (multisite only)
+ if ( is_multisite() && is_super_admin() ) {
+ register_setting(
+ 'atai-settings',
+ 'atai_network_api_key',
+ array(
+ 'sanitize_callback' => array( $this, 'sanitize_yes_no_checkbox' ),
+ 'default' => 'no',
+ )
+ );
+
+ register_setting(
+ 'atai-settings',
+ 'atai_network_all_settings',
+ array(
+ 'sanitize_callback' => array( $this, 'sanitize_yes_no_checkbox' ),
+ 'default' => 'no',
+ )
+ );
+ }
+
register_setting(
'atai-settings',
'atai_lang',
@@ -579,6 +646,11 @@
if ( $delete ) {
delete_option( 'atai_api_key' );
+
+ // If this is a multisite and we're a network admin, also update the network setting
+ if ( is_multisite() && is_super_admin() ) {
+ update_site_option( 'atai_network_api_key', 'no' );
+ }
}
if ( empty( $api_key ) ) {
@@ -596,6 +668,24 @@
return false;
}
+ // Check if the network API key option is set and save it
+ if ( is_multisite() && is_super_admin() ) {
+ if ( isset( $_POST['atai_network_api_key'] ) ) {
+ $network_api_key = $_POST['atai_network_api_key'] === 'yes' ? 'yes' : 'no';
+ update_site_option( 'atai_network_api_key', $network_api_key );
+ }
+
+ if ( isset( $_POST['atai_network_all_settings'] ) ) {
+ $network_all_settings = $_POST['atai_network_all_settings'] === 'yes' ? 'yes' : 'no';
+ update_site_option( 'atai_network_all_settings', $network_all_settings );
+
+ // If enabled, sync all settings to network option for later use by subsites
+ if ( $network_all_settings === 'yes' ) {
+ $this->sync_settings_to_network();
+ }
+ }
+ }
+
// Add custom success message
$message = __( 'API Key saved. Pro tip: Add alt text to all your existing images with our <a href="%s" class="font-medium text-primary-600 hover:text-primary-500">Bulk Generate</a> feature!', 'alttext-ai' );
$message = sprintf( $message, admin_url( 'admin.php?page=atai-bulk-generate' ) );
@@ -605,6 +695,129 @@
}
/**
+ * Sync settings from the main site to the network.
+ *
+ * Uses explicit defaults to avoid propagating unset options as false,
+ * which could lock users out or change behavior unexpectedly on subsites.
+ *
+ * @since 1.10.16
+ * @access private
+ */
+ private function sync_settings_to_network() {
+ if ( ! is_multisite() || ! is_main_site() ) {
+ return;
+ }
+
+ // Settings with their defaults - prevents false from being stored for unset options
+ $settings_with_defaults = array(
+ 'atai_api_key' => '',
+ 'atai_lang' => 'en',
+ 'atai_model_name' => '',
+ 'atai_force_lang' => 'no',
+ 'atai_update_title' => 'no',
+ 'atai_update_caption' => 'no',
+ 'atai_update_description' => 'no',
+ 'atai_enabled' => 'yes',
+ 'atai_skip_filenotfound' => 'no',
+ 'atai_keywords' => 'yes',
+ 'atai_keywords_title' => 'no',
+ 'atai_ecomm' => 'yes',
+ 'atai_ecomm_title' => 'no',
+ 'atai_alt_prefix' => '',
+ 'atai_alt_suffix' => '',
+ 'atai_gpt_prompt' => '',
+ 'atai_type_extensions' => '',
+ 'atai_excluded_post_types' => '',
+ 'atai_bulk_refresh_overwrite' => 'no',
+ 'atai_bulk_refresh_external' => 'no',
+ 'atai_refresh_src_attr' => 'src',
+ 'atai_wp_generate_metadata' => 'no',
+ 'atai_timeout' => '20',
+ 'atai_public' => 'no',
+ 'atai_no_credit_warning' => 'no',
+ 'atai_admin_capability' => 'manage_options',
+ );
+
+ // Create a network_settings array with values from the main site (with defaults)
+ $network_settings = array();
+ foreach ( $settings_with_defaults as $option_name => $default ) {
+ $network_settings[ $option_name ] = get_option( $option_name, $default );
+ }
+
+ // Save all settings to the network options
+ update_site_option( 'atai_network_settings', $network_settings );
+ }
+
+ /**
+ * Refresh network settings cache when a setting is updated.
+ *
+ * This ensures subsites get fresh values when the main site changes settings.
+ *
+ * @since 1.10.16
+ * @access public
+ * @param string $option The option name that was updated.
+ */
+ public function maybe_refresh_network_settings( $option ) {
+ // Only process our plugin's options (all start with 'atai_')
+ if ( strpos( $option, 'atai_' ) !== 0 ) {
+ return;
+ }
+
+ // Only refresh if we're on the main site, multisite is enabled, and network settings are active
+ if ( ! is_multisite() || ! is_main_site() ) {
+ return;
+ }
+
+ $network_all_settings = get_site_option( 'atai_network_all_settings' );
+ if ( $network_all_settings === 'yes' ) {
+ $this->sync_settings_to_network();
+ }
+ }
+
+ /**
+ * Handle network settings update.
+ *
+ * @since 1.10.16
+ * @access public
+ */
+ public function handle_network_settings_update() {
+ if ( ! is_multisite() || ! is_network_admin() ) {
+ return;
+ }
+
+ // Verify user has permission to manage network options
+ if ( ! current_user_can( 'manage_network_options' ) ) {
+ wp_die( esc_html__( 'You do not have permission to manage network settings.', 'alttext-ai' ) );
+ }
+
+ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonces should not be sanitized before verification
+ if ( ! isset( $_POST['atai_network_settings_nonce'] ) || ! wp_verify_nonce( $_POST['atai_network_settings_nonce'], 'atai_network_settings_nonce' ) ) {
+ wp_die( esc_html__( 'Security check failed.', 'alttext-ai' ) );
+ }
+
+ // Update network API key setting
+ $network_api_key = isset( $_POST['atai_network_api_key'] ) ? 'yes' : 'no';
+ update_site_option( 'atai_network_api_key', $network_api_key );
+
+ // Update network all settings option
+ $network_all_settings = isset( $_POST['atai_network_all_settings'] ) ? 'yes' : 'no';
+ update_site_option( 'atai_network_all_settings', $network_all_settings );
+
+ // Update network hide credits option
+ $network_hide_credits = isset( $_POST['atai_network_hide_credits'] ) ? 'yes' : 'no';
+ update_site_option( 'atai_network_hide_credits', $network_hide_credits );
+
+ // Sync settings from main site to network options if enabled
+ if ( $network_all_settings === 'yes' || $network_api_key === 'yes' ) {
+ $this->sync_settings_to_network();
+ }
+
+ // Redirect back to the network settings page with a success message
+ wp_safe_redirect( add_query_arg( 'updated', 'true', network_admin_url( 'settings.php?page=atai-network' ) ) );
+ exit;
+ }
+
+ /**
* Clear error logs on load
*
* @since 1.0.0
@@ -619,8 +832,24 @@
return;
}
+ // Check user has permission
+ $required_capability = ATAI_Utility::get_setting( 'atai_admin_capability', 'manage_options' );
+ if ( ! current_user_can( $required_capability ) ) {
+ wp_die( esc_html__( 'You do not have permission to perform this action.', 'alttext-ai' ) );
+ }
+
+ // Verify CSRF nonce
+ if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'atai_clear_error_logs' ) ) {
+ wp_die(
+ esc_html__( 'Security verification failed. Please refresh the page and try again.', 'alttext-ai' ),
+ esc_html__( 'AltText.ai', 'alttext-ai' ),
+ array( 'back_link' => true )
+ );
+ }
+
delete_option( 'atai_error_logs' );
wp_safe_redirect( add_query_arg( 'atai_action', false ) );
+ exit;
}
/**
@@ -721,7 +950,7 @@
}
// Check user capabilities using configured capability
- $required_capability = get_option( 'atai_admin_capability', 'manage_options' );
+ $required_capability = ATAI_Utility::get_setting( 'atai_admin_capability', 'manage_options' );
if ( ! current_user_can( $required_capability ) ) {
wp_send_json_error( __( 'Insufficient permissions.', 'alttext-ai' ) );
}
--- a/alttext-ai/admin/partials/bulk-generate.php
+++ b/alttext-ai/admin/partials/bulk-generate.php
@@ -97,7 +97,7 @@
}
// Exclude images attached to specific post types
- $excluded_post_types = get_option( 'atai_excluded_post_types' );
+ $excluded_post_types = ATAI_Utility::get_setting( 'atai_excluded_post_types' );
if ( ! empty( $excluded_post_types ) ) {
$post_types = array_map( 'trim', explode( ',', $excluded_post_types ) );
$post_types_placeholders = implode( ',', array_fill( 0, count( $post_types ), '%s' ) );
--- a/alttext-ai/admin/partials/csv-import.php
+++ b/alttext-ai/admin/partials/csv-import.php
@@ -57,6 +57,7 @@
<div class="mt-8">
<p class="block mb-2 text-base font-medium text-gray-900">Step 2: Upload your CSV</p>
<form method="post" enctype="multipart/form-data" id="alttextai-csv-import" class="group" data-file-loaded="false">
+ <?php wp_nonce_field( 'atai_csv_import', 'atai_csv_import_nonce' ); ?>
<div class=" relative flex flex-col items-center gap-2 w-full px-6 py-10 sm:flex mt-2 text-center rounded-lg border-gray-500 hover:bg-gray-200 group transition-colors duration-200 ease-in-out border border-dashed box-border">
<label class="absolute -inset-px size-[calc(100%+2px)] cursor-pointer group-hover:border-gray-500 border border-transparent rounded-lg font-semibold transition-colors duration-200 ease-in-out">
<input
@@ -74,6 +75,27 @@
<p class="text-center mx-auto hidden items-center gap-1.5 group-data-[file-loaded=false]:inline-flex"><span class="text-primary-600 font-medium rounded border border-gray-200 bg-gray-50 px-1.5 py-0.5 ">Choose File</span> or drag and drop.</p>
<p class="text-center mx-auto hidden items-center gap-1.5 group-data-[file-loaded=true]:inline-flex">File added, import to continue.</p>
</div>
+
+ <div id="atai-csv-language-selector" class="mt-6 hidden">
+ <label for="atai-csv-language" class="block mb-2 text-base font-medium text-gray-900">
+ <?php esc_html_e( 'Step 3: Select Language', 'alttext-ai' ); ?>
+ </label>
+ <p class="text-sm text-gray-600 mb-3">
+ <?php esc_html_e( 'Your CSV contains alt text in multiple languages. Choose which language to import:', 'alttext-ai' ); ?>
+ </p>
+
+ <select id="atai-csv-language" name="csv_language" class="w-full max-w-xs px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500">
+ <option value="">
+ <?php esc_html_e( 'Default (alt_text column)', 'alttext-ai' ); ?>
+ </option>
+ <!-- Language options populated via JavaScript -->
+ </select>
+
+ <p class="mt-2 text-xs text-gray-500">
+ <?php esc_html_e( 'Selecting "Default" uses the main alt_text column. This is backward compatible with older exports.', 'alttext-ai' ); ?>
+ </p>
+ </div>
+
<div class="mt-4">
<input type="submit" name="submit" value="Import" class="atai-button blue mt-4 cursor-pointer appearance-none no-underline shadow-sm">
</div>
--- a/alttext-ai/admin/partials/network-settings.php
+++ b/alttext-ai/admin/partials/network-settings.php
@@ -0,0 +1,107 @@
+<?php
+
+/**
+ * Network Settings page for the AltText.ai plugin
+ *
+ * @link https://www.alttext.ai
+ * @since 1.10.16
+ *
+ * @package AltText_AI
+ * @subpackage AltText_AI/admin/partials
+ */
+
+// If this file is called directly, abort.
+if (!defined('WPINC')) {
+ die;
+}
+?>
+
+<div class="wrap">
+ <h1><?php echo esc_html(get_admin_page_title()); ?></h1>
+
+ <?php if ( isset( $_GET['updated'] ) && sanitize_text_field( wp_unslash( $_GET['updated'] ) ) === 'true' ) : ?>
+ <div class="notice notice-success is-dismissible">
+ <p><?php esc_html_e('Network settings saved successfully.', 'alttext-ai'); ?></p>
+ </div>
+ <?php endif; ?>
+
+ <div class="atai-network-settings-container">
+ <form method="post" action="edit.php?action=atai_update_network_settings">
+ <?php wp_nonce_field('atai_network_settings_nonce', 'atai_network_settings_nonce'); ?>
+
+ <div class="atai-card mb-8">
+ <div class="atai-card-header">
+ <h2 class="atai-card-title"><?php esc_html_e('Network Settings', 'alttext-ai'); ?></h2>
+ <p class="atai-card-description"><?php esc_html_e('Configure network-wide settings for AltText.ai', 'alttext-ai'); ?></p>
+ </div>
+
+ <div class="atai-card-body">
+ <div class="mb-6">
+ <h3 class="text-lg font-medium mb-2"><?php esc_html_e('API Key Management', 'alttext-ai'); ?></h3>
+
+ <div class="mb-4 flex items-center relative gap-x-2">
+
+ <input
+ id="atai_network_api_key"
+ name="atai_network_api_key"
+ type="checkbox"
+ value="yes"
+ class="w-4 h-4 !m-0 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
+ <?php checked('yes', get_site_option('atai_network_api_key', 'no')); ?>
+ >
+ <label for="atai_network_api_key" class="text-gray-600"><?php esc_html_e('Apply main site API key to all subsites', 'alttext-ai'); ?></label>
+
+ </div>
+ <div class="-mt-1 text-sm leading-6">
+ <p class="text-xs text-gray-500 mt-1"><?php esc_html_e('When enabled, all subsites will use the API key from the main site.', 'alttext-ai'); ?></p>
+ </div>
+ </div>
+
+ <div class="mb-6">
+ <h3 class="text-lg font-medium mb-2"><?php esc_html_e('Settings Synchronization', 'alttext-ai'); ?></h3>
+
+ <div class="mb-4 flex items-center relative gap-x-2">
+
+ <input
+ id="atai_network_all_settings"
+ name="atai_network_all_settings"
+ type="checkbox"
+ value="yes"
+ class="w-4 h-4 !m-0 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
+ <?php checked('yes', get_site_option('atai_network_all_settings', 'no')); ?>
+ >
+ <label for="atai_network_all_settings" class="text-gray-600"><?php esc_html_e('Apply all settings from main site to all subsites', 'alttext-ai'); ?></label>
+
+ </div>
+ <div class="-mt-1 text-sm leading-6">
+ <p class="text-xs text-gray-500 mt-1"><?php esc_html_e('When enabled, all plugin settings from the main site will be applied to all subsites. Settings on subsites will be disabled and they will use the main site settings.', 'alttext-ai'); ?></p>
+ </div>
+ </div>
+
+ <div class="mb-6">
+ <h3 class="text-lg font-medium mb-2"><?php esc_html_e('Credits Display', 'alttext-ai'); ?></h3>
+
+ <div class="mb-4 flex items-center relative gap-x-2">
+ <input
+ id="atai_network_hide_credits"
+ name="atai_network_hide_credits"
+ type="checkbox"
+ value="yes"
+ class="w-4 h-4 !m-0 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
+ <?php checked('yes', get_site_option('atai_network_hide_credits', 'no')); ?>
+ >
+ <label for="atai_network_hide_credits" class="text-gray-600"><?php esc_html_e('Hide credits display on subsites', 'alttext-ai'); ?></label>
+ </div>
+ <div class="-mt-1 text-sm leading-6">
+ <p class="text-xs text-gray-500 mt-1"><?php esc_html_e('When enabled, the "You have X credits available out of Y" message will be hidden on all subsites.', 'alttext-ai'); ?></p>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="atai-form-actions">
+ <button type="submit" class="button button-primary"><?php esc_html_e('Save Network Settings', 'alttext-ai'); ?></button>
+ </div>
+ </form>
+ </div>
+</div>
--- a/alttext-ai/admin/partials/settings.php
+++ b/alttext-ai/admin/partials/settings.php
@@ -23,14 +23,27 @@
),
'br' => array()
);
+
+ // Multisite network control checks
+ $is_multisite = is_multisite();
+ $is_main_site = is_main_site();
+ $network_controls_api_key = $is_multisite && get_site_option( 'atai_network_api_key' ) === 'yes';
+ $network_controls_all_settings = $is_multisite && get_site_option( 'atai_network_all_settings' ) === 'yes';
+ $network_hides_credits = $is_multisite && ! $is_main_site && get_site_option( 'atai_network_hide_credits' ) === 'yes';
+
+ // Settings are network-controlled only when all settings are shared (not just API key)
+ $settings_network_controlled = $is_multisite && ! $is_main_site && $network_controls_all_settings;
+
+ // API key is locked when either network API key OR all settings are shared
+ $api_key_locked = $is_multisite && ! $is_main_site && ( $network_controls_api_key || $network_controls_all_settings );
?>
<?php
- $lang = get_option( 'atai_lang' );
+ $lang = ATAI_Utility::get_setting( 'atai_lang' );
$supported_languages = ATAI_Utility::supported_languages();
- $ai_model_name = get_option( 'atai_model_name' );
+ $ai_model_name = ATAI_Utility::get_setting( 'atai_model_name' );
$supported_models = ATAI_Utility::supported_model_names();
- $timeout_secs = intval(get_option( 'atai_timeout', 20 ));
+ $timeout_secs = intval(ATAI_Utility::get_setting( 'atai_timeout', 20 ));
$timeout_values = [10, 15, 20, 25, 30];
?>
@@ -109,11 +122,28 @@
<h2 class="mb-0 text-2xl font-bold"><?php esc_html_e( 'AltText.ai WordPress Settings', 'alttext-ai' ); ?></h2>
<?php settings_errors(); ?>
- <form method="post" class="" action="<?php echo esc_url( admin_url() . 'options.php' ); ?>">
+ <?php if ( $settings_network_controlled || $api_key_locked ) : ?>
+ <div class="atai-network-controlled-notice notice notice-info" style="margin: 20px 0; padding: 12px; background-color: #e7f3ff; border-left: 4px solid #2271b1;">
+ <p style="margin: 0;">
+ <strong><?php esc_html_e( 'Network Settings Active:', 'alttext-ai' ); ?></strong>
+ <?php
+ if ( $settings_network_controlled ) {
+ esc_html_e( 'All settings are controlled by the network administrator and cannot be changed on this site.', 'alttext-ai' );
+ } else if ( $api_key_locked ) {
+ esc_html_e( 'The API key is shared across the network and cannot be changed on this site. Other settings can be configured locally.', 'alttext-ai' );
+ }
+ ?>
+ </p>
+ </div>
+ <?php endif; ?>
+
+ <form method="post" class="<?php echo $settings_network_controlled ? 'atai-network-controlled' : ''; ?>" action="<?php echo esc_url( admin_url() . 'options.php' ); ?>">
<?php settings_fields( 'atai-settings' ); ?>
<?php do_settings_sections( 'atai-settings' ); ?>
- <input type="submit" name="submit" value="Save Changes" class="atai-button blue mt-4 cursor-pointer appearance-none no-underline shadow-sm">
+ <?php if ( ! $settings_network_controlled ) : ?>
+ <input type="submit" name="submit" value="Save Changes" class="atai-button blue mt-4 cursor-pointer appearance-none no-underline shadow-sm">
+ <?php endif; ?>
<div class="mt-4 space-y-4 border-b-0 border-t border-solid divide-x-0 divide-y divide-solid sm:space-y-6 border-x-0 border-gray-900/10 divide-gray-900/10">
<div class="">
<div class="pb-12 mt-4 space-y-8 sm:pb-0 sm:space-y-0 sm:border-t">
@@ -127,15 +157,17 @@
name="atai_api_key"
value="<?php echo ( ATAI_Utility::get_api_key() ) ? '*********' : null; ?>"
class="block py-1.5 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset ring-gray-300 shadow-sm sm:max-w-xs sm:text-sm sm:leading-6 focus:ring-2 focus:ring-inset placeholder:text-gray-400 focus:ring-primary-600"
- <?php echo ( $has_file_based_api_key || ATAI_Utility::get_api_key() ) ? 'readonly' : null; ?>
- >
- <input
- type="submit"
- name="handle_api_key"
- class="<?php echo ( ATAI_Utility::get_api_key() ) ? 'atai-button black' : 'atai-button blue'; ?> relative no-underline cursor-pointer shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 whitespace-nowrap"
- value="<?php echo ( ATAI_Utility::get_api_key() ) ? esc_attr__( 'Clear API Key', 'alttext-ai' ) : esc_attr__( 'Add API Key', 'alttext-ai' ); ?>"
- <?php echo ( $has_file_based_api_key ) ? 'disabled' : null; ?>
+ <?php echo ( $has_file_based_api_key || ATAI_Utility::get_api_key() || $api_key_locked ) ? 'readonly' : null; ?>
>
+ <?php if ( ! $api_key_locked ) : ?>
+ <input
+ type="submit"
+ name="handle_api_key"
+ class="<?php echo ( ATAI_Utility::get_api_key() ) ? 'atai-button black' : 'atai-button blue'; ?> relative no-underline cursor-pointer shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 whitespace-nowrap"
+ value="<?php echo ( ATAI_Utility::get_api_key() ) ? esc_attr__( 'Clear API Key', 'alttext-ai' ) : esc_attr__( 'Add API Key', 'alttext-ai' ); ?>"
+ <?php echo ( $has_file_based_api_key ) ? 'disabled' : null; ?>
+ >
+ <?php endif; ?>
</div>
<div class="mt-4 max-w-lg">
<?php if ( ! ATAI_Utility::get_api_key() ) : ?>
@@ -166,7 +198,7 @@
?>
</p>
</div>
- <?php else : ?>
+ <?php elseif ( ! $network_hides_credits ) : ?>
<div class="bg-primary-900/15 p-px rounded-lg">
<p class="py-2 m-0 px-4 leading-relaxed bg-primary-100 rounded-lg sm:p-4">
<?php
@@ -235,7 +267,7 @@
type="checkbox"
value="yes"
class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
- <?php checked( 'yes', get_option( 'atai_force_lang' ) ); ?>
+ <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_force_lang' ) ); ?>
>
</div>
<div class="-mt-1 text-sm leading-6">
@@ -281,7 +313,7 @@
type="checkbox"
value="yes"
class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
- <?php checked( 'yes', get_option( 'atai_update_title' ) ); ?>
+ <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_update_title' ) ); ?>
>
</div>
<div class="-mt-1 text-sm leading-6">
@@ -296,7 +328,7 @@
type="checkbox"
value="yes"
class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
- <?php checked( 'yes', get_option( 'atai_update_caption' ) ); ?>
+ <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_update_caption' ) ); ?>
>
</div>
<div class="-mt-1 text-sm leading-6">
@@ -311,7 +343,7 @@
type="checkbox"
value="yes"
class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
- <?php checked( 'yes', get_option( 'atai_update_description' ) ); ?>
+ <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_update_description' ) ); ?>
>
</div>
<div class="-mt-1 text-sm leading-6">
@@ -326,7 +358,7 @@
name="atai_alt_prefix"
id="atai_alt_prefix"
class="block py-1.5 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset ring-gray-300 shadow-sm sm:text-sm sm:leading-6 focus:ring-2 focus:ring-inset placeholder:text-gray-400 focus:ring-primary-600"
- value="<?php echo esc_html ( get_option( 'atai_alt_prefix' ) ); ?>"
+ value="<?php echo esc_html ( ATAI_Utility::get_setting( 'atai_alt_prefix' ) ); ?>"
>
</div>
</div>
@@ -338,7 +370,7 @@
name="atai_alt_suffix"
id="atai_alt_suffix"
class="block py-1.5 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset ring-gray-300 shadow-sm sm:text-sm sm:leading-6 focus:ring-2 focus:ring-inset placeholder:text-gray-400 focus:ring-primary-600"
- value="<?php echo esc_html ( get_option( 'atai_alt_suffix' ) ); ?>"
+ value="<?php echo esc_html ( ATAI_Utility::get_setting( 'atai_alt_suffix' ) ); ?>"
>
</div>
</div>
@@ -358,7 +390,7 @@
type="checkbox"
value="yes"
class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
- <?php checked( 'yes', get_option( 'atai_enabled' ) ); ?>
+ <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_enabled' ) ); ?>
>
</div>
<div class="-mt-1 text-sm leading-6">
@@ -382,7 +414,7 @@
name="atai_type_extensions"
id="atai_type_extensions"
class="block py-1.5 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset ring-gray-300 shadow-sm sm:text-sm sm:leading-6 focus:ring-2 focus:ring-inset placeholder:text-gray-400 focus:ring-primary-600"
- value="<?php echo esc_html ( get_option( 'atai_type_extensions' ) ); ?>"
+ value="<?php echo esc_html ( ATAI_Utility::get_setting( 'atai_type_extensions' ) ); ?>"
>
</div>
<p class="mt-1 text-gray-500">
@@ -410,7 +442,7 @@
name="atai_excluded_post_types"
id="atai_excluded_post_types"
class="block py-1.5 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset ring-gray-300 shadow-sm sm:text-sm sm:leading-6 focus:ring-2 focus:ring-inset placeholder:text-gray-400 focus:ring-primary-600"
- value="<?php echo esc_html ( get_option( 'atai_excluded_post_types' ) ); ?>"
+ value="<?php echo esc_html ( ATAI_Utility::get_setting( 'atai_excluded_post_types' ) ); ?>"
>
</div>
<p class="mt-1 text-gray-500">
@@ -427,7 +459,7 @@
type="checkbox"
value="yes"
class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
- <?php checked( 'yes', get_option( 'atai_skip_filenotfound' ) ); ?>
+ <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_skip_filenotfound' ) ); ?>
>
</div>
<div class="-mt-1 text-sm leading-6">
@@ -453,7 +485,7 @@
type="checkbox"
value="yes"
class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
- <?php checked( 'yes', get_option( 'atai_keywords' ) ); ?>
+ <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_keywords' ) ); ?>
>
</div>
<div class="-mt-1 text-sm leading-6">
@@ -473,7 +505,7 @@
type="checkbox"
value="yes"
class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
- <?php checked( 'yes', get_option( 'atai_keywords_title' ) ); ?>
+ <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_keywords_title' ) ); ?>
>
</div>
<div class="-mt-1 text-sm leading-6">
@@ -501,7 +533,7 @@
maxlength="512"
class="block py-1.5 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset ring-gray-300 shadow-sm sm:text-sm sm:leading-6 focus:ring-2 focus:ring-inset placeholder:text-gray-400 focus:ring-primary-600"
placeholder="example: Rewrite the following text in the style of Shakespeare: {{AltText}}"
- ><?php echo esc_html ( get_option( 'atai_gpt_prompt' ) ); ?></textarea>
+ ><?php echo esc_html ( ATAI_Utility::get_setting( 'atai_gpt_prompt' ) ); ?></textarea>
</div>
<p class="mt-1 text-gray-500">
<?php esc_html_e( 'Your prompt MUST include the macro {{AltText}}, which will be substituted with the generated alt text, then sent to ChatGPT.', 'alttext-ai' ); ?>
@@ -523,7 +555,7 @@
type="checkbox"
value="yes"
class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
- <?php checked( 'yes', get_option( 'atai_bulk_refresh_overwrite' ) ); ?>
+ <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_bulk_refresh_overwrite' ) ); ?>
>
</div>
<div class="-mt-1 text-sm leading-6">
@@ -539,7 +571,7 @@
type="checkbox"
value="yes"
class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
- <?php checked( 'yes', get_option( 'atai_bulk_refresh_external' ) ); ?>
+ <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_bulk_refresh_external' ) ); ?>
>
</div>
<div class="-mt-1 text-sm leading-6">
@@ -592,7 +624,7 @@
type="checkbox"
value="yes"
class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
- <?php checked( 'yes', get_option( 'atai_ecomm' ) ); ?>
+ <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_ecomm' ) ); ?>
>
</div>
<div class="-mt-1 text-sm leading-6">
@@ -608,7 +640,7 @@
type="checkbox"
value="yes"
class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
- <?php checked( 'yes', get_option( 'atai_ecomm_title' ) ); ?>
+ <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_ecomm_title' ) ); ?>
>
</div>
<div class="-mt-1 text-sm leading-6">
@@ -643,7 +675,7 @@
type="checkbox"
value="yes"
class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
- <?php checked( 'yes', get_option( 'atai_public' ) ); ?>
+ <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_public' ) ); ?>
>
</div>
<div class="-mt-1 text-sm leading-6">
@@ -664,16 +696,16 @@
id="atai_admin_capability"
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:text-sm sm:leading-6"
>
- <option value="manage_options" <?php selected( 'manage_options', get_option( 'atai_admin_capability', 'manage_options' ) ); ?>>
+ <option value="manage_options" <?php selected( 'manage_options', ATAI_Utility::get_setting( 'atai_admin_capability', 'manage_options' ) ); ?>>
<?php esc_html_e( 'Administrators only (recommended)', 'alttext-ai' ); ?>
</option>
- <option value="edit_others_posts" <?php selected( 'edit_others_posts', get_option( 'atai_admin_capability', 'manage_options' ) ); ?>>
+ <option value="edit_others_posts" <?php selected( 'edit_others_posts', ATAI_Utility::get_setting( 'atai_admin_capability', 'manage_options' ) ); ?>>
<?php esc_html_e( 'Editors and Administrators', 'alttext-ai' ); ?>
</option>
- <option value="publish_posts" <?php selected( 'publish_posts', get_option( 'atai_admin_capability', 'manage_options' ) ); ?>>
+ <option value="publish_posts" <?php selected( 'publish_posts', ATAI_Utility::get_setting( 'atai_admin_capability', 'manage_options' ) ); ?>>
<?php esc_html_e( 'Authors, Editors and Administrators', 'alttext-ai' ); ?>
</option>
- <option value="read" <?php selected( 'read', get_option( 'atai_admin_capability', 'manage_options' ) ); ?>>
+ <option value="read" <?php selected( 'read', ATAI_Utility::get_setting( 'atai_admin_capability', 'manage_options' ) ); ?>>
<?php esc_html_e( 'All logged-in users', 'alttext-ai' ); ?>
</option>
</select>
@@ -690,7 +722,7 @@
type="checkbox"
value="yes"
class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
- <?php checked( 'yes', get_option( 'atai_no_credit_warning' ) ); ?>
+ <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_no_credit_warning' ) ); ?>
>
</div>
<div class="-mt-1 text-sm leading-6">
@@ -705,7 +737,7 @@
type="checkbox"
value="yes"
class="w-4 h-4 rounded border-gray-300 checked:bg-white text-primary-600 focus:ring-primary-600"
- <?php checked( 'yes', get_option( 'atai_wp_generate_metadata' ) ); ?>
+ <?php checked( 'yes', ATAI_Utility::get_setting( 'atai_wp_generate_metadata' ) ); ?>
>
</div>
<div class="-mt-1 text-sm leading-6">
@@ -757,7 +789,7 @@
id="atai_refresh_src_attr"
maxlength="128"
class="block py-1.5 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset ring-gray-300 shadow-sm sm:text-sm sm:leading-6 focus:ring-2 focus:ring-inset placeholder:text-gray-400 focus:ring-primary-600"
- value="<?php echo esc_html ( get_option( 'atai_refresh_src_attr' ) ); ?>"
+ value="<?php echo esc_html ( ATAI_Utility::get_setting( 'atai_refresh_src_attr' ) ); ?>"
>
</div>
<p class="mt-1 text-gray-500">
@@ -781,7 +813,7 @@
<?php echo wp_kses( ATAI_Utility::get_error_logs(), $wp_kses_args ); ?>
</div>
<a
- href="<?php echo esc_url( add_query_arg( 'atai_action', 'clear-error-logs' ) ); ?>"
+ href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'atai_action', 'clear-error-logs' ), 'atai_clear_error_logs' ) ); ?>"
class="atai-button blue mt-4 cursor-pointer appearance-none no-underline shadow-sm"
>
<?php esc_html_e( 'Clear Logs', 'alttext-ai' ); ?>
@@ -795,6 +827,23 @@
</div>
</div>
- <input type="submit" name="submit" value="Save Changes" class="atai-button blue mt-4 cursor-pointer appearance-none no-underline shadow-sm">
+ <?php if ( ! $settings_network_controlled ) : ?>
+ <input type="submit" name="submit" value="Save Changes" class="atai-button blue mt-4 cursor-pointer appearance-none no-underline shadow-sm">
+ <?php endif; ?>
</form>
</div>
+
+<?php if ( $settings_network_controlled ) : ?>
+<script type="text/javascript">
+ // Disable all form fields when network-controlled
+ document.addEventListener('DOMContentLoaded', function() {
+ const form = document.querySelector('.atai-network-controlled');
+ if (form) {
+ const inputs = form.querySelectorAll('input:not([type="hidden"]), select, textarea');
+ inputs.forEach(function(input) {
+ input.disabled = true;
+ });
+ }
+ });
+</script>
+<?php endif; ?>
--- a/alttext-ai/atai.php
+++ b/alttext-ai/atai.php
@@ -15,7 +15,7 @@
* Plugin Name: AltText.ai
* Plugin URI: https://alttext.ai/product
* Description: Automatically generate image alt text with AltText.ai.
- * Version: 1.10.15
+ * Version: 1.10.18
* Author: AltText.ai
* Author URI: https://alttext.ai
* License: GPL-2.0+
@@ -33,7 +33,7 @@
/**
* Current plugin version.
*/
-define( 'ATAI_VERSION', '1.10.15' );
+define( 'ATAI_VERSION', '1.10.18' );
/**
* Constant to save the value of the plugin path.
--- a/alttext-ai/includes/class-atai-api.php
+++ b/alttext-ai/includes/class-atai-api.php
@@ -112,7 +112,7 @@
* @param string $attachment_url URL of the image to request alt text for.
*/
public function create_image( $attachment_id, $attachment_url, $api_options, &$response_code ) {
- if ( empty($attachment_id) || get_option( 'atai_public' ) === 'yes' ) {
+ if ( empty($attachment_id) || ATAI_Utility::get_setting( 'atai_public' ) === 'yes' ) {
// If the site is public, get ALT by sending the image URL to the server
$body = array(
'webhook_url' => '',
@@ -126,20 +126,20 @@
// Validate file exists and is readable before attempting to read
if ( ! $file_path || ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
- error_log( "ATAI: File not accessible for attachment {$attachment_id}" );
+ error_log( "ATAI: File not accessible for attachment {$attachment_id}" ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound -- Production error logging
return false;
}
// Use WordPress functions when possible, with error handling
$file_contents = @file_get_contents( $file_path );
if ( $file_contents === false ) {
- error_log( "ATAI: Failed to read file for attachment {$attachment_id}" );
+ error_log( "ATAI: Failed to read file for attachment {$attachment_id}" ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound -- Production error logging
return false;
}
$encoded_content = @base64_encode( $file_contents );
if ( $encoded_content === false ) {
- error_log( "ATAI: Failed to encode file for attachment {$attachment_id}" );
+ error_log( "ATAI: Failed to encode file for attachment {$attachment_id}" ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound -- Production error logging
return false;
}
@@ -154,7 +154,7 @@
}
$body = array_merge( $body, $api_options );
- $timeout_secs = intval(get_option( 'atai_timeout', 20 ));
+ $timeout_secs = intval(ATAI_Utility::get_setting( 'atai_timeout', 20 ));
$response = wp_remote_post(
$this->base_url . '/images',
array(
@@ -172,7 +172,7 @@
if ( ! is_array( $response ) || is_wp_error( $response ) ) {
if ( defined( 'ATAI_DEBUG' ) && ATAI_DEBUG ) {
- error_log( print_r( $response, true ) );
+ error_log( print_r( $response, true ) ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound -- Conditional debug logging
}
ATAI_Utility::log_error(
@@ -199,7 +199,7 @@
if ( $error_message === 'account has insufficient credits' ) {
if ( defined( 'ATAI_DEBUG' ) && ATAI_DEBUG ) {
- error_log( print_r( $response, true ) );
+ error_log( print_r( $response, true ) ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound -- Conditional debug logging
}
ATAI_Utility::log_error(
@@ -212,7 +212,7 @@
)
);
- if ( get_option( 'atai_no_credit_warning' ) != 'yes' ) {
+ if ( ATAI_Utility::get_setting( 'atai_no_credit_warning' ) != 'yes' ) {
set_transient( 'atai_insufficient_credits', TRUE, MONTH_IN_SECONDS );
}
@@ -220,7 +220,7 @@
}
// Check if error indicates URL access issues (when site is marked as public but URLs aren't accessible)
- if ( get_option( 'atai_public' ) === 'yes' &&
+ if ( ATAI_Utility::get_setting( 'atai_public' ) === 'yes' &&
( strpos( strtolower( $error_message ), 'unable to access' ) !== false ||
strpos( strtolower( $error_message ), 'url not accessible' ) !== false ||
strpos( strtolower( $error_message ), 'cannot fetch' ) !== false ||
@@ -240,7 +240,7 @@
return 'url_access_error';
}
- error_log( print_r( $response, true ) );
+ error_log( print_r( $response, true ) ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound -- Production error logging
ATAI_Utility::log_error(
sprintf(
@@ -253,9 +253,9 @@
);
return false;
- } elseif ( substr( $response_code, 0, 1 ) == '4' && get_option( 'atai_public' ) === 'yes' ) {
+ } elseif ( substr( $response_code, 0, 1 ) == '4' && ATAI_Utility::get_setting( 'atai_public' ) === 'yes' ) {
// 4xx errors when site is marked as public likely indicate URL access issues
- error_log( print_r( $response, true ) );
+ error_log( print_r( $response, true ) ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound -- Production error logging
// Extract error message if available
$error_message = '';
@@ -285,7 +285,7 @@
return 'url_access_error';
} elseif ( substr( $response_code, 0, 1 ) != '2' ) {
- error_log( print_r( $response, true ) );
+ error_log( print_r( $response, true ) ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound -- Production error logging
ATAI_Utility::log_error(
sprintf(
--- a/alttext-ai/includes/class-atai-attachment.php
+++ b/alttext-ai/includes/class-atai-attachment.php
@@ -34,6 +34,16 @@
*/
class ATAI_Attachment {
/**
+ * Track attachment IDs that have been processed for alt text generation
+ * in the current request. Prevents duplicate API calls from race conditions
+ * between add_attachment/process_polylang_translations and on_translation_created.
+ *
+ * @since 1.10.17
+ * @var array
+ */
+ private static $processed_attachments = array();
+
+ /**
* Normalize and validate a language code.
*
* Supports 3-tier fallback:
@@ -125,12 +135,12 @@
// Normalize booleans that might arrive as strings/ints via filters
$api_options['overwrite'] = ! empty( $api_options['overwrite'] ) ? true : false;
- $gpt_prompt = get_option('atai_gpt_prompt');
+ $gpt_prompt = ATAI_Utility::get_setting('atai_gpt_prompt');
if ( !empty($gpt_prompt) ) {
$api_options['gpt_prompt'] = $gpt_prompt;
}
- $model_name = get_option('atai_model_name');
+ $model_name = ATAI_Utility::get_setting('atai_model_name');
if ( !empty($model_name) ) {
$api_options['model_name'] = $model_name;
}
@@ -149,7 +159,7 @@
} else {
$api_options['keywords'] = $this->get_seo_keywords( $attachment_id );
}
- if ( ! count( $api_options['keywords'] ) && ( get_option( 'atai_keywords_title' ) === 'yes' ) ) {
+ if ( ! count( $api_options['keywords'] ) && ( ATAI_Utility::get_setting( 'atai_keywords_title' ) === 'yes' ) ) {
$api_options['keyword_source'] = $this->post_title_seo_keywords( $attachment_id );
}
}
@@ -186,8 +196,8 @@
);
// Enforce force_lang setting if enabled (overrides filter and caller language)
- if ( 'yes' === get_option( 'atai_force_lang' ) ) {
- $forced_lang = get_option( 'atai_lang' );
+ if ( 'yes' === ATAI_Utility::get_setting( 'atai_force_lang' ) ) {
+ $forced_lang = ATAI_Utility::get_setting( 'atai_lang' );
if ( is_string( $forced_lang ) && '' !== trim( $forced_lang ) ) {
$api_options['lang'] = $this->normalize_lang(
$forced_lang,
@@ -249,8 +259,8 @@
}
$alt_text = $response['alt_text'];
- $alt_prefix = get_option('atai_alt_prefix');
- $alt_suffix = get_option('atai_alt_suffix');
+ $alt_prefix = ATAI_Utility::get_setting('atai_alt_prefix');
+ $alt_suffix = ATAI_Utility::get_setting('atai_alt_suffix');
if ( ! empty( $alt_prefix ) ) {
$alt_text = trim( $alt_prefix ) . ' ' . $alt_text;
@@ -264,15 +274,15 @@
update_post_meta( $attachment_id, '_wp_attachment_image_alt', $alt_text );
$post_value_updates = array();
- if ( get_option( 'atai_update_title' ) === 'yes' ) {
+ if ( ATAI_Utility::get_setting( 'atai_update_title' ) === 'yes' ) {
$post_value_updates['post_title'] = $alt_text;
};
- if ( get_option( 'atai_update_caption' ) === 'yes' ) {
+ if ( ATAI_Utility::get_setting( 'atai_update_caption' ) === 'yes' ) {
$post_value_updates['post_excerpt'] = $alt_text;
};
- if ( get_option( 'atai_update_description' ) === 'yes' ) {
+ if ( ATAI_Utility::get_setting( 'atai_update_description' ) === 'yes' ) {
$post_value_updates['post_content'] = $alt_text;
};
@@ -297,7 +307,7 @@
* @return boolean True if should be excluded, false otherwise.
*/
private function is_attachment_excluded_by_post_type( $attachment_id ) {
- $excluded_post_types = get_option( 'atai_excluded_post_types' );
+ $excluded_post_types = ATAI_Utility::get_setting( 'atai_excluded_post_types' );
if ( empty( $excluded_post_types ) ) {
return false;
@@ -325,7 +335,7 @@
*
* @return boolean True if eligible, false otherwise.
*/
- public function is_attachment_eligible( $attachment_id, $context = 'generate' ) {
+ public function is_attachment_eligible( $attachment_id, $context = 'generate', $dry_run = false ) {
// Bypass eligibility checks in test mode
if ( defined( 'ATAI_TESTING' ) && ATAI_TESTING ) {
return true;
@@ -364,7 +374,7 @@
$file = ( is_array($meta) && array_key_exists('file', $meta) ) ? ($upload_info['basedir'] . '/' . $meta['file']) : get_attached_file( $attachment_id );
if ( empty( $meta ) && file_exists( $file ) ) {
- if ( ( get_option( 'atai_wp_generate_metadata' ) === 'no' ) ) {
+ if ( $dry_run || ( ATAI_Utility::get_setting( 'atai_wp_generate_metadata' ) === 'no' ) ) {
$meta = array('width' => 100, 'height' => 100); // Default values assuming this is a valid image
}
else {
@@ -392,8 +402,7 @@
if (!$size) {
$offload_meta = get_post_meta($attachment_id, 'amazonS3_info', true) ?: get_post_meta($attachment_id, 'cloudinary_info', true);
if (isset($offload_meta['key'])) {
- $external_url = wp_get_attachment_url($attachment_id);
- $size = ATAI_Utility::get_attachment_size($external_url);
+ $size = ATAI_Utility::get_attachment_size($attachment_id);
}
}
@@ -413,7 +422,7 @@
$is_svg = ($extension_lower === 'svg');
$size_unavailable = ($size === null || $size === false);
- $file_type_extensions = get_option( 'atai_type_extensions' );
+ $file_type_extensions = ATAI_Utility::get_setting( 'atai_type_extensions' );
$attachment_edit_url = get_edit_post_link($attachment_id);
// Logging reasons for ineligibility
@@ -452,7 +461,7 @@
// SVGs often have metadata issues that prevent size detection, skip this check for them
if (!$is_svg && $size_unavailable) {
- if ($should_log && get_option('atai_skip_filenotfound') === 'yes') {
+ if ($should_log && ATAI_Utility::get_setting('atai_skip_filenotfound') === 'yes') {
ATAI_Utility::log_error(
sprintf(
'<a href="%s" target="_blank">Image #%d</a>: %s %s',
@@ -512,11 +521,11 @@
* @return Array ["ecomm" => ["product" => <title>]] or empty array if not found.
*/
public function get_ecomm_data( $attachment_id, $product_id = null ) {
- if ( ( get_option( 'atai_ecomm' ) === 'no' ) || ! ATAI_Utility::has_woocommerce() ) {
+ if ( ( ATAI_Utility::get_setting( 'atai_ecomm' ) === 'no' ) || ! ATAI_Utility::has_woocommerce() ) {
return array();
}
- if ( get_option( 'atai_ecomm_title' ) === 'yes' ) {
+ if ( ATAI_Utility::get_setting( 'atai_ecomm_title' ) === 'yes' ) {
$post = get_post( $attachment_id );
if ( !empty( $post->post_title ) ) {
return array( 'product' => $post->post_title );
@@ -600,7 +609,7 @@
* @return Array of keywords, or empty array if none.
*/
public function get_seo_keywords( $attachment_id, $explicit_post_id = null ) {
- if ( ( get_option( 'atai_keywords' ) === 'no' ) ) {
+ if ( ( ATAI_Utility::get_setting( 'atai_keywords' ) === 'no' ) ) {
return array();
}
@@ -1063,7 +1072,7 @@
* to avoid race conditions with WPML metadata initialization.
*/
public function add_attachment( $attachment_id ) {
- if ( get_option( 'atai_enabled' ) === 'no' ) {
+ if ( ATAI_Utility::get_setting( 'atai_enabled' ) === 'no' ) {
return;
}
@@ -1077,6 +1086,9 @@
// Process WPML translations if applicable
$this->process_wpml_translations( $attachment_id );
+
+ // Process Polylang translations if applicable
+ $this->process_polylang_translations( $attachment_id );
}
/**
@@ -1131,6 +1143,92 @@
}
/**
+ * Process Polylang translations for an attachment.
+ * Returns success/skipped counts and processed IDs for double-processing prevention.
+ *
+ * @since 1.10.16
+ * @access private
+ *
+ * @param int $attachment_id Source attachment ID.
+ * @param array $options Base API options to merge (keywords, negative_keywords, etc.).
+ *
+ * @return array Results with 'success', 'skipped', 'processed_ids' keys.
+ */
+ private function process_polylang_translations( $attachment_id, $options = array() ) {
+ $results = array(
+ 'success' => 0,
+ 'skipped' => 0,
+ 'processed_ids' => array(),
+ );
+
+ if ( ! ATAI_Utility::has_polylang() ) {
+ return $results;
+ }
+
+ // Get list of active language slugs
+ if ( ! function_exists( 'pll_languages_list' ) || ! function_exists( 'pll_get_post' ) ) {
+ return $results;
+ }
+
+ $active_languages = pll_languages_list();
+ if ( empty( $active_languages ) || ! is_array( $active_languages ) ) {
+ return $results;
+ }
+
+ // Get source attachment's language to skip it
+ $source_lang = function_exists( 'pll_get_post_language' )
+ ? pll_get_post_language( $attachment_id )
+ : null;
+
+ foreach ( $active_languages as $lang ) {
+ // Skip source language
+ if ( $lang === $source_lang ) {
+ continue;
+ }
+
+ // Get translated attachment ID
+ $translated_id = pll_get_post( $attachment_id, $lang );
+
+ // Skip non-existent translations
+ if ( ! $translated_id || (int) $translated_id === (int) $attachment_id ) {
+ continue;
+ }
+
+ // Skip invalid or trashed
+ if ( get_post_type( $translated_id ) !== 'attachment' || get_post_status( $translated_id ) === 'trash' ) {
+ $results['skipped']++;
+ $results['processed_ids'][ $translated_id ] = 'skipped';
+ continue;
+ }
+
+ // Skip if already processed in this request (prevents double API calls)
+ if ( isset( self::$processed_attachments[ $translated_id ] ) ) {
+ $results['skipped']++;
+ $results['processed_ids'][ $translated_id ] = 'already_processed';
+ continue;
+ }
+
+ // Mark as processed before API call to prevent race conditions
+ self::$processed_attachments[ $translated_id ] = true;
+
+ // Normalize language code (Polylang may use uppercase or variants)
+ $normalized_lang = strtolower( (string) $lang );
+
+ $response = $this->generate_alt( $translated_id, null, array_merge( $options, array( 'lang' => $normalized_lang ) ) );
+
+ if ( $this->is_generation_error( $response ) ) {
+ $results['skipped']++;
+ $results['processed_ids'][ $translated_id ] = 'error';
+ } else {
+ $results['success']++;
+ $results['processed_ids'][ $translated_id ] = 'success';
+ }
+ }
+
+ return $results;
+ }
+
+ /**
* Check if a generate_alt response is an error.
*
* @since 1.11.0
@@ -1196,6 +1294,7 @@
$images_successful = $images_skipped = $loop_count = 0;
$processed_ids = array(); // Track processed IDs for bulk-select cleanup
$wpml_processed_ids = array(); // Track WPML translation IDs to prevent double-processing
+ $polylang_processed_ids = array(); // Track Polylang translation IDs to prevent double-processing
// Get accumulated skip reasons from previous batches
@@ -1250,7 +1349,7 @@
}
// Exclude images attached to specific post types
- $excluded_post_types = get_option( 'atai_excluded_post_types' );
+ $excluded_post_types = ATAI_Utility::get_setting( 'atai_excluded_post_types' );
if ( ! empty( $excluded_post_types ) ) {
$post_types = array_map( 'trim', explode( ',', $excluded_post_types ) );
$post_types_placeholders = implode( ',', array_fill( 0, count( $post_types ), '%s' ) );
@@ -1356,6 +1455,22 @@
continue;
}
+ // Skip if already processed as Polylang translation (prevents double-processing)
+ if ( in_array( $attachment_id, $polylang_processed_ids, true ) ) {
+ // Don't increment images_skipped to avoid double-counting in stats
+ $skip_reasons['polylang_already_processed'] = ($skip_reasons['polylang_already_processed'] ?? 0) + 1;
+ $last_post_id = $attachment_id;
+
+ if ( $mode === 'bulk-select' ) {
+ $processed_ids[] = $attachment_id;
+ }
+
+ if ( ++$loop_count >= $query_limit ) {
+ break;
+ }
+ continue;
+ }
+
// Skip if attachment is not eligible
if ( ! $this->is_attachment_eligible( $attachment_id, 'bulk' ) ) {
$images_skipped++;
@@ -1438,6 +1553,17 @@
if ( ! empty( $wpml_results['processed_ids'] ) ) {
$wpml_processed_ids = array_merge( $wpml_processed_ids, array_keys( $wpml_results['processed_ids'] ) );
}
+
+ // Process Polylang translations for successfully generated primary images
+ $poly