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

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