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

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

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