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

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