seo_keywords = new AltTextLab_SEO_Keywords();
$this->wc_integration = new AltTextLab_WC_Integration();
$this->assets_storage = new AltTextLab_Storage();
$this->attachment_helper = new AltTextLab_Attachment_Helper();
$this->database_helper = new AltTextLab_Database();
$this->wpml_helper = new AltTextLab_Wpml_Helper();
}
/**
* Generates descriptive alt text for media files and images.
*
*/
public function generate_alt($attachment_id, $attachment_url = null, $options = [])
{
$api_key = AltTextLab_Utility::get_api_key();
// Exit immediately if API key is missing
if (empty($api_key)) {
return false;
}
// Terminate process if attachment doesn't meet requirements
if ($attachment_id && $this->is_attachment_eligible($attachment_id) === false) {
return false;
}
if ($attachment_id){
$attachment_url = wp_get_attachment_image_url($attachment_id, 'full');
$attachment_url = apply_filters('alttextlab_attachment_url', $attachment_url, $attachment_id);
}
// Combine provided options with default settings
$api_options = $this->prepare_api_options($attachment_id, $options);
$api = new AltTextLab_API($api_key);
$response = $this->call_api_with_retry($api, $attachment_id, $attachment_url, $api_options);
if (!is_array($response)) {
return $response;
}
$alt_text = $response['result'];
$alt_prefix = get_option('alttextlab_alt_prefix', '');
$alt_suffix = get_option('alttextlab_alt_suffix', '');
if (!empty($alt_prefix)) {
$alt_text = trim($alt_prefix) . ' ' . $alt_text;
}
if (!empty($alt_suffix)) {
$alt_text = $alt_text . ' ' . trim($alt_suffix);
}
$this->assets_storage->record_alttextlab_asset($attachment_id, $response['id'], $alt_text);
$this->wpml_helper->apply_alt_text_to_attachment($attachment_id, $alt_text);
do_action('alttextlab_alttext_generated', $attachment_id, $alt_text);
return $alt_text;
}
/**
* Calls the AltTextLab API to generate alt text with exponential backoff on rate limiting.
*
*/
private function call_api_with_retry($api, $attachment_id, $attachment_url, $api_options)
{
$max_retries = 5;
$delay = 1;
$response_code = null;
for ($i = 0; $i < $max_retries; $i++) {
$response = $api->generate_alt_text($attachment_id, $attachment_url, $api_options, $response_code);
if ($response_code !== '429') {
return $response;
}
if ($i < $max_retries - 1) {
sleep($delay);
$delay *= 2;
}
}
return null;
}
/**
* Resolve post ID for API context.
*/
private function get_context_post_id($attachment_id, array $api_options)
{
$post_id = null;
if (!empty($attachment_id)) {
$attachment = get_post($attachment_id);
if ($attachment && !empty($attachment->post_parent)) {
$post_id = absint($attachment->post_parent);
}
}
if (empty($post_id) && isset($api_options['explicit_post_id']) && !empty($api_options['explicit_post_id'])) {
$post_id = absint($api_options['explicit_post_id']);
}
return $post_id ? $post_id : 0;
}
/**
* Build context payload for the alt text generation API.
*/
private function get_api_context($attachment_id, array $api_options)
{
if (get_option('alttextlab_use_post_title_as_context', 'no') !== 'yes') {
return array();
}
if (empty($attachment_id)) {
return array();
}
$post_id = $this->get_context_post_id($attachment_id, $api_options);
if (!$post_id) {
return array();
}
$page_title = get_the_title($post_id);
if (!is_string($page_title) || $page_title === '') {
return array();
}
return array(
'pageTitle' => wp_strip_all_tags($page_title),
);
}
/**
* Builds the full set of API request options for generating alt text.
*
*/
private function prepare_api_options($attachment_id, array $options)
{
$api_options = wp_parse_args(
$options,
array(
'ecommerce' => [],
'keywords' => [],
'lang' => $this->attachment_helper->get_attachment_language( $attachment_id )
)
);
if ( empty( $options['lang'] )
&& ! empty( $api_options['explicit_post_id'] )
&& AltTextLab_Utility::is_wpml_active()
) {
$language_details = apply_filters( 'wpml_post_language_details', null, absint( $api_options['explicit_post_id'] ) );
if ( is_array( $language_details ) && ! empty( $language_details['language_code'] ) ) {
$api_options['lang'] = strtolower( (string) $language_details['language_code'] );
}
}
if ( get_option( 'alttextlab_use_single_lang', 'no' ) === 'yes' ) {
$api_options['lang'] = $this->attachment_helper->get_attachment_language( $attachment_id );
}
$model_name = get_option('alttextlab_model_name');
if (!empty($model_name)) {
$api_options['style'] = $model_name;
}
if ($attachment_id) {
if ( empty($api_options['ecommerce']) && AltTextLab_Utility::has_woocommerce()) {
$product_data = $this->wc_integration->get_product_info($attachment_id);
if ($product_data) {
$api_options['ecommerce'] = $product_data;
}
}
$api_options['ecommerce'] = $this->wc_integration->filtered_ecomm_data( $attachment_id, $api_options['ecommerce'] );
if (!count($api_options['keywords'])) {
if (isset($api_options['explicit_post_id']) && !empty($api_options['explicit_post_id'])) {
$api_options['keywords'] = $this->seo_keywords->get_seo_keywords($attachment_id, $api_options['explicit_post_id']);
} else {
$api_options['keywords'] = $this->seo_keywords->get_seo_keywords($attachment_id);
}
}
if (empty($api_options['context'])) {
$context = $this->get_api_context($attachment_id, $api_options);
if ($context) {
$api_options['context'] = $context;
}
}
}
return $api_options;
}
/**
* Determines if a media item meets the requirements for automatic alt text creation.
*
*/
public function is_attachment_eligible($attachment_id, $context = 'generate')
{
/** Verify custom eligibility filter. Skip processing if attachment is filtered out. **/
$custom_skip = apply_filters('alttextlab_skip_attachment', false, $attachment_id);
if ($custom_skip) {
return false;
}
$meta = wp_get_attachment_metadata($attachment_id);
$upload_info = wp_get_upload_dir();
$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)) {
$meta = wp_generate_attachment_metadata($attachment_id, $file);
}
$size = null;
// Examine local file existence
if (file_exists($file)) {
$size = filesize($file);
}
// First try to get size from metadata
if (!$size && isset($meta['filesize'])) {
$size = $meta['filesize'];
}
// Look for size in cloud storage plugin metadata
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 = $this->attachment_helper->get_attachment_size($external_url);
}
}
$width = $meta['width'] ?? 0;
$height = $meta['height'] ?? 0;
$size = $size ? ($size / pow(1024, 2)) : null; // Convert to megabytes
$type = wp_check_filetype($file) ?: [];
$extension = $type['ext'] ?? pathinfo($file, PATHINFO_EXTENSION);
// When WordPress extension detection fails, extract extension from filename directly:
if (empty($extension)) {
$extension = pathinfo($file, PATHINFO_EXTENSION);
}
$attachment_edit_url = get_edit_post_link($attachment_id);
return $this->validate_image_for_generation($attachment_id, $attachment_edit_url, $context, $size, $extension, $width, $height);
}
/**
* Validates all file parameters for alt text generation eligibility.
*
*/
private function validate_image_for_generation($attachment_id, $attachment_edit_url, $context, $size, $extension, $width, $height)
{
$use_base64 = get_option('alttextlab_public') !== 'yes';
if (!in_array(strtolower($extension), self::ALLOWED_EXTENSIONS)) {
$this->log_error('Unsupported extension.', $attachment_id, $attachment_edit_url, $context, $extension);
return false;
}
if ($size === null || $size === false) {
$this->log_error('File not found.', $attachment_id, $attachment_edit_url, $context);
return false;
}
if ($use_base64 && $size > self::MAX_FILE_SIZE_MB) {
$this->log_error('File size exceeds 16MB limit.', $attachment_id, $attachment_edit_url, $context, sprintf('%.2f MB', $size));
return false;
}
if ($width < self::MIN_DIMENSION_PX || $height < self::MIN_DIMENSION_PX) {
$this->log_error('Image dimensions too small.', $attachment_id, $attachment_edit_url, $context, "{$width}x{$height}");
return false;
}
return true;
}
/*
* Records structured error information when an attachment fails validation checks or processing.
*
*/
private function log_error($message, $attachment_id, $edit_url, $context, $details = '')
{
if ($context === 'generate') {
AltTextLab_Logger::log_error(
sprintf(
'Image #%d: %s%s',
esc_url($edit_url),
(int)$attachment_id,
esc_html__($message, 'alttextlab'),
$details ? " ($details)" : ''
)
);
}
}
/**
* Automatically creates alt text for newly uploaded media files
*
*/
public function add_attachment($attachment_id)
{
if (get_option('alttextlab_auto_generation_enabled') === 'no') {
return;
}
if ( ! $this->is_attachment_eligible( $attachment_id ) ) {
return;
}
$options = array();
$post_id = intval($_REQUEST['post_id'] ?? 0);
if ($post_id > 0) {
$options['explicit_post_id'] = $post_id;
}
$generated_alt_text = null;
if ( $this->wpml_helper->is_wpml_generation_allowed_for_attachment( $attachment_id ) ) {
$generated_alt_text = $this->generate_alt($attachment_id, null, $options);
}
if ( is_string( $generated_alt_text ) && $generated_alt_text !== '' && ! in_array( $generated_alt_text, array( self::ERROR_NO_CREDITS, self::ERROR_INVALID_API_KEY, self::ERROR_MONTHLY_LIMIT ), true )) {
$options['generated_alt_text'] = $generated_alt_text;
}
$this->generate_wpml_translation_alts( $attachment_id, $options );
}
/**
* Process a batch of images for alt text generation.
*
*/
public function alttextlab_bulk_generate()
{
check_ajax_referer('security_alttextlab_bulk_generate', 'security');
$last_post_id = intval($_REQUEST['last_post_id'] ?? 0);
$useMediaIDS = sanitize_text_field($_REQUEST['modeUseMediaIds'] ?? 0);
$batch_id = sanitize_text_field($_REQUEST['batchId'] ?? '0');
$redirect_url = admin_url('admin.php?page=alttextlab-bulk-generate');
$recursive = true;
$images_to_update_transient = [];
if ($useMediaIDS){
$images_to_update_transient = get_transient('alttextlab_bulk_select_generate_' . $batch_id);
}
$images_to_update = $this->query_attachments_for_bulk_alt($last_post_id, $useMediaIDS, $batch_id, $images_to_update_transient, $redirect_url);
$raw_attachment_ids = array_map(
static function ($image) {
return (int) $image->post_id;
},
$images_to_update
);
$last_post_id = ! empty($raw_attachment_ids) ? max($raw_attachment_ids) : $last_post_id;
if (count($images_to_update) == 0) {
if (empty($raw_attachment_ids)) {
if ($useMediaIDS) {
delete_transient('alttextlab_bulk_select_generate_' . $batch_id);
delete_transient('alttextlab_bulk_select_generate_redirect_' . $batch_id);
}
$message = $last_post_id > 0
? __('Update complete!', 'alttextlab')
: __('No images to process.', 'alttextlab');
wp_send_json(array(
'status' => 'success',
'message' => $message,
'recursive' => false,
'redirect_url' => $redirect_url,
'last_post_id' => $last_post_id
));
}
wp_send_json(array(
'status' => 'success',
'message' => __('No images to process.', 'alttextlab'),
'recursive' => true,
'images' => array(),
'redirect_url' => $redirect_url,
'last_post_id' => $last_post_id
));
}
if ($useMediaIDS) {
$processed_ids = array_map(
static function ($image) {
return (int) $image->post_id;
},
$images_to_update
);
// Remove only the attachments that were actually queued for this bulk step.
$images_to_update_transient = array_diff($images_to_update_transient, $processed_ids);
set_transient('alttextlab_bulk_select_generate_' . $batch_id, $images_to_update_transient, 2048);
}
wp_send_json(array(
'status' => 'success',
'message' => __('Images successfully retrieved.', 'alttextlab'),
'recursive' => $recursive,
'redirect_url' => $redirect_url,
'images' => $images_to_update,
'last_post_id' => $last_post_id
));
}
/**
* Retrieves a filtered list of image attachments for bulk alt text generation.
*
*/
private function query_attachments_for_bulk_alt($last_post_id, $useMediaIDS, $batch_id, $images_to_update_transient, &$redirect_url)
{
global $wpdb;
$is_bulk_empty = false;
$query_limit = max(intval($_REQUEST['posts_per_page'] ?? 0), 1);
$mode = sanitize_text_field($_REQUEST['mode'] ?? 'missing');
$only_attached = sanitize_text_field($_REQUEST['onlyAttached'] ?? '0');
$only_new = sanitize_text_field($_REQUEST['onlyNew'] ?? '0');
if ($mode === 'all') {
$images_to_update_sql = "
SELECT post_table.ID as post_id
FROM {$wpdb->posts} post_table
WHERE post_table.ID > %d
AND (post_table.post_status = 'inherit')
AND (post_table.post_mime_type LIKE 'image/%')
AND post_table.post_type = 'attachment'
";
} else {
// Use 'missing' mode as default
// Handles only images without alt text attributes
$images_to_update_sql = "
SELECT post_table.ID as post_id
FROM {$wpdb->posts} post_table
LEFT JOIN {$wpdb->postmeta} AS postmeta_talbe
ON (post_table.ID = postmeta_talbe.post_id AND postmeta_talbe.meta_key = '_wp_attachment_image_alt')
LEFT JOIN {$wpdb->postmeta} AS empty_postmeta_table ON (post_table.ID = empty_postmeta_table.post_id)
WHERE post_table.ID > %d
AND (post_table.post_mime_type LIKE 'image/%')
AND ((empty_postmeta_table.meta_key = '_wp_attachment_image_alt' AND empty_postmeta_table.meta_value = '') OR postmeta_talbe.post_id IS NULL)
AND (post_table.post_status = 'inherit')
AND post_table.post_type = 'attachment'
";
}
if ($only_attached === '1') {
$images_to_update_sql = $images_to_update_sql . " AND (post_table.post_parent > 0)";
}
if ($only_new === '1') {
$alttextlab_asset_table = $wpdb->prefix . ALT_TEXT_LAB_DB_ASSET_TABLE;
$images_to_update_sql = $images_to_update_sql . " AND (NOT EXISTS(SELECT 1 FROM {$alttextlab_asset_table} WHERE wp_post_id = post_table.ID))";
}
$params = [$last_post_id];
if ($useMediaIDS) {
if (!empty($images_to_update_transient) && is_array($images_to_update_transient)) {
$placeholders = implode(',', array_fill(0, count($images_to_update_transient), '%d'));
$images_to_update_sql .= " AND post_table.ID IN ($placeholders)";
$params = array_merge($params, array_map('intval', $images_to_update_transient));
} else {
$is_bulk_empty = true;
}
if ($url = get_transient('alttextlab_bulk_select_generate_redirect_' . $batch_id)) {
$redirect_url = $url;
}
}
$images_to_update_sql = $images_to_update_sql . " GROUP BY post_table.ID ORDER BY post_table.ID LIMIT %d";
$params[] = $query_limit;
if ($useMediaIDS && $is_bulk_empty){
$images_to_update = [];
}else{
$images_to_update = $wpdb->get_results($wpdb->prepare($images_to_update_sql, ...$params));
}
return $images_to_update;
}
/**
* Process image to create alt text
*
*/
public function alttextlab_bulk_generate_for_image()
{
check_ajax_referer('security_alttextlab_generate_for_image', 'security');
$is_successful = true;
$post_id = intval($_REQUEST['postId'] ?? 0);
if (!$post_id) {
wp_send_json(array(
'status' => 'success',
'message' => __('No image to process.', 'alttextlab'),
'process_count' => 0,
'success_count' => 0
));
}
if (defined('alttextlab_BULK_DEBUG')) {
AltTextLab_Logger::log_error(sprintf("BulkGenerate: Attachment ID %d", $post_id));
}
if ( ! $this->wpml_helper->is_wpml_generation_allowed_for_attachment( $post_id ) ) {
wp_send_json(array(
'status' => 'success',
'message' => __('Image skipped by WPML language filter.', 'alttextlab'),
'skip_reason' => 'wpml_language_filtered',
'process_count' => 1,
'success_count' => 0
));
}
$run_id = sanitize_text_field( $_REQUEST['runId'] ?? '' );
if ( $this->wpml_helper->apply_wpml_single_lang_bulk_cached_alt( $post_id, $run_id ) ) {
wp_send_json( array(
'status' => 'success',
'message' => __( 'Images successfully updated.', 'alttextlab' ),
'skip_reason' => 'wpml_single_lang_cached',
'process_count' => 1,
'success_count' => 1,
) );
}
$response = $this->generate_alt($post_id, null, array('keywords' => []));
if (!is_array($response) && $response === false) {
$is_successful = false;
}
if ($response === self::ERROR_NO_CREDITS || $response === self::ERROR_INVALID_API_KEY || $response === self::ERROR_MONTHLY_LIMIT) {
if ( $response === self::ERROR_NO_CREDITS ) {
$message = __( 'Images partially updated (no more tokens).', 'alttextlab' );
} elseif ( $response === self::ERROR_INVALID_API_KEY ) {
$message = __( 'Your API key is invalid.', 'alttextlab' );
} else {
$message = __( 'Images partially updated (API key\'s monthly limit reached).', 'alttextlab' );
}
wp_send_json( array(
'status' => 'accountError',
'message' => $message,
'process_count' => 1,
'success_count' => 0
));
}
$this->wpml_helper->remember_wpml_single_lang_bulk_group_origin( $post_id, $response, $run_id );
wp_send_json(array(
'status' => 'success',
'message' => __('Images successfully updated.', 'alttextlab'),
'process_count' => 1,
'success_count' => $is_successful ? 1 : 0
));
}
/**
* Creates filtering option in media library for alt text status
*
*/
public function render_media_alt_filter_dropdown( $post_type ) {
if ( $post_type !== 'attachment' ) {
return;
};
$alttextlab_filter = sanitize_text_field( $_GET['alttextlab_filter'] ?? 'all' );
echo '';
}
/**
* Modifies media library queries to display only images without alt text
*
*/
public function filter_media_library_query_by_alt( $query ) {
if ( function_exists( 'get_current_screen' ) && get_current_screen() ) {
$is_media_screen = get_current_screen()->base === 'upload';
}
else {
$is_media_screen = ( isset($query->query['post_type'] ) && ( $query->query['post_type'] === 'attachment' ) );
}
$alttextlab_filter = sanitize_text_field( $_GET['alttextlab_filter'] ?? 'all' );
if ( ! is_admin() || ! $query->is_main_query() || ! $is_media_screen || $alttextlab_filter === 'all' ) {
return;
}
$meta_query = $query->get('meta_query') ? $query->get('meta_query') : array();
$meta_query[] = array(
'relation' => 'OR',
array(
'key' => '_wp_attachment_image_alt',
'value' => '',
'compare' => '=',
),
array(
'key' => '_wp_attachment_image_alt',
'compare' => 'NOT EXISTS',
),
);
$query->set( 'meta_query', $meta_query );
}
/**
* Modifies ALT text through AJAX request
*
*/
public function ajax_edit_history()
{
check_ajax_referer('alttextlab_edit_history', 'security');
$attachment_id = intval(wp_unslash($_REQUEST['attachment_id']) ?? 0);
$alt_text = sanitize_text_field($_REQUEST['alt_text'] ?? '');
if (!$attachment_id) {
wp_send_json(array(
'status' => 'error',
'message' => __('Invalid request.', 'alttextlab' )
));
}
$this->wpml_helper->apply_alt_text_to_attachment($attachment_id, $alt_text);
wp_send_json(array(
'status' => 'success',
'message' => __('Alt text updated.', 'alttextlab' )
));
}
/**
* Creates ALT text for individual image through AJAX call
*
*/
public function ajax_single_generate()
{
// Exit if attachment ID is missing or not a valid number
if (!isset($_REQUEST['attachment_id']) || empty($_REQUEST['attachment_id']) || !is_numeric($_REQUEST['attachment_id'])) {
return;
}
check_ajax_referer('alttextlab_single_generate', 'security');
$attachment_id = intval(wp_unslash($_REQUEST['attachment_id']) ?? 0);
if (isset($_REQUEST['keywords']) && is_array($_REQUEST['keywords'])) {
$keywords = array_map('sanitize_text_field', $_REQUEST['keywords']);
} else {
$keywords = [];
}
// Create alternative text for the image
$response = $this->generate_alt($attachment_id, null, array('keywords' => $keywords));
if ($response === self::ERROR_NO_CREDITS) {
wp_send_json(array(
'status' => 'error',
'message' => 'You have no more tokens available. Go to your account on AltTextLab to get more tokens.',
));
}
if ($response === self::ERROR_INVALID_API_KEY) {
wp_send_json(array(
'status' => 'error',
'message' => 'Your API key is invalid. Update it in your account settings.',
));
}
if ($response === self::ERROR_MONTHLY_LIMIT) {
wp_send_json(array(
'status' => 'error',
'message' => __( "You've reached the monthly limit for this API key. Increase it in your AltTextLab dashboard, or wait until it resets next month.", 'alttextlab' ),
));
}
if (!is_array($response) && $response !== false) {
$this->generate_wpml_translation_alts(
$attachment_id,
array(
'keywords' => $keywords,
'respect_wpml_language_selection' => false,
)
);
wp_send_json(array(
'status' => 'success',
'alt_text' => $response,
));
}
wp_send_json(array(
'status' => 'error',
));
}
/**
* Includes alt text generation in media library bulk actions menu
*
*/
public function add_bulk_select_action($actions)
{
$actions['alttextlab_options'] = esc_html__('↓ AltTextLab', 'alttextlab' );
$actions['alttextlab_generate_alt'] = esc_html__('Generate Alt Text', 'alttextlab' );
return $actions;
}
/**
* Handles bulk selection operations for alt text generation
*
*/
public function bulk_select_action_handler($redirect, $do_action, $items)
{
// Skip processing if this isn't our alt text generation action
if ($do_action !== 'alttextlab_generate_alt') {
return $redirect;
}
// Create unique identifier for this batch processing request
$batch_id = uniqid();
// Save selected image IDs in temporary WordPress storage
set_transient('alttextlab_bulk_select_generate_' . $batch_id, $items, 2048);
// Preserve return URL in temporary storage for later use
set_transient('alttextlab_bulk_select_generate_redirect_' . $batch_id, $redirect, 2048);
// Send user to the bulk processing page with appropriate parameters
return admin_url('admin.php?page=alttextlab-bulk-generate&alttextlab_action=bulk-select-generate&alttextlab_batch_id=' . $batch_id . '&alttextlab_mode=all');
}
/**
* Verifies image suitability for automatic alt text generation via AJAX
*
*/
public function ajax_is_attachment_eligible()
{
check_ajax_referer('alttextlab_check_attachment_eligibility', 'security');
$attachment_id = intval($_POST['attachment_id'] ?? 0);
// Return error response if attachment ID is invalid
if (!$attachment_id) {
wp_send_json(array(
'status' => 'error',
'message' => __('Invalid post ID.', 'alttextlab' )
));
}
if (!$this->is_attachment_eligible($attachment_id, 'check')) {
wp_send_json(array(
'status' => 'error',
'message' => __('Image is not eligible for auto-generating ALT text.', 'alttextlab' )
));
}
wp_send_json(array(
'status' => 'success',
'message' => __('Image is eligible for auto-generating ALT text.', 'alttextlab' )
));
}
/**
* Retrieves the total count of all image attachments in the media library
*
*/
public function get_all_images_count()
{
global $wpdb;
$all_images_query = $this->database_helper->query_all_images($wpdb);
$all_images_count = (int)$wpdb->get_results($wpdb->prepare($all_images_query))[0]->total_images;
return $all_images_count;
}
/**
* Retrieves the count of image attachments that have missing or empty alt text
*
*/
public function get_images_without_alt_text_count()
{
global $wpdb;
$images_missing_alt_text_query = $this->database_helper->query_images_without_alt_text($wpdb);
$images_missing_alt_text_count = (int)$wpdb->get_results($wpdb->prepare($images_missing_alt_text_query))[0]->total_images;
return $images_missing_alt_text_count;
}
/**
* Retrieves image attachments that have missing or empty alt text.
*
*/
public function get_images_without_alt_text($limit = 20)
{
global $wpdb;
$limit = absint( $limit ) < 1 ? 20 : ( absint( $limit ) > 100 ? 100 : absint( $limit ) );
$images_to_update_sql = "
SELECT post_table.ID as post_id
FROM {$wpdb->posts} post_table
LEFT JOIN {$wpdb->postmeta} AS postmeta_talbe ON (post_table.ID = postmeta_talbe.post_id AND postmeta_talbe.meta_key = '_wp_attachment_image_alt')
LEFT JOIN {$wpdb->postmeta} AS empty_postmeta_table ON (post_table.ID = empty_postmeta_table.post_id)
WHERE (post_table.post_mime_type LIKE 'image/%')
AND ((empty_postmeta_table.meta_key = '_wp_attachment_image_alt' AND empty_postmeta_table.meta_value = '') OR postmeta_talbe.post_id IS NULL)
AND (post_table.post_status = 'inherit')
AND post_table.post_type = 'attachment'
GROUP BY post_table.ID
ORDER BY post_table.ID
LIMIT %d";
$rows = $wpdb->get_results($wpdb->prepare($images_to_update_sql, $limit));
$images = array();
foreach ( $rows as $image ) {
$attachment_id = (int) $image->post_id;
$images[] = array(
'attachment_id' => $attachment_id,
'title' => get_the_title( $attachment_id )
);
}
return $images;
}
/**
* Retrieves count of images based on multiple filtering criteria
*
*/
public function get_filtered_images_count($mode, $only_attached, $only_new, $action, $selected_images)
{
global $wpdb;
$alttextlab_asset_table = $wpdb->prefix . ALT_TEXT_LAB_DB_ASSET_TABLE;
$is_bulk_empty = false;
$params = [];
if ($mode === 'all') {
$result_query = $this->database_helper->query_all_images($wpdb);
} else if ($mode === 'missing') {
$result_query = $this->database_helper->query_images_without_alt_text($wpdb);
}
if ($only_attached === '1') {
$result_query .= " AND post_table.post_parent > %d";
$params[] = 0;
}
if ($only_new === '1') {
$result_query .= " AND (NOT EXISTS(SELECT 1 FROM {$alttextlab_asset_table} WHERE wp_post_id = post_table.ID))";
}
if ($action === 'bulk-select-generate') {
//Add filter by selected id
if (!empty($selected_images) && is_array($selected_images)) {
$placeholders = implode(',', array_fill(0, count($selected_images), '%d'));
$result_query .= " AND post_table.ID IN ($placeholders)";
$params = array_merge($params, array_map('intval', $selected_images));
} else{
$is_bulk_empty = true;
}
}
if ($action === 'bulk-select-generate' && $is_bulk_empty) {
$images_count = 0;
} else {
$prepared_query = $wpdb->prepare($result_query, ...$params);
$images_count = (int)$wpdb->get_results($prepared_query)[0]->total_images;
}
return $images_count;
}
/**
* Generates alt text for multiple image attachments.
*
*/
public function bulk_generate_alt_text_for_attachments( array $attachment_ids, array $options = array() ) {
$attachment_ids = array_values( array_unique( array_filter( array_map( 'absint', $attachment_ids ) ) ) );
$results = array();
$success_count = 0;
$error_count = 0;
foreach ( $attachment_ids as $attachment_id ) {
if ( ! $attachment_id || get_post_type( $attachment_id ) !== 'attachment' ) {
$error_count++;
$results[] = array(
'attachment_id' => $attachment_id,
'success' => false,
'error_code' => 'invalid_attachment',
'message' => __( 'Invalid attachment ID.', 'alttextlab' ),
);
continue;
}
if ( ! wp_attachment_is_image( $attachment_id ) ) {
$error_count++;
$results[] = array(
'attachment_id' => $attachment_id,
'success' => false,
'error_code' => 'not_image',
'message' => __( 'Attachment is not an image.', 'alttextlab' ),
);
continue;
}
if ( ! $this->wpml_helper->is_wpml_generation_allowed_for_attachment( $attachment_id ) ) {
$error_count++;
$results[] = array(
'attachment_id' => $attachment_id,
'success' => false,
'error_code' => 'wpml_language_filtered',
'message' => __( 'Image skipped by WPML language filter.', 'alttextlab' ),
);
continue;
}
if ( $this->wpml_helper->apply_wpml_single_lang_bulk_cached_alt( $attachment_id, $options['run_id'] ?? '' ) ) {
$success_count++;
$results[] = array(
'attachment_id' => $attachment_id,
'success' => true,
'skip_reason' => 'wpml_single_lang_cached',
'message' => __( 'Image updated from cached WPML alt text.', 'alttextlab' ),
);
continue;
}
$alt_text = $this->generate_alt(
$attachment_id,
null,
array()
);
if ( $alt_text === self::ERROR_NO_CREDITS ) {
$error_count++;
$results[] = array(
'attachment_id' => $attachment_id,
'success' => false,
'error_code' => 'alttextlab_no_credits',
'message' => __( 'No AltTextLab credits available.', 'alttextlab' ),
);
break;
}
if ( $alt_text === self::ERROR_INVALID_API_KEY ) {
$error_count++;
$results[] = array(
'attachment_id' => $attachment_id,
'success' => false,
'error_code' => 'alttextlab_invalid_api_key',
'message' => __( 'The AltTextLab API key is invalid.', 'alttextlab' ),
);
break;
}
if ( $alt_text === self::ERROR_MONTHLY_LIMIT ) {
$error_count++;
$results[] = array(
'attachment_id' => $attachment_id,
'success' => false,
'error_code' => 'alttextlab_monthly_limit',
'message' => __( 'Monthly credit limit reached for this API key. Update it anytime in your AltTextLab account settings.', 'alttextlab' ),
);
break;
}
if ( ! is_string( $alt_text ) || $alt_text === '' ) {
$error_count++;
$results[] = array(
'attachment_id' => $attachment_id,
'success' => false,
'error_code' => 'alttextlab_generation_failed',
'message' => __( 'Alt text generation failed.', 'alttextlab' ),
);
continue;
}
$this->wpml_helper->remember_wpml_single_lang_bulk_group_origin(
$attachment_id,
$alt_text,
$options['run_id'] ?? ''
);
$success_count++;
$results[] = array(
'attachment_id' => $attachment_id,
'success' => true,
'alt_text' => $alt_text,
);
}
return array(
'success' => true,
'processed' => count( $results ),
'success_count' => $success_count,
'error_count' => $error_count,
'results' => $results,
);
}
/**
* Apply one generated alt text value to multiple attachment translations.
*/
private function apply_alt_text_to_translations( $attachment_ids, $alt_text ) {
if ( ! is_string( $alt_text ) || $alt_text === '' ) {
return;
}
foreach ( $attachment_ids as $attachment_id ) {
$attachment_id = (int) $attachment_id;
if ( ! $attachment_id ) {
continue;
}
$this->wpml_helper->apply_alt_text_to_attachment( $attachment_id, $alt_text );
do_action( 'alttextlab_alttext_generated', $attachment_id, $alt_text );
}
}
/**
* Generate alt text for an attachment's WPML translations.
*/
private function generate_wpml_translation_alts( $attachment_id, $options = array() ) {
$generated_alt_text = null;
$respect_language_selection = true;
if ( array_key_exists( 'generated_alt_text', $options ) ) {
$generated_alt_text = $options['generated_alt_text'];
unset( $options['generated_alt_text'] );
}
if ( array_key_exists( 'respect_wpml_language_selection', $options ) ) {
$respect_language_selection = ! empty( $options['respect_wpml_language_selection'] );
unset( $options['respect_wpml_language_selection'] );
}
if ( ! AltTextLab_Utility::is_wpml_active() ) {
return;
}
$target_languages = apply_filters( 'wpml_active_languages', null );
if ( empty( $target_languages ) || ! is_array( $target_languages ) ) {
return;
}
$target_languages = $this->wpml_helper->get_target_wpml_languages( $target_languages, $respect_language_selection );
if ( empty( $target_languages ) ) {
return;
}
$translated_attachments = $this->wpml_helper->get_wpml_translation_attachments( $attachment_id, $target_languages );
if ( empty( $translated_attachments ) ) {
return;
}
if ( $this->wpml_helper->is_single_language_mode_enabled() ) {
$translated_ids = array_values( $translated_attachments );
if ( ! is_string( $generated_alt_text ) || $generated_alt_text === '' ) {
$seed_attachment_id = array_shift( $translated_ids );
$generated_alt_text = $this->generate_alt( $seed_attachment_id, null, $options );
}
$this->apply_alt_text_to_translations( $translated_ids, $generated_alt_text );
return;
}
foreach ( $translated_attachments as $language_code => $translated_id ) {
$this->generate_alt( $translated_id, null, array_merge( $options, array( 'lang' => strtolower( (string) $language_code ) ) ));
}
}
}