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

2,057 lines 57.0 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 /**
1088 * The backup file may be the original, un-rotated JPEG that WordPress auto-rotated on
1089 * upload (WordPress resets the orientation on the rotated file, but keeps the original
1090 * orientation in the backup). Correct the orientation of this disposable temporary copy
1091 * so that a Next-Gen version (or a thumbnail) generated from it isn't mis-oriented.
1092 * The backup file itself is never touched.
1093 */
1094 $tmp_file->maybe_correct_exif_orientation();
1095
1096 if ( 'full' === $size ) {
1097 /**
1098 * We create a copy of the backup to be able to create a next-gen version from it.
1099 * That means the optimization process will resize the file if needed, so there is nothing more to do here.
1100 */
1101 return true;
1102 }
1103
1104 // We need to create a thumbnail from it.
1105 $size_data = $sizes[ $size ];
1106 $context_sizes = $media->get_context_instance()->get_thumbnail_sizes();
1107
1108 if ( ! empty( $context_sizes[ $size ] ) ) {
1109 // Not a dynamic size, yay!
1110 $size_data = array_merge( $size_data, $context_sizes[ $size ] );
1111 }
1112
1113 if ( empty( $size_data['path'] ) ) {
1114 // Should not happen.
1115 return false;
1116 }
1117
1118 if ( ! isset( $size_data['crop'] ) ) {
1119 /**
1120 * In case of a dynamic thumbnail we don’t know if the image must be croped or resized.
1121 *
1122 * @since 1.9
1123 *
1124 * @param bool $crop True to crop the thumbnail, false to resize. Null by default.
1125 * @param string $size Name of the thumbnail size.
1126 * @param array $size_data Data of the thumbnail being processed. Contains at least 'width', 'height', and 'path'.
1127 * @param MediaInterface $media The MediaInterface instance corresponding to the image being processed.
1128 */
1129 $crop = apply_filters( 'imagify_crop_thumbnail', null, $size, $size_data, $media );
1130
1131 if ( null !== $crop ) {
1132 $size_data['crop'] = (bool) $crop;
1133 }
1134 }
1135
1136 if ( ! isset( $size_data['crop'] ) ) {
1137 // We don't have the 'crop' data in that case: let’s try to guess it.
1138 if ( ! $size_data['height'] || ! $size_data['width'] ) {
1139 // One of the size dimensions is 0, that means crop is probably disabled.
1140 $size_data['crop'] = false;
1141 } else {
1142 if ( ! $this->filesystem->exists( $size_data['path'] ) ) {
1143 // Screwed.
1144 return false;
1145 }
1146
1147 $thumb_dimensions = $this->filesystem->get_image_size( $size_data['path'] );
1148
1149 if ( ! $thumb_dimensions || ! $thumb_dimensions['width'] || ! $thumb_dimensions['height'] ) {
1150 return false;
1151 }
1152
1153 // Compare dimensions.
1154 $new_height = $thumb_dimensions['width'] * $size_data['height'] / $size_data['width'];
1155 // If the difference is > to 1px, let's assume that crop is enabled.
1156 $size_data['crop'] = abs( $thumb_dimensions['height'] - $new_height ) > 1;
1157 }
1158 }
1159
1160 $resized = $tmp_file->create_thumbnail(
1161 [
1162 'path' => $tmp_path,
1163 'width' => $size_data['width'],
1164 'height' => $size_data['height'],
1165 'crop' => $size_data['crop'],
1166 'adjust_filename' => false,
1167 ]
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
1641 $non_next_gen_keys = array_values(
1642 array_filter(
1643 $keys,
1644 function ( $key ) {
1645 return strpos( (string) $key, $this->format ) === false;
1646 }
1647 )
1648 );
1649
1650 return array_reduce(
1651 $non_next_gen_keys,
1652 function ( $is_fully, $key ) use ( $sizes ) {
1653 return key_exists( $key . $this->format, $sizes ) && $is_fully;
1654 },
1655 true
1656 );
1657 }
1658
1659 /**
1660 * Tell if a Next-Gen version can be created for the given file.
1661 * Make sure the file is an image before using this method.
1662 *
1663 * @since 1.9.5
1664 *
1665 * @param string $file_path Path to the file.
1666 *
1667 * @return bool
1668 */
1669 public function can_create_next_gen_version( $file_path ) {
1670 if ( ! $file_path ) {
1671 return false;
1672 }
1673
1674 $can = apply_filters_deprecated( 'imagify_pre_can_create_webp_version', [ null, $file_path ], '2.2', 'imagify_pre_can_create_next_gen_version' );
1675
1676 /**
1677 * Tell if a next-gen version can be created for the given file.
1678 * The file is an image.
1679 *
1680 * @since 1.9.5
1681 *
1682 * @param bool $can True to create a next-gen version, false otherwise. Null by default.
1683 * @param string $file_path Path to the file.
1684 */
1685 $can = apply_filters( 'imagify_pre_can_create_next_gen_version', $can, $file_path );
1686
1687 if ( isset( $can ) ) {
1688 return (bool) $can;
1689 }
1690
1691 $is_animated_gif = $this->filesystem->is_animated_gif( $file_path );
1692
1693 if ( is_bool( $is_animated_gif ) ) {
1694 // Ok if it’s not an animated gif.
1695 return ! $is_animated_gif;
1696 }
1697
1698 // At this point $is_animated_gif is null, which means the file cannot be read (yet).
1699 return true;
1700 }
1701
1702 /**
1703 * Generate next-gen images if they are missing.
1704 *
1705 * @since 1.9
1706 *
1707 * @return bool|WP_Error True if successfully launched. A WP_Error instance on failure.
1708 */
1709 public function generate_nextgen_versions() {
1710 if ( ! $this->is_valid() ) {
1711 return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
1712 }
1713
1714 $media = $this->get_media();
1715
1716 if ( ! $media->is_image() ) {
1717 return new WP_Error( 'no_next_gen', __( 'This media is not an image and cannot be converted to next-gen format.', 'imagify' ) );
1718 }
1719
1720 if ( ! $media->has_backup() ) {
1721 return new WP_Error( 'no_backup', __( 'This media has no backup file.', 'imagify' ) );
1722 }
1723
1724 $data = $this->get_data();
1725
1726 if ( ! $data->is_optimized() && ! $data->is_already_optimized() ) {
1727 return new WP_Error( 'not_optimized', __( 'This media has not been optimized by Imagify yet.', 'imagify' ) );
1728 }
1729
1730 if ( $this->has_next_gen() ) {
1731 return new WP_Error( 'has_next_gen', __( 'This media already has next-gen versions.', 'imagify' ) );
1732 }
1733
1734 $files = $media->get_media_files();
1735 $sizes = [];
1736 $args = [
1737 'hook_suffix' => 'generate_nextgen_versions',
1738 ];
1739
1740 foreach ( $files as $size_name => $file ) {
1741 $formats = imagify_nextgen_images_formats();
1742
1743 foreach ( $formats as $format ) {
1744 if ( 'avif' === $format ) {
1745 $format_suffix = static::AVIF_SUFFIX;
1746 } elseif ( 'webp' === $format ) {
1747 $format_suffix = static::WEBP_SUFFIX;
1748 }
1749
1750 if ( $this->get_mime_type( $format ) === $files[ $size_name ]['mime-type'] ) {
1751 continue;
1752 }
1753
1754 array_unshift( $sizes, $size_name . $format_suffix );
1755 }
1756 }
1757
1758 if ( ! $sizes ) {
1759 return new WP_Error( 'no_sizes', __( 'This media does not have files that can be converted to next-gen format.', 'imagify' ) );
1760 }
1761
1762 $optimization_level = $data->get_optimization_level();
1763
1764 // Optimize.
1765 return $this->optimize_sizes( $sizes, $optimization_level, $args );
1766 }
1767
1768 /**
1769 * Tell if a process is running for this media.
1770 *
1771 * @since 1.9
1772 *
1773 * @return string|bool The action if locked ('optimizing' or 'restoring'). False if not locked.
1774 */
1775 public function is_locked() {
1776 $name = $this->get_lock_name();
1777
1778 if ( ! $name ) {
1779 return false;
1780 }
1781
1782 $callback = $this->get_media()->get_context_instance()->is_network_wide() ? 'get_site_transient' : 'get_transient';
1783 $action = call_user_func( $callback, $name );
1784
1785 if ( ! $action ) {
1786 return false;
1787 }
1788
1789 return $this->validate_lock_action( $action );
1790 }
1791
1792 /**
1793 * Set the running status to "running" for 10 minutes.
1794 *
1795 * @since 1.9
1796 *
1797 * @param string $action The action performed behind this lock: 'optimizing' or 'restoring'.
1798 */
1799 public function lock( $action = 'optimizing' ) {
1800 $name = $this->get_lock_name();
1801
1802 if ( ! $name ) {
1803 return;
1804 }
1805
1806 $action = $this->validate_lock_action( $action );
1807 $media = $this->get_media();
1808 $callback = $media->get_context_instance()->is_network_wide() ? 'set_site_transient' : 'set_transient';
1809
1810 call_user_func( $callback, $name, $action, 10 * MINUTE_IN_SECONDS );
1811 }
1812
1813 /**
1814 * Unset the running status.
1815 *
1816 * @since 1.9
1817 */
1818 public function unlock() {
1819 $name = $this->get_lock_name();
1820
1821 if ( ! $name ) {
1822 return false;
1823 }
1824
1825 $callback = $this->get_media()->get_context_instance()->is_network_wide() ? 'delete_site_transient' : 'delete_transient';
1826
1827 call_user_func( $callback, $name );
1828 }
1829
1830 /**
1831 * Get the name of the transient that stores the lock status.
1832 *
1833 * @since 1.9
1834 *
1835 * @return string|bool The name on success. False on failure.
1836 */
1837 protected function get_lock_name() {
1838 $media = $this->get_media();
1839
1840 if ( ! $media ) {
1841 return false;
1842 }
1843
1844 /**
1845 * Note that the site transient used by WP Background is named '*_process_lock'.
1846 * That would give something like 'imagify_optimize_media_process_lock' for the optimization process, while here it would be 'imagify_wp_42_process_locked'.
1847 */
1848 return sprintf( static::LOCK_NAME, $media->get_context(), $media->get_id() );
1849 }
1850
1851 /**
1852 * Validate the lock action.
1853 *
1854 * @since 1.9
1855 *
1856 * @param string $action The action performed behind this lock: 'optimizing' or 'restoring'.
1857 * @return string The valid action.
1858 */
1859 protected function validate_lock_action( $action ) {
1860 switch ( $action ) {
1861 case 'restore':
1862 case 'restoring':
1863 $action = 'restoring';
1864 break;
1865
1866 default:
1867 $action = 'optimizing';
1868 }
1869
1870 return $action;
1871 }
1872
1873 /**
1874 * Tell if a size already has optimization data.
1875 *
1876 * @since 1.9
1877 *
1878 * @param string $size The size name.
1879 * @return bool
1880 */
1881 public function size_has_optimization_data( $size ) {
1882 $data = $this->get_data()->get_optimization_data();
1883
1884 return ! empty( $data['sizes'][ $size ] );
1885 }
1886
1887 /**
1888 * Update the optimization data for a size.
1889 *
1890 * @since 1.9
1891 * @since 2.2 - Addition of the format in the call.
1892 *
1893 * @param object $response The API response.
1894 * @param string $size The size name.
1895 * @param int $level The optimization level (0=normal, 1=aggressive, 2=ultra).
1896 *
1897 * @return array {
1898 * The optimization data.
1899 *
1900 * @type int $level The optimization level.
1901 * @type string $status The status: 'success', 'already_optimized', 'error'.
1902 * @type bool $success True if successfully optimized. False on error or if already optimized.
1903 * @type string $error An error message.
1904 * @type int $original_size The weight of the file, before optimization.
1905 * @type int $optimized_size The weight of the file, once optimized.
1906 * }
1907 */
1908 public function update_size_optimization_data( $response, $size, $level ) {
1909 $disabled = false;
1910 $data = $this->data_format;
1911
1912 $data['level'] = is_numeric( $level ) ? (int) $level : $this->get_option( 'optimization_level' );
1913
1914 if ( is_wp_error( $response ) ) {
1915 /**
1916 * Error.
1917 */
1918 $disabled = 'unauthorized_size' === $response->get_error_code();
1919
1920 // Size data.
1921 $data['success'] = false;
1922 $data['error'] = $response->get_error_message();
1923
1924 // Status.
1925 if ( false !== strpos( $data['error'], 'This image is already compressed' ) ) {
1926 $data['status'] = 'already_optimized';
1927 } else {
1928 $data['status'] = 'error';
1929 }
1930 } else {
1931 /**
1932 * Success.
1933 */
1934 $response = (object) array_merge(
1935 [
1936 'original_size' => 0,
1937 'new_size' => 0,
1938 'percent' => 0,
1939 ],
1940 (array) $response
1941 );
1942
1943 // Status.
1944 $data['status'] = 'success';
1945 $data['error'] = null;
1946
1947 // Size data.
1948 $data['success'] = true;
1949
1950 if ( property_exists( $response, 'message' ) ) {
1951 $data['message'] = imagify_translate_api_message( $response->message );
1952 }
1953
1954 $data['original_size'] = $response->original_size;
1955 $data['optimized_size'] = $response->new_size;
1956 }
1957
1958 $_unauthorized = $disabled ? '_unauthorized' : '';
1959
1960 /**
1961 * Filter the optimization data.
1962 *
1963 * @since 1.9
1964 *
1965 * @param array $data {
1966 * The optimization data.
1967 *
1968 * @type int $level The optimization level.
1969 * @type string $status The status: 'success', 'already_optimized', 'error'.
1970 * @type bool $success True if successfully optimized. False on error or if already optimized.
1971 * @type string $error An error message.
1972 * @type int $original_size The weight of the file, before optimization.
1973 * @type int $optimized_size The weight of the file, once optimized.
1974 * }
1975 * @param object $response The API response.
1976 * @param string $size The size name.
1977 * @param int $level The optimization level.
1978 * @param object $media_data The DataInterface instance of the media.
1979 */
1980 $data = (array) apply_filters( "imagify{$_unauthorized}_file_optimization_data", $data, $response, $size, $level, $this->get_data() );
1981
1982 // Store.
1983 $this->get_data()->update_size_optimization_data( $size, $data );
1984
1985 return $data;
1986 }
1987
1988 /**
1989 * Get a plugin’s option.
1990 *
1991 * @since 1.9
1992 *
1993 * @param string $option_name The option name.
1994 *
1995 * @return mixed
1996 */
1997 protected function get_option( $option_name ) {
1998 if ( isset( $this->options[ $option_name ] ) ) {
1999 return $this->options[ $option_name ];
2000 }
2001
2002 $this->options[ $option_name ] = get_imagify_option( $option_name );
2003
2004 return $this->options[ $option_name ];
2005 }
2006
2007 /**
2008 * Sanitize and validate an optimization level.
2009 * If not provided (false, null), fallback to the level set in the plugin's settings.
2010 *
2011 * @since 1.9
2012 *
2013 * @param mixed $optimization_level The optimization level.
2014 *
2015 * @return int
2016 */
2017 protected function sanitize_optimization_level( $optimization_level ) {
2018 if ( ! is_numeric( $optimization_level ) ) {
2019 if ( $this->get_option( 'lossless' ) ) {
2020 return 0;
2021 }
2022
2023 return $this->get_option( 'optimization_level' );
2024 }
2025
2026 return \Imagify_Options::get_instance()->sanitize_and_validate( 'optimization_level', $optimization_level );
2027 }
2028
2029 /**
2030 * Tell if the media has AVIF versions.
2031 *
2032 * @since 2.2
2033 *
2034 * @return bool
2035 */
2036 public function has_avif() {
2037 if ( ! $this->is_valid() ) {
2038 return false;
2039 }
2040
2041 if ( ! $this->get_media()->is_image() ) {
2042 return false;
2043 }
2044
2045 $data = $this->get_data()->get_optimization_data();
2046
2047 if ( empty( $data['sizes'] ) ) {
2048 return false;
2049 }
2050
2051 $needle = static::AVIF_SUFFIX . '";a:4:{s:7:"success";b:1;';
2052 $data = maybe_serialize( $data['sizes'] );
2053
2054 return is_string( $data ) && strpos( $data, $needle );
2055 }
2056 }
2057