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 / Image_Optimizer / Image_Optimizer_Ajax.php

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

969 lines 31.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Image Optimizer AJAX handlers.
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons\Image_Optimizer;
9
10 if (!defined('ABSPATH')) {
11 exit;
12 }
13
14 class Image_Optimizer_Ajax
15 {
16 private static ?Image_Optimizer_Ajax $instance = null;
17
18 public static function instance(): Image_Optimizer_Ajax
19 {
20 if (self::$instance === null) {
21 self::$instance = new self();
22 }
23
24 return self::$instance;
25 }
26
27 private function __construct()
28 {
29 // Get image data for optimization
30 add_action('wp_ajax_king_img_get_image_data', [$this, 'get_image_data']);
31
32 // Save optimized image
33 add_action('wp_ajax_king_img_save_optimized', [$this, 'save_optimized']);
34
35 // Apply WebP URLs (replace in database)
36 add_action('wp_ajax_king_img_apply_webp_urls', [$this, 'apply_webp_urls']);
37
38 // Restore original URLs
39 add_action('wp_ajax_king_img_restore_original', [$this, 'restore_original']);
40
41 // Get images for bulk optimization
42 add_action('wp_ajax_king_img_get_bulk_images', [$this, 'get_bulk_images']);
43
44 // Save settings
45 add_action('wp_ajax_king_img_save_settings', [$this, 'save_settings']);
46
47 // Get settings (used by Media Library auto-optimize to avoid stale localized settings)
48 add_action('wp_ajax_king_img_get_settings', [$this, 'get_settings']);
49
50 // Get fresh Attachment Details card HTML (used to update media modal without re-select)
51 add_action('wp_ajax_king_img_get_attachment_card_html', [$this, 'get_attachment_card_html']);
52
53 // Get global stats
54 add_action('wp_ajax_king_img_get_stats', [$this, 'get_stats']);
55
56 // Get image format breakdown (for dynamic UI refresh)
57 add_action('wp_ajax_king_img_get_breakdown', [$this, 'get_breakdown']);
58
59 // Full restore (delete WebP files)
60 add_action('wp_ajax_king_img_full_restore', [$this, 'full_restore']);
61
62 // Get optimization state for resume
63 add_action('wp_ajax_king_img_get_state', [$this, 'get_optimization_state']);
64
65 // Save optimization state
66 add_action('wp_ajax_king_img_save_state', [$this, 'save_optimization_state']);
67
68 // Clear optimization state
69 add_action('wp_ajax_king_img_clear_state', [$this, 'clear_optimization_state']);
70
71 // Mark image as skipped (e.g., too small)
72 add_action('wp_ajax_king_img_mark_skipped', [$this, 'mark_skipped']);
73
74 // Mark image as failed (so it doesn't stay pending)
75 add_action('wp_ajax_king_img_mark_failed', [$this, 'mark_failed']);
76
77 // Media Library sync (fix attachment paths/filesizes after optimization)
78 add_action('wp_ajax_king_img_get_sync_ids', [$this, 'get_sync_ids']);
79 add_action('wp_ajax_king_img_sync_batch', [$this, 'sync_media_library_batch']);
80
81 // Get optimized image IDs for bulk restore
82 add_action('wp_ajax_king_img_get_optimized_ids', [$this, 'get_optimized_ids']);
83
84 // Bulk restore single image
85 add_action('wp_ajax_king_img_bulk_restore_single', [$this, 'bulk_restore_single']);
86 }
87
88 /**
89 * Get current settings.
90 */
91 public function get_settings(): void
92 {
93 if (!$this->verify_request()) {
94 return;
95 }
96
97 $optimizer = Image_Optimizer::instance();
98
99 wp_send_json_success([
100 'settings' => $optimizer->get_settings(),
101 ]);
102 }
103
104 /**
105 * Get freshly rendered Attachment Details "King Image Optimizer" card HTML.
106 */
107 public function get_attachment_card_html(): void
108 {
109 if (!$this->verify_request()) {
110 return;
111 }
112
113 $attachment_id = absint($_POST['attachment_id'] ?? 0);
114 if (!$attachment_id || !wp_attachment_is_image($attachment_id)) {
115 wp_send_json_error(['message' => 'Invalid attachment'], 400);
116 }
117
118 $optimizer = Image_Optimizer::instance();
119 $html = $optimizer->get_attachment_optimizer_card_html($attachment_id);
120
121 wp_send_json_success([
122 'html' => $html,
123 ]);
124 }
125
126 /**
127 * Verify nonce and capabilities.
128 */
129 private function verify_request(): bool
130 {
131 if (!check_ajax_referer('king_img_optimizer_nonce', 'nonce', false)) {
132 wp_send_json_error(['message' => __('Security check failed.', 'king-addons')]);
133 return false;
134 }
135
136 if (!current_user_can('upload_files')) {
137 wp_send_json_error(['message' => __('Permission denied.', 'king-addons')]);
138 return false;
139 }
140
141 return true;
142 }
143
144 /**
145 * Get image data for optimization.
146 */
147 public function get_image_data(): void
148 {
149 if (!$this->verify_request()) {
150 return;
151 }
152
153 $attachment_id = absint($_POST['attachment_id'] ?? 0);
154 $sizes = isset($_POST['sizes']) ? (is_array($_POST['sizes']) ? array_map('sanitize_text_field', $_POST['sizes']) : sanitize_text_field($_POST['sizes'])) : 'all';
155
156 if (!$attachment_id || !wp_attachment_is_image($attachment_id)) {
157 wp_send_json_error(['message' => __('Invalid attachment ID.', 'king-addons')]);
158 return;
159 }
160
161 $file = get_attached_file($attachment_id);
162 if (!$file || !file_exists($file)) {
163 wp_send_json_error(['message' => __('File not found.', 'king-addons')]);
164 return;
165 }
166
167 $metadata = wp_get_attachment_metadata($attachment_id);
168 $upload_dir = wp_upload_dir();
169 $base_dir = trailingslashit(dirname($file));
170 $mime_type = get_post_mime_type($attachment_id);
171
172 $images = [];
173
174 // Full size
175 if ($sizes === 'all' || in_array('full', (array) $sizes, true)) {
176 $images['full'] = [
177 'url' => wp_get_attachment_url($attachment_id),
178 'path' => $file,
179 'width' => $metadata['width'] ?? 0,
180 'height' => $metadata['height'] ?? 0,
181 'filesize' => filesize($file),
182 'mime_type' => $mime_type,
183 ];
184 }
185
186 // Registered sizes
187 if (!empty($metadata['sizes'])) {
188 foreach ($metadata['sizes'] as $size_name => $size_data) {
189 if ($sizes !== 'all' && !in_array($size_name, (array) $sizes, true)) {
190 continue;
191 }
192
193 $size_file = $base_dir . $size_data['file'];
194 if (file_exists($size_file)) {
195 $images[$size_name] = [
196 'url' => $upload_dir['baseurl'] . '/' . dirname($metadata['file']) . '/' . $size_data['file'],
197 'path' => $size_file,
198 'width' => $size_data['width'],
199 'height' => $size_data['height'],
200 'filesize' => filesize($size_file),
201 'mime_type' => $size_data['mime-type'] ?? $mime_type,
202 ];
203 }
204 }
205 }
206
207 wp_send_json_success([
208 'attachment_id' => $attachment_id,
209 'images' => $images,
210 'base_dir' => $base_dir,
211 ]);
212 }
213
214 /**
215 * Save optimized image.
216 */
217 public function save_optimized(): void
218 {
219 if (!$this->verify_request()) {
220 return;
221 }
222
223 $attachment_id = absint($_POST['attachment_id'] ?? 0);
224 $size = sanitize_text_field($_POST['size'] ?? 'full');
225 $format = 'webp';
226 $image_data = $_POST['image_data'] ?? ''; // Base64 encoded image
227 $original_size = absint($_POST['original_size'] ?? 0);
228 $optimized_size = absint($_POST['optimized_size'] ?? 0);
229 $method = 'canvas';
230
231 if (!$attachment_id || !wp_attachment_is_image($attachment_id)) {
232 wp_send_json_error(['message' => __('Invalid attachment ID.', 'king-addons')]);
233 return;
234 }
235
236 if (empty($image_data)) {
237 wp_send_json_error(['message' => __('No image data provided.', 'king-addons')]);
238 return;
239 }
240
241 $optimizer = Image_Optimizer::instance();
242 $is_pro = $optimizer->is_pro();
243 $month_key = wp_date('Y-m', (int) current_time('timestamp'));
244
245 // Quota is consumed once per attachment per month (not per-size).
246 $existing_meta = Image_Optimizer_DB::get_optimization_meta($attachment_id) ?: [];
247 $already_counted = ((string) ($existing_meta['quota_month_key'] ?? '') === $month_key);
248
249 if (!$is_pro && !$already_counted) {
250 $quota = $optimizer->get_free_quota_state();
251 if (!empty($quota['remaining']) && (int) $quota['remaining'] <= 0) {
252 wp_send_json_error([
253 'message' => __('Free plan limit reached (200 optimizations/month). Upgrade to Unlimited to continue.', 'king-addons'),
254 'code' => 'quota_exceeded',
255 'quota' => $quota,
256 'upgrade_url' => $optimizer->get_upgrade_url('kng-img-optimizer'),
257 ], 403);
258 return;
259 }
260 }
261
262 // Decode base64 image
263 $image_data = preg_replace('#^data:image/\w+;base64,#i', '', $image_data);
264 $decoded = base64_decode($image_data);
265
266 if ($decoded === false) {
267 wp_send_json_error(['message' => __('Failed to decode image data.', 'king-addons')]);
268 return;
269 }
270
271 // Get original file path
272 $file = get_attached_file($attachment_id);
273 $metadata = wp_get_attachment_metadata($attachment_id);
274 $base_dir = trailingslashit(dirname($file));
275
276 // Determine target file path
277 if ($size === 'full') {
278 $original_path = $file;
279 } else {
280 if (empty($metadata['sizes'][$size]['file'])) {
281 wp_send_json_error(['message' => __('Size not found.', 'king-addons')]);
282 return;
283 }
284 $original_path = $base_dir . $metadata['sizes'][$size]['file'];
285 }
286
287 // Get original extension
288 $original_ext = strtolower(pathinfo($original_path, PATHINFO_EXTENSION));
289
290 // Create optimized file path
291 // If format is same as original, we still save as separate file for rollback capability
292 if ($format === $original_ext || ($format === 'jpg' && $original_ext === 'jpeg') || ($format === 'jpeg' && $original_ext === 'jpg')) {
293 // Same format - add suffix before extension to preserve original
294 $optimized_path = preg_replace('/\.([^.]+)$/', '-ka-opt.$1', $original_path);
295 } else {
296 // Different format - change extension
297 $optimized_path = preg_replace('/\.[^.]+$/', '.' . $format, $original_path);
298 }
299
300 // Make sure we're writing to uploads directory
301 $upload_dir = wp_upload_dir();
302 if (strpos($optimized_path, $upload_dir['basedir']) !== 0) {
303 wp_send_json_error(['message' => __('Invalid file path.', 'king-addons')]);
304 return;
305 }
306
307 // Save the optimized file
308 $bytes_written = file_put_contents($optimized_path, $decoded);
309
310 if ($bytes_written === false) {
311 wp_send_json_error(['message' => __('Failed to save optimized image.', 'king-addons')]);
312 return;
313 }
314
315 // Use real on-disk sizes (client-provided sizes are only estimates).
316 $actual_original_size = (file_exists($original_path) ? (int) filesize($original_path) : (int) $original_size);
317 $actual_optimized_size = (file_exists($optimized_path) ? (int) filesize($optimized_path) : (int) $bytes_written);
318
319 // Update optimization metadata
320 $meta = $existing_meta;
321
322 if (!isset($meta['sizes'])) {
323 $meta['sizes'] = [];
324 }
325
326 $saved_bytes = max(0, $actual_original_size - $actual_optimized_size);
327
328 $meta['sizes'][$size] = [
329 'original_path' => $original_path,
330 'optimized_path' => $optimized_path,
331 'webp_path' => $optimized_path, // Alias for backward compatibility
332 'original_size' => $actual_original_size,
333 'optimized_size' => $actual_optimized_size,
334 'saved_bytes' => $saved_bytes,
335 'format' => $format,
336 'method' => $method,
337 'optimized_at' => current_time('mysql'),
338 ];
339
340 // Calculate totals
341 $meta['total_original_bytes'] = 0;
342 $meta['total_optimized_bytes'] = 0;
343 $meta['total_saved_bytes'] = 0;
344
345 foreach ($meta['sizes'] as $s) {
346 $meta['total_original_bytes'] += $s['original_size'];
347 $meta['total_optimized_bytes'] += $s['optimized_size'];
348 $meta['total_saved_bytes'] += $s['saved_bytes'];
349 }
350
351 $meta['savings_percent'] = $meta['total_original_bytes'] > 0
352 ? round(($meta['total_saved_bytes'] / $meta['total_original_bytes']) * 100, 1)
353 : 0;
354
355 $meta['status'] = 'optimized';
356 $meta['completed_at'] = current_time('mysql');
357 $meta['format'] = $format;
358 $meta['method'] = $method;
359
360 // Consume quota only once per attachment per month.
361 $quota_state = null;
362 if (!$is_pro && !$already_counted) {
363 $meta['quota_month_key'] = $month_key;
364 $quota_state = $optimizer->consume_free_quota(1);
365 } else {
366 $quota_state = $optimizer->get_free_quota_state();
367 }
368
369 Image_Optimizer_DB::set_optimization_meta($attachment_id, $meta);
370
371 // Sync Media Library attachment to the optimized files (keeps originals via backup metas)
372 Image_Optimizer_DB::sync_attachment_to_optimized($attachment_id, $meta);
373
374 wp_send_json_success([
375 'attachment_id' => $attachment_id,
376 'size' => $size,
377 'optimized_path' => $optimized_path,
378 'original_size' => $actual_original_size,
379 'optimized_size' => $actual_optimized_size,
380 'saved_bytes' => $saved_bytes,
381 'savings_percent' => $meta['savings_percent'],
382 'quota' => $quota_state,
383 ]);
384 }
385
386 /**
387 * Mark an image as skipped (e.g., too small to optimize).
388 */
389 public function mark_skipped(): void
390 {
391 if (!$this->verify_request()) {
392 return;
393 }
394
395 $attachment_id = absint($_POST['attachment_id'] ?? 0);
396 $reason = sanitize_text_field($_POST['reason'] ?? '');
397
398 if (!$attachment_id || !wp_attachment_is_image($attachment_id)) {
399 wp_send_json_error(['message' => __('Invalid attachment ID.', 'king-addons')]);
400 return;
401 }
402
403 $meta = Image_Optimizer_DB::get_optimization_meta($attachment_id) ?: [];
404 $meta['status'] = 'skipped';
405 $meta['skipped_reason'] = $reason ?: 'skipped';
406 $meta['skipped_at'] = current_time('mysql');
407 $meta['total_original_bytes'] = $meta['total_original_bytes'] ?? 0;
408 $meta['total_optimized_bytes'] = $meta['total_optimized_bytes'] ?? 0;
409 $meta['total_saved_bytes'] = $meta['total_saved_bytes'] ?? 0;
410
411 Image_Optimizer_DB::set_optimization_meta($attachment_id, $meta);
412
413 wp_send_json_success([
414 'attachment_id' => $attachment_id,
415 'status' => 'skipped',
416 ]);
417 }
418
419 /**
420 * Mark an image as failed.
421 */
422 public function mark_failed(): void
423 {
424 if (!$this->verify_request()) {
425 return;
426 }
427
428 $attachment_id = absint($_POST['attachment_id'] ?? 0);
429 $reason = sanitize_text_field($_POST['reason'] ?? '');
430
431 if (!$attachment_id || !wp_attachment_is_image($attachment_id)) {
432 wp_send_json_error(['message' => __('Invalid attachment ID.', 'king-addons')]);
433 return;
434 }
435
436 $meta = Image_Optimizer_DB::get_optimization_meta($attachment_id) ?: [];
437 $meta['status'] = 'failed';
438 $meta['failed_reason'] = $reason ?: 'failed';
439 $meta['failed_at'] = current_time('mysql');
440 $meta['total_original_bytes'] = $meta['total_original_bytes'] ?? 0;
441 $meta['total_optimized_bytes'] = $meta['total_optimized_bytes'] ?? 0;
442 $meta['total_saved_bytes'] = $meta['total_saved_bytes'] ?? 0;
443
444 Image_Optimizer_DB::set_optimization_meta($attachment_id, $meta);
445
446 wp_send_json_success([
447 'attachment_id' => $attachment_id,
448 'status' => 'failed',
449 ]);
450 }
451
452 /**
453 * Get IDs that are optimized and eligible for Media Library sync.
454 */
455 public function get_sync_ids(): void
456 {
457 if (!$this->verify_request()) {
458 return;
459 }
460
461 global $wpdb;
462
463 $ids = $wpdb->get_col(
464 $wpdb->prepare(
465 "SELECT pm.post_id FROM {$wpdb->postmeta} pm
466 INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
467 WHERE pm.meta_key = %s
468 AND p.post_type = 'attachment'
469 AND pm.meta_value LIKE %s",
470 '_king_img_optimized',
471 '%"status";s:9:"optimized"%'
472 )
473 );
474
475 $ids = array_values(array_unique(array_map('absint', $ids)));
476
477 wp_send_json_success([
478 'ids' => $ids,
479 'total' => count($ids),
480 ]);
481 }
482
483 /**
484 * Sync a batch of attachment IDs to their optimized files.
485 */
486 public function sync_media_library_batch(): void
487 {
488 if (!$this->verify_request()) {
489 return;
490 }
491
492 $ids_raw = $_POST['ids'] ?? '[]';
493 if (!is_string($ids_raw)) {
494 $ids_raw = wp_json_encode($ids_raw);
495 }
496
497 $ids = json_decode(stripslashes($ids_raw), true);
498 if (!is_array($ids)) {
499 wp_send_json_error(['message' => __('Invalid IDs payload.', 'king-addons')]);
500 return;
501 }
502
503 $synced = 0;
504 $skipped = 0;
505 $errors = 0;
506
507 foreach ($ids as $attachment_id) {
508 $attachment_id = absint($attachment_id);
509 if (!$attachment_id) {
510 $skipped++;
511 continue;
512 }
513
514 $meta = Image_Optimizer_DB::get_optimization_meta($attachment_id);
515 if (empty($meta) || ($meta['status'] ?? '') !== 'optimized') {
516 $skipped++;
517 continue;
518 }
519
520 $opt_full = $meta['sizes']['full']['optimized_path'] ?? $meta['sizes']['full']['webp_path'] ?? '';
521 if (empty($opt_full) || !is_string($opt_full) || !file_exists($opt_full)) {
522 $skipped++;
523 continue;
524 }
525
526 try {
527 Image_Optimizer_DB::sync_attachment_to_optimized($attachment_id, $meta);
528 $synced++;
529 } catch (\Throwable $e) {
530 $errors++;
531 }
532 }
533
534 wp_send_json_success([
535 'synced' => $synced,
536 'skipped' => $skipped,
537 'errors' => $errors,
538 ]);
539 }
540
541 /**
542 * Apply WebP URLs in database.
543 */
544 public function apply_webp_urls(): void
545 {
546 if (!$this->verify_request()) {
547 return;
548 }
549
550 $attachment_id = absint($_POST['attachment_id'] ?? 0);
551
552 if (!$attachment_id) {
553 wp_send_json_error(['message' => __('Invalid attachment ID.', 'king-addons')]);
554 return;
555 }
556
557 $meta = Image_Optimizer_DB::get_optimization_meta($attachment_id);
558
559 if (empty($meta) || $meta['status'] !== 'optimized') {
560 wp_send_json_error(['message' => __('Image not optimized yet.', 'king-addons')]);
561 return;
562 }
563
564 $updated = Image_Optimizer_DB::apply_webp_urls($attachment_id, $meta);
565
566 wp_send_json_success([
567 'attachment_id' => $attachment_id,
568 'updated_count' => $updated,
569 'message' => sprintf(__('Updated %d URL references.', 'king-addons'), $updated),
570 ]);
571 }
572
573 /**
574 * Restore original URLs.
575 */
576 public function restore_original(): void
577 {
578 if (!$this->verify_request()) {
579 return;
580 }
581
582 $attachment_id = absint($_POST['attachment_id'] ?? 0);
583
584 if (!$attachment_id) {
585 wp_send_json_error(['message' => __('Invalid attachment ID.', 'king-addons')]);
586 return;
587 }
588
589 $updated = Image_Optimizer_DB::revert_to_original_urls($attachment_id);
590
591 wp_send_json_success([
592 'attachment_id' => $attachment_id,
593 'updated_count' => $updated,
594 'message' => sprintf(__('Reverted %d URL references.', 'king-addons'), $updated),
595 ]);
596 }
597
598 /**
599 * Get images for bulk optimization.
600 */
601 public function get_bulk_images(): void
602 {
603 if (!$this->verify_request()) {
604 return;
605 }
606
607 $page = absint($_POST['page'] ?? 1);
608 $per_page = absint($_POST['per_page'] ?? 50);
609 $filter = sanitize_text_field($_POST['filter'] ?? 'all'); // all, pending, optimized
610 $format_filter = isset($_POST['format_filter']) ? array_map('sanitize_text_field', (array) $_POST['format_filter']) : [];
611
612 global $wpdb;
613
614 // Build query
615 $args = [
616 'post_type' => 'attachment',
617 'post_mime_type' => 'image',
618 'posts_per_page' => $per_page,
619 'paged' => $page,
620 'post_status' => 'inherit',
621 'orderby' => 'date',
622 'order' => 'DESC',
623 ];
624
625 // Format filter
626 if (!empty($format_filter)) {
627 $mime_types = [];
628 foreach ($format_filter as $fmt) {
629 if ($fmt === 'jpeg') {
630 $mime_types[] = 'image/jpeg';
631 } elseif ($fmt === 'png') {
632 $mime_types[] = 'image/png';
633 } elseif ($fmt === 'webp') {
634 $mime_types[] = 'image/webp';
635 } elseif ($fmt === 'gif') {
636 $mime_types[] = 'image/gif';
637 }
638 }
639 if (!empty($mime_types)) {
640 $args['post_mime_type'] = $mime_types;
641 }
642 }
643
644 // Status filter
645 if ($filter === 'pending') {
646 $args['meta_query'] = [
647 'relation' => 'OR',
648 [
649 'key' => '_king_img_optimized',
650 'compare' => 'NOT EXISTS',
651 ],
652 [
653 'key' => '_king_img_optimized',
654 'value' => '"status";s:7:"pending"',
655 'compare' => 'LIKE',
656 ],
657 ];
658 } elseif ($filter === 'optimized') {
659 $args['meta_query'] = [
660 [
661 'key' => '_king_img_optimized',
662 'value' => '"status";s:9:"optimized"',
663 'compare' => 'LIKE',
664 ],
665 ];
666 }
667
668 $query = new \WP_Query($args);
669 $images = [];
670
671 foreach ($query->posts as $post) {
672 $file = get_attached_file($post->ID);
673 $metadata = wp_get_attachment_metadata($post->ID);
674 $opt_meta = Image_Optimizer_DB::get_optimization_meta($post->ID);
675
676 $sizes = [];
677
678 // Full size
679 if ($file && file_exists($file)) {
680 $sizes['full'] = [
681 'width' => $metadata['width'] ?? 0,
682 'height' => $metadata['height'] ?? 0,
683 'filesize' => filesize($file),
684 ];
685 }
686
687 // Other sizes
688 if (!empty($metadata['sizes'])) {
689 $base_dir = trailingslashit(dirname($file));
690 foreach ($metadata['sizes'] as $size_name => $size_data) {
691 $size_file = $base_dir . $size_data['file'];
692 if (file_exists($size_file)) {
693 $sizes[$size_name] = [
694 'width' => $size_data['width'],
695 'height' => $size_data['height'],
696 'filesize' => filesize($size_file),
697 ];
698 }
699 }
700 }
701
702 $images[] = [
703 'id' => $post->ID,
704 'title' => $post->post_title,
705 'filename' => basename($file),
706 'url' => wp_get_attachment_url($post->ID),
707 'thumb_url' => wp_get_attachment_image_url($post->ID, 'thumbnail'),
708 'mime_type' => $post->post_mime_type,
709 'status' => $opt_meta['status'] ?? 'pending',
710 'sizes' => $sizes,
711 'total_size' => array_sum(array_column($sizes, 'filesize')),
712 'optimization' => $opt_meta,
713 ];
714 }
715
716 wp_send_json_success([
717 'images' => $images,
718 'total' => $query->found_posts,
719 'pages' => $query->max_num_pages,
720 'page' => $page,
721 ]);
722 }
723
724 /**
725 * Save settings via AJAX.
726 */
727 public function save_settings(): void
728 {
729 if (!$this->verify_request()) {
730 return;
731 }
732
733 if (!current_user_can('manage_options')) {
734 wp_send_json_error(['message' => __('Permission denied.', 'king-addons')]);
735 return;
736 }
737
738 $settings = [
739 'quality' => absint($_POST['quality'] ?? 82),
740 'auto_replace_urls' => !empty($_POST['auto_replace_urls']),
741 'auto_optimize_uploads' => !empty($_POST['auto_optimize_uploads']),
742 'skip_small' => !empty($_POST['skip_small']),
743 'min_size' => absint($_POST['min_size'] ?? 10240),
744 'resize_enabled' => !empty($_POST['resize_enabled']),
745 'max_width' => absint($_POST['max_width'] ?? 2048),
746 'create_backups' => !empty($_POST['create_backups']),
747 ];
748 $optimizer = Image_Optimizer::instance();
749 $saved = $optimizer->save_settings($settings);
750
751 if ($saved) {
752 wp_send_json_success(['message' => __('Settings saved successfully.', 'king-addons')]);
753 } else {
754 wp_send_json_error(['message' => __('Failed to save settings.', 'king-addons')]);
755 }
756 }
757
758 /**
759 * Get global stats.
760 */
761 public function get_stats(): void
762 {
763 if (!$this->verify_request()) {
764 return;
765 }
766
767 $optimizer = Image_Optimizer::instance();
768 $stats = $optimizer->get_global_stats();
769
770 wp_send_json_success($stats);
771 }
772
773 /**
774 * Get image format breakdown.
775 */
776 public function get_breakdown(): void
777 {
778 if (!$this->verify_request()) {
779 return;
780 }
781
782 $optimizer = Image_Optimizer::instance();
783 $stats = $optimizer->get_global_stats();
784 $format_counts = Image_Optimizer_DB::count_images_by_format();
785
786 wp_send_json_success([
787 'total_images' => (int) ($stats['total_images'] ?? 0),
788 'formats' => $format_counts,
789 ]);
790 }
791
792 /**
793 * Full restore - delete optimized files.
794 */
795 public function full_restore(): void
796 {
797 if (!$this->verify_request()) {
798 return;
799 }
800
801 $attachment_id = absint($_POST['attachment_id'] ?? 0);
802
803 if (!$attachment_id) {
804 wp_send_json_error(['message' => __('Invalid attachment ID.', 'king-addons')]);
805 return;
806 }
807
808 // First revert URLs
809 Image_Optimizer_DB::revert_to_original_urls($attachment_id);
810
811 // Get meta and delete optimized files
812 $meta = Image_Optimizer_DB::get_optimization_meta($attachment_id);
813 $deleted_files = 0;
814
815 if (!empty($meta['sizes'])) {
816 foreach ($meta['sizes'] as $size => $data) {
817 // Try optimized_path first, then webp_path for backward compatibility
818 $file_to_delete = $data['optimized_path'] ?? $data['webp_path'] ?? '';
819 if (!empty($file_to_delete) && file_exists($file_to_delete)) {
820 if (@unlink($file_to_delete)) {
821 $deleted_files++;
822 }
823 }
824 }
825 }
826
827 // Delete optimization metadata
828 Image_Optimizer_DB::delete_optimization_meta($attachment_id);
829
830 wp_send_json_success([
831 'attachment_id' => $attachment_id,
832 'deleted_files' => $deleted_files,
833 'message' => sprintf(__('Restored original. Deleted %d optimized files.', 'king-addons'), $deleted_files),
834 ]);
835 }
836
837 /**
838 * Get optimization state for resume.
839 */
840 public function get_optimization_state(): void
841 {
842 if (!$this->verify_request()) {
843 return;
844 }
845
846 $user_id = get_current_user_id();
847 $state = get_user_meta($user_id, '_king_img_bulk_state', true);
848
849 if (empty($state)) {
850 wp_send_json_success(['has_state' => false]);
851 return;
852 }
853
854 wp_send_json_success([
855 'has_state' => true,
856 'state' => $state,
857 ]);
858 }
859
860 /**
861 * Save optimization state.
862 */
863 public function save_optimization_state(): void
864 {
865 if (!$this->verify_request()) {
866 return;
867 }
868
869 $user_id = get_current_user_id();
870 $state = [
871 'currentIndex' => absint($_POST['currentIndex'] ?? 0),
872 'totalImages' => absint($_POST['totalImages'] ?? 0),
873 'successCount' => absint($_POST['successCount'] ?? 0),
874 'errorCount' => absint($_POST['errorCount'] ?? 0),
875 'totalSavedBytes' => absint($_POST['totalSavedBytes'] ?? 0),
876 'imageQueue' => isset($_POST['imageQueue']) ? json_decode(stripslashes($_POST['imageQueue']), true) : [],
877 'settings' => isset($_POST['settings']) ? json_decode(stripslashes($_POST['settings']), true) : [],
878 'saved_at' => current_time('mysql'),
879 ];
880
881 update_user_meta($user_id, '_king_img_bulk_state', $state);
882
883 wp_send_json_success(['message' => __('State saved.', 'king-addons')]);
884 }
885
886 /**
887 * Clear optimization state.
888 */
889 public function clear_optimization_state(): void
890 {
891 if (!$this->verify_request()) {
892 return;
893 }
894
895 $user_id = get_current_user_id();
896 delete_user_meta($user_id, '_king_img_bulk_state');
897
898 wp_send_json_success(['message' => __('State cleared.', 'king-addons')]);
899 }
900
901 /**
902 * Get all optimized image IDs for bulk restore.
903 */
904 public function get_optimized_ids(): void
905 {
906 if (!$this->verify_request()) {
907 return;
908 }
909
910 global $wpdb;
911
912 // Get all attachment IDs that have optimization meta
913 $ids = $wpdb->get_col(
914 $wpdb->prepare(
915 "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s",
916 '_king_img_optimized'
917 )
918 );
919
920 wp_send_json_success([
921 'ids' => array_map('absint', $ids),
922 'total' => count($ids),
923 ]);
924 }
925
926 /**
927 * Bulk restore single image (used in batch processing).
928 */
929 public function bulk_restore_single(): void
930 {
931 if (!$this->verify_request()) {
932 return;
933 }
934
935 $attachment_id = absint($_POST['attachment_id'] ?? 0);
936
937 if (!$attachment_id) {
938 wp_send_json_error(['message' => __('Invalid attachment ID.', 'king-addons')]);
939 return;
940 }
941
942 // Revert URLs in database
943 Image_Optimizer_DB::revert_to_original_urls($attachment_id);
944
945 // Get meta and delete optimized files
946 $meta = Image_Optimizer_DB::get_optimization_meta($attachment_id);
947 $deleted_files = 0;
948
949 if (!empty($meta['sizes'])) {
950 foreach ($meta['sizes'] as $size => $data) {
951 $file_to_delete = $data['optimized_path'] ?? $data['webp_path'] ?? '';
952 if (!empty($file_to_delete) && file_exists($file_to_delete)) {
953 if (@unlink($file_to_delete)) {
954 $deleted_files++;
955 }
956 }
957 }
958 }
959
960 // Delete optimization metadata
961 Image_Optimizer_DB::delete_optimization_meta($attachment_id);
962
963 wp_send_json_success([
964 'attachment_id' => $attachment_id,
965 'deleted_files' => $deleted_files,
966 ]);
967 }
968 }
969