PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.2-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.2-a.1
16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / jetpack_vendor / automattic / jetpack-image-cdn / src / class-image-cdn.php
jetpack / jetpack_vendor / automattic / jetpack-image-cdn / src Last commit date
compatibility 8 months ago class-image-cdn-core.php 8 months ago class-image-cdn-image-sizes.php 1 month ago class-image-cdn-image.php 1 month ago class-image-cdn-setup.php 1 year ago class-image-cdn.php 1 month ago
class-image-cdn.php
1485 lines
1 <?php
2 /**
3 * Class for photon functionality.
4 *
5 * @package automattic/jetpack-image-cdn
6 */
7
8 namespace Automattic\Jetpack\Image_CDN;
9
10 /**
11 * Class Image_CDN
12 */
13 final class Image_CDN {
14
15 const PACKAGE_VERSION = '0.7.29';
16
17 /**
18 * Singleton.
19 *
20 * @var null
21 */
22 private static $instance = null;
23
24 /**
25 * Allowed extensions.
26 *
27 * @var string[] Allowed extensions must match https://code.trac.wordpress.org/browser/photon/index.php#L41
28 */
29 protected static $extensions = array(
30 'gif',
31 'jpg',
32 'jpeg',
33 'png',
34 // Jetpack assumes Photon_OpenCV backend class is being used on the server. See link in docblock.
35 'webp', // Photon_OpenCV supports webp with libwebp-*, getimageformat() returns webp
36 'heic', // Photon_OpenCV supports webp with libheif-*, getimageformat() returns jpeg so does not match docblock
37 );
38
39 /**
40 * Image sizes.
41 *
42 * Don't access this directly. Instead, use self::image_sizes() so it's actually populated with something.
43 *
44 * @var array Image sizes.
45 */
46 private static $image_sizes = null;
47
48 /**
49 * Whether Image CDN is enabled or not.
50 *
51 * This class will be instantiated if any plugin has activated image CDN module. Keeping this variable to check if module is active or not.
52 *
53 * @var bool Whether Image CDN is enabled or not.
54 */
55 private static $is_enabled = false;
56
57 /**
58 * Singleton implementation
59 *
60 * @return object
61 */
62 public static function instance() {
63 if ( ! self::$instance instanceof self ) {
64 self::$instance = new self();
65 self::$instance->setup();
66 self::$is_enabled = true;
67 }
68
69 return self::$instance;
70 }
71
72 /**
73 * Silence is golden.
74 */
75 private function __construct() {}
76
77 /**
78 * Check if image CDN is enabled as a module from Jetpack or any other plugin.
79 */
80 public static function is_enabled() {
81 return self::$is_enabled;
82 }
83
84 /**
85 * Register actions and filters, but only if basic Photon functions are available.
86 * The basic functions are found in ./functions.photon.php.
87 *
88 * @uses add_action, add_filter
89 * @return void
90 */
91 private function setup() {
92 /**
93 * Add a filter to easily apply image CDN urls without applying all `the_content` filters to any content.
94 *
95 * Since this is only applied if the module is active in Jetpack or any other plugin, it's a safe option to apply photon urls to any content.
96 */
97 add_filter( 'jetpack_image_cdn_content', array( __CLASS__, 'filter_the_content' ), 10 );
98
99 // Images in post content and galleries and widgets.
100 add_filter( 'the_content', array( __CLASS__, 'filter_the_content' ), 999999 );
101 add_filter( 'get_post_galleries', array( __CLASS__, 'filter_the_galleries' ), 999999 );
102 add_filter( 'widget_media_image_instance', array( __CLASS__, 'filter_the_image_widget' ), 999999 );
103 add_filter( 'widget_text', array( __CLASS__, 'filter_the_content' ) );
104
105 // Core image retrieval.
106 add_filter( 'image_downsize', array( $this, 'filter_image_downsize' ), 10, 3 );
107 add_filter( 'rest_request_before_callbacks', array( $this, 'should_rest_photon_image_downsize' ), 10, 3 );
108 add_action( 'rest_after_insert_attachment', array( $this, 'should_rest_photon_image_downsize_insert_attachment' ), 10, 2 );
109 add_filter( 'rest_request_after_callbacks', array( $this, 'cleanup_rest_photon_image_downsize' ) );
110
111 // Responsive image srcset substitution.
112 add_filter( 'wp_calculate_image_srcset', array( $this, 'filter_srcset_array' ), 10, 5 );
113 add_filter( 'wp_calculate_image_sizes', array( $this, 'filter_sizes' ), 1, 3 ); // Early so themes can still easily filter.
114
115 /**
116 * Allow Photon to disable uploaded images resizing and use its own resize capabilities instead.
117 *
118 * @module photon
119 *
120 * @since 7.1.0
121 *
122 * @param bool false Should Photon enable noresize mode. Default to false.
123 */
124 if ( apply_filters( 'jetpack_photon_noresize_mode', false ) ) {
125 $this->enable_noresize_mode();
126 }
127 }
128
129 /**
130 * Enables the noresize mode for Photon, allowing to avoid intermediate size files generation.
131 */
132 private function enable_noresize_mode() {
133 // The main objective of noresize mode is to disable additional resized image versions creation.
134 // This filter handles removal of additional sizes.
135 add_filter( 'intermediate_image_sizes_advanced', array( __CLASS__, 'filter_photon_noresize_intermediate_sizes' ) );
136
137 // Load the noresize srcset solution on priority of 20, allowing other plugins to set sizes earlier.
138 add_filter( 'wp_get_attachment_metadata', array( __CLASS__, 'filter_photon_norezise_maybe_inject_sizes' ), 20, 2 );
139
140 // Photonize thumbnail URLs in the API response.
141 add_filter( 'rest_api_thumbnail_size_urls', array( __CLASS__, 'filter_photon_noresize_thumbnail_urls' ) );
142
143 // This allows to assign the Photon domain to images that normally use the home URL as base.
144 add_filter( 'jetpack_photon_domain', array( __CLASS__, 'filter_photon_norezise_domain' ), 10, 2 );
145
146 add_filter( 'the_content', array( __CLASS__, 'filter_content_add' ), 0 );
147
148 // Jetpack hooks in at six nines (999999) so this filter does at seven.
149 add_filter( 'the_content', array( __CLASS__, 'filter_content_remove' ), 9999999 );
150
151 // Regular Photon operation mode filter doesn't run when is_admin(), so we need an additional filter.
152 // This is temporary until Jetpack allows more easily running these filters for is_admin().
153 if ( is_admin() ) {
154 add_filter( 'image_downsize', array( $this, 'filter_image_downsize' ), 5, 3 );
155
156 // Allows any image that gets passed to Photon to be resized via Photon.
157 add_filter( 'jetpack_photon_admin_allow_image_downsize', '__return_true' );
158 }
159 }
160
161 /**
162 * This is our catch-all to strip dimensions from intermediate images in content.
163 * Since this primarily only impacts post_content we do a little dance to add the filter early
164 * to `the_content` and then remove it later on in the same hook.
165 *
166 * @param String $content the post content.
167 * @return String the post content unchanged.
168 */
169 public static function filter_content_add( $content ) {
170 add_filter( 'jetpack_photon_pre_image_url', array( __CLASS__, 'strip_image_dimensions_maybe' ) );
171 return $content;
172 }
173
174 /**
175 * Removing the content filter that was set previously.
176 *
177 * @param String $content the post content.
178 * @return String the post content unchanged.
179 */
180 public static function filter_content_remove( $content ) {
181 remove_filter( 'jetpack_photon_pre_image_url', array( __CLASS__, 'strip_image_dimensions_maybe' ) );
182 return $content;
183 }
184
185 /**
186 * Short circuits the Photon filter to enable Photon processing for any URL.
187 *
188 * @param String $photon_url a proposed Photon URL for the media file.
189 *
190 * @return String an URL to be used for the media file.
191 */
192 public static function filter_photon_norezise_domain( $photon_url ) {
193 return $photon_url;
194 }
195
196 /**
197 * Disables intermediate sizes to disallow resizing.
198 *
199 * @return array Empty array.
200 */
201 public static function filter_photon_noresize_intermediate_sizes() {
202 return array();
203 }
204
205 /**
206 * Filter thumbnail URLS to not generate.
207 *
208 * @param array $sizes Image sizes.
209 *
210 * @return mixed
211 */
212 public static function filter_photon_noresize_thumbnail_urls( $sizes ) {
213 foreach ( $sizes as $size => $url ) {
214 $parts = explode( '?', $url );
215 $arguments = $parts[1] ?? array();
216
217 $sizes[ $size ] = Image_CDN_Core::cdn_url( $url, wp_parse_args( $arguments ) );
218 }
219
220 return $sizes;
221 }
222
223 /**
224 * Inject image sizes to attachment metadata.
225 *
226 * @param array $data Attachment metadata.
227 * @param int $attachment_id Attachment's post ID.
228 *
229 * @return array Attachment metadata.
230 */
231 public static function filter_photon_norezise_maybe_inject_sizes( $data, $attachment_id ) {
232 // Can't do much if data is empty.
233 if ( empty( $data ) ) {
234 return $data;
235 }
236 $sizes_already_exist = (
237 is_array( $data )
238 && array_key_exists( 'sizes', $data )
239 && is_array( $data['sizes'] )
240 && ! empty( $data['sizes'] )
241 );
242 if ( $sizes_already_exist ) {
243 return $data;
244 }
245 // Missing some critical data we need to determine sizes, not processing.
246 if ( ! isset( $data['file'] )
247 || ! isset( $data['width'] )
248 || ! isset( $data['height'] )
249 ) {
250 return $data;
251 }
252
253 $mime_type = get_post_mime_type( $attachment_id );
254 $attachment_is_image = preg_match( '!^image/!', $mime_type );
255
256 if ( 1 === $attachment_is_image ) {
257 $image_sizes = new Image_CDN_Image_Sizes( $attachment_id, $data );
258 $data['sizes'] = $image_sizes->generate_sizes_meta();
259 }
260 return $data;
261 }
262
263 /**
264 * Inject image sizes to Jetpack REST API responses. This wraps the filter_photon_norezise_maybe_inject_sizes function.
265 *
266 * @param array $sizes Attachment sizes data.
267 * @param int $attachment_id Attachment's post ID.
268 *
269 * @return array Attachment sizes array.
270 */
271 public static function filter_photon_norezise_maybe_inject_sizes_api( $sizes, $attachment_id ) {
272 return self::filter_photon_norezise_maybe_inject_sizes( wp_get_attachment_metadata( $attachment_id ), $attachment_id );
273 }
274
275 /**
276 * * IN-CONTENT IMAGE MANIPULATION FUNCTIONS
277 **/
278
279 /**
280 * Match all images and any relevant <a> tags in a block of HTML.
281 *
282 * @param string $content Some HTML.
283 * @return array An array of $images matches, where $images[0] is
284 * an array of full matches, and the link_url, img_tag,
285 * and img_url keys are arrays of those matches.
286 */
287 public static function parse_images_from_html( $content ) {
288 $images = array();
289
290 if ( preg_match_all( '#(?:<a[^>]*?\s+?href=["\'](?P<link_url>[^\s]+?)["\'][^>]*?>\s*)?(?P<img_tag><(?:img|amp-img|amp-anim)[^>]*?\s+?src=["\'](?P<img_url>[^\s]+?)["\'].*?>){1}(?:\s*</a>)?#is', $content, $images ) ) {
291 foreach ( $images as $key => $unused ) {
292 // Simplify the output as much as possible, mostly for confirming test results.
293 if ( is_numeric( $key ) && $key > 0 ) {
294 unset( $images[ $key ] );
295 }
296 }
297
298 return $images;
299 }
300
301 return array();
302 }
303
304 /**
305 * Try to determine height and width from strings WP appends to resized image filenames.
306 *
307 * @param string $src The image URL.
308 * @return array An array consisting of width and height.
309 */
310 public static function parse_dimensions_from_filename( $src ) {
311 $width_height_string = array();
312
313 if ( preg_match( '#-(\d+)x(\d+)\.(?:' . implode( '|', self::$extensions ) . '){1}(?:\?.*)?$#i', $src, $width_height_string ) ) {
314 $width = (int) $width_height_string[1];
315 $height = (int) $width_height_string[2];
316
317 if ( $width && $height ) {
318 return array( $width, $height );
319 }
320 }
321
322 return array( false, false );
323 }
324
325 /**
326 * Identify images in post content, and if images are local (uploaded to the current site), pass through Photon.
327 *
328 * @param string|mixed $content The content; should be a string but will convert to an empty string if not.
329 *
330 * @uses self::validate_image_url, apply_filters, Image_CDN_Core::cdn_url, esc_url
331 * @filter the_content
332 *
333 * @return string The content.
334 */
335 public static function filter_the_content( $content ) {
336 // Early return if content is empty or not a string.
337 if ( ! is_string( $content ) || '' === $content ) {
338 return '';
339 }
340
341 static $image_tags = array( 'IMG', 'AMP-IMG', 'AMP-ANIM' );
342 $content_width = null;
343 $image_sizes = null;
344 $upload_dir = null;
345 $processor = new \WP_HTML_Tag_Processor( $content );
346 $nearest_preceding_href = null;
347
348 // Visit every image-containing tag in the document.
349 while ( $processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
350 /*
351 * When an image is wrapped by an A element, the goal is to modify
352 * both elements. Thus it's important to track links that may be
353 * opened before reaching any image. In normative HTML this detection
354 * is reliable enough, but it could be confused in cases where other
355 * tags implicitly close the A. If this additional reliability is
356 * required, replace the Tag Processor with the HTML Processor.
357 */
358 if ( 'A' === $processor->get_tag() ) {
359 // If this is a closing tag the attribute will be `null`.
360 $nearest_preceding_href = $processor->get_attribute( 'href' );
361 $processor->set_bookmark( 'link' );
362 continue;
363 }
364
365 // Identify image source.
366 $src_orig = $processor->get_attribute( 'src' );
367 $src = $src_orig;
368 $placeholder_src = null;
369 $placeholder_src_orig = null;
370
371 /*
372 * Only examine tags that are considered an image,
373 * with a valid src attribute.
374 * If encountering a closing tag then this is not the image being sought.
375 */
376 if (
377 $processor->is_tag_closer()
378 || ! in_array( $processor->get_tag(), $image_tags, true )
379 || ! is_string( $src )
380 || $src === ''
381 ) {
382 continue;
383 }
384
385 $processor->set_bookmark( 'image' );
386
387 /*
388 * At this point a target image has been found. Initialize the
389 * shared data and then process each image as it appears.
390 */
391 if ( null === $content_width ) {
392 $content_width = Image_CDN_Core::get_jetpack_content_width();
393 $image_sizes = self::image_sizes();
394 $upload_dir = wp_get_upload_dir();
395 }
396
397 /*
398 * To preserve legacy behaviors for filtering by third-party plugins,
399 * create a normalized HTML string representing the tag. This will
400 * present all attributes as double-quoted attributes and include at
401 * most one copy of each attribute, escaping all values appropriately.
402 */
403 $tag_name = strtolower( (string) $processor->get_tag() );
404 $tag = new \WP_HTML_Tag_Processor( "<{$tag_name}>" );
405 $tag->next_tag();
406 foreach ( $processor->get_attribute_names_with_prefix( '' ) ?? array() as $name ) {
407 $tag->set_attribute( $name, $processor->get_attribute( $name ) );
408 }
409 $tag = $tag->get_updated_html();
410
411 // Default to resize, though fit may be used in certain cases where a dimension cannot be ascertained.
412 $transform = 'resize';
413
414 // Flag if we need to munge a fullsize URL.
415 $fullsize_url = false;
416
417 /**
418 * Allow specific images to be skipped by Photon.
419 *
420 * @module photon
421 *
422 * @since 2.0.3
423 *
424 * @param bool false Should Photon ignore this image. Default to false.
425 * @param string $src Image URL.
426 * @param string|array|null $tag Image Tag (Image HTML output) or array of image details for srcset.
427 */
428 if ( apply_filters( 'jetpack_photon_skip_image', false, $src, $tag ) ) {
429 continue;
430 }
431
432 $data_lazy_src = $processor->get_attribute( 'data-lazy-src' );
433 $data_lazy_original = $processor->get_attribute( 'data-lazy-original' );
434
435 $source_type = 'src';
436 $chosen_data_src = null;
437
438 // Prefer a URL from the `data-lazy-src` attribute.
439 if ( null === $chosen_data_src && is_string( $data_lazy_src ) && ! empty( $data_lazy_src ) ) {
440 $source_type = 'data-lazy-src';
441 $chosen_data_src = $data_lazy_src;
442 }
443
444 // Fall back to a URL from the `data-lazy-original` attribute.
445 if ( null === $chosen_data_src && is_string( $data_lazy_original ) && ! empty( $data_lazy_original ) ) {
446 $source_type = 'data-lazy-original';
447 $chosen_data_src = $data_lazy_original;
448 }
449
450 // Update the src if one was provided in the `data-lazy-` attributes.
451 if ( 'src' !== $source_type ) {
452 $placeholder_src_orig = $src;
453 $placeholder_src = $placeholder_src_orig;
454 $src_orig = $chosen_data_src;
455 $src = $src_orig;
456 }
457
458 // Check if image URL should be used with Photon.
459 if ( self::validate_image_url( $src ) ) {
460 $width = $processor->get_attribute( 'width' );
461 $height = $processor->get_attribute( 'height' );
462
463 // First, check the image tag. Note we only check for pixel sizes now; HTML4 percentages have never been correctly
464 // supported, so we stopped pretending to support them in JP 9.1.0.
465 if ( ! is_string( $width ) || ! ctype_digit( $width ) ) {
466 $width = false;
467 }
468
469 if ( ! is_string( $height ) || ! ctype_digit( $height ) ) {
470 $height = false;
471 }
472
473 $needs_sizing = false === $width && false === $height;
474 $size = null;
475
476 if ( $needs_sizing ) {
477 // Find the first CSS class listed with a prefix of `size-`, e.g. `size-full-width`
478 foreach ( $processor->class_list() ?? array() as $class_name ) {
479 if ( str_starts_with( $class_name, 'size-' ) ) {
480 $size = substr( $class_name, strlen( 'size-' ) );
481 break;
482 }
483 }
484 }
485
486 if ( $needs_sizing && 'full' !== $size && is_string( $size ) && isset( $image_sizes[ $size ] ) && is_array( $image_sizes[ $size ] ) ) {
487 $width = (int) $image_sizes[ $size ]['width'];
488 $height = (int) $image_sizes[ $size ]['height'];
489 $transform = $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
490 } else {
491 unset( $size );
492 }
493
494 // WP Attachment ID, if uploaded to this site.
495 $attachment_id = null;
496 foreach ( $processor->class_list() ?? array() as $class_name ) {
497 if ( str_starts_with( $class_name, 'wp-image-' ) ) {
498 $attachment_id = substr( $class_name, strlen( 'wp-image-' ) );
499 break;
500 }
501 }
502
503 // These values have not been used for a very long time, but removing them could break something.
504 $images = array();
505 $index = 0;
506 if (
507 $attachment_id &&
508 preg_match( '#^[1-9][0-9]*$#', $attachment_id ) &&
509 is_array( $upload_dir ) &&
510 str_starts_with( $src, $upload_dir['baseurl'] ) &&
511 /**
512 * Filter whether an image using an attachment ID in its class has to be uploaded to the local site to go through Photon.
513 *
514 * @module photon
515 *
516 * @since 2.0.3
517 *
518 * @param bool false Was the image uploaded to the local site. Default to false.
519 * @param array $args {
520 * Array of image details.
521 *
522 * @type $src Image URL.
523 * @type tag Image tag (Image HTML output).
524 * }
525 */
526 apply_filters( 'jetpack_photon_image_is_local', false, compact( 'src', 'tag', 'images', 'index' ) )
527 ) {
528 $attachment_id = (int) $attachment_id;
529 $attachment = get_post( $attachment_id );
530
531 // Basic check on returned post object.
532 if ( is_object( $attachment ) && ! is_wp_error( $attachment ) && 'attachment' === $attachment->post_type ) {
533 $src_per_wp = wp_get_attachment_image_src( $attachment_id, $size ?? 'full' );
534
535 if ( self::validate_image_url( $src_per_wp[0] ) ) {
536 $src = $src_per_wp[0];
537 $fullsize_url = true;
538
539 // Prevent image distortion if a detected dimension exceeds the image's natural dimensions.
540 if ( ( false !== $width && $width > $src_per_wp[1] ) || ( false !== $height && $height > $src_per_wp[2] ) ) {
541 $width = false === $width ? false : min( $width, $src_per_wp[1] );
542 $height = false === $height ? false : min( $height, $src_per_wp[2] );
543 }
544
545 // If no width and height are found, max out at source image's natural dimensions.
546 // Otherwise, respect registered image sizes' cropping setting.
547 if ( false === $width && false === $height ) {
548 $width = $src_per_wp[1];
549 $height = $src_per_wp[2];
550 $transform = 'fit';
551 } elseif ( isset( $size ) && is_array( $image_sizes ) && array_key_exists( $size, $image_sizes ) && isset( $image_sizes[ $size ]['crop'] ) ) {
552 $transform = $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
553 }
554 }
555 } else {
556 unset( $attachment_id );
557 unset( $attachment );
558 }
559 }
560
561 // If image tag lacks width and height arguments, try to determine from strings WP appends to resized image filenames.
562 if ( false === $width && false === $height ) {
563 list( $width, $height ) = self::parse_dimensions_from_filename( $src );
564 }
565
566 $width_orig = $width;
567 $height_orig = $height;
568 $transform_orig = $transform;
569
570 // If width is available, constrain to $content_width.
571 if ( false !== $width && is_numeric( $content_width ) && $width > $content_width ) {
572 if ( false !== $height ) {
573 $height = round( ( $content_width * $height ) / $width );
574 }
575 $width = $content_width;
576 }
577
578 // Set a width if none is found and $content_width is available.
579 // If width is set in this manner and height is available, use `fit` instead of `resize` to prevent skewing.
580 if ( false === $width && is_numeric( $content_width ) ) {
581 $width = (int) $content_width;
582
583 if ( false !== $height ) {
584 $transform = 'fit';
585 }
586 }
587
588 // Detect if image source is for a custom-cropped thumbnail and prevent further URL manipulation.
589 if ( ! $fullsize_url && preg_match_all( '#-e[a-z0-9]+(-\d+x\d+)?\.(' . implode( '|', self::$extensions ) . '){1}$#i', basename( $src ), $filename ) ) {
590 $fullsize_url = true;
591 }
592
593 // Build URL, first maybe removing WP's resized string so we pass the original image to Photon.
594 if ( ! $fullsize_url && is_array( $upload_dir ) && str_starts_with( $src, $upload_dir['baseurl'] ) ) {
595 $src = self::strip_image_dimensions_maybe( $src );
596 }
597
598 // Build array of Photon args and expose to filter before passing to Photon URL function.
599 $args = array();
600
601 if ( false !== $width && false !== $height ) {
602 $args[ $transform ] = $width . ',' . $height;
603 } elseif ( false !== $width ) {
604 $args['w'] = $width;
605 } elseif ( false !== $height ) {
606 $args['h'] = $height;
607 }
608
609 /**
610 * Filter the array of Photon arguments added to an image when it goes through Photon.
611 * By default, only includes width and height values.
612 *
613 * @see https://developer.wordpress.com/docs/photon/api/
614 *
615 * @module photon
616 *
617 * @since 2.0.0
618 * @since 0.4.7 Passes image tag name instead of full HTML of tag.
619 *
620 * @param array $args Array of Photon Arguments.
621 * @param array $details {
622 * Array of image details.
623 *
624 * @type string $tag Image tag (Image HTML output).
625 * @type string $src Image URL.
626 * @type string $src_orig Original Image URL.
627 * @type int|false $width Image width.
628 * @type int|false $height Image height.
629 * @type int|false $width_orig Original image width before constrained by content_width.
630 * @type int|false $height_orig Original Image height before constrained by content_width.
631 * @type string $transform Transform.
632 * @type string $transform_orig Original transform before constrained by content_width.
633 * }
634 */
635 $args = apply_filters( 'jetpack_photon_post_image_args', $args, compact( 'tag', 'src', 'src_orig', 'width', 'height', 'width_orig', 'height_orig', 'transform', 'transform_orig' ) );
636
637 $photon_url = Image_CDN_Core::cdn_url( $src, $args );
638
639 // Modify image tag if Photon function provides a URL
640 // Ensure changes are only applied to the current image by copying and modifying the matched tag, then replacing the entire tag with our modified version.
641 if ( $src !== $photon_url ) {
642 // If present, replace the link href with a Photoned URL for the full-size image.
643 if ( is_string( $nearest_preceding_href ) && self::validate_image_url( $nearest_preceding_href ) ) {
644 $processor->seek( 'link' );
645 $processor->set_attribute( 'href', Image_CDN_Core::cdn_url( $nearest_preceding_href ) );
646 $processor->seek( 'image' );
647 }
648
649 // Supplant the original source value with our Photon URL.
650 $processor->set_attribute( 'src', $photon_url );
651
652 // If Lazy Load is in use, pass placeholder image through Photon.
653 if ( $placeholder_src !== null && self::validate_image_url( $placeholder_src ) ) {
654 $placeholder_src = Image_CDN_Core::cdn_url( $placeholder_src );
655
656 if ( $placeholder_src !== $placeholder_src_orig ) {
657 $processor->set_attribute( $source_type, $placeholder_src );
658 }
659 }
660
661 // If we are not transforming the image with resize, fit, or letterbox (lb), then we should remove
662 // the width and height arguments (including HTML4 percentages) from the image to prevent distortion.
663 // Even if $args['w'] and $args['h'] are present, Photon does not crop to those dimensions. Instead,
664 // it appears to favor height.
665 //
666 // If we are transforming the image via one of those methods, let's update the width and height attributes.
667 if ( empty( $args['resize'] ) && empty( $args['fit'] ) && empty( $args['lb'] ) ) {
668 $processor->remove_attribute( 'width' );
669 $processor->remove_attribute( 'height' );
670 } else {
671 $resize_args = $args['resize'] ?? false;
672 if ( false === $resize_args ) {
673 $resize_args = ( ! $resize_args && isset( $args['fit'] ) )
674 ? $args['fit']
675 : false;
676 }
677 if ( false === $resize_args ) {
678 $resize_args = ( ! $resize_args && isset( $args['lb'] ) )
679 ? $args['lb']
680 : false;
681 }
682
683 list( $resize_width, $resize_height ) = explode( ',', $resize_args );
684 $processor->set_attribute( 'width', trim( $resize_width ) );
685 $processor->set_attribute( 'height', trim( $resize_height ) );
686 }
687
688 // Tag an image for dimension checking.
689 if ( ! self::is_amp_endpoint() ) {
690 $processor->set_attribute( 'data-recalc-dims', '1' );
691 }
692 }
693 } elseif (
694 preg_match( '#^http(s)?://i[\d]{1}.wp.com#', $src )
695 && is_string( $nearest_preceding_href )
696 && self::validate_image_url( $nearest_preceding_href )
697 ) {
698 $processor->seek( 'link' );
699 $processor->set_attribute( 'href', Image_CDN_Core::cdn_url( $nearest_preceding_href ) );
700 $processor->seek( 'image' );
701 }
702 }
703
704 return $processor->get_updated_html();
705 }
706
707 /**
708 * Filter Core galleries
709 *
710 * @param array $galleries Gallery array.
711 *
712 * @return array
713 */
714 public static function filter_the_galleries( $galleries ) {
715 if ( empty( $galleries ) || ! is_array( $galleries ) ) {
716 return $galleries;
717 }
718
719 // Pass by reference, so we can modify them in place.
720 foreach ( $galleries as &$this_gallery ) {
721 if ( is_string( $this_gallery ) ) {
722 $this_gallery = self::filter_the_content( $this_gallery );
723 }
724 }
725 unset( $this_gallery ); // break the reference.
726
727 return $galleries;
728 }
729
730 /**
731 * Runs the image widget through photon.
732 *
733 * @param array $instance Image widget instance data.
734 * @return array
735 */
736 public static function filter_the_image_widget( $instance ) {
737 if ( ! $instance['attachment_id'] && $instance['url'] ) {
738 Image_CDN_Core::cdn_url(
739 $instance['url'],
740 array(
741 'w' => $instance['width'],
742 'h' => $instance['height'],
743 )
744 );
745 }
746
747 return $instance;
748 }
749
750 /**
751 * * CORE IMAGE RETRIEVAL
752 **/
753
754 /**
755 * Filter post thumbnail image retrieval, passing images through Photon
756 *
757 * @param string|bool $image Image URL.
758 * @param int $attachment_id Attachment ID.
759 * @param string|array $size Declared size or a size array.
760 * @uses is_admin, apply_filters, wp_get_attachment_url, self::validate_image_url, this::image_sizes, jetpack_photon_url
761 * @filter image_downsize
762 * @return string|bool
763 */
764 public function filter_image_downsize( $image, $attachment_id, $size ) {
765 // Don't foul up the admin side of things, unless a plugin wants to.
766 if ( is_admin() &&
767 /**
768 * Provide plugins a way of running Photon for images in the WordPress Dashboard (wp-admin).
769 *
770 * Note: enabling this will result in Photon URLs added to your post content, which could make migrations across domains (and off Photon) a bit more challenging.
771 *
772 * @module photon
773 *
774 * @since 4.8.0
775 *
776 * @param bool false Stop Photon from being run on the Dashboard. Default to false.
777 * @param array $args {
778 * Array of image details.
779 *
780 * @type $image Image URL.
781 * @type $attachment_id Attachment ID of the image.
782 * @type $size Image size. Can be a string (name of the image size, e.g. full) or an array of width and height.
783 * }
784 */
785 false === apply_filters( 'jetpack_photon_admin_allow_image_downsize', false, compact( 'image', 'attachment_id', 'size' ) )
786 ) {
787 return $image;
788 }
789
790 /**
791 * Provide plugins a way of preventing Photon from being applied to images retrieved from WordPress Core.
792 *
793 * @module photon
794 *
795 * @since 2.0.0
796 *
797 * @param bool false Stop Photon from being applied to the image. Default to false.
798 * @param array $args {
799 * Array of image details.
800 *
801 * @type $image Image URL.
802 * @type $attachment_id Attachment ID of the image.
803 * @type $size Image size. Can be a string (name of the image size, e.g. full) or an array of width and height.
804 * }
805 */
806 if ( apply_filters( 'jetpack_photon_override_image_downsize', false, compact( 'image', 'attachment_id', 'size' ) ) ) {
807 return $image;
808 }
809
810 // Get the image URL and proceed with Photon-ification if successful.
811 $image_url = wp_get_attachment_url( $attachment_id );
812
813 // Set this to true later when we know we have size meta.
814 $has_size_meta = false;
815
816 if ( $image_url ) {
817 // Check if image URL should be used with Photon.
818 if ( ! self::validate_image_url( $image_url ) ) {
819 return $image;
820 }
821
822 $intermediate = true; // For the fourth array item returned by the image_downsize filter.
823
824 $registered_size = self::find_registered_image_size( $size );
825
826 // If an image is requested with a size known to WordPress, use that size's settings with Photon.
827 if ( $registered_size ) {
828 $size = $registered_size;
829 $image_args = self::image_sizes();
830 $image_args = $image_args[ $size ];
831
832 $photon_args = array();
833
834 $image_meta = image_get_intermediate_size( $attachment_id, $size );
835
836 // 'full' is a special case: We need consistent data regardless of the requested size.
837 if ( 'full' === $size ) {
838 $image_meta = wp_get_attachment_metadata( $attachment_id );
839 $intermediate = false;
840 } elseif ( ! $image_meta ) {
841 // If we still don't have any image meta at this point, it's probably from a custom thumbnail size
842 // for an image that was uploaded before the custom image was added to the theme. Try to determine the size manually.
843 $image_meta = wp_get_attachment_metadata( $attachment_id );
844
845 if ( isset( $image_meta['width'] ) && isset( $image_meta['height'] ) ) {
846 $image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $image_args['width'], $image_args['height'], $image_args['crop'] );
847 if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
848 $image_meta['width'] = $image_resized[6];
849 $image_meta['height'] = $image_resized[7];
850 }
851 }
852 }
853
854 if ( isset( $image_meta['width'] ) && isset( $image_meta['height'] ) ) {
855 $image_args['width'] = (int) $image_meta['width'];
856 $image_args['height'] = (int) $image_meta['height'];
857
858 list( $image_args['width'], $image_args['height'] ) = image_constrain_size_for_editor( $image_args['width'], $image_args['height'], $size, 'display' );
859 $has_size_meta = true;
860 }
861
862 // Expose determined arguments to a filter before passing to Photon.
863 $transform = $image_args['crop'] ? 'resize' : 'fit';
864
865 // Check specified image dimensions and account for possible zero values; photon fails to resize if a dimension is zero.
866 if ( 0 === $image_args['width'] || 0 === $image_args['height'] ) {
867 if ( 0 === $image_args['width'] && 0 < $image_args['height'] ) {
868 $photon_args['h'] = $image_args['height'];
869 } elseif ( 0 === $image_args['height'] && 0 < $image_args['width'] ) {
870 $photon_args['w'] = $image_args['width'];
871 }
872 } else {
873 $image_meta = wp_get_attachment_metadata( $attachment_id );
874 if ( ( 'resize' === $transform ) && $image_meta ) {
875 if ( isset( $image_meta['width'] ) && isset( $image_meta['height'] ) ) {
876 // Lets make sure that we don't upscale images since wp never upscales them as well.
877 $smaller_width = ( ( $image_meta['width'] < $image_args['width'] ) ? $image_meta['width'] : $image_args['width'] );
878 $smaller_height = ( ( $image_meta['height'] < $image_args['height'] ) ? $image_meta['height'] : $image_args['height'] );
879
880 $photon_args[ $transform ] = $smaller_width . ',' . $smaller_height;
881 }
882 } else {
883 $photon_args[ $transform ] = $image_args['width'] . ',' . $image_args['height'];
884 }
885 }
886
887 /**
888 * Filter the Photon Arguments added to an image when going through Photon, when that image size is a string.
889 * Image size will be a string (e.g. "full", "medium") when it is known to WordPress.
890 *
891 * @module photon
892 *
893 * @since 2.0.0
894 *
895 * @param array $photon_args Array of Photon arguments.
896 * @param array $args {
897 * Array of image details.
898 *
899 * @type array $image_args Array of Image arguments (width, height, crop).
900 * @type string $image_url Image URL.
901 * @type int $attachment_id Attachment ID of the image.
902 * @type string|int $size Image size. Can be a string (name of the image size, e.g. full) or an integer.
903 * @type string $transform Value can be resize or fit.
904 * @see https://developer.wordpress.com/docs/photon/api
905 * }
906 */
907 $photon_args = apply_filters( 'jetpack_photon_image_downsize_string', $photon_args, compact( 'image_args', 'image_url', 'attachment_id', 'size', 'transform' ) );
908
909 // Generate Photon URL.
910 $image = array(
911 Image_CDN_Core::cdn_url( $image_url, $photon_args ),
912 $has_size_meta ? $image_args['width'] : false,
913 $has_size_meta ? $image_args['height'] : false,
914 $intermediate,
915 );
916 } elseif ( is_array( $size ) ) {
917 // Pull width and height values from the provided array, if possible.
918 $width = isset( $size[0] ) ? (int) $size[0] : false;
919 $height = isset( $size[1] ) ? (int) $size[1] : false;
920
921 // Don't bother if necessary parameters aren't passed.
922 if ( ! $width || ! $height ) {
923 return $image;
924 }
925
926 $image_meta = wp_get_attachment_metadata( $attachment_id );
927 if ( isset( $image_meta['width'] ) && isset( $image_meta['height'] ) ) {
928 $image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $width, $height );
929
930 if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
931 $width = $image_resized[6];
932 $height = $image_resized[7];
933 } else {
934 $width = $image_meta['width'];
935 $height = $image_meta['height'];
936 }
937
938 $has_size_meta = true;
939 }
940
941 list( $width, $height ) = image_constrain_size_for_editor( $width, $height, $size );
942
943 // Expose arguments to a filter before passing to Photon.
944 $photon_args = array(
945 'fit' => $width . ',' . $height,
946 );
947
948 /**
949 * Filter the Photon Arguments added to an image when going through Photon,
950 * when the image size is an array of height and width values.
951 *
952 * @module photon
953 *
954 * @since 2.0.0
955 *
956 * @param array $photon_args Array of Photon arguments.
957 * @param array $args {
958 * Array of image details.
959 *
960 * @type $width Image width.
961 * @type height Image height.
962 * @type $image_url Image URL.
963 * @type $attachment_id Attachment ID of the image.
964 * }
965 */
966 $photon_args = apply_filters( 'jetpack_photon_image_downsize_array', $photon_args, compact( 'width', 'height', 'image_url', 'attachment_id' ) );
967
968 // Generate Photon URL.
969 $image = array(
970 Image_CDN_Core::cdn_url( $image_url, $photon_args ),
971 $has_size_meta ? $width : false,
972 $has_size_meta ? $height : false,
973 $intermediate,
974 );
975 }
976 }
977
978 return $image;
979 }
980
981 /**
982 * Filters an array of image `srcset` values, replacing each URL with its Photon equivalent.
983 *
984 * @param array $sources An array of image urls and widths.
985 * @param array $size_array The size array for srcset.
986 * @param string $image_src The image src attribute.
987 * @param array $image_meta The image meta.
988 * @param int $attachment_id Attachment ID.
989 *
990 * @uses self::validate_image_url, Image_CDN_Core::cdn_url
991 * @uses Image_CDN::strip_image_dimensions_maybe, Image_CDN_Core::get_jetpack_content_width
992 *
993 * @return array An array of Photon image urls and widths.
994 */
995 public function filter_srcset_array( $sources = array(), $size_array = array(), $image_src = '', $image_meta = array(), $attachment_id = 0 ) {
996 // Check if we are supposed to skip the main image.
997 if ( $this->photon_should_skip_image( $image_src ) ) {
998 return $sources;
999 }
1000
1001 if ( ! is_array( $sources ) || array() === $sources ) {
1002 return $sources;
1003 }
1004 $upload_dir = wp_get_upload_dir();
1005
1006 foreach ( $sources as $i => $source ) {
1007 if ( ! self::validate_image_url( $source['url'] ) ) {
1008 continue;
1009 }
1010
1011 /** This filter is already documented in class-image-cdn.php */
1012 if ( apply_filters( 'jetpack_photon_skip_image', false, $source['url'], $source ) ) {
1013 continue;
1014 }
1015
1016 $url = $source['url'];
1017 list( $width, $height ) = self::parse_dimensions_from_filename( $url );
1018
1019 // It's quicker to get the full size with the data we have already, if available.
1020 if ( ! empty( $attachment_id ) ) {
1021 $url = wp_get_attachment_url( $attachment_id );
1022 } else {
1023 $url = self::strip_image_dimensions_maybe( $url );
1024 }
1025
1026 $args = array();
1027 if ( 'w' === $source['descriptor'] ) {
1028 if ( $height && ( (int) $source['value'] === $width ) ) {
1029 $args['resize'] = $width . ',' . $height;
1030 } else {
1031 $args['w'] = $source['value'];
1032 }
1033 }
1034
1035 $sources[ $i ]['url'] = Image_CDN_Core::cdn_url( $url, $args );
1036 }
1037
1038 /**
1039 * At this point, $sources is the original srcset with Photonized URLs.
1040 * Now, we're going to construct additional sizes based on multiples of the content_width.
1041 * This will reduce the gap between the largest defined size and the original image.
1042 */
1043
1044 /**
1045 * Filter the multiplier Photon uses to create new srcset items.
1046 * Return false to short-circuit and bypass auto-generation.
1047 *
1048 * @module photon
1049 *
1050 * @since 4.0.4
1051 *
1052 * @param array|bool $multipliers Array of multipliers to use or false to bypass.
1053 */
1054 $multipliers = apply_filters( 'jetpack_photon_srcset_multipliers', array( 2, 3 ) );
1055 $url = trailingslashit( $upload_dir['baseurl'] ) . $image_meta['file'];
1056
1057 if (
1058 /** Short-circuit via jetpack_photon_srcset_multipliers filter. */
1059 is_array( $multipliers )
1060 /** This filter is already documented in class-image-cdn.php */
1061 && ! apply_filters( 'jetpack_photon_skip_image', false, $url, null )
1062 /** Verify basic meta is intact. */
1063 && isset( $image_meta['width'] ) && isset( $image_meta['height'] ) && isset( $image_meta['file'] )
1064 /** Verify we have the requested width/height. */
1065 && isset( $size_array[0] ) && isset( $size_array[1] )
1066 ) {
1067
1068 $fullwidth = $image_meta['width'];
1069 $fullheight = $image_meta['height'];
1070 $reqwidth = $size_array[0];
1071 $reqheight = $size_array[1];
1072
1073 $constrained_size = wp_constrain_dimensions( $fullwidth, $fullheight, $reqwidth );
1074 $expected_size = array( $reqwidth, $reqheight );
1075
1076 if ( abs( $constrained_size[0] - $expected_size[0] ) <= 1 && abs( $constrained_size[1] - $expected_size[1] ) <= 1 ) {
1077 $crop = 'soft';
1078 $base = Image_CDN_Core::get_jetpack_content_width() ? Image_CDN_Core::get_jetpack_content_width() : 1000; // Provide a default width if none set by the theme.
1079 } else {
1080 $crop = 'hard';
1081 $base = $reqwidth;
1082 }
1083
1084 $currentwidths = array_keys( $sources );
1085 $newsources = null;
1086
1087 foreach ( $multipliers as $multiplier ) {
1088
1089 $newwidth = $base * $multiplier;
1090 foreach ( $currentwidths as $currentwidth ) {
1091 // If a new width would be within 100 pixes of an existing one or larger than the full size image, skip.
1092 if ( abs( $currentwidth - $newwidth ) < 50 || ( $newwidth > $fullwidth ) ) {
1093 continue 2; // Bump out back to the $multipliers as $multiplier.
1094 }
1095 } //end foreach ( $currentwidths as $currentwidth ){
1096
1097 if ( 'soft' === $crop ) {
1098 $args = array(
1099 'w' => $newwidth,
1100 );
1101 } else { // hard crop, e.g. add_image_size( 'example', 200, 200, true ).
1102 $args = array(
1103 'zoom' => $multiplier,
1104 'resize' => $reqwidth . ',' . $reqheight,
1105 );
1106 }
1107
1108 $newsources[ $newwidth ] = array(
1109 'url' => Image_CDN_Core::cdn_url( $url, $args ),
1110 'descriptor' => 'w',
1111 'value' => $newwidth,
1112 );
1113 } //end foreach ( $multipliers as $multiplier )
1114 if ( is_array( $newsources ) ) {
1115 $sources = array_replace( $sources, $newsources );
1116 }
1117 } //end if isset( $image_meta['width'] ) && isset( $image_meta['file'] ) )
1118
1119 return $sources;
1120 }
1121
1122 /**
1123 * Filters an array of image `sizes` values, using $content_width instead of image's full size.
1124 *
1125 * @param array $sizes An array of media query breakpoints.
1126 * @param array $size Width and height of the image.
1127 * @param string $image_url The image URL.
1128 *
1129 * @uses Jetpack::get_content_width
1130 * @return array An array of media query breakpoints.
1131 */
1132 public function filter_sizes( $sizes, $size, $image_url ) {
1133 if ( $this->photon_should_skip_image( $image_url ) ) {
1134 return $sizes;
1135 }
1136
1137 if ( ! doing_filter( 'the_content' ) ) {
1138 return $sizes;
1139 }
1140 $content_width = Image_CDN_Core::get_jetpack_content_width();
1141 if ( ! $content_width ) {
1142 $content_width = 1000;
1143 }
1144
1145 if ( ( is_array( $size ) && $size[0] < $content_width ) ) {
1146 return $sizes;
1147 }
1148
1149 return sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $content_width );
1150 }
1151
1152 /**
1153 * Whether to skip the image from being processed by Photon.
1154 *
1155 * @param string $image_url The image URL.
1156 *
1157 * @return bool Whether to skip the image.
1158 */
1159 private function photon_should_skip_image( $image_url ) {
1160 return apply_filters( 'jetpack_photon_skip_image', false, $image_url, null );
1161 }
1162
1163 /**
1164 * * GENERAL FUNCTIONS
1165 **/
1166
1167 /**
1168 * Ensure image URL is valid for Photon.
1169 * Though Photon functions address some of the URL issues, we should avoid unnecessary processing if we know early on that the image isn't supported.
1170 *
1171 * @param string $url Image URL.
1172 * @uses wp_parse_args
1173 * @return bool
1174 */
1175 protected static function validate_image_url( $url ) {
1176 $parsed_url = wp_parse_url( $url );
1177
1178 if ( ! $parsed_url ) {
1179 return false;
1180 }
1181
1182 // Parse URL and ensure needed keys exist, since the array returned by `wp_parse_url` only includes the URL components it finds.
1183 $url_info = wp_parse_args(
1184 $parsed_url,
1185 array(
1186 'scheme' => null,
1187 'host' => null,
1188 'port' => null,
1189 'path' => null,
1190 )
1191 );
1192
1193 // Bail if scheme isn't http or port is set that isn't port 80.
1194 if (
1195 ( 'http' !== $url_info['scheme'] || ! in_array( $url_info['port'], array( 80, null ), true ) ) &&
1196 /**
1197 * Allow Photon to fetch images that are served via HTTPS.
1198 *
1199 * @module photon
1200 *
1201 * @since 2.4.0
1202 * @since 3.9.0 Default to false.
1203 *
1204 * @param bool $reject_https Should Photon ignore images using the HTTPS scheme. Default to false.
1205 */
1206 apply_filters( 'jetpack_photon_reject_https', false )
1207 ) {
1208 return false;
1209 }
1210
1211 // Bail if no host is found.
1212 if ( $url_info['host'] === null ) {
1213 return false;
1214 }
1215
1216 // Bail if the image already went through Photon.
1217 if ( preg_match( '#^i[\d]{1}.wp.com$#i', $url_info['host'] ) ) {
1218 return false;
1219 }
1220
1221 // Bail if no path is found.
1222 if ( $url_info['path'] === null ) {
1223 return false;
1224 }
1225
1226 // Ensure image extension is acceptable.
1227 if ( ! in_array( strtolower( pathinfo( $url_info['path'], PATHINFO_EXTENSION ) ), self::$extensions, true ) ) {
1228 return false;
1229 }
1230
1231 // If we got this far, we should have an acceptable image URL
1232 // But let folks filter to decline if they prefer.
1233 /**
1234 * Overwrite the results of the validation steps an image goes through before to be considered valid to be used by Photon.
1235 *
1236 * @module photon
1237 *
1238 * @since 3.0.0
1239 *
1240 * @param bool true Is the image URL valid and can it be used by Photon. Default to true.
1241 * @param string $url Image URL.
1242 * @param array $parsed_url Array of information about the image.
1243 */
1244 return apply_filters( 'photon_validate_image_url', true, $url, $parsed_url );
1245 }
1246
1247 /**
1248 * Checks if the file exists before it passes the file to photon.
1249 *
1250 * @param string $src The image URL.
1251 * @return string
1252 **/
1253 public static function strip_image_dimensions_maybe( $src ) {
1254 $stripped_src = $src;
1255
1256 // Build URL, first removing WP's resized string so we pass the original image to Photon.
1257 if ( preg_match( '#(-\d+x\d+)\.(' . implode( '|', self::$extensions ) . '){1}$#i', $src, $src_parts ) ) {
1258 $stripped_src = str_replace( $src_parts[1], '', $src );
1259 $upload_dir = wp_get_upload_dir();
1260
1261 // Extracts the file path to the image minus the base url.
1262 $file_path = substr( $stripped_src, strlen( $upload_dir['baseurl'] ) );
1263
1264 if ( file_exists( $upload_dir['basedir'] . $file_path ) ) {
1265 $src = $stripped_src;
1266 }
1267 }
1268
1269 return $src;
1270 }
1271
1272 /**
1273 * Provide an array of available image sizes and corresponding dimensions.
1274 * Similar to get_intermediate_image_sizes() except that it includes image sizes' dimensions, not just their names.
1275 *
1276 * @global $wp_additional_image_sizes
1277 * @uses get_option
1278 * @return array
1279 */
1280 protected static function image_sizes() {
1281 if ( null === self::$image_sizes ) {
1282 global $_wp_additional_image_sizes;
1283
1284 // Populate an array matching the data structure of $_wp_additional_image_sizes so we have a consistent structure for image sizes.
1285 $images = array(
1286 'thumb' => array(
1287 'width' => (int) get_option( 'thumbnail_size_w' ),
1288 'height' => (int) get_option( 'thumbnail_size_h' ),
1289 'crop' => (bool) get_option( 'thumbnail_crop' ),
1290 ),
1291 'medium' => array(
1292 'width' => (int) get_option( 'medium_size_w' ),
1293 'height' => (int) get_option( 'medium_size_h' ),
1294 'crop' => false,
1295 ),
1296 'medium_large' => array(
1297 'width' => (int) get_option( 'medium_large_size_w' ),
1298 'height' => (int) get_option( 'medium_large_size_h' ),
1299 'crop' => false,
1300 ),
1301 'large' => array(
1302 'width' => (int) get_option( 'large_size_w' ),
1303 'height' => (int) get_option( 'large_size_h' ),
1304 'crop' => false,
1305 ),
1306 'full' => array(
1307 'width' => null,
1308 'height' => null,
1309 'crop' => false,
1310 ),
1311 );
1312
1313 // Compatibility mapping as found in wp-includes/media.php.
1314 $images['thumbnail'] = $images['thumb'];
1315
1316 // Update class variable, merging in $_wp_additional_image_sizes if any are set.
1317 if ( is_array( $_wp_additional_image_sizes ) && ! empty( $_wp_additional_image_sizes ) ) {
1318 self::$image_sizes = array_merge( $images, $_wp_additional_image_sizes );
1319 } else {
1320 self::$image_sizes = $images;
1321 }
1322 }
1323
1324 return is_array( self::$image_sizes ) ? self::$image_sizes : array();
1325 }
1326
1327 /**
1328 * Find registered image size name if it exists.
1329 *
1330 * @param string|int|int[] $size Image size name if registered, or false if not.
1331 */
1332 protected static function find_registered_image_size( $size ) {
1333 $sizes = self::image_sizes();
1334
1335 // WP states that `add_image_size()` should use a string for the name, but doesn't enforce that.
1336 if ( ( is_string( $size ) || is_int( $size ) ) && array_key_exists( $size, self::image_sizes() ) ) {
1337 return $size;
1338 }
1339
1340 if ( is_array( $size ) && isset( $size[0] ) && isset( $size[1] ) ) {
1341 foreach ( $sizes as $name => $args ) {
1342 if ( $args['width'] === $size[0] && $args['height'] === $size[1] ) {
1343 return $name;
1344 }
1345 }
1346 }
1347
1348 return false;
1349 }
1350
1351 /**
1352 * Determine if image_downsize should utilize Photon via REST API.
1353 *
1354 * The WordPress Block Editor (Gutenberg) and other REST API consumers using the wp/v2/media endpoint, especially in the "edit"
1355 * context is more akin to the is_admin usage of Photon (see filter_image_downsize). Since consumers are trying to edit content in posts,
1356 * Photon should not fire as it will fire later on display. By aborting an attempt to Photonize an image here, we
1357 * prevents issues like https://github.com/Automattic/jetpack/issues/10580 .
1358 *
1359 * To determine if we're using the wp/v2/media endpoint, we hook onto the `rest_request_before_callbacks` filter and
1360 * if determined we are using it in the edit context, we'll false out the `jetpack_photon_override_image_downsize` filter.
1361 *
1362 * @see Image_CDN::filter_image_downsize()
1363 *
1364 * @param null|\WP_Error $response REST API response.
1365 * @param array $endpoint_data Endpoint data. Not used, but part of the filter.
1366 * @param \WP_REST_Request $request Request used to generate the response.
1367 *
1368 * @return null|\WP_Error The original response object without modification.
1369 */
1370 public function should_rest_photon_image_downsize( $response, $endpoint_data, $request ) {
1371 if ( ! is_a( $request, '\WP_REST_Request' ) ) {
1372 return $response; // Something odd is happening. Do nothing and return the response.
1373 }
1374
1375 if ( is_wp_error( $response ) ) {
1376 // If we're going to return an error, we don't need to do anything with Photon.
1377 return $response;
1378 }
1379
1380 $this->should_rest_photon_image_downsize_override( $request );
1381
1382 return $response;
1383 }
1384
1385 /**
1386 * Helper function to check if a WP_REST_Request is the media endpoint in the edit context.
1387 *
1388 * @param \WP_REST_Request $request The current REST request.
1389 */
1390 private function should_rest_photon_image_downsize_override( \WP_REST_Request $request ) {
1391 $route = $request->get_route();
1392
1393 if (
1394 (
1395 str_contains( $route, 'wp/v2/media' )
1396 && 'edit' === $request->get_param( 'context' )
1397 )
1398 || str_contains( $route, 'wpcom/v2/external-media/copy' )
1399 || (bool) $request->get_header( 'x-wp-api-fetch-from-editor' )
1400 ) {
1401 // Don't use `__return_true()`: Use something unique. See ::_override_image_downsize_in_rest_edit_context()
1402 // Late execution to avoid conflict with other plugins as we really don't want to run in this situation.
1403 add_filter(
1404 'jetpack_photon_override_image_downsize',
1405 array(
1406 $this,
1407 'override_image_downsize_in_rest_edit_context',
1408 ),
1409 999999
1410 );
1411 }
1412 }
1413
1414 /**
1415 * Brings in should_rest_photon_image_downsize for the rest_after_insert_attachment hook.
1416 *
1417 * @param \WP_Post $attachment Inserted or updated attachment object.
1418 * @param \WP_REST_Request $request Request object.
1419 */
1420 public function should_rest_photon_image_downsize_insert_attachment( \WP_Post $attachment, \WP_REST_Request $request ) {
1421 if ( ! is_a( $request, '\WP_REST_Request' ) ) {
1422 // Something odd is happening.
1423 return;
1424 }
1425
1426 $this->should_rest_photon_image_downsize_override( $request );
1427 }
1428
1429 /**
1430 * Remove the override we may have added in ::should_rest_photon_image_downsize()
1431 * Since ::_override_image_downsize_in_rest_edit_context() is only
1432 * every used here, we can always remove it without ever worrying
1433 * about breaking any other configuration.
1434 *
1435 * @param mixed $response REST API Response.
1436 * @return mixed Unchanged $response
1437 */
1438 public function cleanup_rest_photon_image_downsize( $response ) {
1439 remove_filter(
1440 'jetpack_photon_override_image_downsize',
1441 array(
1442 $this,
1443 'override_image_downsize_in_rest_edit_context',
1444 ),
1445 999999
1446 );
1447 return $response;
1448 }
1449
1450 /**
1451 * Used internally by ::should_rest_photon_image_downsize() to not photonize
1452 * image URLs in ?context=edit REST requests.
1453 * MUST NOT be used anywhere else.
1454 * We use a unique function instead of __return_true so that we can clean up
1455 * after ourselves without breaking anyone else's filters.
1456 *
1457 * @internal
1458 * @return true
1459 */
1460 public function override_image_downsize_in_rest_edit_context() {
1461 return true;
1462 }
1463
1464 /**
1465 * Return whether the current page is AMP.
1466 *
1467 * This is only present for the sake of WordPress.com where the Jetpack_AMP_Support
1468 * class does not yet exist. This mehod may only be called at the wp action or later.
1469 *
1470 * @return bool Whether AMP page.
1471 */
1472 private static function is_amp_endpoint() {
1473 return class_exists( '\Jetpack_AMP_Support' ) && \Jetpack_AMP_Support::is_amp_request();
1474 }
1475
1476 /**
1477 * Get the list of supported image extensions
1478 *
1479 * @return string[] Array of supported extensions
1480 */
1481 public static function get_supported_extensions() {
1482 return self::$extensions;
1483 }
1484 }
1485