PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.83
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.83, at includes/extensions/Image_Optimizer/Image_Optimizer_Ajax.php

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