PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 2.3.0
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v2.3.0
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
imagify / classes / Bulk / Bulk.php

Bulk.php in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF 2.3.0, at classes/Bulk/Bulk.php

732 lines 18.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Imagify\Bulk;
3
4 use Exception;
5 use Imagify\Traits\InstanceGetterTrait;
6 use Imagify\Optimization\Process\ProcessInterface;
7 use WP_Error;
8
9 /**
10 * Bulk optimization
11 */
12 final class Bulk {
13 use InstanceGetterTrait;
14
15 /**
16 * Class init: launch hooks.
17 *
18 * @since 2.1
19 */
20 public function init() {
21 add_action( 'imagify_optimize_media', [ $this, 'optimize_media' ], 10, 3 );
22 add_action( 'imagify_convert_next_gen', [ $this, 'generate_nextgen_versions' ], 10, 2 ); // @phpstan-ignore-line
23 add_action( 'wp_ajax_imagify_bulk_optimize', [ $this, 'bulk_optimize_callback' ] );
24 add_action( 'wp_ajax_imagify_missing_nextgen_generation', [ $this, 'missing_nextgen_callback' ] );
25 add_action( 'wp_ajax_imagify_get_folder_type_data', [ $this, 'get_folder_type_data_callback' ] );
26 add_action( 'wp_ajax_imagify_bulk_info_seen', [ $this, 'bulk_info_seen_callback' ] );
27 add_action( 'wp_ajax_imagify_bulk_get_stats', [ $this, 'bulk_get_stats_callback' ] );
28 add_action( 'imagify_after_optimize', [ $this, 'check_optimization_status' ], 10, 2 );
29 add_action( 'imagify_deactivation', [ $this, 'delete_transients_data' ] );
30 add_action( 'update_option_imagify_settings', [ $this, 'maybe_generate_missing_nextgen' ], 10, 2 );
31 }
32
33 /**
34 * Delete transients data on deactivation
35 *
36 * @return void
37 */
38 public function delete_transients_data() {
39 delete_transient( 'imagify_custom-folders_optimize_running' );
40 delete_transient( 'imagify_wp_optimize_running' );
41 delete_transient( 'imagify_bulk_optimization_complete' );
42 delete_transient( 'imagify_missing_next_gen_total' );
43 }
44
45 /**
46 * Checks bulk optimization status after each optimization task
47 *
48 * @param ProcessInterface $process The optimization process.
49 * @param array $item The item being processed.
50 *
51 * @return void
52 */
53 public function check_optimization_status( $process, $item ) {
54 $custom_folders = get_transient( 'imagify_custom-folders_optimize_running' );
55 $library_wp = get_transient( 'imagify_wp_optimize_running' );
56
57 if (
58 ! $custom_folders
59 &&
60 ! $library_wp
61 ) {
62 return;
63 }
64
65 $data = $process->get_data();
66
67 if ( ! $data ) {
68 return;
69 }
70
71 $progress = get_transient( 'imagify_bulk_optimization_result' );
72
73 if ( $data->is_optimized() ) {
74 $size_data = $data->get_size_data();
75
76 if ( false === $progress ) {
77 $progress = [
78 'total' => 0,
79 'original_size' => 0,
80 'optimized_size' => 0,
81 ];
82 }
83
84 ++$progress['total'];
85
86 $progress['original_size'] += $size_data['original_size'];
87 $progress['optimized_size'] += $size_data['optimized_size'];
88
89 set_transient( 'imagify_bulk_optimization_result', $progress, DAY_IN_SECONDS );
90 }
91
92 $remaining = 0;
93
94 if ( false !== $custom_folders ) {
95 if ( false !== strpos( $item['process_class'], 'CustomFolders' ) ) {
96 --$custom_folders['remaining'];
97
98 set_transient( 'imagify_custom-folders_optimize_running', $custom_folders, DAY_IN_SECONDS );
99
100 $remaining += $custom_folders['remaining'];
101 }
102 }
103
104 if ( false !== $library_wp ) {
105 if ( false !== strpos( $item['process_class'], 'WP' ) ) {
106 --$library_wp['remaining'];
107
108 set_transient( 'imagify_wp_optimize_running', $library_wp, DAY_IN_SECONDS );
109
110 $remaining += $library_wp['remaining'];
111 }
112 }
113
114 if ( 0 >= $remaining ) {
115 delete_transient( 'imagify_custom-folders_optimize_running' );
116 delete_transient( 'imagify_wp_optimize_running' );
117 set_transient( 'imagify_bulk_optimization_complete', 1, DAY_IN_SECONDS );
118 }
119 }
120
121 /**
122 * Decrease optimization running counter for the given context
123 *
124 * @param string $context Context to update.
125 *
126 * @return void
127 */
128 private function decrease_counter( string $context ) {
129 $counter = get_transient( "imagify_{$context}_optimize_running" );
130
131 if ( false === $counter ) {
132 return;
133 }
134
135 $counter['total'] = $counter['total'] - 1;
136 $counter['remaining'] = $counter['remaining'] - 1;
137
138 if (
139 0 === $counter['total']
140 &&
141 0 >= $counter['remaining']
142 ) {
143 delete_transient( "imagify_{$context}_optimize_running" );
144 }
145
146 set_transient( "imagify_{$context}_optimize_running", $counter, DAY_IN_SECONDS );
147 }
148
149 /**
150 * Process a media with the requested imagify bulk action.
151 *
152 * @since 2.1
153 *
154 * @param int $media_id Media ID.
155 * @param string $context Current context.
156 * @param int $optimization_level Optimization level.
157 */
158 public function optimize_media( int $media_id, string $context, int $optimization_level ) {
159 if ( ! $media_id || ! $context ) {
160 $this->decrease_counter( $context );
161
162 return;
163 }
164
165 $this->force_optimize( $media_id, $context, $optimization_level );
166 }
167
168 /**
169 * Runs the bulk optimization
170 *
171 * @param string $context Current context (WP/Custom folders).
172 * @param int $optimization_level Optimization level.
173 *
174 * @return array
175 */
176 public function run_optimize( string $context, int $optimization_level ) {
177 if ( ! $this->can_optimize() ) {
178 return [
179 'success' => false,
180 'message' => 'over-quota',
181 ];
182 }
183
184 $media_ids = $this->get_bulk_instance( $context )->get_unoptimized_media_ids( $optimization_level );
185
186 if ( empty( $media_ids ) ) {
187 return [
188 'success' => false,
189 'message' => 'no-images',
190 ];
191 }
192
193 foreach ( $media_ids as $media_id ) {
194 try {
195 as_enqueue_async_action(
196 'imagify_optimize_media',
197 [
198 'id' => $media_id,
199 'context' => $context,
200 'level' => $optimization_level,
201 ],
202 "imagify-{$context}-optimize-media"
203 );
204 } catch ( Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
205 // nothing to do.
206 }
207 }
208
209 $data = [
210 'total' => count( $media_ids ),
211 'remaining' => count( $media_ids ),
212 ];
213
214 set_transient( "imagify_{$context}_optimize_running", $data, DAY_IN_SECONDS );
215
216 return [
217 'success' => true,
218 'message' => 'success',
219 ];
220 }
221
222 /**
223 * Runs the bulk restore for a given context.
224 *
225 * Restores all optimized media to their original state synchronously.
226 * Does not consume API quota since restore is a local file operation.
227 *
228 * @since 2.3
229 *
230 * @param string $context Current context (WP/Custom folders).
231 *
232 * @return array {
233 * @type bool $success Whether any media was found to restore.
234 * @type string $message Status message.
235 * @type int $restored Number of media successfully restored.
236 * @type int $errors Number of media that failed to restore.
237 * @type int $total Total number of media processed.
238 * }
239 */
240 public function run_restore( string $context ) {
241 $media_ids = $this->get_bulk_instance( $context )->get_optimized_media_ids();
242
243 if ( empty( $media_ids ) ) {
244 return [
245 'success' => false,
246 'message' => 'no-images',
247 'restored' => 0,
248 'errors' => 0,
249 'total' => 0,
250 ];
251 }
252
253 $restored = 0;
254 $errors = 0;
255
256 foreach ( $media_ids as $media_id ) {
257 $result = imagify_get_optimization_process( $media_id, $context )->restore();
258
259 if ( is_wp_error( $result ) ) {
260 ++$errors;
261 continue;
262 }
263
264 ++$restored;
265 }
266
267 return [
268 'success' => true,
269 'message' => 'success',
270 'restored' => $restored,
271 'errors' => $errors,
272 'total' => count( $media_ids ),
273 ];
274 }
275
276 /**
277 * Runs the next-gen generation
278 *
279 * @param array $contexts An array of contexts (WP/Custom folders).
280 * @param array $formats An array of format to generate.
281 *
282 * @return array
283 */
284 public function run_generate_nextgen( array $contexts, array $formats ) {
285 if ( ! $this->can_optimize() ) {
286 return [
287 'success' => false,
288 'message' => 'over-quota',
289 ];
290 }
291
292 delete_transient( 'imagify_stat_without_next_gen' );
293
294 $medias = [];
295
296 foreach ( $contexts as $context ) {
297 foreach ( $formats as $format ) {
298 $media = $this->get_bulk_instance( $context )->get_optimized_media_ids_without_format( $format );
299 if ( ! $media['ids'] && $media['errors']['no_backup'] ) {
300 // No backup, no next-gen.
301 return [
302 'success' => false,
303 'message' => 'no-backup',
304 ];
305 } elseif ( ! $media['ids'] && $media['errors']['no_file_path'] ) {
306 // Error.
307 return [
308 'success' => false,
309 'message' => __( 'The path to the selected files could not be retrieved.', 'imagify' ),
310 ];
311 }
312
313 $medias[ $context ] = $media['ids'];
314 }
315 }
316
317 if ( empty( $medias ) ) {
318 return [
319 'success' => false,
320 'message' => 'no-images',
321 ];
322 }
323
324 $total = 0;
325
326 foreach ( $medias as $context => $media_ids ) {
327 $total += count( $media_ids );
328
329 foreach ( $media_ids as $media_id ) {
330 try {
331 as_enqueue_async_action(
332 'imagify_convert_next_gen',
333 [
334 'id' => $media_id,
335 'context' => $context,
336 ],
337 "imagify-{$context}-convert-nextgen"
338 );
339 } catch ( Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
340 // nothing to do.
341 }
342 }
343 }
344
345 set_transient( 'imagify_missing_next_gen_total', $total, HOUR_IN_SECONDS );
346
347 return [
348 'success' => true,
349 'message' => $total,
350 ];
351 }
352
353 /**
354 * Get the Bulk class name depending on a context.
355 *
356 * @since 2.1
357 *
358 * @param string $context The context name. Default values are 'wp' and 'custom-folders'.
359 * @return string The Bulk class name.
360 */
361 private function get_bulk_class_name( string $context ): string {
362 switch ( $context ) {
363 case 'wp':
364 $class_name = WP::class;
365 break;
366
367 case 'custom-folders':
368 $class_name = CustomFolders::class;
369 break;
370
371 default:
372 $class_name = Noop::class;
373 }
374
375 /**
376 * Filter the name of the class to use for bulk process.
377 *
378 * @since 1.9
379 *
380 * @param string $class_name The class name.
381 * @param string $context The context name.
382 */
383 $class_name = wpm_apply_filters_typed( 'string', 'imagify_bulk_class_name', $class_name, $context );
384
385 return '\\' . ltrim( $class_name, '\\' );
386 }
387
388 /**
389 * Get the Bulk instance depending on a context.
390 *
391 * @since 2.1
392 *
393 * @param string $context The context name. Default values are 'wp' and 'custom-folders'.
394 *
395 * @return BulkInterface The optimization process instance.
396 */
397 public function get_bulk_instance( string $context ): BulkInterface {
398 $class_name = $this->get_bulk_class_name( $context );
399 return new $class_name();
400 }
401
402 /**
403 * Optimize all files from a media, whatever this media’s previous optimization status (will be restored if needed).
404 * This is used by the bulk optimization page.
405 *
406 * @since 1.9
407 *
408 * @param int $media_id The media ID.
409 * @param string $context The context.
410 * @param int $level The optimization level.
411 *
412 * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure.
413 */
414 private function force_optimize( int $media_id, string $context, int $level ) {
415 if ( ! $this->can_optimize() ) {
416 $this->decrease_counter( $context );
417
418 return false;
419 }
420
421 $process = imagify_get_optimization_process( $media_id, $context );
422 $data = $process->get_data();
423
424 // Restore before re-optimizing.
425 if ( $data->is_optimized() ) {
426 $result = $process->restore();
427
428 if ( is_wp_error( $result ) ) {
429 $this->decrease_counter( $context );
430
431 // Return an error message.
432 return $result;
433 }
434 }
435
436 return $process->optimize( $level );
437 }
438
439 /**
440 * Generate next-gen images if they are missing.
441 *
442 * @since 2.1
443 *
444 * @param int $media_id Media ID.
445 * @param string $context Current context.
446 *
447 * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure.
448 */
449 public function generate_nextgen_versions( int $media_id, string $context ) {
450 if ( ! $this->can_optimize() ) {
451 return false;
452 }
453
454 return imagify_get_optimization_process( $media_id, $context )->generate_nextgen_versions();
455 }
456
457 /**
458 * Check if the user has a valid account and has quota. Die on failure.
459 *
460 * @since 2.1
461 */
462 public function can_optimize() {
463 if ( ! \Imagify_Requirements::is_api_key_valid() ) {
464 return false;
465 }
466
467 if ( \Imagify_Requirements::is_over_quota() ) {
468 return false;
469 }
470
471 return true;
472 }
473
474 /**
475 * Get the submitted context.
476 *
477 * @since 1.9
478 *
479 * @param string $method The method used: 'GET' (default), or 'POST'.
480 * @param string $parameter The name of the parameter to look for.
481 *
482 * @return string
483 */
484 public function get_context( $method = 'GET', $parameter = 'context' ) {
485 if ( empty( $_POST[ $parameter ] ) && empty( $_GET[ $parameter ] ) ) {
486 // No context.
487 return 'noop';
488 }
489
490 $context = 'POST' === $method ? sanitize_text_field( wp_unslash( $_POST[ $parameter ] ) ) : sanitize_text_field( wp_unslash( $_GET[ $parameter ] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
491
492 return imagify_sanitize_context( $context );
493 }
494
495 /**
496 * Get the submitted optimization level.
497 *
498 * @since 1.7
499 * @since 1.9 Added $method and $parameter parameters.
500 * @author Grégory Viguier
501 *
502 * @param string $method The method used: 'GET' (default), or 'POST'.
503 * @param string $parameter The name of the parameter to look for.
504 * @return int
505 */
506 public function get_optimization_level( $method = 'GET', $parameter = 'optimization_level' ) {
507 $method = 'POST' === $method ? INPUT_POST : INPUT_GET;
508 $level = filter_input( $method, $parameter );
509
510 if ( ! is_numeric( $level ) || $level < 0 || $level > 2 ) {
511 if ( get_imagify_option( 'lossless' ) ) {
512 return 0;
513 }
514
515 return get_imagify_option( 'optimization_level' );
516 }
517
518 return (int) $level;
519 }
520
521 /**
522 * Launch the bulk optimization action
523 *
524 * @return void
525 */
526 public function bulk_optimize_callback() {
527 imagify_check_nonce( 'imagify-bulk-optimize' );
528
529 $context = $this->get_context();
530 $level = $this->get_optimization_level();
531
532 if ( ! imagify_get_context( $context )->current_user_can( 'bulk-optimize' ) ) {
533 imagify_die();
534 }
535
536 $data = $this->run_optimize( $context, $level );
537
538 if ( false === $data['success'] ) {
539 wp_send_json_error( [ 'message' => $data['message'] ] );
540 }
541
542 wp_send_json_success( [ 'total' => $data['message'] ] );
543 }
544
545 /**
546 * Launch the missing Next-gen versions generation
547 *
548 * @return void
549 */
550 public function missing_nextgen_callback() {
551 imagify_check_nonce( 'imagify-bulk-optimize' );
552
553 $contexts = $this->get_contexts();
554
555 foreach ( $contexts as $context ) {
556 if ( ! imagify_get_context( $context )->current_user_can( 'bulk-optimize' ) ) {
557 imagify_die();
558 }
559 }
560
561 $formats = imagify_nextgen_images_formats();
562
563 $data = $this->run_generate_nextgen( $contexts, $formats );
564 if ( false === $data['success'] ) {
565 wp_send_json_error( [ 'message' => $data['message'] ] );
566 }
567
568 wp_send_json_success( [ 'total' => $data['message'] ] );
569 }
570
571 /**
572 * Get stats data for a specific folder type.
573 *
574 * @since 1.7
575 */
576 public function get_folder_type_data_callback() {
577 imagify_check_nonce( 'imagify-bulk-optimize' );
578
579 $context = $this->get_context();
580
581 if ( ! $context ) {
582 imagify_die( __( 'Invalid request', 'imagify' ) );
583 }
584
585 if ( ! imagify_get_context( $context )->current_user_can( 'bulk-optimize' ) ) {
586 imagify_die();
587 }
588
589 $bulk = $this->get_bulk_instance( $context );
590
591 wp_send_json_success( $bulk->get_context_data() );
592 }
593
594 /**
595 * Set the "bulk info" popup state as "seen".
596 *
597 * @since 1.7
598 */
599 public function bulk_info_seen_callback() {
600 imagify_check_nonce( 'imagify-bulk-optimize' );
601
602 $context = $this->get_context();
603
604 if ( ! $context ) {
605 imagify_die( __( 'Invalid request', 'imagify' ) );
606 }
607
608 if ( ! imagify_get_context( $context )->current_user_can( 'bulk-optimize' ) ) {
609 imagify_die();
610 }
611
612 set_transient( 'imagify_bulk_optimization_infos', 1, WEEK_IN_SECONDS );
613
614 wp_send_json_success();
615 }
616
617 /**
618 * Get generic stats to display in the bulk page.
619 *
620 * @since 1.7.1
621 */
622 public function bulk_get_stats_callback() {
623 imagify_check_nonce( 'imagify-bulk-optimize' );
624
625 $folder_types = filter_input( INPUT_GET, 'types', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
626 $folder_types = is_array( $folder_types ) ? $folder_types : [];
627
628 if ( ! $folder_types ) {
629 imagify_die( __( 'Invalid request', 'imagify' ) );
630 }
631
632 foreach ( $folder_types as $folder_type_data ) {
633 $context = ! empty( $folder_type_data['context'] ) ? $folder_type_data['context'] : 'noop';
634
635 if ( ! imagify_get_context( $context )->current_user_can( 'bulk-optimize' ) ) {
636 imagify_die();
637 }
638 }
639
640 wp_send_json_success( imagify_get_bulk_stats( array_flip( $folder_types ) ) );
641 }
642
643 /**
644 * Update Options callback to start bulk optimization.
645 *
646 * @since 2.2
647 *
648 * @param array $old_value The old option value.
649 * @param array $value The new option value.
650 *
651 * @return void
652 */
653 public function maybe_generate_missing_nextgen( $old_value, $value ) {
654 if ( ! isset( $old_value['optimization_format'], $value['optimization_format'] ) ) {
655 return;
656 }
657
658 if ( $old_value['optimization_format'] === $value['optimization_format'] ) {
659 // Old value = new value so do nothing.
660 return;
661 }
662
663 if ( 'off' === $value['optimization_format'] ) {
664 // No need to generate next-gen images.
665 return;
666 }
667
668 $contexts = $this->get_contexts();
669 $formats = imagify_nextgen_images_formats();
670
671 $this->run_generate_nextgen( $contexts, $formats );
672 }
673
674 /**
675 * Get the context for the bulk optimization page.
676 *
677 * @since 2.2
678 *
679 * @return array The array of unique contexts ('wp' or 'custom-folders').
680 */
681 public function get_contexts() {
682 $contexts = [];
683 $types = [];
684
685 // Library: in each site.
686 if ( ! is_network_admin() ) {
687 $types['library|wp'] = 1;
688 }
689
690 // Custom folders: in network admin only if network activated, in each site otherwise.
691 if (
692 imagify_can_optimize_custom_folders()
693 &&
694 (
695 ( imagify_is_active_for_network() && is_network_admin() )
696 ||
697 ! imagify_is_active_for_network()
698 )
699 ) {
700 $types['custom-folders|custom-folders'] = 1;
701 }
702
703 /**
704 * Filter the types to display in the bulk optimization page.
705 *
706 * @since 1.7.1
707 *
708 * @param array $types The folder types displayed on the page. If a folder type is "library", the context should be suffixed after a pipe character. They are passed as array keys.
709 */
710 $types = wpm_apply_filters_typed( 'array', 'imagify_bulk_page_types', $types );
711 $types = array_filter( (array) $types );
712
713 if ( isset( $types['library|wp'] ) ) {
714 $contexts[] = 'wp';
715 }
716
717 if ( isset( $types['custom-folders|custom-folders'] ) ) {
718 $folders_instance = \Imagify_Folders_DB::get_instance();
719
720 if ( ! $folders_instance->has_items() ) {
721 if ( ! in_array( 'wp', $contexts, true ) ) {
722 $contexts[] = 'wp';
723 }
724 } elseif ( $folders_instance->has_active_folders() ) {
725 $contexts[] = 'custom-folders';
726 }
727 }
728
729 return $contexts;
730 }
731 }
732