Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 99
0.00% covered (danger)
0.00%
0 / 8
CRAP
n/a
0 / 0
jetpack_wpcom_maybe_convert_heif_to_jpg
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 1
42
jetpack_wpcom_maybe_log_heif_to_jpg
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
jetpack_wpcom_transparently_convert_heif_upload_to_jpg
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
jetpack_wpcom_add_heif_mimes_to_supported_sideload_types
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
jetpack_wpcom_add_heif_mimes_to_supported_upload_types
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
jetpack_wpcom_allow_heif_uploads_in_rest_api
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
jetpack_wpcom_add_heif_rest_api_filter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
jetpack_wpcom_disable_heic_plupload_error
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * HEIF/HEIF support for WordPress.com sites.
4 *
5 * @package automattic/jetpack-mu-plugins
6 */
7
8/**
9 * Convert HEIF/HEIC uploads to JPEG.
10 *
11 * @param string $filename Path to the file.
12 * @return bool True if the file was converted, false otherwise.
13 */
14function jetpack_wpcom_maybe_convert_heif_to_jpg( $filename ) {
15    if ( ! class_exists( 'Photon_OpenCV' ) ) {
16        return false;
17    }
18
19    if ( empty( $filename ) ) {
20        jetpack_wpcom_maybe_log_heif_to_jpg(
21            array(
22                'message'  => 'file path is empty',
23                'severity' => 'error',
24            )
25        );
26        return false;
27    }
28
29    if ( ! file_exists( $filename ) ) {
30        jetpack_wpcom_maybe_log_heif_to_jpg(
31            array(
32                'message'  => sprintf( 'file does not exist: %s', $filename ),
33                'severity' => 'error',
34            )
35        );
36        return false;
37    }
38
39    $valid_magic_bytes = array(
40        'ftypheic',
41        'ftypheix',
42        'ftyphevc',
43        'ftypheim',
44        'ftypheis',
45        'ftyphevm',
46        'ftyphevs',
47        'ftypmif1',
48        'ftypmsf1',
49    );
50
51    // Read the first 8 bytes of the file.
52    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
53    $magic_bytes = file_get_contents( $filename, false, null, 4, 8 );
54    if ( ! in_array( $magic_bytes, $valid_magic_bytes, true ) ) {
55        return false;
56    }
57
58    // Log that we found a HEIF image.
59    jetpack_wpcom_maybe_log_heif_to_jpg(
60        array(
61            'message'  => sprintf( 'Found HEIF image: %s with magic bytes: %s', $filename, $magic_bytes ),
62            'severity' => 'info',
63        )
64    );
65
66    $img = new Photon_OpenCV();
67    try {
68        $img->readimage( $filename );
69    } catch ( Exception $e ) {
70        // Bad detection or malformed image
71        /** This action is documented in modules/widgets/social-media-icons.php */
72        do_action( 'jetpack_bump_stats_extras', 'heif2jpg', 'failed-to-read' );
73        jetpack_wpcom_maybe_log_heif_to_jpg(
74            array(
75                'message'    => sprintf( 'failed to read image: %s', $e->getMessage() ),
76                'error_code' => $e->getCode(),
77                'severity'   => 'error',
78            )
79        );
80        return false;
81    }
82
83    $img->setimageformat( 'jpg' );
84    // This should never fail
85    $img->writeimage( $filename );
86
87    return true;
88}
89
90/**
91 * Log information about HEIF/HEIC conversions.
92 *
93 * @param array $params The parameters to log.
94 *
95 * @return void
96 */
97function jetpack_wpcom_maybe_log_heif_to_jpg( $params = array() ) {
98
99    if ( ! file_exists( WP_CONTENT_DIR . '/lib/log2logstash/log2logstash.php' ) ) {
100        return;
101    }
102
103    require_once WP_CONTENT_DIR . '/lib/log2logstash/log2logstash.php';
104
105    $default = array(
106        'feature' => 'heif2jpg',
107        'blog_id' => get_current_blog_id(),
108    );
109
110    log2logstash( wp_parse_args( $params, $default ) );
111}
112
113/**
114 * Attempts to convert HEIF/HEIC uploads to JPEG.
115 *
116 * @param array $file The file array.
117 * @return array The file array.
118 */
119function jetpack_wpcom_transparently_convert_heif_upload_to_jpg( $file ) {
120    if ( ! class_exists( 'Photon_OpenCV' ) ) {
121        return $file;
122    }
123
124    // $file only has `name` and `tmp_name` when sideloading
125    $original_size = filesize( $file['tmp_name'] );
126
127    if ( false === jetpack_wpcom_maybe_convert_heif_to_jpg( $file['tmp_name'] ) ) {
128        return $file;
129    }
130
131    // tmp_name is reused, cache needs to be cleared
132    clearstatcache();
133    $new_file = array(
134        'name'     => pathinfo( $file['name'], PATHINFO_FILENAME ) . '.jpg',
135        'type'     => 'image/jpeg',
136        'tmp_name' => $file['tmp_name'],
137        'error'    => 0,
138        'size'     => filesize( $file['tmp_name'] ),
139    );
140
141    /** This action is documented in modules/widgets/social-media-icons.php */
142    do_action( 'jetpack_bump_stats_extras', 'heif2jpg', 'conversions' );
143    /** This action is documented in modules/widgets/social-media-icons.php */
144    do_action( 'jetpack_bump_stats_extras', 'heif2jpg', 'bytes-added', $new_file['size'] - $original_size );
145
146    return $new_file;
147}
148add_filter( 'wp_handle_upload_prefilter', 'jetpack_wpcom_transparently_convert_heif_upload_to_jpg' );
149add_filter( 'wp_handle_sideload_prefilter', 'jetpack_wpcom_transparently_convert_heif_upload_to_jpg' );
150
151/**
152 * Add HEIF/HEIC to the list of supported mime types for sideloading.
153 *
154 * @param array $mimes The list of supported mime types.
155 * @return array The list of supported mime types.
156 */
157function jetpack_wpcom_add_heif_mimes_to_supported_sideload_types( $mimes ) {
158    if ( ! class_exists( 'Photon_OpenCV' ) ) {
159        return $mimes;
160    }
161    $mimes[] = 'image/heif';
162    $mimes[] = 'image/heic';
163    return $mimes;
164}
165add_filter( 'jetpack_supported_media_sideload_types', 'jetpack_wpcom_add_heif_mimes_to_supported_sideload_types' );
166
167/**
168 * Add HEIF/HEIC to the list of supported mime types for uploads.
169 *
170 * @param array $mimes The list of supported mime types.
171 * @return array The list of supported mime types.
172 */
173function jetpack_wpcom_add_heif_mimes_to_supported_upload_types( $mimes ) {
174    if ( ! class_exists( 'Photon_OpenCV' ) ) {
175        return $mimes;
176    }
177
178    $mimes['heif'] = 'image/heif';
179
180    // HEIC is supported by default, so maybe we don't need it anymore.
181    // See https://github.com/WordPress/wordpress-develop/blob/80b7747ef165dd5ed0150003a8c2f957f097609e/src/wp-includes/functions.php#L3416.
182    $mimes['heic'] = 'image/heic';
183    return $mimes;
184}
185add_filter( 'upload_mimes', 'jetpack_wpcom_add_heif_mimes_to_supported_upload_types' );
186
187/**
188 * Prevent WordPress from blocking HEIF/HEIC uploads in REST API.
189 *
190 * This allows HEIF/HEIC files to pass through the REST API validation
191 * so they can be converted to JPEG by our conversion filter.
192 *
193 * @param bool        $check_mime Whether to check the mime type.
194 * @param string|null $mime_type  The mime type being uploaded.
195 * @return bool
196 */
197function jetpack_wpcom_allow_heif_uploads_in_rest_api( $check_mime, $mime_type ) {
198    if ( wp_is_heic_image_mime_type( $mime_type ) ) {
199        return false;
200    }
201
202    return $check_mime;
203}
204
205/**
206 * Add the HEIF/HEIC upload filter on REST API initialization.
207 */
208function jetpack_wpcom_add_heif_rest_api_filter() {
209    add_filter( 'wp_prevent_unsupported_mime_type_uploads', 'jetpack_wpcom_allow_heif_uploads_in_rest_api', 10, 2 );
210}
211add_action( 'rest_api_init', 'jetpack_wpcom_add_heif_rest_api_filter' );
212
213/**
214 * Disable HEIC upload error in plupload settings for WordPress.com.
215 *
216 * @param array $defaults The default plupload settings.
217 * @return array The modified settings.
218 */
219function jetpack_wpcom_disable_heic_plupload_error( $defaults ) {
220    $defaults['heic_upload_error'] = false;
221    return $defaults;
222}
223add_filter( 'plupload_default_settings', 'jetpack_wpcom_disable_heic_plupload_error' );