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

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