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 / Optimization / Process / AbstractProcess.php

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

2,033 lines 56.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 declare(strict_types=1);
3
4 namespace Imagify\Optimization\Process;
5
6 use Imagify\Deprecated\Traits\Optimization\Process\AbstractProcessDeprecatedTrait;
7 use Imagify\Job\MediaOptimization;
8 use Imagify\Optimization\Data\DataInterface;
9 use Imagify\Optimization\File;
10 use WP_Error;
11
12 /**
13 * Abstract class used to optimize medias.
14 *
15 * @since 1.9
16 */
17 abstract class AbstractProcess implements ProcessInterface {
18 use AbstractProcessDeprecatedTrait;
19
20 /**
21 * The suffix used in the thumbnail size name.
22 *
23 * @var string
24 * @since 1.9
25 */
26 const WEBP_SUFFIX = '@imagify-webp';
27
28 /**
29 * The suffix used in the thumbnail size name.
30 *
31 * @var string
32 * @since 2.2
33 */
34 const AVIF_SUFFIX = '@imagify-avif';
35
36 /**
37 * The suffix used in file name to create a temporary copy of the full size.
38 *
39 * @var string
40 * @since 1.9
41 */
42 const TMP_SUFFIX = '@imagify-tmp';
43
44 /**
45 * Used for the name of the transient telling if a media is locked.
46 * %1$s is the context, %2$s is the media ID.
47 *
48 * @var string
49 * @since 1.9
50 */
51 const LOCK_NAME = 'imagify_%1$s_%2$s_process_locked';
52
53 /**
54 * The data optimization object.
55 *
56 * @var DataInterface
57 * @since 1.9
58 */
59 protected $data;
60
61 /**
62 * The optimization data format.
63 *
64 * @var array
65 * @since 1.9
66 */
67 protected $data_format = [
68 'level' => null,
69 'status' => null,
70 'success' => null,
71 'error' => null,
72 'original_size' => null,
73 'optimized_size' => null,
74 ];
75
76 /**
77 * A File instance.
78 *
79 * @var File
80 * @since 1.9
81 */
82 protected $file;
83
84 /**
85 * Filesystem object.
86 *
87 * @var Imagify_Filesystem
88 * @since 1.9
89 */
90 protected $filesystem;
91
92 /**
93 * Used to cache the plugin’s options.
94 *
95 * @var array
96 * @since 1.9
97 */
98 protected $options = [];
99
100 /**
101 * Tells the format we are currently processing
102 *
103 * @var string
104 * @since 2.2
105 */
106 protected $format;
107
108 /**
109 * Array of image extensions processed.
110 *
111 * @var array
112 */
113 protected $extensions = [
114 'webp',
115 'avif',
116 ];
117
118 /**
119 * The constructor.
120 *
121 * @since 1.9
122 * @see self::constructor_accepts()
123 *
124 * @param mixed $id An ID, or whatever type the constructor accepts.
125 */
126 public function __construct( $id ) {
127 if ( $id instanceof DataInterface ) {
128 $this->data = $id;
129 } elseif ( static::constructor_accepts( $id ) ) {
130 $data_class = str_replace( '\\Optimization\\Process\\', '\\Optimization\\Data\\', get_called_class() );
131 $data_class = '\\' . ltrim( $data_class, '\\' );
132 $this->data = new $data_class( $id );
133 } else {
134 $this->data = false;
135 }
136
137 $this->filesystem = \Imagify_Filesystem::get_instance();
138 $this->format = $this->get_current_format();
139 }
140
141 /**
142 * Tell if the given entry can be accepted in the constructor.
143 *
144 * @since 1.9
145 *
146 * @param mixed $id Whatever.
147 *
148 * @return bool
149 */
150 public static function constructor_accepts( $id ) {
151 if ( $id instanceof DataInterface ) {
152 return true;
153 }
154
155 $data_class = str_replace( '\\Optimization\\Process\\', '\\Optimization\\Data\\', get_called_class() );
156 $data_class = '\\' . ltrim( $data_class, '\\' );
157
158 return $data_class::constructor_accepts( $id );
159 }
160
161 /**
162 * Get the data instance.
163 *
164 * @since 1.9
165 *
166 * @return DataInterface|false
167 */
168 public function get_data() {
169 return $this->data;
170 }
171
172 /**
173 * Get the media instance.
174 *
175 * @since 1.9
176 *
177 * @return MediaInterface|false
178 */
179 public function get_media() {
180 if ( ! $this->get_data() ) {
181 return false;
182 }
183
184 return $this->get_data()->get_media();
185 }
186
187 /**
188 * Get the File instance of the original file.
189 *
190 * @since 1.9.8
191 *
192 * @return File|false
193 */
194 public function get_original_file() {
195 if ( isset( $this->file ) ) {
196 return $this->file;
197 }
198
199 $this->file = false;
200
201 if ( $this->get_media() ) {
202 $this->file = new File( $this->get_media()->get_raw_original_path() );
203 }
204
205 return $this->file;
206 }
207
208 /**
209 * Get the File instance of the full size file.
210 *
211 * @since 1.9.8
212 *
213 * @return File|false
214 */
215 public function get_fullsize_file() {
216 if ( isset( $this->file ) ) {
217 return $this->file;
218 }
219
220 $this->file = false;
221
222 if ( $this->get_media() ) {
223 $this->file = new File( $this->get_media()->get_raw_fullsize_path() );
224 }
225
226 return $this->file;
227 }
228
229 /**
230 * Tell if the current media is valid.
231 *
232 * @since 1.9
233 *
234 * @return bool
235 */
236 public function is_valid() {
237 return $this->get_media() && $this->get_media()->is_valid();
238 }
239
240 /**
241 * Tell if the current user is allowed to operate Imagify in this context.
242 *
243 * @since 1.9
244 *
245 * @param string $describer Capacity describer. See \Imagify\Context\ContextInterface->get_capacity() for possible values. Can also be a "real" user capacity.
246 * @return bool
247 */
248 public function current_user_can( $describer ) {
249 if ( ! $this->is_valid() ) {
250 return false;
251 }
252
253 $media = $this->get_media();
254
255 return $media->get_context_instance()->current_user_can( $describer, $media->get_id() );
256 }
257
258 /**
259 * Optimize a media files.
260 *
261 * @since 1.9
262 *
263 * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
264 * @param array $args An array of optionnal arguments.
265 *
266 * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure.
267 */
268 public function optimize( $optimization_level = null, $args = [] ) {
269 if ( ! $this->is_valid() ) {
270 return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
271 }
272
273 $media = $this->get_media();
274
275 if ( ! $media->is_supported() ) {
276 return new WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) );
277 }
278
279 $data = $this->get_data();
280
281 if ( $data->is_optimized() ) {
282 return new WP_Error( 'optimized', __( 'This media has already been optimized by Imagify.', 'imagify' ) );
283 }
284
285 if ( $data->is_already_optimized() && $this->has_next_gen() ) {
286 // If already optimized but has next-gen, delete next-gen versions and optimization data.
287 $data->delete_optimization_data();
288 $deleted = $this->delete_nextgen_files();
289
290 if ( is_wp_error( $deleted ) ) {
291 return new WP_Error( 'next_gen_not_deleted', __( 'Previous Next-Gen files could not be deleted.', 'imagify' ) );
292 }
293 }
294
295 $sizes = $media->get_media_files();
296 $args = is_array( $args ) ? $args : [];
297
298 $args['hook_suffix'] = 'optimize_media';
299
300 // Optimize.
301 return $this->optimize_sizes( array_keys( $sizes ), $optimization_level, $args );
302 }
303
304 /**
305 * Re-optimize a media files with a different level.
306 *
307 * @since 1.9
308 *
309 * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
310 * @param array $args An array of optionnal arguments.
311 *
312 * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure.
313 */
314 public function reoptimize( $optimization_level = null, $args = [] ) {
315 if ( ! $this->is_valid() ) {
316 return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
317 }
318
319 $media = $this->get_media();
320
321 if ( ! $media->is_supported() ) {
322 return new WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) );
323 }
324
325 $data = $this->get_data();
326
327 if ( ! $data->get_optimization_status() ) {
328 return new WP_Error( 'not_processed_yet', __( 'This media has not been processed yet.', 'imagify' ) );
329 }
330
331 $optimization_level = $this->sanitize_optimization_level( $optimization_level );
332
333 if ( $data->get_optimization_level() === $optimization_level ) {
334 return new WP_Error( 'identical_optimization_level', __( 'This media is already optimized with this level.', 'imagify' ) );
335 }
336
337 $this->restore();
338
339 $sizes = $media->get_media_files();
340 $args = is_array( $args ) ? $args : [];
341
342 $args['hook_suffix'] = 'reoptimize_media';
343
344 // Optimize.
345 return $this->optimize_sizes( array_keys( $sizes ), $optimization_level, $args );
346 }
347
348 /**
349 * Optimize several file sizes by pushing tasks into the queue.
350 *
351 * @since 1.9
352 * @see MediaOptimization->task_before()
353 * @see MediaOptimization->task_after()
354 *
355 * @since 2.2
356 * Addition of the image format
357 *
358 * @param array $sizes An array of media sizes (strings). Use "full" for the size of the main file.
359 * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
360 * @param array $args {
361 * An array of optionnal arguments.
362 *
363 * @type string $hook_suffix Suffix used to trigger hooks before and after optimization.
364 * }
365 *
366 * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure.
367 */
368 public function optimize_sizes( $sizes, $optimization_level = null, $args = [] ) {
369 if ( ! $this->is_valid() ) {
370 return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
371 }
372
373 $media = $this->get_media();
374
375 if ( ! $media->is_supported() ) {
376 return new WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) );
377 }
378
379 if ( ! $sizes ) {
380 return new WP_Error( 'no_sizes', __( 'No sizes given to be optimized.', 'imagify' ) );
381 }
382
383 if ( empty( $args['locked'] ) ) {
384 if ( $this->is_locked() ) {
385 return new WP_Error( 'media_locked', __( 'This media is already being processed.', 'imagify' ) );
386 }
387
388 $this->lock();
389 }
390
391 if ( $media->is_image() ) {
392 // Add Next-Gen conversion.
393 $formats = imagify_nextgen_images_formats();
394
395 foreach ( $formats as $format ) {
396 if ( 'avif' === $format ) {
397 $format_suffix = static::AVIF_SUFFIX;
398 } elseif ( 'webp' === $format ) {
399 $format_suffix = static::WEBP_SUFFIX;
400 }
401
402 $files = $media->get_media_files();
403
404 foreach ( $sizes as $size_name ) {
405 if ( empty( $files[ $size_name ] ) ) {
406 continue;
407 }
408
409 if ( $this->get_mime_type( $format ) === $files[ $size_name ]['mime-type'] ) {
410 continue;
411 }
412
413 if ( in_array( $size_name . $format_suffix, $sizes, true ) ) {
414 continue;
415 }
416
417 array_unshift( $sizes, $size_name . $format_suffix );
418 }
419 }
420
421 if ( ! $media->get_context_instance()->can_backup() && ! $media->get_backup_path() && ! $this->get_data()->get_size_data( 'full', 'success' ) ) {
422 /**
423 * Backup is NOT activated, and a backup file does NOT exist yet, and the full size is NOT optimized yet.
424 * Next-Gen conversion needs a backup file, even a temporary one: we’ll create one.
425 */
426 $next_gen = false;
427
428 foreach ( $sizes as $size_name ) {
429 if ( $this->is_size_next_gen( $size_name ) ) {
430 $next_gen = true;
431 break;
432 }
433 }
434
435 if ( $next_gen ) {
436 // We have at least one next-gen conversion to do: create a temporary backup.
437 $backuped = $this->get_original_file()->backup( $media->get_raw_backup_path() );
438
439 if ( $backuped ) {
440 // See \Imagify\Job\MediaOptimization->delete_backup().
441 $args['delete_backup'] = true;
442 }
443 }
444 }
445 }
446
447 $sizes = array_unique( $sizes );
448 $optimization_level = $this->sanitize_optimization_level( $optimization_level );
449
450 /**
451 * Filter the data sent to the optimization process.
452 *
453 * @since 1.9
454 *
455 * @param array $new_args Additional data to send to the optimization process.
456 * @param array $args Current data sent to the process.
457 * @param ProcessInterface $process The current optimization process.
458 * @param array $sizes Sizes being processed.
459 * @param int $optimization_level Optimization level.
460 */
461 $new_args = apply_filters( 'imagify_optimize_sizes_args', [], $args, $this, $sizes, $optimization_level );
462
463 if ( $new_args && is_array( $new_args ) ) {
464 $args = array_merge( $new_args, $args );
465 }
466
467 /**
468 * Push the item to the queue, save the queue in the DB, empty the queue.
469 * A "batch" is then created in the DB with this unique item, it is then free to loop through its steps (files) without another item interfering (each media optimization has its own dedicated batch/queue).
470 */
471 MediaOptimization::get_instance()->push_to_queue( [
472 'id' => $media->get_id(),
473 'sizes' => $sizes,
474 'optimization_level' => $optimization_level,
475 'process_class' => get_class( $this ),
476 'data' => $args,
477 ] )->save();
478
479 return true;
480 }
481
482 /**
483 * Optimize one file with Imagify directly.
484 *
485 * @since 1.9
486 *
487 * @param string $size The media size.
488 * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
489 *
490 * @return array|WP_Error Optimized image data. A WP_Error object on error.
491 */
492 public function optimize_size( $size, $optimization_level = null ) {
493 if ( ! $this->is_valid() ) { // Bail out.
494 return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
495 }
496
497 $media = $this->get_media();
498 $sizes = $media->get_media_files();
499 $thumb_size = $size;
500 $next_gen = $this->is_size_next_gen( $size );
501 $path_is_temp = false;
502
503 if ( $next_gen ) {
504 // We'll make sure the file is an image later.
505 $thumb_size = $next_gen; // Contains the name of the non-next-gen size.
506 $next_gen = true;
507 }
508
509 if ( empty( $sizes[ $thumb_size ]['path'] ) ) { // Bail out.
510 // This size is not in our list.
511 return new WP_Error(
512 'unknown_size',
513 sprintf(
514 /* translators: %s is a size name. */
515 __( 'The size %s is unknown.', 'imagify' ),
516 '<code>' . esc_html( $thumb_size ) . '</code>'
517 )
518 );
519 }
520
521 if ( $this->get_data()->get_size_data( $size, 'success' ) ) { // Bail out.
522 // This size is already optimized with Imagify, and must not be optimized again.
523 if ( $next_gen ) {
524 return new WP_Error(
525 'size_is_successfully_optimized',
526 sprintf(
527 /* translators: %s is a size name. */
528 __( 'The Next-Gen format for the size %s already exists.', 'imagify' ),
529 '<code>' . esc_html( $thumb_size ) . '</code>'
530 )
531 );
532 } else {
533 return new WP_Error(
534 'size_is_successfully_optimized',
535 sprintf(
536 /* translators: %s is a size name. */
537 __( 'The size %s is already optimized by Imagify.', 'imagify' ),
538 '<code>' . esc_html( $thumb_size ) . '</code>'
539 )
540 );
541 }
542 }
543
544 /**
545 * Starting from here, errors will be stored in the optimization data of the size.
546 */
547 $path = $sizes[ $thumb_size ]['path'];
548
549 $optimization_level = $this->sanitize_optimization_level( $optimization_level );
550
551 if ( $next_gen && $this->get_data()->get_size_data( $thumb_size, 'success' ) ) {
552 // We want a next-gen version but the source file is already optimized by Imagify.
553 $result = $this->create_temporary_copy( $thumb_size, $sizes );
554
555 if ( ! $result ) { // Bail out.
556 // Could not create a copy of the non-next-gen version.
557 $response = new WP_Error(
558 'non_next_gen_copy_failed',
559 sprintf(
560 /* translators: %s is a size name. */
561 __( 'Could not create an unoptimized copy of the size %s.', 'imagify' ),
562 '<code>' . esc_html( $thumb_size ) . '</code>'
563 )
564 );
565
566 $this->update_size_optimization_data( $response, $size, $optimization_level );
567
568 return $response;
569 }
570
571 /**
572 * $path now targets a temporary file.
573 */
574 $path = $this->get_temporary_copy_path( $thumb_size, $sizes );
575 $path_is_temp = true;
576 }
577
578 $file = new File( $path ); // Original file or temporary copy.
579
580 if ( ! $file->is_supported( $media->get_allowed_mime_types() ) ) { // Bail out.
581 // This file type is not supported.
582 $extension = $file->get_extension();
583
584 if ( '' === $extension ) {
585 $response = new WP_Error(
586 'no_extension',
587 __( 'With no extension, this file cannot be optimized.', 'imagify' )
588 );
589 } else {
590 $response = new WP_Error(
591 'extension_not_supported',
592 sprintf(
593 /* translators: %s is a file extension. */
594 __( '%s cannot be optimized.', 'imagify' ),
595 '<code>' . esc_html( strtolower( $extension ) ) . '</code>'
596 )
597 );
598 }
599
600 if ( $path_is_temp ) {
601 $this->filesystem->delete( $path );
602 }
603
604 $this->update_size_optimization_data( $response, $size, $optimization_level );
605
606 return $response;
607 }
608
609 if ( $next_gen && ! $file->is_image() ) { // Bail out.
610 if ( $path_is_temp ) {
611 $this->filesystem->delete( $path );
612 }
613
614 $response = new WP_Error(
615 'no_next_gen',
616 __( 'This file is not an image and cannot be converted to Next-Gen format.', 'imagify' )
617 );
618
619 $this->update_size_optimization_data( $response, $size, $optimization_level );
620
621 return $response;
622 }
623
624 $is_disabled = ! empty( $sizes[ $thumb_size ]['disabled'] );
625
626 /**
627 * Fires before optimizing a file.
628 * Return a WP_Error object to prevent the optimization.
629 *
630 * @since 1.9
631 *
632 * @param null|WP_Error $response Null by default. Return a WP_Error object to prevent optimization.
633 * @param ProcessInterface $process The optimization process instance.
634 * @param File $file The file instance. If $webp is true, $file references the non-WebP file.
635 * @param string $thumb_size The media size.
636 * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
637 * @param bool $webp The image will be converted to WebP.
638 * @param bool $is_disabled Tell if this size is disabled from optimization.
639 */
640 $response = apply_filters( 'imagify_before_optimize_size', null, $this, $file, $thumb_size, $optimization_level, $next_gen, $is_disabled );
641
642 if ( ! is_wp_error( $response ) ) {
643 if ( $is_disabled ) {
644 // This size must not be optimized.
645 $response = new WP_Error(
646 'unauthorized_size',
647 sprintf(
648 /* translators: %s is a size name. */
649 __( 'The size %s is not authorized to be optimized. Update your Imagify settings if you want to optimize it.', 'imagify' ),
650 '<code>' . esc_html( $thumb_size ) . '</code>'
651 )
652 );
653 } elseif ( ! $this->filesystem->exists( $file->get_path() ) ) {
654 $response = new WP_Error(
655 'file_not_exists',
656 sprintf(
657 /* translators: %s is a file path. */
658 __( 'The file %s does not seem to exist.', 'imagify' ),
659 '<code>' . esc_html( $this->filesystem->make_path_relative( $file->get_path() ) ) . '</code>'
660 )
661 );
662 } elseif ( $next_gen && ! $this->can_create_next_gen_version( $file->get_path() ) ) {
663 $response = new WP_Error(
664 'is_animated_gif',
665 __( 'This file is an animated gif: since Imagify does not support animated WebP/AVIF, WebP/AVIF creation for animated gif is disabled.', 'imagify' )
666 );
667 } elseif ( ! $this->filesystem->is_writable( $file->get_path() ) ) {
668 $response = new WP_Error(
669 'file_not_writable',
670 sprintf(
671 /* translators: %s is a file path. */
672 __( 'The file %s does not seem to be writable.', 'imagify' ),
673 '<code>' . esc_html( $this->filesystem->make_path_relative( $file->get_path() ) ) . '</code>'
674 )
675 );
676 } else {
677 // Maybe resize the file.
678 $response = $this->maybe_resize( $thumb_size, $file );
679
680 $convert = '';
681
682 if ( $next_gen ) {
683 if ( strpos( $size, static::AVIF_SUFFIX ) ) {
684 $convert = 'avif';
685 } elseif ( strpos( $size, static::WEBP_SUFFIX ) ) {
686 $convert = 'webp';
687 }
688 }
689
690 if ( ! is_wp_error( $response ) ) {
691 // Resizing succeeded: optimize the file.
692 $response = $file->optimize( [
693 'backup' => ! $response['backuped'] && $this->can_backup( $size ),
694 'backup_path' => $media->get_raw_backup_path(),
695 'backup_source' => 'full' === $thumb_size ? $media->get_original_path() : null,
696 'optimization_level' => $optimization_level,
697 'convert' => $convert,
698 'keep_exif' => true,
699 'context' => $media->get_context(),
700 'original_size' => $response['file_size'],
701 ] );
702
703 $response = $this->compare_next_gen_file_size( [
704 'response' => $response,
705 'file' => $file,
706 'is_next_gen' => $next_gen,
707 'next_gen_format' => $convert,
708 'non_next_gen_thumb_size' => $thumb_size,
709 'non_next_gen_file_path' => $sizes[ $thumb_size ]['path'], // Don't use $path nor $file->get_path(), it may return the path to a temporary file.
710 'optimization_level' => $optimization_level,
711 ] );
712
713 if ( property_exists( $response, 'message' ) ) {
714 $path_is_temp = false;
715 if ( $path !== $sizes[ $thumb_size ]['path'] ) {
716 $this->filesystem->delete( $path );
717 }
718 $path = $sizes[ $thumb_size ]['path'];
719 }
720 }
721 }
722 }
723
724 $data = $this->update_size_optimization_data( $response, $size, $optimization_level );
725
726 /**
727 * Fires after optimizing a file.
728 *
729 * @since 1.9
730 *
731 * @param ProcessInterface $process The optimization process instance.
732 * @param File $file The file instance.
733 * @param string $thumb_size The media size.
734 * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
735 * @param bool $webp The image was supposed to be converted to WebP.
736 * @param bool $is_disabled Tell if this size is disabled from optimization.
737 */
738 do_action( 'imagify_after_optimize_size', $this, $file, $thumb_size, $optimization_level, $next_gen, $is_disabled );
739
740 if ( ! $path_is_temp ) {
741 return $data;
742 }
743
744 // Delete the temporary copy.
745 $this->filesystem->delete( $path );
746
747 if ( is_wp_error( $response ) ) {
748 return $data;
749 }
750
751 // Rename the optimized file.
752 $destination_path = str_replace( static::TMP_SUFFIX . '.', '.', $file->get_path() );
753
754 $this->filesystem->move( $file->get_path(), $destination_path, true );
755
756 return $data;
757 }
758
759 /**
760 * Compare the file size of a file and its Next-Gen version: if the Next-Gen version is heavier than the non-next-gen file, delete it.
761 *
762 * @since 2.2
763 *
764 * @param array $args {
765 * A list of mandatory arguments.
766 *
767 * @type \sdtClass|WP_Error $response Optimized image data. A WP_Error object on error.
768 * @type File $file The File instance of the file currently being optimized.
769 * @type bool $is_next_gen Tell if we're requesting a next-gen file.
770 * @type string $non_next_gen_thumb_size Name of the corresponding non-next-gen thumbnail size. If we're not creating a Next-Gen file, this corresponds to the current thumbnail size.
771 * @type string $non_next_gen_file_path Path to the corresponding non-next-gen file. If we're not creating a Next-Gen file, this corresponds to the current file path.
772 * @type string $optimization_level The optimization level.
773 * }
774 *
775 * @return \sdtClass|WP_Error Optimized image data. A WP_Error object on error.
776 */
777 protected function compare_next_gen_file_size( $args ) {
778 static $keep_large_next_gen;
779
780 if ( ! isset( $keep_large_next_gen ) ) {
781 /**
782 * Allow to not store next-gen images that are larger than their non-next-gen version.
783 *
784 * @since 1.9.4
785 *
786 * @param bool $keep_large_next-gen Set to false if you prefer your visitors over your Pagespeed score. Default value is true.
787 */
788 $keep_large_next_gen = apply_filters( 'imagify_keep_large_next_gen', true );
789 }
790
791 if ( $keep_large_next_gen || is_wp_error( $args['response'] ) || ! $args['file']->is_image() ) {
792 return $args['response'];
793 }
794
795 // Optimization succeeded.
796 if ( ! property_exists( $args['response'], 'message' ) && $args['is_next_gen'] ) {
797 /**
798 * We just created a next-gen version:
799 * Check if it is lighter than the (maybe optimized) non-next-gen file.
800 */
801 $data = $this->get_data()->get_size_data( $args['non_next_gen_thumb_size'] );
802
803 if ( ! $data ) {
804 // We haven’t tried to optimize the non-next-gen size yet.
805 return $args['response'];
806 }
807
808 if ( ! empty( $data['optimized_size'] ) ) {
809 // The non-next-gen size is optimized, we know the file size.
810 $non_next_gen_file_size = $data['optimized_size'];
811 } else {
812 // The non-next-gen size is "already optimized" or "error": grab the file size directly from the file.
813 $non_next_gen_file_size = $this->filesystem->size( $args['non_next_gen_file_path'] );
814 }
815
816 if ( ! $non_next_gen_file_size || $non_next_gen_file_size > $args['response']->new_size ) {
817 // The new next-gen file is lighter.
818 return $args['response'];
819 }
820
821 // The new next-gen file is heavier than the non-next-gen file: delete it and return an error.
822 $this->filesystem->delete( $args['file']->get_path() );
823
824 return new WP_Error(
825 'next_gen_heavy',
826 sprintf(
827 /* translators: %s is a size name. */
828 __( 'The Next-Gen version of the size %s is heavier than its non-next-gen version.', 'imagify' ),
829 '<code>' . esc_html( $args['non_next_gen_thumb_size'] ) . '</code>'
830 )
831 );
832 }
833
834 /**
835 * We just created a non-next-gen version:
836 * Check if its next-gen version file is lighter than this one.
837 */
838 $next_gen_size = $args['non_next_gen_thumb_size'] . $args['next_gen_format'];
839 $next_gen_file_size = $this->get_data()->get_size_data( $next_gen_size, 'optimized_size' );
840
841 if ( property_exists( $args['response'], 'message' ) || ! $next_gen_file_size || $next_gen_file_size < $args['response']->new_size ) {
842 // The next-gen file is lighter than this one.
843 return $args['response'];
844 }
845
846 // The new optimized file is lighter than the next-gen file: delete the next-gen file and store an error.
847 $next_gen_path = $args['file']->get_path_to_nextgen( $args['next_gen_format'] );
848
849 if ( $next_gen_path && $this->filesystem->is_writable( $next_gen_path ) ) {
850 $this->filesystem->delete( $next_gen_path );
851 }
852
853 $next_gen_response = new WP_Error(
854 'next_gen_heavy',
855 sprintf(
856 /* translators: %s is a size name. */
857 __( 'The Next-Gen version of the size %s is heavier than its non-next-gen version.', 'imagify' ),
858 '<code>' . esc_html( $args['non_next_gen_thumb_size'] ) . '</code>'
859 )
860 );
861
862 $this->update_size_optimization_data( $next_gen_response, $next_gen_size, $args['optimization_level'] );
863
864 return $args['response'];
865 }
866
867 /**
868 * Restore the media files from the backup file.
869 *
870 * @since 1.9
871 *
872 * @return bool|WP_Error True on success. A WP_Error instance on failure.
873 */
874 public function restore() {
875 if ( ! $this->is_valid() ) {
876 return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
877 }
878
879 $media = $this->get_media();
880
881 if ( ! $media->is_supported() ) {
882 return new WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) );
883 }
884
885 if ( ! $media->has_backup() ) {
886 return new WP_Error( 'no_backup', __( 'This media has no backup file.', 'imagify' ) );
887 }
888
889 if ( $this->is_locked() ) {
890 return new WP_Error( 'media_locked', __( 'This media is already being processed.', 'imagify' ) );
891 }
892
893 $this->lock( 'restoring' );
894
895 $backup_path = $media->get_backup_path();
896 $original_path = $media->get_raw_original_path();
897
898 if ( $backup_path === $original_path ) {
899 // Uh?!
900 $this->unlock();
901 return new WP_Error( 'same_path', __( 'Image path and backup path are identical.', 'imagify' ) );
902 }
903
904 $dest_dir = $this->filesystem->dir_path( $original_path );
905
906 if ( ! $this->filesystem->exists( $dest_dir ) ) {
907 $this->filesystem->make_dir( $dest_dir );
908 }
909
910 $dest_file_is_writable = ! $this->filesystem->exists( $original_path ) || $this->filesystem->is_writable( $original_path );
911
912 if ( ! $dest_file_is_writable || ! $this->filesystem->is_writable( $dest_dir ) ) {
913 $this->unlock();
914 return new WP_Error( 'destination_not_writable', __( 'The image to replace is not writable.', 'imagify' ) );
915 }
916
917 // Get some data before doing anything.
918 $data = $this->get_data()->get_optimization_data();
919 $files = $media->get_media_files();
920
921 /**
922 * Fires before restoring a media.
923 * Return a WP_Error object to prevent the restoration.
924 *
925 * @since 1.9
926 *
927 * @param null|WP_Error $response Null by default. Return a WP_Error object to prevent optimization.
928 * @param ProcessInterface $process Instance of this process.
929 */
930 $response = apply_filters( 'imagify_before_restore_media', null, $this );
931
932 if ( ! is_wp_error( $response ) ) {
933 // Create the original image from the backup.
934 $response = $this->filesystem->copy( $backup_path, $original_path, true );
935
936 if ( ! $response ) {
937 // Failure.
938 $response = new WP_Error( 'copy_failed', __( 'The backup file could not be copied over the optimized one.', 'imagify' ) );
939 } else {
940 // Backup successfully copied.
941 $this->filesystem->chmod_file( $original_path );
942
943 // Remove old optimization data.
944 $this->get_data()->delete_optimization_data();
945
946 if ( $media->is_image() ) {
947 // Restore the original dimensions in the database.
948 $media->update_dimensions();
949
950 // Delete the WebP version.
951 $this->delete_nextgen_file( $original_path );
952
953 // Restore the thumbnails.
954 $response = $this->restore_thumbnails();
955 }
956 }
957 }
958
959 /**
960 * Fires after restoring a media.
961 *
962 * @since 1.9
963 *
964 * @param ProcessInterface $process Instance of this process.
965 * @param bool|WP_Error $response The result of the operation: true on success, a WP_Error object on failure.
966 * @param array $files The list of files, before restoring them.
967 * @param array $data The optimization data, before deleting it.
968 */
969 do_action( 'imagify_after_restore_media', $this, $response, $files, $data );
970
971 $this->unlock();
972
973 return $response;
974 }
975
976 /**
977 * Restore the thumbnails.
978 *
979 * @since 1.9
980 *
981 * @return bool|WP_Error True on success. A WP_Error instance on failure.
982 */
983 protected function restore_thumbnails() {
984 $media = $this->get_media();
985
986 /**
987 * Delete the next-gen versions.
988 * If the full size file and the original file are not the same, the full size is considered like a thumbnail.
989 * In that case we must also delete the next-gen file associated to the full size.
990 */
991 $keep_full_next_gen = $media->get_raw_original_path() === $media->get_raw_fullsize_path();
992 $this->delete_nextgen_files( $keep_full_next_gen );
993
994 // Generate new thumbnails.
995 return $media->generate_thumbnails();
996 }
997
998 /**
999 * Delete the backup file.
1000 *
1001 * @since 1.9
1002 */
1003 public function delete_backup() {
1004 if ( ! $this->is_valid() ) {
1005 return;
1006 }
1007
1008 $backup_path = $this->get_media()->get_backup_path();
1009
1010 if ( $backup_path ) {
1011 $this->filesystem->delete( $backup_path );
1012
1013 // Check for the -scaled version in the backup.
1014 $scaled_backup_path = preg_replace( '/(\.)([^\.]+)$/', '-scaled.$2', $backup_path );
1015 if ( $this->filesystem->exists( $scaled_backup_path ) ) {
1016 // Delete the -scaled version from the backup.
1017 $this->filesystem->delete( $scaled_backup_path );
1018 }
1019 }
1020 }
1021
1022 /**
1023 * Create a temporary copy of a size file.
1024 *
1025 * If we need to create a next-gen version, we must create it from an unoptimized image.
1026 * The full size is always optimized before the next-gen version creation, and in some cases it’s the same for the thumbnails.
1027 * Then we use the backup file to create temporary files.
1028 *
1029 * @since 1.9
1030 *
1031 * @param string $size The image size name.
1032 * @param array $sizes A list of thumbnail sizes being optimized.
1033 *
1034 * @return bool True if the file exists/is created. False on failure.
1035 */
1036 protected function create_temporary_copy( $size, $sizes = null ) {
1037 $media = $this->get_media();
1038
1039 if ( ! isset( $sizes ) ) {
1040 $sizes = $media->get_media_files();
1041 }
1042
1043 if ( empty( $sizes[ $size ] ) ) {
1044 // What?
1045 return false;
1046 }
1047
1048 $tmp_path = $this->get_temporary_copy_path( $size, $sizes );
1049
1050 if ( $tmp_path && $this->filesystem->exists( $tmp_path ) ) {
1051 // The temporary file already exists.
1052 return true;
1053 }
1054
1055 $tmp_file = new File( $tmp_path );
1056
1057 if ( ! $tmp_file->is_image() ) {
1058 // The file is not an image.
1059 return false;
1060 }
1061
1062 if ( ! $tmp_file->is_supported( $media->get_allowed_mime_types() ) ) {
1063 // The file is not supported.
1064 return false;
1065 }
1066
1067 /**
1068 * Use the backup file as source.
1069 */
1070 $backup_path = $media->get_backup_path();
1071
1072 if ( ! $backup_path ) {
1073 // No backup, no hope for you.
1074 return false;
1075 }
1076
1077 /**
1078 * In all cases we must make a copy of the backup file, and not use the backup directly:
1079 * sometimes the backup image does not have a valid file extension (yes I’m looking at you NextGEN Gallery).
1080 */
1081 $copied = $this->filesystem->copy( $backup_path, $tmp_path, true );
1082
1083 if ( ! $copied ) {
1084 return false;
1085 }
1086
1087 if ( 'full' === $size ) {
1088 /**
1089 * We create a copy of the backup to be able to create a next-gen version from it.
1090 * That means the optimization process will resize the file if needed, so there is nothing more to do here.
1091 */
1092 return true;
1093 }
1094
1095 // We need to create a thumbnail from it.
1096 $size_data = $sizes[ $size ];
1097 $context_sizes = $media->get_context_instance()->get_thumbnail_sizes();
1098
1099 if ( ! empty( $context_sizes[ $size ] ) ) {
1100 // Not a dynamic size, yay!
1101 $size_data = array_merge( $size_data, $context_sizes[ $size ] );
1102 }
1103
1104 if ( empty( $size_data['path'] ) ) {
1105 // Should not happen.
1106 return false;
1107 }
1108
1109 if ( ! isset( $size_data['crop'] ) ) {
1110 /**
1111 * In case of a dynamic thumbnail we don’t know if the image must be croped or resized.
1112 *
1113 * @since 1.9
1114 *
1115 * @param bool $crop True to crop the thumbnail, false to resize. Null by default.
1116 * @param string $size Name of the thumbnail size.
1117 * @param array $size_data Data of the thumbnail being processed. Contains at least 'width', 'height', and 'path'.
1118 * @param MediaInterface $media The MediaInterface instance corresponding to the image being processed.
1119 */
1120 $crop = apply_filters( 'imagify_crop_thumbnail', null, $size, $size_data, $media );
1121
1122 if ( null !== $crop ) {
1123 $size_data['crop'] = (bool) $crop;
1124 }
1125 }
1126
1127 if ( ! isset( $size_data['crop'] ) ) {
1128 // We don't have the 'crop' data in that case: let’s try to guess it.
1129 if ( ! $size_data['height'] || ! $size_data['width'] ) {
1130 // One of the size dimensions is 0, that means crop is probably disabled.
1131 $size_data['crop'] = false;
1132 } else {
1133 if ( ! $this->filesystem->exists( $size_data['path'] ) ) {
1134 // Screwed.
1135 return false;
1136 }
1137
1138 $thumb_dimensions = $this->filesystem->get_image_size( $size_data['path'] );
1139
1140 if ( ! $thumb_dimensions || ! $thumb_dimensions['width'] || ! $thumb_dimensions['height'] ) {
1141 // ( ; Đ” ; )
1142 return false;
1143 }
1144
1145 // Compare dimensions.
1146 $new_height = $thumb_dimensions['width'] * $size_data['height'] / $size_data['width'];
1147 // If the difference is > to 1px, let's assume that crop is enabled.
1148 $size_data['crop'] = abs( $thumb_dimensions['height'] - $new_height ) > 1;
1149 }
1150 }
1151
1152 $resized = $tmp_file->create_thumbnail( [
1153 'path' => $tmp_path,
1154 'width' => $size_data['width'],
1155 'height' => $size_data['height'],
1156 'crop' => $size_data['crop'],
1157 'adjust_filename' => false,
1158 ] );
1159
1160 if ( is_wp_error( $resized ) ) {
1161 return false;
1162 }
1163
1164 // Make sure the new file has the expected name.
1165 $new_tmp_path = $this->filesystem->dir_path( $tmp_path ) . $resized['file'];
1166
1167 if ( $new_tmp_path === $tmp_path ) {
1168 return true;
1169 }
1170
1171 return $this->filesystem->move( $new_tmp_path, $tmp_path, true );
1172 }
1173
1174 /**
1175 * Get the path to a temporary copy of a size file.
1176 *
1177 * @since 1.9
1178 *
1179 * @param string $size The image size name.
1180 * @param array $sizes A list of thumbnail sizes being optimized.
1181 *
1182 * @return string|bool An image path. False on failure.
1183 */
1184 protected function get_temporary_copy_path( $size, $sizes = null ) {
1185 if ( 'full' === $size ) {
1186 $path = $this->get_media()->get_raw_fullsize_path();
1187 } else {
1188 if ( ! isset( $sizes ) ) {
1189 $sizes = $this->get_media()->get_media_files();
1190 }
1191
1192 $path = ! empty( $sizes[ $size ]['path'] ) ? $sizes[ $size ]['path'] : false;
1193 }
1194
1195 if ( ! $path ) {
1196 return false;
1197 }
1198
1199 $info = $this->filesystem->path_info( $path );
1200
1201 if ( ! $info['file_base'] ) {
1202 return false;
1203 }
1204
1205 return $info['dir_path'] . $info['file_base'] . static::TMP_SUFFIX . '.' . $info['extension'];
1206 }
1207
1208 /**
1209 * Maybe resize an image.
1210 *
1211 * @since 1.9
1212 *
1213 * @param string $size The size name.
1214 * @param File $file A File instance.
1215 *
1216 * @return array|WP_Error A WP_Error instance on failure, an array on success as follow: {
1217 * @type bool $resized True when the image has been resized.
1218 * @type bool $backuped True when the image has been backuped.
1219 * @type int $file_size The file size in bytes.
1220 * }
1221 */
1222 public function maybe_resize( $size, $file ) {
1223 if ( ! $this->can_resize( $size, $file ) ) {
1224 // This file should not be resized.
1225 return [
1226 'resized' => false,
1227 'backuped' => false,
1228 'file_size' => 0,
1229 ];
1230 }
1231
1232 $dimensions = $file->get_dimensions();
1233
1234 if ( ! $dimensions['width'] ) {
1235 // Could not get the image dimensions.
1236 return new WP_Error(
1237 'no_dimensions',
1238 sprintf(
1239 /* translators: %s is an error message. */
1240 __( 'Resizing failed: %s', 'imagify' ),
1241 __( 'Imagify could not get the image dimensions.', 'imagify' )
1242 )
1243 );
1244 }
1245
1246 $media = $this->get_media();
1247 $resize_width = $media->get_context_instance()->get_resizing_threshold();
1248
1249 if ( $resize_width >= $dimensions['width'] ) {
1250 // No need to resize.
1251 return [
1252 'resized' => false,
1253 'backuped' => false,
1254 'file_size' => 0,
1255 ];
1256 }
1257
1258 $resized_path = $file->resize( $dimensions, $resize_width );
1259
1260 if ( is_wp_error( $resized_path ) ) {
1261 // The resizement failed.
1262 return new WP_Error(
1263 'resize_failure',
1264 sprintf(
1265 /* translators: %s is an error message. */
1266 __( 'Resizing failed: %s', 'imagify' ),
1267 $resized_path->get_error_message()
1268 )
1269 );
1270 }
1271
1272 if ( $this->can_backup( $size ) ) {
1273 $source = 'full' === $size ? $media->get_original_path() : null;
1274 $backuped = $file->backup( $media->get_raw_backup_path(), $source );
1275
1276 if ( is_wp_error( $backuped ) ) {
1277 // The backup failed.
1278 return new WP_Error(
1279 'backup_failure',
1280 sprintf(
1281 /* translators: %s is an error message. */
1282 __( 'Backup failed: %s', 'imagify' ),
1283 $backuped->get_error_message()
1284 )
1285 );
1286 }
1287 } else {
1288 $backuped = false;
1289 }
1290
1291 $file_size = (int) $this->filesystem->size( $file->get_path() );
1292 $resized = $this->filesystem->move( $resized_path, $file->get_path(), true );
1293
1294 if ( ! $resized ) {
1295 // The resizement failed.
1296 return new WP_Error(
1297 'resize_move_failure',
1298 __( 'The image could not be replaced by the resized one.', 'imagify' )
1299 );
1300 }
1301
1302 // Store the new dimensions.
1303 $media->update_dimensions();
1304
1305 return [
1306 'resized' => true,
1307 'backuped' => $backuped,
1308 'file_size' => $file_size,
1309 ];
1310 }
1311
1312 /**
1313 * Tell if a size should be resized.
1314 *
1315 * @since 1.9
1316 *
1317 * @param string $size The size name.
1318 * @param File $file A File instance.
1319 *
1320 * @return bool
1321 */
1322 protected function can_resize( $size, $file ) {
1323 if ( ! $this->is_valid() ) {
1324 return false;
1325 }
1326
1327 if (
1328 'full' !== $size
1329 &&
1330 (
1331 'full' . static::WEBP_SUFFIX !== $size
1332 ||
1333 'full' . static::AVIF_SUFFIX !== $size
1334 )
1335 ) {
1336 // We resize only the main file and its next-gen version.
1337 return false;
1338 }
1339
1340 if ( ! $file->is_image() ) {
1341 return false;
1342 }
1343
1344 return $this->get_media()->get_context_instance()->can_resize();
1345 }
1346
1347 /**
1348 * Tell if a size should be backuped.
1349 *
1350 * @since 1.9
1351 *
1352 * @param string $size The size name.
1353 *
1354 * @return bool
1355 */
1356 protected function can_backup( $size ) {
1357 if ( ! $this->is_valid() ) {
1358 return false;
1359 }
1360
1361 if ( 'full' !== $size ) {
1362 // We backup only the main file.
1363 return false;
1364 }
1365
1366 return $this->get_media()->get_context_instance()->can_backup();
1367 }
1368
1369 /**
1370 * Get mime type
1371 *
1372 * @param string $format nextgen image format.
1373 */
1374 private function get_mime_type( $format ) {
1375 $mime_types = [
1376 'avif' => 'image/avif',
1377 'webp' => 'image/webp',
1378 ];
1379
1380 return isset( $mime_types[ $format ] ) ? $mime_types[ $format ] : false;
1381 }
1382
1383 /**
1384 * Delete the next gen format images.
1385 * This doesn't delete the related optimization data.
1386 *
1387 * @since 2.2
1388 *
1389 * @param bool $keep_full Set to true to keep the full size.
1390 * @param bool $all_next_gen True: will delete every next-gen format. False: will delete only the current enabled format.
1391 *
1392 * @return bool|WP_Error True on success. A WP_Error object on failure.
1393 */
1394 public function delete_nextgen_files( $keep_full = false, $all_next_gen = false ) {
1395 if ( ! $this->is_valid() ) {
1396 return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
1397 }
1398
1399 $media = $this->get_media();
1400
1401 if ( ! $media->is_image() ) {
1402 return new WP_Error( 'media_not_an_image', __( 'This media is not an image.', 'imagify' ) );
1403 }
1404
1405 $files = $media->get_media_files();
1406
1407 if ( $keep_full ) {
1408 unset( $files['full'] );
1409 }
1410
1411 if ( ! $files ) {
1412 return true;
1413 }
1414
1415 $error_count = 0;
1416
1417 foreach ( $files as $file ) {
1418 if ( 0 === strpos( $file['mime-type'], 'image/' ) ) {
1419 $deleted = $this->delete_nextgen_file( $file['path'], $all_next_gen );
1420
1421 if ( is_wp_error( $deleted ) ) {
1422 ++$error_count;
1423 }
1424 }
1425 }
1426
1427 if ( $error_count ) {
1428 return new WP_Error(
1429 'files_not_deleted',
1430 sprintf(
1431 /* translators: %s is a formatted number, don’t use %d. */
1432 _n( '%s file could not be deleted.', '%s files could not be deleted.', $error_count, 'imagify' ),
1433 number_format_i18n( $error_count )
1434 )
1435 );
1436 }
1437
1438 return true;
1439 }
1440
1441 /**
1442 * Delete a next gen format image, given its non-next-gen version's path.
1443 * This doesn't delete the related optimization data.
1444 *
1445 * @since 2.2
1446 *
1447 * @param string $file_path Path to the non-next-gen file.
1448 * @param bool $all_next_gen True: will delete every next-gen format. False: will delete only the current enabled format.
1449 *
1450 * @return void|WP_Error A \WP_Error object on failure.
1451 */
1452 protected function delete_nextgen_file( $file_path, $all_next_gen = false ) {
1453 if ( ! $file_path ) {
1454 return new WP_Error( 'no_path', __( 'Path to non-next-gen file not provided.', 'imagify' ) );
1455 }
1456
1457 $next_gen_file = new File( $file_path );
1458 $formats = $this->extensions;
1459
1460 if ( ! $all_next_gen ) {
1461 $formats = imagify_nextgen_images_formats();
1462 }
1463 // Delete next-gen images.
1464 foreach ( $formats as $extension ) {
1465 $path = $next_gen_file->get_path_to_nextgen( $extension );
1466
1467 if ( ! $path ) {
1468 continue;
1469 }
1470
1471 $this->delete_file( $path );
1472 }
1473 }
1474
1475 /**
1476 * Delete a next gen format image, given its non-next-gen version's path.
1477 *
1478 * @param string $next_gen_path Path to the non-next-gen file.
1479 *
1480 * @return bool|WP_Error True on success. A WP_Error object on failure.
1481 */
1482 protected function delete_file( string $next_gen_path ) {
1483 if ( empty( $next_gen_path ) ) {
1484 return new WP_Error( 'no_$next_gen_path', __( 'Could not get the path to the Next-Gen format file.', 'imagify' ) );
1485 }
1486
1487 if ( ! $this->filesystem->exists( $next_gen_path ) ) {
1488 return true;
1489 }
1490
1491 if ( ! $this->filesystem->is_writable( $next_gen_path ) ) {
1492 return new WP_Error(
1493 'file_not_writable',
1494 sprintf(
1495 /* translators: %s is a file path. */
1496 __( 'The file %s does not seem to be writable.', 'imagify' ),
1497 '<code>' . esc_html( $this->filesystem->make_path_relative( $next_gen_path ) ) . '</code>'
1498 )
1499 );
1500 }
1501
1502 if ( ! $this->filesystem->is_file( $next_gen_path ) ) {
1503 return new WP_Error(
1504 'not_a_file',
1505 sprintf(
1506 /* translators: %s is a file path. */
1507 __( 'This does not seem to be a file: %s.', 'imagify' ),
1508 '<code>' . esc_html( $this->filesystem->make_path_relative( $next_gen_path ) ) . '</code>'
1509 )
1510 );
1511 }
1512
1513 $deleted = $this->filesystem->delete( $next_gen_path, false, 'f' );
1514
1515 if ( ! $deleted ) {
1516 return new WP_Error(
1517 'file_not_deleted',
1518 sprintf(
1519 /* translators: %s is a file path. */
1520 __( 'The file %s could not be deleted.', 'imagify' ),
1521 '<code>' . esc_html( $this->filesystem->make_path_relative( $next_gen_path ) ) . '</code>'
1522 )
1523 );
1524 }
1525
1526 return true;
1527 }
1528
1529 /**
1530 * Gives the next-gen image format we are processing.
1531 *
1532 * @return string Current format we are targeting.
1533 */
1534 public function get_current_format() {
1535 return $this->get_option( 'convert_to_avif' ) ? static::AVIF_SUFFIX : static::WEBP_SUFFIX;
1536 }
1537
1538 /**
1539 * Tell if a thumbnail size is an "Imagify Next-Gen" size.
1540 *
1541 * @since 1.9
1542 * @since 2.2 addition of the format parameter.
1543 *
1544 * @param string $size_name The size name.
1545 *
1546 * @return string|bool The unsuffixed name of the size if next-gen. False if not next-gen.
1547 */
1548 public function is_size_next_gen( $size_name ) {
1549 $formats = imagify_nextgen_images_formats();
1550
1551 foreach ( $formats as $format ) {
1552 $suffix = preg_quote( $this->get_suffix_from_format( $format ), '/' );
1553
1554 if ( preg_match( '/^(?<size>.+)' . $suffix . '$/', $size_name, $matches ) ) {
1555 return $matches['size'];
1556 }
1557 }
1558
1559 return false;
1560 }
1561
1562 /**
1563 * Get suffix from format.
1564 *
1565 * @param string $format Format extension of next-gen image.
1566 * @return string
1567 */
1568 private function get_suffix_from_format( string $format ): string {
1569 $suffixes = [
1570 'avif' => static::AVIF_SUFFIX,
1571 'webp' => static::WEBP_SUFFIX,
1572 ];
1573
1574 return $suffixes[ $format ];
1575 }
1576
1577 /**
1578 * Tell if the media has a next gen format.
1579 *
1580 * @since 2.2
1581 *
1582 * @return bool
1583 */
1584 public function has_next_gen() {
1585 if ( ! $this->is_valid() ) {
1586 return false;
1587 }
1588
1589 if ( ! $this->get_media()->is_image() ) {
1590 return false;
1591 }
1592
1593 $data = $this->get_data()->get_optimization_data();
1594
1595 if ( empty( $data['sizes'] ) ) {
1596 return false;
1597 }
1598
1599 $needle = $this->format . '";a:4:{s:7:"success";b:1;';
1600 $data = maybe_serialize( $data['sizes'] );
1601
1602 return is_string( $data ) && strpos( $data, $needle );
1603 }
1604
1605 /**
1606 * Tell if the media has all Next-Gen versions.
1607 *
1608 * @return bool
1609 */
1610 public function is_full_next_gen() {
1611 if ( ! $this->is_valid() ) {
1612 return false;
1613 }
1614
1615 if ( ! $this->get_media()->is_image() ) {
1616 return false;
1617 }
1618
1619 $data = $this->get_data()->get_optimization_data();
1620
1621 $sizes = $data['sizes'];
1622
1623 if ( empty( $sizes ) ) {
1624 return false;
1625 }
1626
1627 $keys = array_keys( $sizes );
1628 $non_next_gen_keys = array_values(array_filter($keys, function ( $key ) {
1629 return strpos( $key, $this->format ) === false;
1630 }));
1631
1632 return array_reduce($non_next_gen_keys, function ( $is_fully, $key ) use ( $sizes ) {
1633 return key_exists( $key . $this->format, $sizes ) && $is_fully;
1634 }, true);
1635 }
1636
1637 /**
1638 * Tell if a Next-Gen version can be created for the given file.
1639 * Make sure the file is an image before using this method.
1640 *
1641 * @since 1.9.5
1642 *
1643 * @param string $file_path Path to the file.
1644 *
1645 * @return bool
1646 */
1647 public function can_create_next_gen_version( $file_path ) {
1648 if ( ! $file_path ) {
1649 return false;
1650 }
1651
1652 $can = apply_filters_deprecated( 'imagify_pre_can_create_webp_version', array( null, $file_path ), '2.2', 'imagify_pre_can_create_next_gen_version' );
1653
1654 /**
1655 * Tell if a next-gen version can be created for the given file.
1656 * The file is an image.
1657 *
1658 * @since 1.9.5
1659 *
1660 * @param bool $can True to create a next-gen version, false otherwise. Null by default.
1661 * @param string $file_path Path to the file.
1662 */
1663 $can = apply_filters( 'imagify_pre_can_create_next_gen_version', $can, $file_path );
1664
1665 if ( isset( $can ) ) {
1666 return (bool) $can;
1667 }
1668
1669 $is_animated_gif = $this->filesystem->is_animated_gif( $file_path );
1670
1671 if ( is_bool( $is_animated_gif ) ) {
1672 // Ok if it’s not an animated gif.
1673 return ! $is_animated_gif;
1674 }
1675
1676 // At this point $is_animated_gif is null, which means the file cannot be read (yet).
1677 return true;
1678 }
1679
1680 /**
1681 * Generate next-gen images if they are missing.
1682 *
1683 * @since 1.9
1684 *
1685 * @return bool|WP_Error True if successfully launched. A WP_Error instance on failure.
1686 */
1687 public function generate_nextgen_versions() {
1688 if ( ! $this->is_valid() ) {
1689 return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
1690 }
1691
1692 $media = $this->get_media();
1693
1694 if ( ! $media->is_image() ) {
1695 return new WP_Error( 'no_next_gen', __( 'This media is not an image and cannot be converted to next-gen format.', 'imagify' ) );
1696 }
1697
1698 if ( ! $media->has_backup() ) {
1699 return new WP_Error( 'no_backup', __( 'This media has no backup file.', 'imagify' ) );
1700 }
1701
1702 $data = $this->get_data();
1703
1704 if ( ! $data->is_optimized() && ! $data->is_already_optimized() ) {
1705 return new WP_Error( 'not_optimized', __( 'This media has not been optimized by Imagify yet.', 'imagify' ) );
1706 }
1707
1708 if ( $this->has_next_gen() ) {
1709 return new WP_Error( 'has_next_gen', __( 'This media already has next-gen versions.', 'imagify' ) );
1710 }
1711
1712 $files = $media->get_media_files();
1713 $sizes = [];
1714 $args = [
1715 'hook_suffix' => 'generate_nextgen_versions',
1716 ];
1717
1718 foreach ( $files as $size_name => $file ) {
1719 $formats = imagify_nextgen_images_formats();
1720
1721 foreach ( $formats as $format ) {
1722 if ( 'avif' === $format ) {
1723 $format_suffix = static::AVIF_SUFFIX;
1724 } elseif ( 'webp' === $format ) {
1725 $format_suffix = static::WEBP_SUFFIX;
1726 }
1727
1728 if ( $this->get_mime_type( $format ) === $files[ $size_name ]['mime-type'] ) {
1729 continue;
1730 }
1731
1732 array_unshift( $sizes, $size_name . $format_suffix );
1733 }
1734 }
1735
1736 if ( ! $sizes ) {
1737 return new WP_Error( 'no_sizes', __( 'This media does not have files that can be converted to next-gen format.', 'imagify' ) );
1738 }
1739
1740 $optimization_level = $data->get_optimization_level();
1741
1742 // Optimize.
1743 return $this->optimize_sizes( $sizes, $optimization_level, $args );
1744 }
1745
1746 /**
1747 * Tell if a process is running for this media.
1748 *
1749 * @since 1.9
1750 *
1751 * @return string|bool The action if locked ('optimizing' or 'restoring'). False if not locked.
1752 */
1753 public function is_locked() {
1754 $name = $this->get_lock_name();
1755
1756 if ( ! $name ) {
1757 return false;
1758 }
1759
1760 $callback = $this->get_media()->get_context_instance()->is_network_wide() ? 'get_site_transient' : 'get_transient';
1761 $action = call_user_func( $callback, $name );
1762
1763 if ( ! $action ) {
1764 return false;
1765 }
1766
1767 return $this->validate_lock_action( $action );
1768 }
1769
1770 /**
1771 * Set the running status to "running" for 10 minutes.
1772 *
1773 * @since 1.9
1774 *
1775 * @param string $action The action performed behind this lock: 'optimizing' or 'restoring'.
1776 */
1777 public function lock( $action = 'optimizing' ) {
1778 $name = $this->get_lock_name();
1779
1780 if ( ! $name ) {
1781 return;
1782 }
1783
1784 $action = $this->validate_lock_action( $action );
1785 $media = $this->get_media();
1786 $callback = $media->get_context_instance()->is_network_wide() ? 'set_site_transient' : 'set_transient';
1787
1788 call_user_func( $callback, $name, $action, 10 * MINUTE_IN_SECONDS );
1789 }
1790
1791 /**
1792 * Unset the running status.
1793 *
1794 * @since 1.9
1795 */
1796 public function unlock() {
1797 $name = $this->get_lock_name();
1798
1799 if ( ! $name ) {
1800 return false;
1801 }
1802
1803 $callback = $this->get_media()->get_context_instance()->is_network_wide() ? 'delete_site_transient' : 'delete_transient';
1804
1805 call_user_func( $callback, $name );
1806 }
1807
1808 /**
1809 * Get the name of the transient that stores the lock status.
1810 *
1811 * @since 1.9
1812 *
1813 * @return string|bool The name on success. False on failure.
1814 */
1815 protected function get_lock_name() {
1816 $media = $this->get_media();
1817
1818 if ( ! $media ) {
1819 return false;
1820 }
1821
1822 /**
1823 * Note that the site transient used by WP Background is named '*_process_lock'.
1824 * That would give something like 'imagify_optimize_media_process_lock' for the optimization process, while here it would be 'imagify_wp_42_process_locked'.
1825 */
1826 return sprintf( static::LOCK_NAME, $media->get_context(), $media->get_id() );
1827 }
1828
1829 /**
1830 * Validate the lock action.
1831 *
1832 * @since 1.9
1833 *
1834 * @param string $action The action performed behind this lock: 'optimizing' or 'restoring'.
1835 * @return string The valid action.
1836 */
1837 protected function validate_lock_action( $action ) {
1838 switch ( $action ) {
1839 case 'restore':
1840 case 'restoring':
1841 $action = 'restoring';
1842 break;
1843
1844 default:
1845 $action = 'optimizing';
1846 }
1847
1848 return $action;
1849 }
1850
1851 /**
1852 * Tell if a size already has optimization data.
1853 *
1854 * @since 1.9
1855 *
1856 * @param string $size The size name.
1857 * @return bool
1858 */
1859 public function size_has_optimization_data( $size ) {
1860 $data = $this->get_data()->get_optimization_data();
1861
1862 return ! empty( $data['sizes'][ $size ] );
1863 }
1864
1865 /**
1866 * Update the optimization data for a size.
1867 *
1868 * @since 1.9
1869 * @since 2.2 - Addition of the format in the call.
1870 *
1871 * @param object $response The API response.
1872 * @param string $size The size name.
1873 * @param int $level The optimization level (0=normal, 1=aggressive, 2=ultra).
1874 *
1875 * @return array {
1876 * The optimization data.
1877 *
1878 * @type int $level The optimization level.
1879 * @type string $status The status: 'success', 'already_optimized', 'error'.
1880 * @type bool $success True if successfully optimized. False on error or if already optimized.
1881 * @type string $error An error message.
1882 * @type int $original_size The weight of the file, before optimization.
1883 * @type int $optimized_size The weight of the file, once optimized.
1884 * }
1885 */
1886 public function update_size_optimization_data( $response, $size, $level ) {
1887 $disabled = false;
1888 $data = $this->data_format;
1889
1890 $data['level'] = is_numeric( $level ) ? (int) $level : $this->get_option( 'optimization_level' );
1891
1892 if ( is_wp_error( $response ) ) {
1893 /**
1894 * Error.
1895 */
1896 $disabled = 'unauthorized_size' === $response->get_error_code();
1897
1898 // Size data.
1899 $data['success'] = false;
1900 $data['error'] = $response->get_error_message();
1901
1902 // Status.
1903 if ( false !== strpos( $data['error'], 'This image is already compressed' ) ) {
1904 $data['status'] = 'already_optimized';
1905 } else {
1906 $data['status'] = 'error';
1907 }
1908 } else {
1909 /**
1910 * Success.
1911 */
1912 $response = (object) array_merge( [
1913 'original_size' => 0,
1914 'new_size' => 0,
1915 'percent' => 0,
1916 ], (array) $response );
1917
1918 // Status.
1919 $data['status'] = 'success';
1920 $data['error'] = null;
1921
1922 // Size data.
1923 $data['success'] = true;
1924 if ( property_exists( $response, 'message' ) ) {
1925 $data['message'] = imagify_translate_api_message( $response->message );
1926 }
1927 $data['original_size'] = $response->original_size;
1928 $data['optimized_size'] = $response->new_size;
1929 }
1930
1931 $_unauthorized = $disabled ? '_unauthorized' : '';
1932
1933 /**
1934 * Filter the optimization data.
1935 *
1936 * @since 1.9
1937 *
1938 * @param array $data {
1939 * The optimization data.
1940 *
1941 * @type int $level The optimization level.
1942 * @type string $status The status: 'success', 'already_optimized', 'error'.
1943 * @type bool $success True if successfully optimized. False on error or if already optimized.
1944 * @type string $error An error message.
1945 * @type int $original_size The weight of the file, before optimization.
1946 * @type int $optimized_size The weight of the file, once optimized.
1947 * }
1948 * @param object $response The API response.
1949 * @param string $size The size name.
1950 * @param int $level The optimization level.
1951 * @param object $media_data The DataInterface instance of the media.
1952 */
1953 $data = (array) apply_filters( "imagify{$_unauthorized}_file_optimization_data", $data, $response, $size, $level, $this->get_data() );
1954
1955 if ( property_exists( $response, 'message' ) ) {
1956 $size = str_replace( $this->format, '', $size );
1957 }
1958 // Store.
1959 $this->get_data()->update_size_optimization_data( $size, $data );
1960
1961 return $data;
1962 }
1963
1964 /**
1965 * Get a plugin’s option.
1966 *
1967 * @since 1.9
1968 *
1969 * @param string $option_name The option name.
1970 *
1971 * @return mixed
1972 */
1973 protected function get_option( $option_name ) {
1974 if ( isset( $this->options[ $option_name ] ) ) {
1975 return $this->options[ $option_name ];
1976 }
1977
1978 $this->options[ $option_name ] = get_imagify_option( $option_name );
1979
1980 return $this->options[ $option_name ];
1981 }
1982
1983 /**
1984 * Sanitize and validate an optimization level.
1985 * If not provided (false, null), fallback to the level set in the plugin's settings.
1986 *
1987 * @since 1.9
1988 *
1989 * @param mixed $optimization_level The optimization level.
1990 *
1991 * @return int
1992 */
1993 protected function sanitize_optimization_level( $optimization_level ) {
1994 if ( ! is_numeric( $optimization_level ) ) {
1995 if ( $this->get_option( 'lossless' ) ) {
1996 return 0;
1997 }
1998
1999 return $this->get_option( 'optimization_level' );
2000 }
2001
2002 return \Imagify_Options::get_instance()->sanitize_and_validate( 'optimization_level', $optimization_level );
2003 }
2004
2005 /**
2006 * Tell if the media has AVIF versions.
2007 *
2008 * @since 2.2
2009 *
2010 * @return bool
2011 */
2012 public function has_avif() {
2013 if ( ! $this->is_valid() ) {
2014 return false;
2015 }
2016
2017 if ( ! $this->get_media()->is_image() ) {
2018 return false;
2019 }
2020
2021 $data = $this->get_data()->get_optimization_data();
2022
2023 if ( empty( $data['sizes'] ) ) {
2024 return false;
2025 }
2026
2027 $needle = static::AVIF_SUFFIX . '";a:4:{s:7:"success";b:1;';
2028 $data = maybe_serialize( $data['sizes'] );
2029
2030 return is_string( $data ) && strpos( $data, $needle );
2031 }
2032 }
2033