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 / AI_SEO_Tools / Bulk_Alt_Text_Module.php

Bulk_Alt_Text_Module.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.78, at includes/extensions/AI_SEO_Tools/Bulk_Alt_Text_Module.php

306 lines 10.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Bulk Alt Text module for AI SEO Tools.
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons\AI_SEO_Tools;
9
10 use King_Addons\Alt_Text_Generator;
11
12 if (!defined('ABSPATH')) {
13 exit;
14 }
15
16 class Bulk_Alt_Text_Module
17 {
18 private const BULK_OPTION_PENDING = 'king_addons_ai_seo_bulk_alt_pending_ids';
19 private const BULK_OPTION_PROGRESS = 'king_addons_ai_seo_bulk_alt_progress';
20 private const BULK_OPTION_LOCK = 'king_addons_ai_seo_bulk_alt_lock';
21 private const CRON_HOOK = 'king_addons_ai_seo_bulk_alt_cron';
22 private const BATCH_SIZE = 1;
23
24 private Alt_Text_Generator $alt_text_generator;
25
26 public function __construct(Alt_Text_Generator $alt_text_generator)
27 {
28 $this->alt_text_generator = $alt_text_generator;
29
30 add_action('wp_ajax_king_addons_ai_seo_start_bulk_alt', [$this, 'handle_ajax_start_bulk_alt']);
31 add_action('wp_ajax_king_addons_ai_seo_get_bulk_alt_status', [$this, 'handle_ajax_get_bulk_alt_status']);
32 add_action('wp_ajax_king_addons_ai_seo_stop_bulk_alt', [$this, 'handle_ajax_stop_bulk_alt']);
33 add_action('wp_ajax_king_addons_ai_seo_get_alt_stats', [$this, 'handle_ajax_get_stats']);
34 add_action(self::CRON_HOOK, [$this, 'process_bulk_alt_text_batch']);
35 }
36
37 public function handle_ajax_start_bulk_alt(): void
38 {
39 check_ajax_referer('king_addons_ai_seo_bulk_alt_start_nonce', 'nonce');
40
41 if (!current_user_can('manage_options')) {
42 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
43 }
44
45 $ka_ai_opts = get_option('king_addons_ai_options', []);
46 if (empty($ka_ai_opts['openai_api_key'])) {
47 wp_send_json_error(['message' => esc_html__('OpenAI API key is not set. Please add it in AI Settings.', 'king-addons'), 'code' => 'no_api_key'], 400);
48 }
49
50 $limit = isset($_POST['limit']) ? absint($_POST['limit']) : 0;
51
52 wp_clear_scheduled_hook(self::CRON_HOOK);
53
54 $pending_ids = $this->get_images_without_alt();
55 $total_missing = count($pending_ids);
56
57 if ($limit > 0 && $limit < $total_missing) {
58 $pending_ids = array_slice($pending_ids, 0, $limit);
59 $total_to_process = $limit;
60 } else {
61 $total_to_process = $total_missing;
62 }
63
64 if (empty($pending_ids)) {
65 update_option(self::BULK_OPTION_PROGRESS, [
66 'status' => 'complete',
67 'total' => 0,
68 'processed' => 0,
69 'errors' => [],
70 'current_id' => null,
71 'current_item' => null,
72 'recent_success_ids' => [],
73 'last_success' => null,
74 ], false);
75 delete_option(self::BULK_OPTION_PENDING);
76 wp_send_json_success([
77 'status' => 'complete',
78 'message' => esc_html__('No images without alt text were found.', 'king-addons'),
79 ]);
80 return;
81 }
82
83 update_option(self::BULK_OPTION_PENDING, $pending_ids, false);
84 update_option(self::BULK_OPTION_PROGRESS, [
85 'status' => 'running',
86 'total' => $total_to_process,
87 'processed' => 0,
88 'last_run' => 0,
89 'errors' => [],
90 'current_id' => null,
91 'current_item' => null,
92 'recent_success_ids' => [],
93 'last_success' => null,
94 ], false);
95
96 wp_schedule_single_event(time(), self::CRON_HOOK);
97
98 wp_send_json_success([
99 'status' => 'running',
100 'total' => $total_to_process,
101 'processed' => 0,
102 ]);
103 }
104
105 public function handle_ajax_get_bulk_alt_status(): void
106 {
107 check_ajax_referer('king_addons_ai_seo_bulk_alt_status_nonce', 'nonce');
108
109 if (!current_user_can('manage_options')) {
110 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
111 }
112
113 $progress = get_option(self::BULK_OPTION_PROGRESS, [
114 'status' => 'idle',
115 'total' => 0,
116 'processed' => 0,
117 'last_run' => 0,
118 'errors' => [],
119 'current_id' => null,
120 'current_item' => null,
121 'recent_success_ids' => [],
122 'last_success' => null,
123 ]);
124
125 $progress = $this->maybe_kick_bulk_processing($progress);
126
127 wp_send_json_success($progress);
128 }
129
130 public function handle_ajax_stop_bulk_alt(): void
131 {
132 check_ajax_referer('king_addons_ai_seo_bulk_alt_stop_nonce', 'nonce');
133
134 if (!current_user_can('manage_options')) {
135 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
136 }
137
138 wp_clear_scheduled_hook(self::CRON_HOOK);
139 delete_option(self::BULK_OPTION_PENDING);
140 delete_transient(self::BULK_OPTION_LOCK);
141
142 $progress = get_option(self::BULK_OPTION_PROGRESS, []);
143 $progress['status'] = 'stopped';
144 $progress['current_id'] = null;
145 $progress['current_item'] = null;
146 update_option(self::BULK_OPTION_PROGRESS, $progress, false);
147
148 wp_send_json_success($progress);
149 }
150
151 public function process_bulk_alt_text_batch(): void
152 {
153 if ((bool) get_transient(self::BULK_OPTION_LOCK)) {
154 return;
155 }
156
157 set_transient(self::BULK_OPTION_LOCK, 1, 60);
158
159 $pending_ids = get_option(self::BULK_OPTION_PENDING, []);
160 $progress = get_option(self::BULK_OPTION_PROGRESS, []);
161
162 if (empty($pending_ids) || !is_array($pending_ids) || ($progress['status'] ?? '') !== 'running') {
163 wp_clear_scheduled_hook(self::CRON_HOOK);
164 delete_transient(self::BULK_OPTION_LOCK);
165 return;
166 }
167
168 $batch_ids = array_slice($pending_ids, 0, self::BATCH_SIZE);
169
170 $processed_in_batch = 0;
171 $batch_errors = [];
172 $batch_success_ids = [];
173 $batch_success_items = [];
174
175 foreach ($batch_ids as $attachment_id_raw) {
176 $attachment_id = (int) $attachment_id_raw;
177 $progress['current_id'] = $attachment_id;
178 $progress['current_item'] = $this->get_image_debug_data($attachment_id);
179 update_option(self::BULK_OPTION_PROGRESS, $progress, false);
180
181 $current_alt = get_post_meta($attachment_id, '_wp_attachment_image_alt', true);
182 if (wp_attachment_is_image($attachment_id) && empty($current_alt)) {
183 $result = $this->alt_text_generator->generate_alt_text_for_image($attachment_id, true);
184 if (is_wp_error($result)) {
185 $batch_errors[$attachment_id] = $result->get_error_message();
186 } elseif (!is_string($result) || $result === '') {
187 $batch_errors[$attachment_id] = esc_html__('Unknown error.', 'king-addons');
188 } else {
189 $batch_success_ids[] = $attachment_id;
190 $item = $this->get_image_debug_data($attachment_id);
191 $item['alt_text'] = $result;
192 $batch_success_items[] = $item;
193 }
194 }
195
196 $processed_in_batch++;
197 }
198
199 $remaining_ids = array_slice($pending_ids, $processed_in_batch);
200 update_option(self::BULK_OPTION_PENDING, $remaining_ids, false);
201
202 $progress['processed'] = (int) ($progress['processed'] ?? 0) + $processed_in_batch;
203 $progress['last_run'] = time();
204 $progress['errors'] = array_slice(array_merge($progress['errors'] ?? [], $batch_errors), -20);
205 $progress['recent_success_ids'] = array_slice(array_merge($batch_success_ids, $progress['recent_success_ids'] ?? []), 0, 5);
206 $progress['current_id'] = null;
207 $progress['current_item'] = null;
208 if (!empty($batch_success_items)) {
209 $progress['last_success'] = $batch_success_items[0];
210 }
211
212 if (empty($remaining_ids)) {
213 $progress['status'] = 'complete';
214 delete_option(self::BULK_OPTION_PENDING);
215 wp_clear_scheduled_hook(self::CRON_HOOK);
216 } else {
217 $delay = $this->get_bulk_delay();
218 wp_schedule_single_event(time() + $delay, self::CRON_HOOK);
219 }
220
221 update_option(self::BULK_OPTION_PROGRESS, $progress, false);
222 delete_transient(self::BULK_OPTION_LOCK);
223 }
224
225 private function maybe_kick_bulk_processing(array $progress): array
226 {
227 if (($progress['status'] ?? 'idle') !== 'running') {
228 return $progress;
229 }
230
231 $next_scheduled = wp_next_scheduled(self::CRON_HOOK);
232 $last_run = (int) ($progress['last_run'] ?? 0);
233 $delay = $this->get_bulk_delay();
234 $is_stale = $last_run > 0 && (time() - $last_run) > ($delay + 5);
235
236 if (($next_scheduled === false && !(bool) get_transient(self::BULK_OPTION_LOCK)) || $is_stale) {
237 $this->process_bulk_alt_text_batch();
238 return get_option(self::BULK_OPTION_PROGRESS, $progress);
239 }
240
241 return $progress;
242 }
243
244 private function get_bulk_delay(): int
245 {
246 $options = get_option('king_addons_ai_options', []);
247 $delay = isset($options['ai_seo_bulk_processing_delay']) ? (int) $options['ai_seo_bulk_processing_delay'] : 2;
248 return max(1, min(30, $delay));
249 }
250
251 private function get_image_debug_data(int $attachment_id): array
252 {
253 $file_path = get_attached_file($attachment_id);
254 $file_name = is_string($file_path) ? wp_basename($file_path) : '';
255
256 return [
257 'id' => $attachment_id,
258 'title' => (string) get_the_title($attachment_id),
259 'filename' => (string) $file_name,
260 'thumb_url' => (string) wp_get_attachment_image_url($attachment_id, 'thumbnail'),
261 ];
262 }
263
264 public function handle_ajax_get_stats(): void
265 {
266 check_ajax_referer('king_addons_ai_seo_get_alt_stats_nonce', 'nonce');
267
268 if (!current_user_can('manage_options')) {
269 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
270 }
271
272 wp_send_json_success(Alt_Text_Generator::get_alt_text_stats());
273 }
274
275 /**
276 * @return int[]
277 */
278 private function get_images_without_alt(): array
279 {
280 $ids = get_posts([
281 'post_type' => 'attachment',
282 'post_mime_type' => 'image',
283 'post_status' => 'inherit',
284 'posts_per_page' => -1,
285 'fields' => 'ids',
286 'no_found_rows' => true,
287 'meta_query' => [
288 [
289 'relation' => 'OR',
290 [
291 'key' => '_wp_attachment_image_alt',
292 'compare' => 'NOT EXISTS',
293 ],
294 [
295 'key' => '_wp_attachment_image_alt',
296 'value' => '',
297 'compare' => '=',
298 ],
299 ],
300 ],
301 ]);
302
303 return array_map('intval', $ids);
304 }
305 }
306