PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / trunk
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF vtrunk
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
← All changes | classes/Optimization/File.php +113 -9 2.3.1 → trunk View file →
@@ -627,21 +627,58 @@
627 627 if ( is_wp_error( $temp_file ) ) {
628 628 return new \WP_Error( 'temp_file_not_found', $temp_file->get_error_message() );
629 629 }
630 630
631 - if ( property_exists( $response, 'message' ) ) {
631 + $formats = [
632 + 'webp',
633 + 'avif',
634 + ];
635 + $is_nextgen_request = in_array( $args['convert'], $formats, true );
636 +
637 + if ( property_exists( $response, 'message' ) && ! $is_nextgen_request ) {
632 638 $args['convert'] = '';
633 639 }
634 640
635 - $formats = [
636 - 'webp',
637 - 'avif',
638 - ];
639 - if ( in_array( $args['convert'], $formats, true ) ) {
641 + if ( $is_nextgen_request && property_exists( $response, 'message' ) ) {
642 + /*
643 + * The API can return a `message` alongside the source's own bytes instead of a
644 + * converted file (e.g. "Webp is less performant than original" or "already
645 + * compressed"). In that case the downloaded file is NOT the requested next-gen
646 + * format. Writing it to the next-gen path would create a corrupt file, and
647 + * writing it to `$this->path` would overwrite the original thumbnail. Verify the
648 + * actual bytes before doing either.
649 + */
650 + if ( ! $this->is_file_format( $temp_file, $args['convert'] ) ) {
651 + $this->filesystem->delete( $temp_file );
652 +
653 + return new \WP_Error(
654 + 'no_next_gen_returned',
655 + $response->message
656 + );
657 + }
658 + }
659 +
660 + if ( $is_nextgen_request ) {
640 661 $destination_path = $this->get_path_to_nextgen( $args['convert'] );
641 - $this->path = $destination_path;
642 - $this->file_type = null;
643 - $this->editor = null;
662 +
663 + /*
664 + * get_path_to_nextgen() returns false when the file is not an image, or when it
665 + * already is in a next-gen format. Passing that along would hand an empty path to
666 + * the filesystem, which throws an uncaught ValueError on PHP 8 and kills the
667 + * background process mid-batch, leaving its lock behind and stalling the queue.
668 + */
669 + if ( empty( $destination_path ) ) {
670 + $this->filesystem->delete( $temp_file );
671 +
672 + return new \WP_Error(
673 + 'no_nextgen_destination',
674 + __( 'Could not determine the destination path for the next-gen file.', 'imagify' )
675 + );
676 + }
677 +
678 + $this->path = $destination_path;
679 + $this->file_type = null;
680 + $this->editor = null;
644 681 } else {
645 682 $destination_path = $this->path;
646 683 }
647 684
@@ -911,8 +948,75 @@
911 948 * @return bool
912 949 */
913 950 public function is_avif() {
914 951 return preg_match( '@(?!^|/|\\\)\.avif$@i', $this->path );
952 + }
953 +
954 + /**
955 + * Tell if a file's actual content matches the given next-gen format, by reading its
956 + * magic bytes. The file's extension can't be trusted here, since it may be a temp
957 + * file downloaded with an unpredictable name (see download_url()).
958 + *
959 + * @since 2.3.2
960 + *
961 + * @param string $file_path Absolute path to the file to check.
962 + * @param string $format 'webp' or 'avif'.
963 + * @return bool
964 + */
965 + protected function is_file_format( $file_path, $format ) {
966 + $contents = $this->filesystem->get_contents( $file_path );
967 +
968 + if ( ! is_string( $contents ) || strlen( $contents ) < 12 ) {
969 + return false;
970 + }
971 +
972 + if ( 'webp' === $format ) {
973 + // RIFF....WEBP.
974 + return 'RIFF' === substr( $contents, 0, 4 ) && 'WEBP' === substr( $contents, 8, 4 );
975 + }
976 +
977 + if ( 'avif' === $format ) {
978 + return $this->is_avif_ftyp_box( $contents );
979 + }
980 +
981 + return false;
982 + }
983 +
984 + /**
985 + * Tell if the given content starts with an AVIF `ftyp` box, i.e. its major brand or one of
986 + * its compatible brands is `avif`/`avis`. A file can legitimately declare a major brand of
987 + * `mif1`/`msf1` (generic HEIF-family brands) while listing `avif` only among the compatible
988 + * brands, so both must be checked.
989 + *
990 + * @since 2.3.2
991 + *
992 + * @param string $contents The file content (or at least its leading bytes).
993 + * @return bool
994 + */
995 + private function is_avif_ftyp_box( $contents ) {
996 + if ( 'ftyp' !== substr( $contents, 4, 4 ) ) {
997 + return false;
998 + }
999 +
1000 + $avif_brands = [ 'avif', 'avis' ];
1001 +
1002 + if ( in_array( substr( $contents, 8, 4 ), $avif_brands, true ) ) {
1003 + // Major brand.
1004 + return true;
1005 + }
1006 +
1007 + // Box size (big-endian uint32), bounding how far the compatible brands list extends.
1008 + $box_size = unpack( 'N', substr( $contents, 0, 4 ) )[1];
1009 + $box_size = min( $box_size, strlen( $contents ) );
1010 +
1011 + // Compatible brands: 4-byte entries, starting right after the minor version, at offset 16.
1012 + for ( $offset = 16; $offset + 4 <= $box_size; $offset += 4 ) {
1013 + if ( in_array( substr( $contents, $offset, 4 ), $avif_brands, true ) ) {
1014 + return true;
1015 + }
1016 + }
1017 +
1018 + return false;
915 1019 }
916 1020
917 1021 /**
918 1022 * Get the file mime type + file extension.