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

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