| @@ -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. |