PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.78
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.78
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / extensions / alt-text-generator / Alt_Text_Generator.php

Alt_Text_Generator.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.78, at includes/extensions/alt-text-generator/Alt_Text_Generator.php

669 lines 27.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles automatic alt text generation for images using AI.
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons;
9
10 if (!defined('ABSPATH')) {
11 exit; // Exit if accessed directly.
12 }
13
14 /**
15 * Class Alt_Text_Generator
16 */
17 class Alt_Text_Generator {
18
19 // Constants for batch processing
20 private const BATCH_OPTION_PENDING = 'king_addons_alt_text_pending_queue';
21 private const BATCH_OPTION_PROCESSING = 'king_addons_alt_text_processing';
22 private const CRON_HOOK = 'king_addons_process_alt_text_queue';
23 private const BATCH_SIZE = 1; // Process 1 image at a time to respect rate limits
24 private const RETRY_LIMIT = 3; // Maximum retry attempts
25 private const RATE_LIMIT_DELAY = 5; // Delay between batches in seconds
26
27 /**
28 * Constructor.
29 * Hooks into WordPress actions.
30 */
31 public function __construct() {
32 // Check if AI features are enabled
33 $options = get_option('king_addons_ai_options', []);
34
35 // Check if any alt text feature is enabled (with proper defaults)
36 $button_enabled = isset($options['enable_ai_alt_text_button']) ? (bool) $options['enable_ai_alt_text_button'] : true; // Default to true
37 $auto_enabled = isset($options['enable_ai_alt_text_auto_generation']) ? (bool) $options['enable_ai_alt_text_auto_generation'] : false; // Default to false
38
39 if (!$button_enabled && !$auto_enabled) {
40 return; // Don't initialize if both features are disabled
41 }
42
43 $has_api_key = !empty($options['openai_api_key']);
44
45 // Hook for new attachments (only if auto generation is enabled and API key exists).
46 if ($auto_enabled && $has_api_key) {
47 if(king_addons_freemius()->can_use_premium_code()){
48 add_action('add_attachment', array($this, 'add_to_processing_queue'));
49 add_action(self::CRON_HOOK, array($this, 'process_alt_text_queue'));
50
51 // Add custom cron interval
52 add_filter('cron_schedules', array($this, 'add_custom_cron_intervals'));
53
54 // Schedule recurring cron job if not already scheduled
55 if (!wp_next_scheduled(self::CRON_HOOK)) {
56 wp_schedule_event(time(), 'king_addons_alt_text_interval', self::CRON_HOOK);
57 }
58 }
59 }
60
61 // Hooks for Media Library integration (always enable if button is enabled, regardless of API key).
62 if ($button_enabled) {
63 add_filter('manage_media_columns', array($this, 'add_alt_text_column'));
64 add_action('manage_media_custom_column', array($this, 'display_alt_text_column'), 10, 2);
65 add_action('admin_enqueue_scripts', array($this, 'enqueue_media_scripts'));
66
67 // AJAX handler for manual generation (only if API key exists).
68 if ($has_api_key) {
69 add_action('wp_ajax_king_addons_generate_single_alt', array($this, 'handle_ajax_generate_single_alt'));
70 }
71
72 // AJAX handler for queue status (debugging)
73 add_action('wp_ajax_king_addons_alt_text_queue_status', array($this, 'handle_ajax_queue_status'));
74 }
75
76 // Hook for cleanup on plugin deactivation
77 register_deactivation_hook(KING_ADDONS_PATH . 'king-addons.php', array($this, 'cleanup_cron_jobs'));
78
79 // Hook to update cron schedule when settings change
80 add_action('update_option_king_addons_ai_options', array($this, 'update_cron_schedule'), 10, 3);
81 }
82
83 /**
84 * Enqueues scripts needed for the media library screen.
85 *
86 * @param string $hook The current admin page hook.
87 */
88 public function enqueue_media_scripts(string $hook): void {
89 // Load on the upload.php screen (Media Library List view) and post editing screens (for media modal)
90 if (!in_array($hook, ['upload.php', 'post.php', 'post-new.php', 'page.php', 'page-new.php'])) {
91 return;
92 }
93
94 wp_enqueue_style(
95 'king-addons-media-alt-text-styles',
96 KING_ADDONS_URL . 'includes/extensions/alt-text-generator/alt-text-styles.css',
97 array(),
98 KING_ADDONS_VERSION
99 );
100
101 wp_enqueue_script(
102 'king-addons-media-alt-text',
103 KING_ADDONS_URL . 'includes/extensions/alt-text-generator/alt-text-media.js',
104 array('jquery'),
105 KING_ADDONS_VERSION,
106 true
107 );
108
109 // Check if API key exists
110 $options = get_option('king_addons_ai_options', []);
111 $has_api_key = !empty($options['openai_api_key']);
112
113 // Pass data to JavaScript.
114 wp_localize_script('king-addons-media-alt-text', 'kingAddonsMediaAltText', array(
115 'ajax_url' => admin_url('admin-ajax.php'),
116 'nonce' => wp_create_nonce('king_addons_generate_alt_nonce'),
117 'generating_text' => esc_html__('Generate with AI', 'king-addons'),
118 'error_text' => esc_html__('Error', 'king-addons'),
119 'has_api_key' => $has_api_key,
120 'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
121 ));
122 }
123
124 /**
125 * Adds a custom column to the Media Library list view.
126 *
127 * @param array $columns Existing columns.
128 * @return array Modified columns.
129 */
130 public function add_alt_text_column($columns): array {
131 $columns = is_array($columns) ? $columns : array();
132 // Add column before 'Date'.
133 $new_columns = array();
134 foreach ($columns as $key => $title) {
135 if ('date' === $key) {
136 $new_columns['king_addons_alt_text'] = esc_html__('AI Alt Text', 'king-addons');
137 }
138 $new_columns[$key] = $title;
139 }
140 // If 'date' column wasn't found, add it at the end.
141 if (!isset($new_columns['king_addons_alt_text'])) {
142 $new_columns['king_addons_alt_text'] = esc_html__('AI Alt Text', 'king-addons');
143 }
144 return $new_columns;
145 }
146
147 /**
148 * Displays content for the custom alt text column.
149 *
150 * @param string $column_name The name of the column being displayed.
151 * @param int $attachment_id The ID of the current attachment.
152 */
153 public function display_alt_text_column(string $column_name, int $attachment_id): void {
154 if ('king_addons_alt_text' !== $column_name) {
155 return;
156 }
157
158 // Only show for images.
159 if (!wp_attachment_is_image($attachment_id)) {
160 echo ''; // Not an image
161 return;
162 }
163
164 $alt_text = get_post_meta($attachment_id, '_wp_attachment_image_alt', true);
165 $options = get_option('king_addons_ai_options', []);
166 $has_api_key = !empty($options['openai_api_key']);
167
168 echo '<div class="king-addons-alt-text-status" data-attachment-id="' . esc_attr($attachment_id) . '">';
169 if (!empty($alt_text)) {
170 echo '<span>' . esc_html($alt_text) . '</span>';
171 } else {
172 if ($has_api_key) {
173 // Button to generate alt text when API key exists.
174 printf(
175 '<button type="button" class="button button-secondary button-small king-addons-generate-alt-button">%s</button>',
176 esc_html__('Generate', 'king-addons')
177 );
178 echo '<span class="king-addons-alt-text-result king-addons-status-inline"></span>'; // For displaying results/errors
179 echo '<span class="spinner king-addons-spinner-inline"></span>';
180 } else {
181 // Link to settings when API key is missing.
182 printf(
183 '<a href="%s" class="button button-secondary button-small" target="_blank">%s</a>',
184 esc_url(admin_url('admin.php?page=king-addons-ai-settings')),
185 esc_html__('Set API Key', 'king-addons')
186 );
187 echo '<span class="king-addons-alt-text-result king-addons-status-inline"></span>';
188 }
189 }
190 echo '</div>';
191 }
192
193 /**
194 * Handles the AJAX request to generate alt text for a single image.
195 */
196 public function handle_ajax_generate_single_alt(): void {
197 check_ajax_referer('king_addons_generate_alt_nonce', 'nonce');
198
199 if (!current_user_can('upload_files')) {
200 wp_send_json_error(array('message' => esc_html__('Permission denied.', 'king-addons')), 403);
201 }
202
203 // Check if API key exists
204 $options = get_option('king_addons_ai_options', []);
205 if (empty($options['openai_api_key'])) {
206 wp_send_json_error(array(
207 'message' => esc_html__('OpenAI API key is not configured. Please set it in the AI settings.', 'king-addons'),
208 'needs_setup' => true
209 ), 400);
210 }
211
212 $attachment_id = isset($_POST['attachment_id']) ? intval($_POST['attachment_id']) : 0;
213
214 if (!$attachment_id || !wp_attachment_is_image($attachment_id)) {
215 wp_send_json_error(array('message' => esc_html__('Invalid attachment ID.', 'king-addons')), 400);
216 }
217
218 // Call the existing generation logic, but slightly refactored to return the result.
219 $result = $this->generate_alt_text_for_image($attachment_id, true); // Pass true to indicate AJAX context.
220
221 if (is_wp_error($result)) {
222 wp_send_json_error(array('message' => $result->get_error_message()), 500);
223 } elseif ($result === false) {
224 // Handle cases where generation failed silently within the function (e.g., API key missing)
225 wp_send_json_error(array('message' => esc_html__('Alt text generation failed. Check logs or API key.', 'king-addons')), 500);
226 } elseif (is_string($result)) {
227 // Success! Return the generated alt text.
228 wp_send_json_success(array('alt_text' => $result));
229 } else {
230 // Unexpected result.
231 wp_send_json_error(array('message' => esc_html__('An unexpected error occurred.', 'king-addons')), 500);
232 }
233 }
234
235 /**
236 * Handles the AJAX request to get queue status.
237 */
238 public function handle_ajax_queue_status(): void {
239 check_ajax_referer('king_addons_generate_alt_nonce', 'nonce');
240
241 if (!current_user_can('manage_options')) {
242 wp_send_json_error(array('message' => esc_html__('Permission denied.', 'king-addons')), 403);
243 }
244
245 $status = $this->get_queue_status();
246 wp_send_json_success($status);
247 }
248
249 /**
250 * Adds custom cron intervals.
251 *
252 * Note: Recommended interval is 60+ seconds to avoid OpenAI API rate limits.
253 * Lower intervals may cause API errors during high usage periods.
254 *
255 * @param array $schedules Existing cron schedules.
256 * @return array Modified schedules.
257 */
258 public function add_custom_cron_intervals(array $schedules): array {
259 // Get the interval from settings, default to 20 seconds
260 $options = get_option('king_addons_ai_options', []);
261 $interval = isset($options['ai_alt_text_generation_interval']) ? (int) $options['ai_alt_text_generation_interval'] : 20;
262
263 // Ensure interval is within acceptable range
264 $interval = max(10, min(3600, $interval));
265
266 $schedules['king_addons_alt_text_interval'] = array(
267 'interval' => $interval,
268 'display' => sprintf(esc_html__('Every %d Seconds (King Addons Alt Text)', 'king-addons'), $interval)
269 );
270 return $schedules;
271 }
272
273 /**
274 * Adds a new attachment to the processing queue instead of immediate processing.
275 *
276 * @param int $attachment_id The ID of the attachment just added.
277 */
278 public function add_to_processing_queue(int $attachment_id): void {
279 // Check if the attachment is an image.
280 if (!wp_attachment_is_image($attachment_id)) {
281 return;
282 }
283
284 // Check if alt text already exists
285 $existing_alt = get_post_meta($attachment_id, '_wp_attachment_image_alt', true);
286 if (!empty($existing_alt)) {
287 return; // Skip if alt text already exists
288 }
289
290 // Get current queue
291 $queue = get_option(self::BATCH_OPTION_PENDING, []);
292
293 // Add to queue if not already present
294 if (!in_array($attachment_id, $queue)) {
295 $queue[] = $attachment_id;
296 update_option(self::BATCH_OPTION_PENDING, $queue, false);
297 }
298 }
299
300 /**
301 * Processes the alt text generation queue in batches.
302 */
303 public function process_alt_text_queue(): void {
304 // Check if already processing to avoid conflicts
305 $processing = get_option(self::BATCH_OPTION_PROCESSING, false);
306 if ($processing && (time() - $processing) < 300) { // 5 minute timeout
307 return; // Another process is already running
308 }
309
310 // Set processing flag
311 update_option(self::BATCH_OPTION_PROCESSING, time(), false);
312
313 try {
314 // Get pending queue
315 $queue = get_option(self::BATCH_OPTION_PENDING, []);
316
317 if (empty($queue)) {
318 delete_option(self::BATCH_OPTION_PROCESSING);
319 return; // Nothing to process
320 }
321
322 // Process batch
323 $batch = array_splice($queue, 0, self::BATCH_SIZE);
324
325 foreach ($batch as $attachment_id) {
326 $this->process_single_attachment_with_retry($attachment_id);
327
328 // Add delay between requests to respect rate limits
329 if (count($batch) > 1) {
330 sleep(2); // 2 second delay between individual requests
331 }
332 }
333
334 // Update queue
335 update_option(self::BATCH_OPTION_PENDING, $queue, false);
336
337 // Schedule next batch if queue is not empty
338 if (!empty($queue)) {
339 wp_schedule_single_event(time() + self::RATE_LIMIT_DELAY, self::CRON_HOOK);
340 }
341
342 } finally {
343 // Always clear processing flag
344 delete_option(self::BATCH_OPTION_PROCESSING);
345 }
346 }
347
348 /**
349 * Processes a single attachment with retry logic.
350 *
351 * @param int $attachment_id The attachment ID to process.
352 */
353 private function process_single_attachment_with_retry(int $attachment_id): void {
354 $retry_count = get_post_meta($attachment_id, '_king_addons_alt_retry_count', true);
355 $retry_count = $retry_count ? (int) $retry_count : 0;
356
357 if ($retry_count >= self::RETRY_LIMIT) {
358 // Max retries reached, skip this attachment
359 delete_post_meta($attachment_id, '_king_addons_alt_retry_count');
360 return;
361 }
362
363 $result = $this->generate_alt_text_for_image($attachment_id, false);
364
365 if ($result !== true) {
366 // Generation failed, increment retry count
367 update_post_meta($attachment_id, '_king_addons_alt_retry_count', $retry_count + 1);
368
369 // Add back to queue for retry (at the end)
370 $queue = get_option(self::BATCH_OPTION_PENDING, []);
371 if (!in_array($attachment_id, $queue)) {
372 $queue[] = $attachment_id;
373 update_option(self::BATCH_OPTION_PENDING, $queue, false);
374 }
375 } else {
376 // Success, clean up retry count
377 delete_post_meta($attachment_id, '_king_addons_alt_retry_count');
378 }
379 }
380
381 /**
382 * Generates alt text for a given image attachment using an AI service.
383 *
384 * @param int $attachment_id The ID of the attachment.
385 * @param bool $is_ajax Optional. Indicates if called via AJAX context. If true, returns result/error instead of void.
386 * @return bool|string|WP_Error Returns true on success (non-AJAX), generated alt text (string) on success (AJAX), specific error message (string) on failure (non-AJAX), or WP_Error on failure (AJAX).
387 */
388 public function generate_alt_text_for_image(int $attachment_id, bool $is_ajax = false) {
389 // Verify it's an image.
390 if (!wp_attachment_is_image($attachment_id)) {
391 $error_msg = esc_html__('Not an image.', 'king-addons');
392 return $is_ajax ? new \WP_Error('invalid_attachment', $error_msg) : $error_msg;
393 }
394
395 // Check if alt text already exists (only if not forced, future enhancement).
396 $existing_alt = get_post_meta($attachment_id, '_wp_attachment_image_alt', true);
397 if (!empty($existing_alt)) {
398 // If called via AJAX, maybe return existing text or an indication it wasn't generated.
399 // For now, we assume the button won't be shown if alt exists, so this path is mainly for the cron job.
400 return $is_ajax ? $existing_alt : true;
401 }
402
403 // Retrieve OpenAI API key and settings from King Addons AI options.
404 $options = get_option('king_addons_ai_options', []);
405 $api_key = $options['openai_api_key'] ?? '';
406 // Use dedicated vision model for image analysis (fallback to text model).
407 $model = $options['openai_vision_model'] ?? ($options['openai_model'] ?? 'gpt-4o-mini');
408 // Get image detail level from settings (default to 'low')
409 $image_detail_level = $options['ai_alt_text_image_detail_level'] ?? 'low';
410
411 if (empty($api_key)) {
412 $error_msg = esc_html__('OpenAI API key is missing.', 'king-addons');
413 return $is_ajax ? new \WP_Error('missing_api_key', $error_msg) : $error_msg;
414 }
415
416 // Get image path instead of URL.
417 $image_path = get_attached_file($attachment_id);
418 if (!$image_path || !file_exists($image_path)) {
419 $error_msg = esc_html__('Could not retrieve image file path.', 'king-addons');
420 return $is_ajax ? new \WP_Error('no_image_path', $error_msg) : $error_msg;
421 }
422
423 // Read image data.
424 $image_data = file_get_contents($image_path);
425 if (false === $image_data) {
426 $error_msg = esc_html__('Could not read image file.', 'king-addons');
427 return $is_ajax ? new \WP_Error('read_image_failed', $error_msg) : $error_msg;
428 }
429
430 // Get MIME type.
431 $file_info = wp_check_filetype(basename($image_path));
432 if (!$file_info || empty($file_info['type'])) {
433 $error_msg = esc_html__('Could not determine image type.', 'king-addons');
434 return $is_ajax ? new \WP_Error('mime_type_failed', $error_msg) : $error_msg;
435 }
436 $mime_type = $file_info['type'];
437
438 // Encode image data in Base64.
439 $base64_image = base64_encode($image_data);
440
441 // Create the data URI.
442 $image_data_uri = "data:{$mime_type};base64,{$base64_image}";
443
444 // --- OpenAI API Call --- //
445 $api_endpoint = 'https://api.openai.com/v1/chat/completions';
446
447 $options_for_lang = get_option('king_addons_ai_options', []);
448 $custom_lang_enabled = !empty($options_for_lang['content_language_custom_enable']);
449 $custom_lang = trim($options_for_lang['content_language_custom'] ?? '');
450
451 $prompt_text = 'Generate a concise, descriptive alt text for this image, suitable for SEO and accessibility. Focus on the main subject and action. Maximum 125 characters.';
452 if ($custom_lang_enabled && $custom_lang !== '') {
453 $prompt_text .= ' Use language: ' . $custom_lang;
454 } else {
455 $prompt_text .= ' Respond in English only.';
456 }
457
458 $payload = array(
459 'model' => $model,
460 'messages' => array(
461 array(
462 'role' => 'user',
463 'content' => array(
464 array(
465 'type' => 'text',
466 'text' => $prompt_text
467 ),
468 array(
469 'type' => 'image_url',
470 'image_url' => array(
471 'url' => $image_data_uri,
472 'detail' => $image_detail_level
473 )
474 )
475 )
476 )
477 ),
478 'max_tokens' => 50 // Limit the response length
479 );
480
481 $args = array(
482 'headers' => array(
483 'Authorization' => 'Bearer ' . $api_key,
484 'Content-Type' => 'application/json',
485 ),
486 'body' => wp_json_encode($payload),
487 'timeout' => 60, // Increased timeout
488 'method' => 'POST',
489 'data_format' => 'body',
490 );
491
492 $response = wp_remote_post($api_endpoint, $args);
493
494 // Handle the response.
495 if (is_wp_error($response)) {
496 $error_message = $response->get_error_message();
497 /* translators: %s: Error message returned from the network request. */
498 return $is_ajax ? $response : sprintf(esc_html__('Network error: %s', 'king-addons'), $error_message);
499 }
500
501 $response_code = wp_remote_retrieve_response_code($response);
502 $response_body = wp_remote_retrieve_body($response);
503 $decoded_body = json_decode($response_body, true);
504
505 if ($response_code !== 200 || !isset($decoded_body['choices'][0]['message']['content'])) {
506 $api_error_message = isset($decoded_body['error']['message']) ? $decoded_body['error']['message'] : esc_html__('API request failed or returned unexpected data.', 'king-addons');
507 /* translators: 1: HTTP response code, 2: API error message. */
508 return $is_ajax ? new \WP_Error('api_error', $api_error_message, array('status' => $response_code)) : sprintf(esc_html__('API error (%1$d): %2$s', 'king-addons'), $response_code, $api_error_message);
509 }
510
511 $generated_alt_text = $decoded_body['choices'][0]['message']['content'];
512 // --- End API Call --- //
513
514 // If the response starts with "I'm sorry" (case-insensitive, allow whitespace before), treat as error and do not save
515 if (preg_match('/^\s*I\'m sorry/i', $generated_alt_text)) {
516 $generated_alt_text = '';
517 }
518
519 // Sanitize and potentially trim the generated alt text.
520 $sanitized_alt_text = sanitize_text_field(trim($generated_alt_text));
521 $sanitized_alt_text = trim($sanitized_alt_text, '"'); // Remove surrounding quotes
522 $sanitized_alt_text = mb_substr($sanitized_alt_text, 0, 125); // Enforce length limit
523
524 // Update the image alt text meta data.
525 if (!empty($sanitized_alt_text)) {
526 if (update_post_meta($attachment_id, '_wp_attachment_image_alt', $sanitized_alt_text)) {
527 // Log success for debugging
528 // error_log("King Addons Alt Text: Successfully generated alt text for attachment {$attachment_id}: {$sanitized_alt_text}");
529 return $is_ajax ? $sanitized_alt_text : true;
530 } else {
531 $error_msg = esc_html__('Failed to save the generated alt text.', 'king-addons');
532 // error_log("King Addons Alt Text: Failed to save alt text for attachment {$attachment_id}");
533 return $is_ajax ? new \WP_Error('update_failed', $error_msg) : $error_msg;
534 }
535 } else {
536 $error_msg = esc_html__('AI returned empty or invalid text.', 'king-addons');
537 // error_log("King Addons Alt Text: AI returned empty text for attachment {$attachment_id}");
538 return $is_ajax ? new \WP_Error('empty_alt_text', $error_msg) : $error_msg;
539 }
540
541 // Should not be reached in normal flow due to checks above
542 $error_msg = esc_html__('An unknown error occurred during generation.', 'king-addons');
543 return $is_ajax ? new \WP_Error('unknown_error', $error_msg) : $error_msg;
544 }
545
546 /**
547 * Retrieves statistics about image alt text in the Media Library.
548 *
549 * @return array{total:int,with_alt:int,without_alt:int}
550 */
551 public static function get_alt_text_stats(): array
552 {
553 $cache_key = 'king_addons_alt_text_stats';
554 $cached = wp_cache_get($cache_key, 'king-addons');
555 if (false !== $cached && is_array($cached)) {
556 return $cached;
557 }
558
559 $all_images = get_posts([
560 'post_type' => 'attachment',
561 'post_mime_type' => 'image',
562 'post_status' => 'inherit',
563 'posts_per_page' => -1,
564 'fields' => 'ids',
565 'no_found_rows' => true,
566 ]);
567
568 $with_alt = get_posts([
569 'post_type' => 'attachment',
570 'post_mime_type' => 'image',
571 'post_status' => 'inherit',
572 'posts_per_page' => -1,
573 'fields' => 'ids',
574 'no_found_rows' => true,
575 'meta_query' => [
576 [
577 'key' => '_wp_attachment_image_alt',
578 'value' => '',
579 'compare' => '!=',
580 ],
581 ],
582 ]);
583
584 $total = count($all_images);
585 $with_alt_count = count($with_alt);
586 $result = [
587 'total' => $total,
588 'with_alt' => $with_alt_count,
589 'without_alt' => max(0, $total - $with_alt_count),
590 ];
591
592 wp_cache_set($cache_key, $result, 'king-addons', 300);
593 return $result;
594 }
595
596 /**
597 * Cleans up cron jobs and options on plugin deactivation.
598 */
599 public function cleanup_cron_jobs(): void {
600 // Clear scheduled cron jobs
601 $timestamp = wp_next_scheduled(self::CRON_HOOK);
602 if ($timestamp) {
603 wp_unschedule_event($timestamp, self::CRON_HOOK);
604 }
605
606 // Clear options
607 delete_option(self::BATCH_OPTION_PENDING);
608 delete_option(self::BATCH_OPTION_PROCESSING);
609
610 // Clean up retry count meta for all attachments
611 global $wpdb;
612 $wpdb->delete(
613 $wpdb->postmeta,
614 array('meta_key' => '_king_addons_alt_retry_count'),
615 array('%s')
616 );
617 }
618
619 /**
620 * Updates the cron schedule if the relevant settings change.
621 *
622 * @param mixed $old_value The old value.
623 * @param mixed $new_value The new value.
624 * @param string $option The option name.
625 */
626 public function update_cron_schedule($old_value, $new_value, string $option = 'king_addons_ai_options'): void {
627
628 // Check if interval setting changed
629 $old_interval = isset($old_value['ai_alt_text_generation_interval']) ? (int) $old_value['ai_alt_text_generation_interval'] : 20;
630 $new_interval = isset($new_value['ai_alt_text_generation_interval']) ? (int) $new_value['ai_alt_text_generation_interval'] : 20;
631
632 // Check if auto generation setting changed
633 $old_auto = isset($old_value['enable_ai_alt_text_auto_generation']) ? (bool) $old_value['enable_ai_alt_text_auto_generation'] : false;
634 $new_auto = isset($new_value['enable_ai_alt_text_auto_generation']) ? (bool) $new_value['enable_ai_alt_text_auto_generation'] : false;
635
636 if ($old_interval !== $new_interval || $old_auto !== $new_auto) {
637 // Clear existing cron job
638 $timestamp = wp_next_scheduled(self::CRON_HOOK);
639 if ($timestamp) {
640 wp_unschedule_event($timestamp, self::CRON_HOOK);
641 }
642
643 // Reschedule if auto generation is enabled
644 if ($new_auto) {
645 wp_schedule_event(time(), 'king_addons_alt_text_interval', self::CRON_HOOK);
646 }
647 }
648 }
649
650 /**
651 * Gets queue status for debugging purposes.
652 *
653 * @return array Queue status information.
654 */
655 public function get_queue_status(): array {
656 $queue = get_option(self::BATCH_OPTION_PENDING, []);
657 $processing = get_option(self::BATCH_OPTION_PROCESSING, false);
658 $next_scheduled = wp_next_scheduled(self::CRON_HOOK);
659
660 return array(
661 'pending_count' => count($queue),
662 'pending_ids' => $queue,
663 'is_processing' => $processing ? true : false,
664 'processing_since' => $processing ? date('Y-m-d H:i:s', $processing) : null,
665 'next_run' => $next_scheduled ? date('Y-m-d H:i:s', $next_scheduled) : null,
666 );
667 }
668 }
669