PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 2.3.1
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v2.3.1
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.1, at classes/Bulk/Bulk.php

751 lines 19.2 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 implements BulkOptimizerInterface {
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 ): array {
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 $media_ids = array_values(
187 array_filter(
188 $media_ids,
189 function ( $media_id ) use ( $context, $optimization_level ) {
190 /**
191 * Filter the list of media to optimize during a bulk optimization, per media.
192 *
193 * Return false to exclude a given media from the bulk optimization run
194 * (e.g. to skip a specific file type such as PDFs).
195 *
196 * @since 2.3
197 *
198 * @param bool $optimize True to optimize this media, false to exclude it.
199 * @param int $media_id The media ID (attachment ID, or custom-folder file ID).
200 * @param string $context The optimization context ('wp' or 'custom-folders').
201 * @param int $optimization_level The optimization level.
202 */
203 return wpm_apply_filters_typed( 'boolean', 'imagify_bulk_optimize_media', true, $media_id, $context, $optimization_level );
204 }
205 )
206 );
207
208 if ( empty( $media_ids ) ) {
209 return [
210 'success' => false,
211 'message' => 'no-images',
212 ];
213 }
214
215 foreach ( $media_ids as $media_id ) {
216 try {
217 as_enqueue_async_action(
218 'imagify_optimize_media',
219 [
220 'id' => $media_id,
221 'context' => $context,
222 'level' => $optimization_level,
223 ],
224 "imagify-{$context}-optimize-media"
225 );
226 } catch ( Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
227 // nothing to do.
228 }
229 }
230
231 $data = [
232 'total' => count( $media_ids ),
233 'remaining' => count( $media_ids ),
234 ];
235
236 set_transient( "imagify_{$context}_optimize_running", $data, DAY_IN_SECONDS );
237
238 return [
239 'success' => true,
240 'message' => 'success',
241 ];
242 }
243
244 /**
245 * Runs the bulk restore for a given context.
246 *
247 * Restores all optimized media to their original state synchronously.
248 * Does not consume API quota since restore is a local file operation.
249 *
250 * @since 2.3
251 *
252 * @param string $context Current context (WP/Custom folders).
253 *
254 * @return array {
255 * @type bool $success Whether any media was found to restore.
256 * @type string $message Status message.
257 * @type int $restored Number of media successfully restored.
258 * @type int $errors Number of media that failed to restore.
259 * @type int $total Total number of media processed.
260 * }
261 */
262 public function run_restore( string $context ) {
263 $media_ids = $this->get_bulk_instance( $context )->get_optimized_media_ids();
264
265 if ( empty( $media_ids ) ) {
266 return [
267 'success' => false,
268 'message' => 'no-images',
269 'restored' => 0,
270 'errors' => 0,
271 'total' => 0,
272 ];
273 }
274
275 $restored = 0;
276 $errors = 0;
277
278 foreach ( $media_ids as $media_id ) {
279 $result = imagify_get_optimization_process( $media_id, $context )->restore();
280
281 if ( is_wp_error( $result ) ) {
282 ++$errors;
283 continue;
284 }
285
286 ++$restored;
287 }
288
289 return [
290 'success' => true,
291 'message' => 'success',
292 'restored' => $restored,
293 'errors' => $errors,
294 'total' => count( $media_ids ),
295 ];
296 }
297
298 /**
299 * Runs the next-gen generation
300 *
301 * @param array $contexts An array of contexts (WP/Custom folders).
302 * @param array $formats An array of format to generate.
303 *
304 * @return array
305 */
306 public function run_generate_nextgen( array $contexts, array $formats ) {
307 if ( ! $this->can_optimize() ) {
308 return [
309 'success' => false,
310 'message' => 'over-quota',
311 ];
312 }
313
314 delete_transient( 'imagify_stat_without_next_gen' );
315
316 $medias = [];
317
318 foreach ( $contexts as $context ) {
319 foreach ( $formats as $format ) {
320 $media = $this->get_bulk_instance( $context )->get_optimized_media_ids_without_format( $format );
321
322 if ( ! $media['ids'] && $media['errors']['no_file_path'] ) {
323 // Error.
324 return [
325 'success' => false,
326 'message' => __( 'The path to the selected files could not be retrieved.', 'imagify' ),
327 ];
328 }
329
330 if ( $media['ids'] ) {
331 $medias[ $context ] = $media['ids'];
332 }
333 }
334 }
335
336 if ( empty( $medias ) ) {
337 return [
338 'success' => false,
339 'message' => 'no-images',
340 ];
341 }
342
343 $total = 0;
344
345 foreach ( $medias as $context => $media_ids ) {
346 $total += count( $media_ids );
347
348 foreach ( $media_ids as $media_id ) {
349 try {
350 as_enqueue_async_action(
351 'imagify_convert_next_gen',
352 [
353 'id' => $media_id,
354 'context' => $context,
355 ],
356 "imagify-{$context}-convert-nextgen"
357 );
358 } catch ( Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
359 // nothing to do.
360 }
361 }
362 }
363
364 set_transient( 'imagify_missing_next_gen_total', $total, HOUR_IN_SECONDS );
365
366 return [
367 'success' => true,
368 'message' => $total,
369 ];
370 }
371
372 /**
373 * Get the Bulk class name depending on a context.
374 *
375 * @since 2.1
376 *
377 * @param string $context The context name. Default values are 'wp' and 'custom-folders'.
378 * @return string The Bulk class name.
379 */
380 private function get_bulk_class_name( string $context ): string {
381 switch ( $context ) {
382 case 'wp':
383 $class_name = WP::class;
384 break;
385
386 case 'custom-folders':
387 $class_name = CustomFolders::class;
388 break;
389
390 default:
391 $class_name = Noop::class;
392 }
393
394 /**
395 * Filter the name of the class to use for bulk process.
396 *
397 * @since 1.9
398 *
399 * @param string $class_name The class name.
400 * @param string $context The context name.
401 */
402 $class_name = wpm_apply_filters_typed( 'string', 'imagify_bulk_class_name', $class_name, $context );
403
404 return '\\' . ltrim( $class_name, '\\' );
405 }
406
407 /**
408 * Get the Bulk instance depending on a context.
409 *
410 * @since 2.1
411 *
412 * @param string $context The context name. Default values are 'wp' and 'custom-folders'.
413 *
414 * @return BulkInterface The optimization process instance.
415 */
416 public function get_bulk_instance( string $context ): BulkInterface {
417 $class_name = $this->get_bulk_class_name( $context );
418 return new $class_name();
419 }
420
421 /**
422 * Optimize all files from a media, whatever this media’s previous optimization status (will be restored if needed).
423 * This is used by the bulk optimization page.
424 *
425 * @since 1.9
426 *
427 * @param int $media_id The media ID.
428 * @param string $context The context.
429 * @param int $level The optimization level.
430 *
431 * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure.
432 */
433 private function force_optimize( int $media_id, string $context, int $level ) {
434 if ( ! $this->can_optimize() ) {
435 $this->decrease_counter( $context );
436
437 return false;
438 }
439
440 $process = imagify_get_optimization_process( $media_id, $context );
441 $data = $process->get_data();
442
443 // Restore before re-optimizing.
444 if ( $data->is_optimized() ) {
445 $result = $process->restore();
446
447 if ( is_wp_error( $result ) ) {
448 $this->decrease_counter( $context );
449
450 // Return an error message.
451 return $result;
452 }
453 }
454
455 return $process->optimize( $level );
456 }
457
458 /**
459 * Generate next-gen images if they are missing.
460 *
461 * @since 2.1
462 *
463 * @param int $media_id Media ID.
464 * @param string $context Current context.
465 *
466 * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure.
467 */
468 public function generate_nextgen_versions( int $media_id, string $context ) {
469 if ( ! $this->can_optimize() ) {
470 return false;
471 }
472
473 return imagify_get_optimization_process( $media_id, $context )->generate_nextgen_versions();
474 }
475
476 /**
477 * Check if the user has a valid account and has quota. Die on failure.
478 *
479 * @since 2.1
480 */
481 public function can_optimize() {
482 if ( ! \Imagify_Requirements::is_api_key_valid() ) {
483 return false;
484 }
485
486 if ( \Imagify_Requirements::is_over_quota() ) {
487 return false;
488 }
489
490 return true;
491 }
492
493 /**
494 * Get the submitted context.
495 *
496 * @since 1.9
497 *
498 * @param string $method The method used: 'GET' (default), or 'POST'.
499 * @param string $parameter The name of the parameter to look for.
500 *
501 * @return string
502 */
503 public function get_context( $method = 'GET', $parameter = 'context' ) {
504 if ( empty( $_POST[ $parameter ] ) && empty( $_GET[ $parameter ] ) ) {
505 // No context.
506 return 'noop';
507 }
508
509 $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
510
511 return imagify_sanitize_context( $context );
512 }
513
514 /**
515 * Get the submitted optimization level.
516 *
517 * @since 1.7
518 * @since 1.9 Added $method and $parameter parameters.
519 * @author Grégory Viguier
520 *
521 * @param string $method The method used: 'GET' (default), or 'POST'.
522 * @param string $parameter The name of the parameter to look for.
523 * @return int
524 */
525 public function get_optimization_level( $method = 'GET', $parameter = 'optimization_level' ) {
526 $method = 'POST' === $method ? INPUT_POST : INPUT_GET;
527 $level = filter_input( $method, $parameter );
528
529 if ( ! is_numeric( $level ) || $level < 0 || $level > 2 ) {
530 if ( get_imagify_option( 'lossless' ) ) {
531 return 0;
532 }
533
534 return get_imagify_option( 'optimization_level' );
535 }
536
537 return (int) $level;
538 }
539
540 /**
541 * Launch the bulk optimization action
542 *
543 * @return void
544 */
545 public function bulk_optimize_callback() {
546 imagify_check_nonce( 'imagify-bulk-optimize' );
547
548 $context = $this->get_context();
549 $level = $this->get_optimization_level();
550
551 if ( ! imagify_get_context( $context )->current_user_can( 'bulk-optimize' ) ) {
552 imagify_die();
553 }
554
555 $data = $this->run_optimize( $context, $level );
556
557 if ( false === $data['success'] ) {
558 wp_send_json_error( [ 'message' => $data['message'] ] );
559 }
560
561 wp_send_json_success( [ 'total' => $data['message'] ] );
562 }
563
564 /**
565 * Launch the missing Next-gen versions generation
566 *
567 * @return void
568 */
569 public function missing_nextgen_callback() {
570 imagify_check_nonce( 'imagify-bulk-optimize' );
571
572 $contexts = $this->get_contexts();
573
574 foreach ( $contexts as $context ) {
575 if ( ! imagify_get_context( $context )->current_user_can( 'bulk-optimize' ) ) {
576 imagify_die();
577 }
578 }
579
580 $formats = imagify_nextgen_images_formats();
581
582 $data = $this->run_generate_nextgen( $contexts, $formats );
583 if ( false === $data['success'] ) {
584 wp_send_json_error( [ 'message' => $data['message'] ] );
585 }
586
587 wp_send_json_success( [ 'total' => $data['message'] ] );
588 }
589
590 /**
591 * Get stats data for a specific folder type.
592 *
593 * @since 1.7
594 */
595 public function get_folder_type_data_callback() {
596 imagify_check_nonce( 'imagify-bulk-optimize' );
597
598 $context = $this->get_context();
599
600 if ( ! $context ) {
601 imagify_die( __( 'Invalid request', 'imagify' ) );
602 }
603
604 if ( ! imagify_get_context( $context )->current_user_can( 'bulk-optimize' ) ) {
605 imagify_die();
606 }
607
608 $bulk = $this->get_bulk_instance( $context );
609
610 wp_send_json_success( $bulk->get_context_data() );
611 }
612
613 /**
614 * Set the "bulk info" popup state as "seen".
615 *
616 * @since 1.7
617 */
618 public function bulk_info_seen_callback() {
619 imagify_check_nonce( 'imagify-bulk-optimize' );
620
621 $context = $this->get_context();
622
623 if ( ! $context ) {
624 imagify_die( __( 'Invalid request', 'imagify' ) );
625 }
626
627 if ( ! imagify_get_context( $context )->current_user_can( 'bulk-optimize' ) ) {
628 imagify_die();
629 }
630
631 set_transient( 'imagify_bulk_optimization_infos', 1, WEEK_IN_SECONDS );
632
633 wp_send_json_success();
634 }
635
636 /**
637 * Get generic stats to display in the bulk page.
638 *
639 * @since 1.7.1
640 */
641 public function bulk_get_stats_callback() {
642 imagify_check_nonce( 'imagify-bulk-optimize' );
643
644 $folder_types = filter_input( INPUT_GET, 'types', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
645 $folder_types = is_array( $folder_types ) ? $folder_types : [];
646
647 if ( ! $folder_types ) {
648 imagify_die( __( 'Invalid request', 'imagify' ) );
649 }
650
651 foreach ( $folder_types as $folder_type_data ) {
652 $context = ! empty( $folder_type_data['context'] ) ? $folder_type_data['context'] : 'noop';
653
654 if ( ! imagify_get_context( $context )->current_user_can( 'bulk-optimize' ) ) {
655 imagify_die();
656 }
657 }
658
659 wp_send_json_success( imagify_get_bulk_stats( array_flip( $folder_types ) ) );
660 }
661
662 /**
663 * Update Options callback to start bulk optimization.
664 *
665 * @since 2.2
666 *
667 * @param array $old_value The old option value.
668 * @param array $value The new option value.
669 *
670 * @return void
671 */
672 public function maybe_generate_missing_nextgen( $old_value, $value ) {
673 if ( ! isset( $old_value['optimization_format'], $value['optimization_format'] ) ) {
674 return;
675 }
676
677 if ( $old_value['optimization_format'] === $value['optimization_format'] ) {
678 // Old value = new value so do nothing.
679 return;
680 }
681
682 if ( 'off' === $value['optimization_format'] ) {
683 // No need to generate next-gen images.
684 return;
685 }
686
687 $contexts = $this->get_contexts();
688 $formats = imagify_nextgen_images_formats();
689
690 $this->run_generate_nextgen( $contexts, $formats );
691 }
692
693 /**
694 * Get the context for the bulk optimization page.
695 *
696 * @since 2.2
697 *
698 * @return array The array of unique contexts ('wp' or 'custom-folders').
699 */
700 public function get_contexts() {
701 $contexts = [];
702 $types = [];
703
704 // Library: in each site.
705 if ( ! is_network_admin() ) {
706 $types['library|wp'] = 1;
707 }
708
709 // Custom folders: in network admin only if network activated, in each site otherwise.
710 if (
711 imagify_can_optimize_custom_folders()
712 &&
713 (
714 ( imagify_is_active_for_network() && is_network_admin() )
715 ||
716 ! imagify_is_active_for_network()
717 )
718 ) {
719 $types['custom-folders|custom-folders'] = 1;
720 }
721
722 /**
723 * Filter the types to display in the bulk optimization page.
724 *
725 * @since 1.7.1
726 *
727 * @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.
728 */
729 $types = wpm_apply_filters_typed( 'array', 'imagify_bulk_page_types', $types );
730 $types = array_filter( (array) $types );
731
732 if ( isset( $types['library|wp'] ) ) {
733 $contexts[] = 'wp';
734 }
735
736 if ( isset( $types['custom-folders|custom-folders'] ) ) {
737 $folders_instance = \Imagify_Folders_DB::get_instance();
738
739 if ( ! $folders_instance->has_items() ) {
740 if ( ! in_array( 'wp', $contexts, true ) ) {
741 $contexts[] = 'wp';
742 }
743 } elseif ( $folders_instance->has_active_folders() ) {
744 $contexts[] = 'custom-folders';
745 }
746 }
747
748 return $contexts;
749 }
750 }
751