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

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