PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 2.2.3.1
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v2.2.3.1
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
imagify / classes / Optimization / Process / AbstractProcess.php

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

2,045 lines 56.8 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 'extension_not_mime',
587 __( 'This file has an extension that does not match a mime type.', 'imagify' )
588 );
589 } elseif ( '' === $extension ) {
590 $response = new WP_Error(
591 'no_extension',
592 __( 'With no extension, this file cannot be optimized.', 'imagify' )
593 );
594 } elseif ( ! $extension ) {
595 $response = new WP_Error(
596 'extension_not_mime',
597 __( 'This file has an extension that does not match a mime type.', 'imagify' )
598 );
599 } else {
600 $response = new WP_Error(
601 'extension_not_supported',
602 sprintf(
603 /* translators: %s is a file extension. */
604 __( '%s cannot be optimized.', 'imagify' ),
605 '<code>' . esc_html( strtolower( $extension ) ) . '</code>'
606 )
607 );
608 }
609
610 if ( $path_is_temp ) {
611 $this->filesystem->delete( $path );
612 }
613
614 $this->update_size_optimization_data( $response, $size, $optimization_level );
615
616 return $response;
617 }
618
619 if ( $next_gen && ! $file->is_image() ) { // Bail out.
620 if ( $path_is_temp ) {
621 $this->filesystem->delete( $path );
622 }
623
624 $response = new WP_Error(
625 'no_next_gen',
626 __( 'This file is not an image and cannot be converted to Next-Gen format.', 'imagify' )
627 );
628
629 $this->update_size_optimization_data( $response, $size, $optimization_level );
630
631 return $response;
632 }
633
634 $is_disabled = ! empty( $sizes[ $thumb_size ]['disabled'] );
635
636 /**
637 * Fires before optimizing a file.
638 * Return a WP_Error object to prevent the optimization.
639 *
640 * @since 1.9
641 *
642 * @param null|WP_Error $response Null by default. Return a WP_Error object to prevent optimization.
643 * @param ProcessInterface $process The optimization process instance.
644 * @param File $file The file instance. If $webp is true, $file references the non-WebP file.
645 * @param string $thumb_size The media size.
646 * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
647 * @param bool $webp The image will be converted to WebP.
648 * @param bool $is_disabled Tell if this size is disabled from optimization.
649 */
650 $response = apply_filters( 'imagify_before_optimize_size', null, $this, $file, $thumb_size, $optimization_level, $next_gen, $is_disabled );
651
652 if ( ! is_wp_error( $response ) ) {
653 if ( $is_disabled ) {
654 // This size must not be optimized.
655 $response = new WP_Error(
656 'unauthorized_size',
657 sprintf(
658 /* translators: %s is a size name. */
659 __( 'The size %s is not authorized to be optimized. Update your Imagify settings if you want to optimize it.', 'imagify' ),
660 '<code>' . esc_html( $thumb_size ) . '</code>'
661 )
662 );
663 } elseif ( ! $this->filesystem->exists( $file->get_path() ) ) {
664 $response = new WP_Error(
665 'file_not_exists',
666 sprintf(
667 /* translators: %s is a file path. */
668 __( 'The file %s does not seem to exist.', 'imagify' ),
669 '<code>' . esc_html( $this->filesystem->make_path_relative( $file->get_path() ) ) . '</code>'
670 )
671 );
672 } elseif ( $next_gen && ! $this->can_create_next_gen_version( $file->get_path() ) ) {
673 $response = new WP_Error(
674 'is_animated_gif',
675 __( 'This file is an animated gif: since Imagify does not support animated WebP/AVIF, WebP/AVIF creation for animated gif is disabled.', 'imagify' )
676 );
677 } elseif ( ! $this->filesystem->is_writable( $file->get_path() ) ) {
678 $response = new WP_Error(
679 'file_not_writable',
680 sprintf(
681 /* translators: %s is a file path. */
682 __( 'The file %s does not seem to be writable.', 'imagify' ),
683 '<code>' . esc_html( $this->filesystem->make_path_relative( $file->get_path() ) ) . '</code>'
684 )
685 );
686 } else {
687 // Maybe resize the file.
688 $response = $this->maybe_resize( $thumb_size, $file );
689
690 $convert = '';
691
692 if ( $next_gen ) {
693 if ( strpos( $size, static::AVIF_SUFFIX ) ) {
694 $convert = 'avif';
695 } elseif ( strpos( $size, static::WEBP_SUFFIX ) ) {
696 $convert = 'webp';
697 }
698 }
699
700 if ( ! is_wp_error( $response ) ) {
701 // Resizing succeeded: optimize the file.
702 $response = $file->optimize( [
703 'backup' => ! $response['backuped'] && $this->can_backup( $size ),
704 'backup_path' => $media->get_raw_backup_path(),
705 'backup_source' => 'full' === $thumb_size ? $media->get_original_path() : null,
706 'optimization_level' => $optimization_level,
707 'convert' => $convert,
708 'keep_exif' => true,
709 'context' => $media->get_context(),
710 'original_size' => $response['file_size'],
711 ] );
712
713 $response = $this->compare_next_gen_file_size( [
714 'response' => $response,
715 'file' => $file,
716 'is_next_gen' => $next_gen,
717 'next_gen_format' => $convert,
718 'non_next_gen_thumb_size' => $thumb_size,
719 '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.
720 'optimization_level' => $optimization_level,
721 ] );
722
723 if ( property_exists( $response, 'message' ) ) {
724 $path_is_temp = false;
725 if ( $path !== $sizes[ $thumb_size ]['path'] ) {
726 $this->filesystem->delete( $path );
727 }
728 $path = $sizes[ $thumb_size ]['path'];
729 }
730 }
731 }
732 }
733
734 $data = $this->update_size_optimization_data( $response, $size, $optimization_level );
735
736 /**
737 * Fires after optimizing a file.
738 *
739 * @since 1.9
740 *
741 * @param ProcessInterface $process The optimization process instance.
742 * @param File $file The file instance.
743 * @param string $thumb_size The media size.
744 * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra).
745 * @param bool $webp The image was supposed to be converted to WebP.
746 * @param bool $is_disabled Tell if this size is disabled from optimization.
747 */
748 do_action( 'imagify_after_optimize_size', $this, $file, $thumb_size, $optimization_level, $next_gen, $is_disabled );
749
750 if ( ! $path_is_temp ) {
751 return $data;
752 }
753
754 // Delete the temporary copy.
755 $this->filesystem->delete( $path );
756
757 if ( is_wp_error( $response ) ) {
758 return $data;
759 }
760
761 // Rename the optimized file.
762 $destination_path = str_replace( static::TMP_SUFFIX . '.', '.', $file->get_path() );
763
764 $this->filesystem->move( $file->get_path(), $destination_path, true );
765
766 return $data;
767 }
768
769 /**
770 * 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.
771 *
772 * @since 2.2
773 *
774 * @param array $args {
775 * A list of mandatory arguments.
776 *
777 * @type \sdtClass|WP_Error $response Optimized image data. A WP_Error object on error.
778 * @type File $file The File instance of the file currently being optimized.
779 * @type bool $is_next_gen Tell if we're requesting a next-gen file.
780 * @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.
781 * @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.
782 * @type string $optimization_level The optimization level.
783 * }
784 *
785 * @return \sdtClass|WP_Error Optimized image data. A WP_Error object on error.
786 */
787 protected function compare_next_gen_file_size( $args ) {
788 static $keep_large_next_gen;
789
790 if ( ! isset( $keep_large_next_gen ) ) {
791 /**
792 * Allow to not store next-gen images that are larger than their non-next-gen version.
793 *
794 * @since 1.9.4
795 *
796 * @param bool $keep_large_next-gen Set to false if you prefer your visitors over your Pagespeed score. Default value is true.
797 */
798 $keep_large_next_gen = apply_filters( 'imagify_keep_large_next_gen', true );
799 }
800
801 if ( $keep_large_next_gen || is_wp_error( $args['response'] ) || ! $args['file']->is_image() ) {
802 return $args['response'];
803 }
804
805 // Optimization succeeded.
806 if ( ! property_exists( $args['response'], 'message' ) && $args['is_next_gen'] ) {
807 /**
808 * We just created a next-gen version:
809 * Check if it is lighter than the (maybe optimized) non-next-gen file.
810 */
811 $data = $this->get_data()->get_size_data( $args['non_next_gen_thumb_size'] );
812
813 if ( ! $data ) {
814 // We haven’t tried to optimize the non-next-gen size yet.
815 return $args['response'];
816 }
817
818 if ( ! empty( $data['optimized_size'] ) ) {
819 // The non-next-gen size is optimized, we know the file size.
820 $non_next_gen_file_size = $data['optimized_size'];
821 } else {
822 // The non-next-gen size is "already optimized" or "error": grab the file size directly from the file.
823 $non_next_gen_file_size = $this->filesystem->size( $args['non_next_gen_file_path'] );
824 }
825
826 if ( ! $non_next_gen_file_size || $non_next_gen_file_size > $args['response']->new_size ) {
827 // The new next-gen file is lighter.
828 return $args['response'];
829 }
830
831 // The new next-gen file is heavier than the non-next-gen file: delete it and return an error.
832 $this->filesystem->delete( $args['file']->get_path() );
833
834 return new WP_Error(
835 'next_gen_heavy',
836 sprintf(
837 /* translators: %s is a size name. */
838 __( 'The Next-Gen version of the size %s is heavier than its non-next-gen version.', 'imagify' ),
839 '<code>' . esc_html( $args['non_next_gen_thumb_size'] ) . '</code>'
840 )
841 );
842 }
843
844 /**
845 * We just created a non-next-gen version:
846 * Check if its next-gen version file is lighter than this one.
847 */
848 $next_gen_size = $args['non_next_gen_thumb_size'] . $args['next_gen_format'];
849 $next_gen_file_size = $this->get_data()->get_size_data( $next_gen_size, 'optimized_size' );
850
851 if ( property_exists( $args['response'], 'message' ) || ! $next_gen_file_size || $next_gen_file_size < $args['response']->new_size ) {
852 // The next-gen file is lighter than this one.
853 return $args['response'];
854 }
855
856 // The new optimized file is lighter than the next-gen file: delete the next-gen file and store an error.
857 $next_gen_path = $args['file']->get_path_to_nextgen( $args['next_gen_format'] );
858
859 if ( $next_gen_path && $this->filesystem->is_writable( $next_gen_path ) ) {
860 $this->filesystem->delete( $next_gen_path );
861 }
862
863 $next_gen_response = new WP_Error(
864 'next_gen_heavy',
865 sprintf(
866 /* translators: %s is a size name. */
867 __( 'The Next-Gen version of the size %s is heavier than its non-next-gen version.', 'imagify' ),
868 '<code>' . esc_html( $args['non_next_gen_thumb_size'] ) . '</code>'
869 )
870 );
871
872 $this->update_size_optimization_data( $next_gen_response, $next_gen_size, $args['optimization_level'] );
873
874 return $args['response'];
875 }
876
877 /**
878 * Restore the media files from the backup file.
879 *
880 * @since 1.9
881 *
882 * @return bool|WP_Error True on success. A WP_Error instance on failure.
883 */
884 public function restore() {
885 if ( ! $this->is_valid() ) {
886 return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
887 }
888
889 $media = $this->get_media();
890
891 if ( ! $media->is_supported() ) {
892 return new WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) );
893 }
894
895 if ( ! $media->has_backup() ) {
896 return new WP_Error( 'no_backup', __( 'This media has no backup file.', 'imagify' ) );
897 }
898
899 if ( $this->is_locked() ) {
900 return new WP_Error( 'media_locked', __( 'This media is already being processed.', 'imagify' ) );
901 }
902
903 $this->lock( 'restoring' );
904
905 $backup_path = $media->get_backup_path();
906 $original_path = $media->get_raw_original_path();
907
908 if ( $backup_path === $original_path ) {
909 // Uh?!
910 $this->unlock();
911 return new WP_Error( 'same_path', __( 'Image path and backup path are identical.', 'imagify' ) );
912 }
913
914 $dest_dir = $this->filesystem->dir_path( $original_path );
915
916 if ( ! $this->filesystem->exists( $dest_dir ) ) {
917 $this->filesystem->make_dir( $dest_dir );
918 }
919
920 $dest_file_is_writable = ! $this->filesystem->exists( $original_path ) || $this->filesystem->is_writable( $original_path );
921
922 if ( ! $dest_file_is_writable || ! $this->filesystem->is_writable( $dest_dir ) ) {
923 $this->unlock();
924 return new WP_Error( 'destination_not_writable', __( 'The image to replace is not writable.', 'imagify' ) );
925 }
926
927 // Get some data before doing anything.
928 $data = $this->get_data()->get_optimization_data();
929 $files = $media->get_media_files();
930
931 /**
932 * Fires before restoring a media.
933 * Return a WP_Error object to prevent the restoration.
934 *
935 * @since 1.9
936 *
937 * @param null|WP_Error $response Null by default. Return a WP_Error object to prevent optimization.
938 * @param ProcessInterface $process Instance of this process.
939 */
940 $response = apply_filters( 'imagify_before_restore_media', null, $this );
941
942 if ( ! is_wp_error( $response ) ) {
943 // Create the original image from the backup.
944 $response = $this->filesystem->copy( $backup_path, $original_path, true );
945
946 if ( ! $response ) {
947 // Failure.
948 $response = new WP_Error( 'copy_failed', __( 'The backup file could not be copied over the optimized one.', 'imagify' ) );
949 } else {
950 // Backup successfully copied.
951 $this->filesystem->chmod_file( $original_path );
952
953 // Remove old optimization data.
954 $this->get_data()->delete_optimization_data();
955
956 if ( $media->is_image() ) {
957 // Restore the original dimensions in the database.
958 $media->update_dimensions();
959
960 // Delete the WebP version.
961 $this->delete_nextgen_file( $original_path, true );
962
963 // Restore the thumbnails.
964 $response = $this->restore_thumbnails();
965 }
966 }
967 }
968
969 /**
970 * Fires after restoring a media.
971 *
972 * @since 1.9
973 *
974 * @param ProcessInterface $process Instance of this process.
975 * @param bool|WP_Error $response The result of the operation: true on success, a WP_Error object on failure.
976 * @param array $files The list of files, before restoring them.
977 * @param array $data The optimization data, before deleting it.
978 */
979 do_action( 'imagify_after_restore_media', $this, $response, $files, $data );
980
981 $this->unlock();
982
983 return $response;
984 }
985
986 /**
987 * Restore the thumbnails.
988 *
989 * @since 1.9
990 *
991 * @return bool|WP_Error True on success. A WP_Error instance on failure.
992 */
993 protected function restore_thumbnails() {
994 $media = $this->get_media();
995
996 /**
997 * Delete the next-gen versions.
998 * If the full size file and the original file are not the same, the full size is considered like a thumbnail.
999 * In that case we must also delete the next-gen file associated to the full size.
1000 */
1001 $keep_full_next_gen = $media->get_raw_original_path() === $media->get_raw_fullsize_path();
1002 $this->delete_nextgen_files( $keep_full_next_gen, true );
1003
1004 // Generate new thumbnails.
1005 return $media->generate_thumbnails();
1006 }
1007
1008 /**
1009 * Delete the backup file.
1010 *
1011 * @since 1.9
1012 */
1013 public function delete_backup() {
1014 if ( ! $this->is_valid() ) {
1015 return;
1016 }
1017
1018 $backup_path = $this->get_media()->get_backup_path();
1019
1020 if ( $backup_path ) {
1021 $this->filesystem->delete( $backup_path );
1022
1023 // Check for the -scaled version in the backup.
1024 $scaled_backup_path = preg_replace( '/(\.)([^\.]+)$/', '-scaled.$2', $backup_path );
1025 if ( $this->filesystem->exists( $scaled_backup_path ) ) {
1026 // Delete the -scaled version from the backup.
1027 $this->filesystem->delete( $scaled_backup_path );
1028 }
1029 }
1030 }
1031
1032 /**
1033 * Create a temporary copy of a size file.
1034 *
1035 * If we need to create a next-gen version, we must create it from an unoptimized image.
1036 * The full size is always optimized before the next-gen version creation, and in some cases it’s the same for the thumbnails.
1037 * Then we use the backup file to create temporary files.
1038 *
1039 * @since 1.9
1040 *
1041 * @param string $size The image size name.
1042 * @param array $sizes A list of thumbnail sizes being optimized.
1043 *
1044 * @return bool True if the file exists/is created. False on failure.
1045 */
1046 protected function create_temporary_copy( $size, $sizes = null ) {
1047 $media = $this->get_media();
1048
1049 if ( ! isset( $sizes ) ) {
1050 $sizes = $media->get_media_files();
1051 }
1052
1053 if ( empty( $sizes[ $size ] ) ) {
1054 // What?
1055 return false;
1056 }
1057
1058 $tmp_path = $this->get_temporary_copy_path( $size, $sizes );
1059
1060 if ( $tmp_path && $this->filesystem->exists( $tmp_path ) ) {
1061 // The temporary file already exists.
1062 return true;
1063 }
1064
1065 $tmp_file = new File( $tmp_path );
1066
1067 if ( ! $tmp_file->is_image() ) {
1068 // The file is not an image.
1069 return false;
1070 }
1071
1072 if ( ! $tmp_file->is_supported( $media->get_allowed_mime_types() ) ) {
1073 // The file is not supported.
1074 return false;
1075 }
1076
1077 /**
1078 * Use the backup file as source.
1079 */
1080 $backup_path = $media->get_backup_path();
1081
1082 if ( ! $backup_path ) {
1083 // No backup, no hope for you.
1084 return false;
1085 }
1086
1087 /**
1088 * In all cases we must make a copy of the backup file, and not use the backup directly:
1089 * sometimes the backup image does not have a valid file extension (yes I’m looking at you NextGEN Gallery).
1090 */
1091 $copied = $this->filesystem->copy( $backup_path, $tmp_path, true );
1092
1093 if ( ! $copied ) {
1094 return false;
1095 }
1096
1097 if ( 'full' === $size ) {
1098 /**
1099 * We create a copy of the backup to be able to create a next-gen version from it.
1100 * That means the optimization process will resize the file if needed, so there is nothing more to do here.
1101 */
1102 return true;
1103 }
1104
1105 // We need to create a thumbnail from it.
1106 $size_data = $sizes[ $size ];
1107 $context_sizes = $media->get_context_instance()->get_thumbnail_sizes();
1108
1109 if ( ! empty( $context_sizes[ $size ] ) ) {
1110 // Not a dynamic size, yay!
1111 $size_data = array_merge( $size_data, $context_sizes[ $size ] );
1112 }
1113
1114 if ( empty( $size_data['path'] ) ) {
1115 // Should not happen.
1116 return false;
1117 }
1118
1119 if ( ! isset( $size_data['crop'] ) ) {
1120 /**
1121 * In case of a dynamic thumbnail we don’t know if the image must be croped or resized.
1122 *
1123 * @since 1.9
1124 *
1125 * @param bool $crop True to crop the thumbnail, false to resize. Null by default.
1126 * @param string $size Name of the thumbnail size.
1127 * @param array $size_data Data of the thumbnail being processed. Contains at least 'width', 'height', and 'path'.
1128 * @param MediaInterface $media The MediaInterface instance corresponding to the image being processed.
1129 */
1130 $crop = apply_filters( 'imagify_crop_thumbnail', null, $size, $size_data, $media );
1131
1132 if ( null !== $crop ) {
1133 $size_data['crop'] = (bool) $crop;
1134 }
1135 }
1136
1137 if ( ! isset( $size_data['crop'] ) ) {
1138 // We don't have the 'crop' data in that case: let’s try to guess it.
1139 if ( ! $size_data['height'] || ! $size_data['width'] ) {
1140 // One of the size dimensions is 0, that means crop is probably disabled.
1141 $size_data['crop'] = false;
1142 } else {
1143 if ( ! $this->filesystem->exists( $size_data['path'] ) ) {
1144 // Screwed.
1145 return false;
1146 }
1147
1148 $thumb_dimensions = $this->filesystem->get_image_size( $size_data['path'] );
1149
1150 if ( ! $thumb_dimensions || ! $thumb_dimensions['width'] || ! $thumb_dimensions['height'] ) {
1151 // ( ; Đ” ; )
1152 return false;
1153 }
1154
1155 // Compare dimensions.
1156 $new_height = $thumb_dimensions['width'] * $size_data['height'] / $size_data['width'];
1157 // If the difference is > to 1px, let's assume that crop is enabled.
1158 $size_data['crop'] = abs( $thumb_dimensions['height'] - $new_height ) > 1;
1159 }
1160 }
1161
1162 $resized = $tmp_file->create_thumbnail( [
1163 'path' => $tmp_path,
1164 'width' => $size_data['width'],
1165 'height' => $size_data['height'],
1166 'crop' => $size_data['crop'],
1167 'adjust_filename' => false,
1168 ] );
1169
1170 if ( is_wp_error( $resized ) ) {
1171 return false;
1172 }
1173
1174 // Make sure the new file has the expected name.
1175 $new_tmp_path = $this->filesystem->dir_path( $tmp_path ) . $resized['file'];
1176
1177 if ( $new_tmp_path === $tmp_path ) {
1178 return true;
1179 }
1180
1181 return $this->filesystem->move( $new_tmp_path, $tmp_path, true );
1182 }
1183
1184 /**
1185 * Get the path to a temporary copy of a size file.
1186 *
1187 * @since 1.9
1188 *
1189 * @param string $size The image size name.
1190 * @param array $sizes A list of thumbnail sizes being optimized.
1191 *
1192 * @return string|bool An image path. False on failure.
1193 */
1194 protected function get_temporary_copy_path( $size, $sizes = null ) {
1195 if ( 'full' === $size ) {
1196 $path = $this->get_media()->get_raw_fullsize_path();
1197 } else {
1198 if ( ! isset( $sizes ) ) {
1199 $sizes = $this->get_media()->get_media_files();
1200 }
1201
1202 $path = ! empty( $sizes[ $size ]['path'] ) ? $sizes[ $size ]['path'] : false;
1203 }
1204
1205 if ( ! $path ) {
1206 return false;
1207 }
1208
1209 $info = $this->filesystem->path_info( $path );
1210
1211 if ( ! $info['file_base'] ) {
1212 return false;
1213 }
1214
1215 return $info['dir_path'] . $info['file_base'] . static::TMP_SUFFIX . '.' . $info['extension'];
1216 }
1217
1218 /**
1219 * Maybe resize an image.
1220 *
1221 * @since 1.9
1222 *
1223 * @param string $size The size name.
1224 * @param File $file A File instance.
1225 *
1226 * @return array|WP_Error A WP_Error instance on failure, an array on success as follow: {
1227 * @type bool $resized True when the image has been resized.
1228 * @type bool $backuped True when the image has been backuped.
1229 * @type int $file_size The file size in bytes.
1230 * }
1231 */
1232 public function maybe_resize( $size, $file ) {
1233 if ( ! $this->can_resize( $size, $file ) ) {
1234 // This file should not be resized.
1235 return [
1236 'resized' => false,
1237 'backuped' => false,
1238 'file_size' => 0,
1239 ];
1240 }
1241
1242 $dimensions = $file->get_dimensions();
1243
1244 if ( ! $dimensions['width'] ) {
1245 // Could not get the image dimensions.
1246 return new WP_Error(
1247 'no_dimensions',
1248 sprintf(
1249 /* translators: %s is an error message. */
1250 __( 'Resizing failed: %s', 'imagify' ),
1251 __( 'Imagify could not get the image dimensions.', 'imagify' )
1252 )
1253 );
1254 }
1255
1256 $media = $this->get_media();
1257 $resize_width = $media->get_context_instance()->get_resizing_threshold();
1258
1259 if ( $resize_width >= $dimensions['width'] ) {
1260 // No need to resize.
1261 return [
1262 'resized' => false,
1263 'backuped' => false,
1264 'file_size' => 0,
1265 ];
1266 }
1267
1268 $resized_path = $file->resize( $dimensions, $resize_width );
1269
1270 if ( is_wp_error( $resized_path ) ) {
1271 // The resizement failed.
1272 return new WP_Error(
1273 'resize_failure',
1274 sprintf(
1275 /* translators: %s is an error message. */
1276 __( 'Resizing failed: %s', 'imagify' ),
1277 $resized_path->get_error_message()
1278 )
1279 );
1280 }
1281
1282 if ( $this->can_backup( $size ) ) {
1283 $source = 'full' === $size ? $media->get_original_path() : null;
1284 $backuped = $file->backup( $media->get_raw_backup_path(), $source );
1285
1286 if ( is_wp_error( $backuped ) ) {
1287 // The backup failed.
1288 return new WP_Error(
1289 'backup_failure',
1290 sprintf(
1291 /* translators: %s is an error message. */
1292 __( 'Backup failed: %s', 'imagify' ),
1293 $backuped->get_error_message()
1294 )
1295 );
1296 }
1297 } else {
1298 $backuped = false;
1299 }
1300
1301 $file_size = (int) $this->filesystem->size( $file->get_path() );
1302 $resized = $this->filesystem->move( $resized_path, $file->get_path(), true );
1303
1304 if ( ! $resized ) {
1305 // The resizement failed.
1306 return new WP_Error(
1307 'resize_move_failure',
1308 __( 'The image could not be replaced by the resized one.', 'imagify' )
1309 );
1310 }
1311
1312 // Store the new dimensions.
1313 $media->update_dimensions();
1314
1315 return [
1316 'resized' => true,
1317 'backuped' => $backuped,
1318 'file_size' => $file_size,
1319 ];
1320 }
1321
1322 /**
1323 * Tell if a size should be resized.
1324 *
1325 * @since 1.9
1326 *
1327 * @param string $size The size name.
1328 * @param File $file A File instance.
1329 *
1330 * @return bool
1331 */
1332 protected function can_resize( $size, $file ) {
1333 if ( ! $this->is_valid() ) {
1334 return false;
1335 }
1336
1337 if (
1338 'full' !== $size
1339 &&
1340 (
1341 'full' . static::WEBP_SUFFIX !== $size
1342 ||
1343 'full' . static::AVIF_SUFFIX !== $size
1344 )
1345 ) {
1346 // We resize only the main file and its next-gen version.
1347 return false;
1348 }
1349
1350 if ( ! $file->is_image() ) {
1351 return false;
1352 }
1353
1354 return $this->get_media()->get_context_instance()->can_resize();
1355 }
1356
1357 /**
1358 * Tell if a size should be backuped.
1359 *
1360 * @since 1.9
1361 *
1362 * @param string $size The size name.
1363 *
1364 * @return bool
1365 */
1366 protected function can_backup( $size ) {
1367 if ( ! $this->is_valid() ) {
1368 return false;
1369 }
1370
1371 if ( 'full' !== $size ) {
1372 // We backup only the main file.
1373 return false;
1374 }
1375
1376 return $this->get_media()->get_context_instance()->can_backup();
1377 }
1378
1379 /**
1380 * Get mime type
1381 *
1382 * @param string $format nextgen image format.
1383 */
1384 private function get_mime_type( $format ) {
1385 $mime_types = [
1386 'avif' => 'image/avif',
1387 'webp' => 'image/webp',
1388 ];
1389
1390 return isset( $mime_types[ $format ] ) ? $mime_types[ $format ] : false;
1391 }
1392
1393 /**
1394 * Delete the next gen format images.
1395 * This doesn't delete the related optimization data.
1396 *
1397 * @since 2.2
1398 *
1399 * @param bool $keep_full Set to true to keep the full size.
1400 * @param bool $all_next_gen True: will delete every next-gen format. False: will delete only the current enabled format.
1401 *
1402 * @return bool|WP_Error True on success. A WP_Error object on failure.
1403 */
1404 public function delete_nextgen_files( $keep_full = false, $all_next_gen = false ) {
1405 if ( ! $this->is_valid() ) {
1406 return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
1407 }
1408
1409 $media = $this->get_media();
1410
1411 if ( ! $media->is_image() ) {
1412 return new WP_Error( 'media_not_an_image', __( 'This media is not an image.', 'imagify' ) );
1413 }
1414
1415 $files = $media->get_media_files();
1416
1417 if ( $keep_full ) {
1418 unset( $files['full'] );
1419 }
1420
1421 if ( ! $files ) {
1422 return true;
1423 }
1424
1425 $error_count = 0;
1426
1427 foreach ( $files as $file ) {
1428 if ( 0 === strpos( $file['mime-type'], 'image/' ) ) {
1429 $deleted = $this->delete_nextgen_file( $file['path'], $all_next_gen );
1430
1431 if ( is_wp_error( $deleted ) ) {
1432 ++$error_count;
1433 }
1434 }
1435 }
1436
1437 if ( $error_count ) {
1438 return new WP_Error(
1439 'files_not_deleted',
1440 sprintf(
1441 /* translators: %s is a formatted number, don’t use %d. */
1442 _n( '%s file could not be deleted.', '%s files could not be deleted.', $error_count, 'imagify' ),
1443 number_format_i18n( $error_count )
1444 )
1445 );
1446 }
1447
1448 return true;
1449 }
1450
1451 /**
1452 * Delete a next gen format image, given its non-next-gen version's path.
1453 * This doesn't delete the related optimization data.
1454 *
1455 * @since 2.2
1456 *
1457 * @param string $file_path Path to the non-next-gen file.
1458 * @param bool $all_next_gen True: will delete every next-gen format. False: will delete only the current enabled format.
1459 *
1460 * @return void|WP_Error A \WP_Error object on failure.
1461 */
1462 protected function delete_nextgen_file( $file_path, $all_next_gen = false ) {
1463 if ( ! $file_path ) {
1464 return new WP_Error( 'no_path', __( 'Path to non-next-gen file not provided.', 'imagify' ) );
1465 }
1466
1467 $next_gen_file = new File( $file_path );
1468 $formats = $this->extensions;
1469
1470 if ( ! $all_next_gen ) {
1471 $formats = imagify_nextgen_images_formats();
1472 }
1473 // Delete next-gen images.
1474 foreach ( $formats as $extension ) {
1475 $path = $next_gen_file->get_path_to_nextgen( $extension );
1476
1477 if ( ! $path ) {
1478 continue;
1479 }
1480
1481 $this->delete_file( $path );
1482 }
1483 }
1484
1485 /**
1486 * Delete a next gen format image, given its non-next-gen version's path.
1487 *
1488 * @param string $next_gen_path Path to the non-next-gen file.
1489 *
1490 * @return bool|WP_Error True on success. A WP_Error object on failure.
1491 */
1492 protected function delete_file( string $next_gen_path ) {
1493 if ( empty( $next_gen_path ) ) {
1494 return new WP_Error( 'no_$next_gen_path', __( 'Could not get the path to the Next-Gen format file.', 'imagify' ) );
1495 }
1496
1497 if ( ! $this->filesystem->exists( $next_gen_path ) ) {
1498 return true;
1499 }
1500
1501 if ( ! $this->filesystem->is_writable( $next_gen_path ) ) {
1502 return new WP_Error(
1503 'file_not_writable',
1504 sprintf(
1505 /* translators: %s is a file path. */
1506 __( 'The file %s does not seem to be writable.', 'imagify' ),
1507 '<code>' . esc_html( $this->filesystem->make_path_relative( $next_gen_path ) ) . '</code>'
1508 )
1509 );
1510 }
1511
1512 if ( ! $this->filesystem->is_file( $next_gen_path ) ) {
1513 return new WP_Error(
1514 'not_a_file',
1515 sprintf(
1516 /* translators: %s is a file path. */
1517 __( 'This does not seem to be a file: %s.', 'imagify' ),
1518 '<code>' . esc_html( $this->filesystem->make_path_relative( $next_gen_path ) ) . '</code>'
1519 )
1520 );
1521 }
1522
1523 $deleted = $this->filesystem->delete( $next_gen_path, false, 'f' );
1524
1525 if ( ! $deleted ) {
1526 return new WP_Error(
1527 'file_not_deleted',
1528 sprintf(
1529 /* translators: %s is a file path. */
1530 __( 'The file %s could not be deleted.', 'imagify' ),
1531 '<code>' . esc_html( $this->filesystem->make_path_relative( $next_gen_path ) ) . '</code>'
1532 )
1533 );
1534 }
1535
1536 return true;
1537 }
1538
1539 /**
1540 * Gives the next-gen image format we are processing.
1541 *
1542 * @return string Current format we are targeting.
1543 */
1544 public function get_current_format() {
1545 $format = get_imagify_option( 'optimization_format' );
1546
1547 return ( 'avif' === $format ) ? static::AVIF_SUFFIX : static::WEBP_SUFFIX;
1548 }
1549
1550 /**
1551 * Tell if a thumbnail size is an "Imagify Next-Gen" size.
1552 *
1553 * @since 1.9
1554 * @since 2.2 addition of the format parameter.
1555 *
1556 * @param string $size_name The size name.
1557 *
1558 * @return string|bool The unsuffixed name of the size if next-gen. False if not next-gen.
1559 */
1560 public function is_size_next_gen( $size_name ) {
1561 $formats = imagify_nextgen_images_formats();
1562
1563 foreach ( $formats as $format ) {
1564 $suffix = preg_quote( $this->get_suffix_from_format( $format ), '/' );
1565
1566 if ( preg_match( '/^(?<size>.+)' . $suffix . '$/', (string) $size_name, $matches ) ) {
1567 return $matches['size'];
1568 }
1569 }
1570
1571 return false;
1572 }
1573
1574 /**
1575 * Get suffix from format.
1576 *
1577 * @param string $format Format extension of next-gen image.
1578 * @return string
1579 */
1580 private function get_suffix_from_format( string $format ): string {
1581 $suffixes = [
1582 'avif' => static::AVIF_SUFFIX,
1583 'webp' => static::WEBP_SUFFIX,
1584 ];
1585
1586 return $suffixes[ $format ];
1587 }
1588
1589 /**
1590 * Tell if the media has a next gen format.
1591 *
1592 * @since 2.2
1593 *
1594 * @return bool
1595 */
1596 public function has_next_gen() {
1597 if ( ! $this->is_valid() ) {
1598 return false;
1599 }
1600
1601 if ( ! $this->get_media()->is_image() ) {
1602 return false;
1603 }
1604
1605 $data = $this->get_data()->get_optimization_data();
1606
1607 if ( empty( $data['sizes'] ) ) {
1608 return false;
1609 }
1610
1611 $needle = $this->format . '";a:4:{s:7:"success";b:1;';
1612 $data = maybe_serialize( $data['sizes'] );
1613
1614 return is_string( $data ) && strpos( $data, $needle );
1615 }
1616
1617 /**
1618 * Tell if the media has all Next-Gen versions.
1619 *
1620 * @return bool
1621 */
1622 public function is_full_next_gen() {
1623 if ( ! $this->is_valid() ) {
1624 return false;
1625 }
1626
1627 if ( ! $this->get_media()->is_image() ) {
1628 return false;
1629 }
1630
1631 $data = $this->get_data()->get_optimization_data();
1632
1633 $sizes = $data['sizes'];
1634
1635 if ( empty( $sizes ) ) {
1636 return false;
1637 }
1638
1639 $keys = array_keys( $sizes );
1640 $non_next_gen_keys = array_values(array_filter($keys, function ( $key ) {
1641 return strpos( (string) $key, $this->format ) === false;
1642 }));
1643
1644 return array_reduce($non_next_gen_keys, function ( $is_fully, $key ) use ( $sizes ) {
1645 return key_exists( $key . $this->format, $sizes ) && $is_fully;
1646 }, true);
1647 }
1648
1649 /**
1650 * Tell if a Next-Gen version can be created for the given file.
1651 * Make sure the file is an image before using this method.
1652 *
1653 * @since 1.9.5
1654 *
1655 * @param string $file_path Path to the file.
1656 *
1657 * @return bool
1658 */
1659 public function can_create_next_gen_version( $file_path ) {
1660 if ( ! $file_path ) {
1661 return false;
1662 }
1663
1664 $can = apply_filters_deprecated( 'imagify_pre_can_create_webp_version', array( null, $file_path ), '2.2', 'imagify_pre_can_create_next_gen_version' );
1665
1666 /**
1667 * Tell if a next-gen version can be created for the given file.
1668 * The file is an image.
1669 *
1670 * @since 1.9.5
1671 *
1672 * @param bool $can True to create a next-gen version, false otherwise. Null by default.
1673 * @param string $file_path Path to the file.
1674 */
1675 $can = apply_filters( 'imagify_pre_can_create_next_gen_version', $can, $file_path );
1676
1677 if ( isset( $can ) ) {
1678 return (bool) $can;
1679 }
1680
1681 $is_animated_gif = $this->filesystem->is_animated_gif( $file_path );
1682
1683 if ( is_bool( $is_animated_gif ) ) {
1684 // Ok if it’s not an animated gif.
1685 return ! $is_animated_gif;
1686 }
1687
1688 // At this point $is_animated_gif is null, which means the file cannot be read (yet).
1689 return true;
1690 }
1691
1692 /**
1693 * Generate next-gen images if they are missing.
1694 *
1695 * @since 1.9
1696 *
1697 * @return bool|WP_Error True if successfully launched. A WP_Error instance on failure.
1698 */
1699 public function generate_nextgen_versions() {
1700 if ( ! $this->is_valid() ) {
1701 return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
1702 }
1703
1704 $media = $this->get_media();
1705
1706 if ( ! $media->is_image() ) {
1707 return new WP_Error( 'no_next_gen', __( 'This media is not an image and cannot be converted to next-gen format.', 'imagify' ) );
1708 }
1709
1710 if ( ! $media->has_backup() ) {
1711 return new WP_Error( 'no_backup', __( 'This media has no backup file.', 'imagify' ) );
1712 }
1713
1714 $data = $this->get_data();
1715
1716 if ( ! $data->is_optimized() && ! $data->is_already_optimized() ) {
1717 return new WP_Error( 'not_optimized', __( 'This media has not been optimized by Imagify yet.', 'imagify' ) );
1718 }
1719
1720 if ( $this->has_next_gen() ) {
1721 return new WP_Error( 'has_next_gen', __( 'This media already has next-gen versions.', 'imagify' ) );
1722 }
1723
1724 $files = $media->get_media_files();
1725 $sizes = [];
1726 $args = [
1727 'hook_suffix' => 'generate_nextgen_versions',
1728 ];
1729
1730 foreach ( $files as $size_name => $file ) {
1731 $formats = imagify_nextgen_images_formats();
1732
1733 foreach ( $formats as $format ) {
1734 if ( 'avif' === $format ) {
1735 $format_suffix = static::AVIF_SUFFIX;
1736 } elseif ( 'webp' === $format ) {
1737 $format_suffix = static::WEBP_SUFFIX;
1738 }
1739
1740 if ( $this->get_mime_type( $format ) === $files[ $size_name ]['mime-type'] ) {
1741 continue;
1742 }
1743
1744 array_unshift( $sizes, $size_name . $format_suffix );
1745 }
1746 }
1747
1748 if ( ! $sizes ) {
1749 return new WP_Error( 'no_sizes', __( 'This media does not have files that can be converted to next-gen format.', 'imagify' ) );
1750 }
1751
1752 $optimization_level = $data->get_optimization_level();
1753
1754 // Optimize.
1755 return $this->optimize_sizes( $sizes, $optimization_level, $args );
1756 }
1757
1758 /**
1759 * Tell if a process is running for this media.
1760 *
1761 * @since 1.9
1762 *
1763 * @return string|bool The action if locked ('optimizing' or 'restoring'). False if not locked.
1764 */
1765 public function is_locked() {
1766 $name = $this->get_lock_name();
1767
1768 if ( ! $name ) {
1769 return false;
1770 }
1771
1772 $callback = $this->get_media()->get_context_instance()->is_network_wide() ? 'get_site_transient' : 'get_transient';
1773 $action = call_user_func( $callback, $name );
1774
1775 if ( ! $action ) {
1776 return false;
1777 }
1778
1779 return $this->validate_lock_action( $action );
1780 }
1781
1782 /**
1783 * Set the running status to "running" for 10 minutes.
1784 *
1785 * @since 1.9
1786 *
1787 * @param string $action The action performed behind this lock: 'optimizing' or 'restoring'.
1788 */
1789 public function lock( $action = 'optimizing' ) {
1790 $name = $this->get_lock_name();
1791
1792 if ( ! $name ) {
1793 return;
1794 }
1795
1796 $action = $this->validate_lock_action( $action );
1797 $media = $this->get_media();
1798 $callback = $media->get_context_instance()->is_network_wide() ? 'set_site_transient' : 'set_transient';
1799
1800 call_user_func( $callback, $name, $action, 10 * MINUTE_IN_SECONDS );
1801 }
1802
1803 /**
1804 * Unset the running status.
1805 *
1806 * @since 1.9
1807 */
1808 public function unlock() {
1809 $name = $this->get_lock_name();
1810
1811 if ( ! $name ) {
1812 return false;
1813 }
1814
1815 $callback = $this->get_media()->get_context_instance()->is_network_wide() ? 'delete_site_transient' : 'delete_transient';
1816
1817 call_user_func( $callback, $name );
1818 }
1819
1820 /**
1821 * Get the name of the transient that stores the lock status.
1822 *
1823 * @since 1.9
1824 *
1825 * @return string|bool The name on success. False on failure.
1826 */
1827 protected function get_lock_name() {
1828 $media = $this->get_media();
1829
1830 if ( ! $media ) {
1831 return false;
1832 }
1833
1834 /**
1835 * Note that the site transient used by WP Background is named '*_process_lock'.
1836 * That would give something like 'imagify_optimize_media_process_lock' for the optimization process, while here it would be 'imagify_wp_42_process_locked'.
1837 */
1838 return sprintf( static::LOCK_NAME, $media->get_context(), $media->get_id() );
1839 }
1840
1841 /**
1842 * Validate the lock action.
1843 *
1844 * @since 1.9
1845 *
1846 * @param string $action The action performed behind this lock: 'optimizing' or 'restoring'.
1847 * @return string The valid action.
1848 */
1849 protected function validate_lock_action( $action ) {
1850 switch ( $action ) {
1851 case 'restore':
1852 case 'restoring':
1853 $action = 'restoring';
1854 break;
1855
1856 default:
1857 $action = 'optimizing';
1858 }
1859
1860 return $action;
1861 }
1862
1863 /**
1864 * Tell if a size already has optimization data.
1865 *
1866 * @since 1.9
1867 *
1868 * @param string $size The size name.
1869 * @return bool
1870 */
1871 public function size_has_optimization_data( $size ) {
1872 $data = $this->get_data()->get_optimization_data();
1873
1874 return ! empty( $data['sizes'][ $size ] );
1875 }
1876
1877 /**
1878 * Update the optimization data for a size.
1879 *
1880 * @since 1.9
1881 * @since 2.2 - Addition of the format in the call.
1882 *
1883 * @param object $response The API response.
1884 * @param string $size The size name.
1885 * @param int $level The optimization level (0=normal, 1=aggressive, 2=ultra).
1886 *
1887 * @return array {
1888 * The optimization data.
1889 *
1890 * @type int $level The optimization level.
1891 * @type string $status The status: 'success', 'already_optimized', 'error'.
1892 * @type bool $success True if successfully optimized. False on error or if already optimized.
1893 * @type string $error An error message.
1894 * @type int $original_size The weight of the file, before optimization.
1895 * @type int $optimized_size The weight of the file, once optimized.
1896 * }
1897 */
1898 public function update_size_optimization_data( $response, $size, $level ) {
1899 $disabled = false;
1900 $data = $this->data_format;
1901
1902 $data['level'] = is_numeric( $level ) ? (int) $level : $this->get_option( 'optimization_level' );
1903
1904 if ( is_wp_error( $response ) ) {
1905 /**
1906 * Error.
1907 */
1908 $disabled = 'unauthorized_size' === $response->get_error_code();
1909
1910 // Size data.
1911 $data['success'] = false;
1912 $data['error'] = $response->get_error_message();
1913
1914 // Status.
1915 if ( false !== strpos( $data['error'], 'This image is already compressed' ) ) {
1916 $data['status'] = 'already_optimized';
1917 } else {
1918 $data['status'] = 'error';
1919 }
1920 } else {
1921 /**
1922 * Success.
1923 */
1924 $response = (object) array_merge( [
1925 'original_size' => 0,
1926 'new_size' => 0,
1927 'percent' => 0,
1928 ], (array) $response );
1929
1930 // Status.
1931 $data['status'] = 'success';
1932 $data['error'] = null;
1933
1934 // Size data.
1935 $data['success'] = true;
1936 if ( property_exists( $response, 'message' ) ) {
1937 $data['message'] = imagify_translate_api_message( $response->message );
1938 }
1939 $data['original_size'] = $response->original_size;
1940 $data['optimized_size'] = $response->new_size;
1941 }
1942
1943 $_unauthorized = $disabled ? '_unauthorized' : '';
1944
1945 /**
1946 * Filter the optimization data.
1947 *
1948 * @since 1.9
1949 *
1950 * @param array $data {
1951 * The optimization data.
1952 *
1953 * @type int $level The optimization level.
1954 * @type string $status The status: 'success', 'already_optimized', 'error'.
1955 * @type bool $success True if successfully optimized. False on error or if already optimized.
1956 * @type string $error An error message.
1957 * @type int $original_size The weight of the file, before optimization.
1958 * @type int $optimized_size The weight of the file, once optimized.
1959 * }
1960 * @param object $response The API response.
1961 * @param string $size The size name.
1962 * @param int $level The optimization level.
1963 * @param object $media_data The DataInterface instance of the media.
1964 */
1965 $data = (array) apply_filters( "imagify{$_unauthorized}_file_optimization_data", $data, $response, $size, $level, $this->get_data() );
1966
1967 if ( property_exists( $response, 'message' ) ) {
1968 $size = str_replace( $this->format, '', $size );
1969 }
1970 // Store.
1971 $this->get_data()->update_size_optimization_data( $size, $data );
1972
1973 return $data;
1974 }
1975
1976 /**
1977 * Get a plugin’s option.
1978 *
1979 * @since 1.9
1980 *
1981 * @param string $option_name The option name.
1982 *
1983 * @return mixed
1984 */
1985 protected function get_option( $option_name ) {
1986 if ( isset( $this->options[ $option_name ] ) ) {
1987 return $this->options[ $option_name ];
1988 }
1989
1990 $this->options[ $option_name ] = get_imagify_option( $option_name );
1991
1992 return $this->options[ $option_name ];
1993 }
1994
1995 /**
1996 * Sanitize and validate an optimization level.
1997 * If not provided (false, null), fallback to the level set in the plugin's settings.
1998 *
1999 * @since 1.9
2000 *
2001 * @param mixed $optimization_level The optimization level.
2002 *
2003 * @return int
2004 */
2005 protected function sanitize_optimization_level( $optimization_level ) {
2006 if ( ! is_numeric( $optimization_level ) ) {
2007 if ( $this->get_option( 'lossless' ) ) {
2008 return 0;
2009 }
2010
2011 return $this->get_option( 'optimization_level' );
2012 }
2013
2014 return \Imagify_Options::get_instance()->sanitize_and_validate( 'optimization_level', $optimization_level );
2015 }
2016
2017 /**
2018 * Tell if the media has AVIF versions.
2019 *
2020 * @since 2.2
2021 *
2022 * @return bool
2023 */
2024 public function has_avif() {
2025 if ( ! $this->is_valid() ) {
2026 return false;
2027 }
2028
2029 if ( ! $this->get_media()->is_image() ) {
2030 return false;
2031 }
2032
2033 $data = $this->get_data()->get_optimization_data();
2034
2035 if ( empty( $data['sizes'] ) ) {
2036 return false;
2037 }
2038
2039 $needle = static::AVIF_SUFFIX . '";a:4:{s:7:"success";b:1;';
2040 $data = maybe_serialize( $data['sizes'] );
2041
2042 return is_string( $data ) && strpos( $data, $needle );
2043 }
2044 }
2045