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

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