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

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