Completed
Push — update/editor-blocks-icon-colo... ( 093ab2...3cfb5e )
by
unknown
08:47
created

class.photon.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
use Automattic\Jetpack\Assets;
4
5
class Jetpack_Photon {
6
	/**
7
	 * Class variables
8
	 */
9
	// Oh look, a singleton
10
	private static $__instance = null;
11
12
	// Allowed extensions must match https://code.trac.wordpress.org/browser/photon/index.php#L31
13
	protected static $extensions = array(
14
		'gif',
15
		'jpg',
16
		'jpeg',
17
		'png',
18
	);
19
20
	// Don't access this directly. Instead, use self::image_sizes() so it's actually populated with something.
21
	protected static $image_sizes = null;
22
23
	/**
24
	 * Singleton implementation
25
	 *
26
	 * @return object
27
	 */
28
	public static function instance() {
29
		if ( ! is_a( self::$__instance, 'Jetpack_Photon' ) ) {
30
			self::$__instance = new Jetpack_Photon();
31
			self::$__instance->setup();
32
		}
33
34
		return self::$__instance;
35
	}
36
37
	/**
38
	 * Silence is golden.
39
	 */
40
	private function __construct() {}
41
42
	/**
43
	 * Register actions and filters, but only if basic Photon functions are available.
44
	 * The basic functions are found in ./functions.photon.php.
45
	 *
46
	 * @uses add_action, add_filter
47
	 * @return null
48
	 */
49
	private function setup() {
50
		if ( ! function_exists( 'jetpack_photon_url' ) ) {
51
			return;
52
		}
53
54
		// Images in post content and galleries
55
		add_filter( 'the_content', array( __CLASS__, 'filter_the_content' ), 999999 );
56
		add_filter( 'get_post_galleries', array( __CLASS__, 'filter_the_galleries' ), 999999 );
57
		add_filter( 'widget_media_image_instance', array( __CLASS__, 'filter_the_image_widget' ), 999999 );
58
59
		// Core image retrieval
60
		add_filter( 'image_downsize', array( $this, 'filter_image_downsize' ), 10, 3 );
61
		add_filter( 'rest_request_before_callbacks', array( $this, 'should_rest_photon_image_downsize' ), 10, 3 );
62
		add_filter( 'rest_request_after_callbacks', array( $this, 'cleanup_rest_photon_image_downsize' ) );
63
64
		// Responsive image srcset substitution
65
		add_filter( 'wp_calculate_image_srcset', array( $this, 'filter_srcset_array' ), 10, 5 );
66
		add_filter( 'wp_calculate_image_sizes', array( $this, 'filter_sizes' ), 1, 2 ); // Early so themes can still easily filter.
67
68
		// Helpers for maniuplated images
69
		add_action( 'wp_enqueue_scripts', array( $this, 'action_wp_enqueue_scripts' ), 9 );
70
71
		/**
72
		 * Allow Photon to disable uploaded images resizing and use its own resize capabilities instead.
73
		 *
74
		 * @module photon
75
		 *
76
		 * @since 7.1.0
77
		 *
78
		 * @param bool false Should Photon enable noresize mode. Default to false.
79
		 */
80
		if ( apply_filters( 'jetpack_photon_noresize_mode', false ) ) {
81
			$this->enable_noresize_mode();
82
		}
83
	}
84
85
	/**
86
	 * Enables the noresize mode for Photon, allowing to avoid intermediate size files generation.
87
	 */
88
	private function enable_noresize_mode() {
89
		jetpack_require_lib( 'class.jetpack-photon-image-sizes' );
90
91
		// The main objective of noresize mode is to disable additional resized image versions creation.
92
		// This filter handles removal of additional sizes.
93
		add_filter( 'intermediate_image_sizes_advanced', array( __CLASS__, 'filter_photon_noresize_intermediate_sizes' ) );
94
95
		// Load the noresize srcset solution on priority of 20, allowing other plugins to set sizes earlier.
96
		add_filter( 'wp_get_attachment_metadata', array( __CLASS__, 'filter_photon_norezise_maybe_inject_sizes' ), 20, 2 );
97
98
		// Photonize thumbnail URLs in the API response.
99
		add_filter( 'rest_api_thumbnail_size_urls', array( __CLASS__, 'filter_photon_noresize_thumbnail_urls' ) );
100
101
		// This allows to assign the Photon domain to images that normally use the home URL as base.
102
		add_filter( 'jetpack_photon_domain', array( __CLASS__, 'filter_photon_norezise_domain' ), 10, 2 );
103
104
		add_filter( 'the_content', array( __CLASS__, 'filter_content_add' ), 0 );
105
106
		// Jetpack hooks in at six nines (999999) so this filter does at seven.
107
		add_filter( 'the_content', array( __CLASS__, 'filter_content_remove' ), 9999999 );
108
109
		// Regular Photon operation mode filter doesn't run when is_admin(), so we need an additional filter.
110
		// This is temporary until Jetpack allows more easily running these filters for is_admin().
111
		if ( is_admin() ) {
112
			add_filter( 'image_downsize', array( $this, 'filter_image_downsize' ), 5, 3 );
113
114
			// Allows any image that gets passed to Photon to be resized via Photon.
115
			add_filter( 'jetpack_photon_admin_allow_image_downsize', '__return_true' );
116
		}
117
	}
118
119
	/**
120
	 * This is our catch-all to strip dimensions from intermediate images in content.
121
	 * Since this primarily only impacts post_content we do a little dance to add the filter early
122
	 * to `the_content` and then remove it later on in the same hook.
123
	 *
124
	 * @param String $content the post content.
125
	 * @return String the post content unchanged.
126
	 */
127
	public static function filter_content_add( $content ) {
128
		add_filter( 'jetpack_photon_pre_image_url', array( __CLASS__, 'strip_image_dimensions_maybe' ) );
129
		return $content;
130
	}
131
132
	/**
133
	 * Removing the content filter that was set previously.
134
	 *
135
	 * @param String $content the post content.
136
	 * @return String the post content unchanged.
137
	 */
138
	public static function filter_content_remove( $content ) {
139
		remove_filter( 'jetpack_photon_pre_image_url', array( __CLASS__, 'strip_image_dimensions_maybe' ) );
140
		return $content;
141
	}
142
143
	/**
144
	 * Short circuits the Photon filter to enable Photon processing for any URL.
145
	 *
146
	 * @param String $photon_url a proposed Photon URL for the media file.
147
	 * @param String $image_url the original media URL.
148
	 * @return String an URL to be used for the media file.
149
	 */
150
	public static function filter_photon_norezise_domain( $photon_url, $image_url ) {
151
		return $photon_url;
152
	}
153
154
	/**
155
	 * Disables intermediate sizes to disallow resizing.
156
	 *
157
	 * @param Array $sizes an array containing image sizes.
158
	 * @return Boolean
159
	 */
160
	public static function filter_photon_noresize_intermediate_sizes( $sizes ) {
161
		return array();
162
	}
163
164
	public static function filter_photon_noresize_thumbnail_urls( $sizes ) {
165
		foreach ( $sizes as $size => $url ) {
166
			$parts     = explode( '?', $url );
167
			$arguments = isset( $parts[1] ) ? $parts[1] : array();
168
169
			$sizes[ $size ] = jetpack_photon_url( $url, wp_parse_args( $arguments ) );
170
		}
171
172
		return $sizes;
173
	}
174
175
	/**
176
	 * Inject image sizes to attachment metadata.
177
	 *
178
	 * @param array $data          Attachment metadata.
179
	 * @param int   $attachment_id Attachment's post ID.
180
	 *
181
	 * @return array Attachment metadata.
182
	 */
183
	public static function filter_photon_norezise_maybe_inject_sizes( $data, $attachment_id ) {
184
		// Can't do much if data is empty.
185
		if ( empty( $data ) ) {
186
			return $data;
187
		}
188
		$sizes_already_exist = (
189
			true === is_array( $data )
190
			&& true === array_key_exists( 'sizes', $data )
191
			&& true === is_array( $data['sizes'] )
192
			&& false === empty( $data['sizes'] )
193
		);
194
		if ( $sizes_already_exist ) {
195
			return $data;
196
		}
197
		// Missing some critical data we need to determine sizes, not processing.
198
		if ( ! isset( $data['file'] )
199
			|| ! isset( $data['width'] )
200
			|| ! isset( $data['height'] )
201
		) {
202
			return $data;
203
		}
204
205
		$mime_type           = get_post_mime_type( $attachment_id );
206
		$attachment_is_image = preg_match( '!^image/!', $mime_type );
207
208
		if ( 1 === $attachment_is_image ) {
209
			$image_sizes   = new Jetpack_Photon_ImageSizes( $attachment_id, $data );
210
			$data['sizes'] = $image_sizes->generate_sizes_meta();
211
		}
212
		return $data;
213
	}
214
215
	/**
216
	 * Inject image sizes to Jetpack REST API responses. This wraps the filter_photon_norezise_maybe_inject_sizes function.
217
	 *
218
	 * @param array $data          Attachment sizes data.
0 ignored issues
show
There is no parameter named $data. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
219
	 * @param int   $attachment_id Attachment's post ID.
220
	 *
221
	 * @return array Attachment sizes array.
222
	 */
223
	public static function filter_photon_norezise_maybe_inject_sizes_api( $sizes, $attachment_id ) {
224
		return self::filter_photon_norezise_maybe_inject_sizes( wp_get_attachment_metadata( $attachment_id ), $attachment_id );
225
	}
226
227
	/**
228
	 * * IN-CONTENT IMAGE MANIPULATION FUNCTIONS
229
	 **/
230
231
	/**
232
	 * Match all images and any relevant <a> tags in a block of HTML.
233
	 *
234
	 * @param string $content Some HTML.
235
	 * @return array An array of $images matches, where $images[0] is
236
	 *         an array of full matches, and the link_url, img_tag,
237
	 *         and img_url keys are arrays of those matches.
238
	 */
239
	public static function parse_images_from_html( $content ) {
240
		$images = array();
241
242
		if ( preg_match_all( '#(?:<a[^>]+?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 ) ) {
243
			foreach ( $images as $key => $unused ) {
244
				// Simplify the output as much as possible, mostly for confirming test results.
245
				if ( is_numeric( $key ) && $key > 0 ) {
246
					unset( $images[ $key ] );
247
				}
248
			}
249
250
			return $images;
251
		}
252
253
		return array();
254
	}
255
256
	/**
257
	 * Try to determine height and width from strings WP appends to resized image filenames.
258
	 *
259
	 * @param string $src The image URL.
260
	 * @return array An array consisting of width and height.
261
	 */
262
	public static function parse_dimensions_from_filename( $src ) {
263
		$width_height_string = array();
264
265
		if ( preg_match( '#-(\d+)x(\d+)\.(?:' . implode( '|', self::$extensions ) . '){1}$#i', $src, $width_height_string ) ) {
266
			$width  = (int) $width_height_string[1];
267
			$height = (int) $width_height_string[2];
268
269
			if ( $width && $height ) {
270
				return array( $width, $height );
271
			}
272
		}
273
274
		return array( false, false );
275
	}
276
277
	/**
278
	 * Identify images in post content, and if images are local (uploaded to the current site), pass through Photon.
279
	 *
280
	 * @param string $content
281
	 * @uses self::validate_image_url, apply_filters, jetpack_photon_url, esc_url
282
	 * @filter the_content
283
	 * @return string
284
	 */
285
	public static function filter_the_content( $content ) {
286
		$images = self::parse_images_from_html( $content );
287
288
		if ( ! empty( $images ) ) {
289
			$content_width = Jetpack::get_content_width();
290
291
			$image_sizes = self::image_sizes();
292
293
			$upload_dir = wp_get_upload_dir();
294
295
			foreach ( $images[0] as $index => $tag ) {
296
				// Default to resize, though fit may be used in certain cases where a dimension cannot be ascertained
297
				$transform = 'resize';
298
299
				// Start with a clean attachment ID each time
300
				$attachment_id = false;
301
302
				// Flag if we need to munge a fullsize URL
303
				$fullsize_url = false;
304
305
				// Identify image source
306
				$src = $src_orig = $images['img_url'][ $index ];
307
308
				/**
309
				 * Allow specific images to be skipped by Photon.
310
				 *
311
				 * @module photon
312
				 *
313
				 * @since 2.0.3
314
				 *
315
				 * @param bool false Should Photon ignore this image. Default to false.
316
				 * @param string $src Image URL.
317
				 * @param string $tag Image Tag (Image HTML output).
318
				 */
319
				if ( apply_filters( 'jetpack_photon_skip_image', false, $src, $tag ) ) {
320
					continue;
321
				}
322
323
				// Support Automattic's Lazy Load plugin
324
				// Can't modify $tag yet as we need unadulterated version later
325
				if ( preg_match( '#data-lazy-src=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
326
					$placeholder_src = $placeholder_src_orig = $src;
327
					$src             = $src_orig = $lazy_load_src[1];
328
				} elseif ( preg_match( '#data-lazy-original=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
329
					$placeholder_src = $placeholder_src_orig = $src;
330
					$src             = $src_orig = $lazy_load_src[1];
331
				}
332
333
				// Check if image URL should be used with Photon
334
				if ( self::validate_image_url( $src ) ) {
335
					// Find the width and height attributes
336
					$width = $height = false;
337
338
					// First, check the image tag
339
					if ( preg_match( '#[\s|"|\']width=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $width_string ) ) {
340
						$width = $width_string[1];
341
					}
342
343
					if ( preg_match( '#[\s|"|\']height=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $height_string ) ) {
344
						$height = $height_string[1];
345
					}
346
347
					// Can't pass both a relative width and height, so unset the height in favor of not breaking the horizontal layout.
348
					if ( false !== strpos( $width, '%' ) && false !== strpos( $height, '%' ) ) {
349
						$width = $height = false;
350
					}
351
352
					// Detect WP registered image size from HTML class
353
					if ( preg_match( '#class=["|\']?[^"\']*size-([^"\'\s]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $size ) ) {
354
						$size = array_pop( $size );
355
356
						if ( false === $width && false === $height && 'full' != $size && array_key_exists( $size, $image_sizes ) ) {
357
							$width     = (int) $image_sizes[ $size ]['width'];
358
							$height    = (int) $image_sizes[ $size ]['height'];
359
							$transform = $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
360
						}
361
					} else {
362
						unset( $size );
363
					}
364
365
					// WP Attachment ID, if uploaded to this site
366
					if (
367
						preg_match( '#class=["|\']?[^"\']*wp-image-([\d]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $attachment_id ) &&
368
						0 === strpos( $src, $upload_dir['baseurl'] ) &&
369
						/**
370
						 * Filter whether an image using an attachment ID in its class has to be uploaded to the local site to go through Photon.
371
						 *
372
						 * @module photon
373
						 *
374
						 * @since 2.0.3
375
						 *
376
						 * @param bool false Was the image uploaded to the local site. Default to false.
377
						 * @param array $args {
378
						 *   Array of image details.
379
						 *
380
						 *   @type $src Image URL.
381
						 *   @type tag Image tag (Image HTML output).
382
						 *   @type $images Array of information about the image.
383
						 *   @type $index Image index.
384
						 * }
385
						 */
386
						apply_filters( 'jetpack_photon_image_is_local', false, compact( 'src', 'tag', 'images', 'index' ) )
387
					) {
388
						$attachment_id = intval( array_pop( $attachment_id ) );
389
390
						if ( $attachment_id ) {
391
							$attachment = get_post( $attachment_id );
392
393
							// Basic check on returned post object
394
							if ( is_object( $attachment ) && ! is_wp_error( $attachment ) && 'attachment' == $attachment->post_type ) {
395
								$src_per_wp = wp_get_attachment_image_src( $attachment_id, isset( $size ) ? $size : 'full' );
396
397
								if ( self::validate_image_url( $src_per_wp[0] ) ) {
398
									$src          = $src_per_wp[0];
399
									$fullsize_url = true;
400
401
									// Prevent image distortion if a detected dimension exceeds the image's natural dimensions
402
									if ( ( false !== $width && $width > $src_per_wp[1] ) || ( false !== $height && $height > $src_per_wp[2] ) ) {
403
										$width  = false === $width ? false : min( $width, $src_per_wp[1] );
404
										$height = false === $height ? false : min( $height, $src_per_wp[2] );
405
									}
406
407
									// If no width and height are found, max out at source image's natural dimensions
408
									// Otherwise, respect registered image sizes' cropping setting
409
									if ( false === $width && false === $height ) {
410
										$width     = $src_per_wp[1];
411
										$height    = $src_per_wp[2];
412
										$transform = 'fit';
413
									} elseif ( isset( $size ) && array_key_exists( $size, $image_sizes ) && isset( $image_sizes[ $size ]['crop'] ) ) {
414
										$transform = (bool) $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
415
									}
416
								}
417
							} else {
418
								unset( $attachment_id );
419
								unset( $attachment );
420
							}
421
						}
422
					}
423
424
					// If image tag lacks width and height arguments, try to determine from strings WP appends to resized image filenames.
425
					if ( false === $width && false === $height ) {
426
						list( $width, $height ) = self::parse_dimensions_from_filename( $src );
427
					}
428
429
					$width_orig     = $width;
430
					$height_orig    = $height;
431
					$transform_orig = $transform;
432
433
					// If width is available, constrain to $content_width
434
					if ( false !== $width && false === strpos( $width, '%' ) && is_numeric( $content_width ) ) {
435
						if ( $width > $content_width && false !== $height && false === strpos( $height, '%' ) ) {
436
							$height = round( ( $content_width * $height ) / $width );
437
							$width  = $content_width;
438
						} elseif ( $width > $content_width ) {
439
							$width = $content_width;
440
						}
441
					}
442
443
					// Set a width if none is found and $content_width is available
444
					// If width is set in this manner and height is available, use `fit` instead of `resize` to prevent skewing
445
					if ( false === $width && is_numeric( $content_width ) ) {
446
						$width = (int) $content_width;
447
448
						if ( false !== $height ) {
449
							$transform = 'fit';
450
						}
451
					}
452
453
					// Detect if image source is for a custom-cropped thumbnail and prevent further URL manipulation.
454
					if ( ! $fullsize_url && preg_match_all( '#-e[a-z0-9]+(-\d+x\d+)?\.(' . implode( '|', self::$extensions ) . '){1}$#i', basename( $src ), $filename ) ) {
455
						$fullsize_url = true;
456
					}
457
458
					// Build URL, first maybe removing WP's resized string so we pass the original image to Photon
459
					if ( ! $fullsize_url && 0 === strpos( $src, $upload_dir['baseurl'] ) ) {
460
						$src = self::strip_image_dimensions_maybe( $src );
461
					}
462
463
					// Build array of Photon args and expose to filter before passing to Photon URL function
464
					$args = array();
465
466
					if ( false !== $width && false !== $height && false === strpos( $width, '%' ) && false === strpos( $height, '%' ) ) {
467
						$args[ $transform ] = $width . ',' . $height;
468
					} elseif ( false !== $width ) {
469
						$args['w'] = $width;
470
					} elseif ( false !== $height ) {
471
						$args['h'] = $height;
472
					}
473
474
					/**
475
					 * Filter the array of Photon arguments added to an image when it goes through Photon.
476
					 * By default, only includes width and height values.
477
					 *
478
					 * @see https://developer.wordpress.com/docs/photon/api/
479
					 *
480
					 * @module photon
481
					 *
482
					 * @since 2.0.0
483
					 *
484
					 * @param array $args Array of Photon Arguments.
485
					 * @param array $details {
486
					 *     Array of image details.
487
					 *
488
					 *     @type string    $tag            Image tag (Image HTML output).
489
					 *     @type string    $src            Image URL.
490
					 *     @type string    $src_orig       Original Image URL.
491
					 *     @type int|false $width          Image width.
492
					 *     @type int|false $height         Image height.
493
					 *     @type int|false $width_orig     Original image width before constrained by content_width.
494
					 *     @type int|false $height_orig    Original Image height before constrained by content_width.
495
					 *     @type string    $transform      Transform.
496
					 *     @type string    $transform_orig Original transform before constrained by content_width.
497
					 * }
498
					 */
499
					$args = apply_filters( 'jetpack_photon_post_image_args', $args, compact( 'tag', 'src', 'src_orig', 'width', 'height', 'width_orig', 'height_orig', 'transform', 'transform_orig' ) );
500
501
					$photon_url = jetpack_photon_url( $src, $args );
502
503
					// Modify image tag if Photon function provides a URL
504
					// 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.
505
					if ( $src != $photon_url ) {
506
						$new_tag = $tag;
507
508
						// If present, replace the link href with a Photoned URL for the full-size image.
509
						if ( ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
510
							$new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . jetpack_photon_url( $images['link_url'][ $index ] ) . '\2', $new_tag, 1 );
511
						}
512
513
						// Supplant the original source value with our Photon URL
514
						$photon_url = esc_url( $photon_url );
515
						$new_tag    = str_replace( $src_orig, $photon_url, $new_tag );
516
517
						// If Lazy Load is in use, pass placeholder image through Photon
518
						if ( isset( $placeholder_src ) && self::validate_image_url( $placeholder_src ) ) {
519
							$placeholder_src = jetpack_photon_url( $placeholder_src );
520
521
							if ( $placeholder_src != $placeholder_src_orig ) {
522
								$new_tag = str_replace( $placeholder_src_orig, esc_url( $placeholder_src ), $new_tag );
523
							}
524
525
							unset( $placeholder_src );
526
						}
527
528
						// If we are not transforming the image with resize, fit, or letterbox (lb), then we should remove
529
						// the width and height arguments from the image to prevent distortion. Even if $args['w'] and $args['h']
530
						// are present, Photon does not crop to those dimensions. Instead, it appears to favor height.
531
						//
532
						// If we are transforming the image via one of those methods, let's update the width and height attributes.
533
						if ( empty( $args['resize'] ) && empty( $args['fit'] ) && empty( $args['lb'] ) ) {
534
							$new_tag = preg_replace( '#(?<=\s)(width|height)=["|\']?[\d%]+["|\']?\s?#i', '', $new_tag );
535
						} else {
536
							$resize_args = isset( $args['resize'] ) ? $args['resize'] : false;
537 View Code Duplication
							if ( false == $resize_args ) {
538
								$resize_args = ( ! $resize_args && isset( $args['fit'] ) )
539
									? $args['fit']
540
									: false;
541
							}
542 View Code Duplication
							if ( false == $resize_args ) {
543
								$resize_args = ( ! $resize_args && isset( $args['lb'] ) )
544
									? $args['lb']
545
									: false;
546
							}
547
548
							$resize_args = array_map( 'trim', explode( ',', $resize_args ) );
549
550
							// (?<=\s)         - Ensure width or height attribute is preceded by a space
551
							// (width=["|\']?) - Matches, and captures, width=, width=", or width='
552
							// [\d%]+          - Matches 1 or more digits
553
							// (["|\']?)       - Matches, and captures, ", ', or empty string
554
							// \s              - Ensures there's a space after the attribute
555
							$new_tag = preg_replace( '#(?<=\s)(width=["|\']?)[\d%]+(["|\']?)\s?#i', sprintf( '${1}%d${2} ', $resize_args[0] ), $new_tag );
556
							$new_tag = preg_replace( '#(?<=\s)(height=["|\']?)[\d%]+(["|\']?)\s?#i', sprintf( '${1}%d${2} ', $resize_args[1] ), $new_tag );
557
						}
558
559
						// Tag an image for dimension checking
560
						if ( ! self::is_amp_endpoint() ) {
561
							$new_tag = preg_replace( '#(\s?/)?>(\s*</a>)?$#i', ' data-recalc-dims="1"\1>\2', $new_tag );
562
						}
563
564
						// Replace original tag with modified version
565
						$content = str_replace( $tag, $new_tag, $content );
566
					}
567
				} elseif ( preg_match( '#^http(s)?://i[\d]{1}.wp.com#', $src ) && ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
568
					$new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . jetpack_photon_url( $images['link_url'][ $index ] ) . '\2', $tag, 1 );
569
570
					$content = str_replace( $tag, $new_tag, $content );
571
				}
572
			}
573
		}
574
575
		return $content;
576
	}
577
578
	public static function filter_the_galleries( $galleries ) {
579
		if ( empty( $galleries ) || ! is_array( $galleries ) ) {
580
			return $galleries;
581
		}
582
583
		// Pass by reference, so we can modify them in place.
584
		foreach ( $galleries as &$this_gallery ) {
585
			if ( is_string( $this_gallery ) ) {
586
				$this_gallery = self::filter_the_content( $this_gallery );
587
				// LEAVING COMMENTED OUT as for the moment it doesn't seem
588
				// necessary and I'm not sure how it would propagate through.
589
				// } elseif ( is_array( $this_gallery )
590
				// && ! empty( $this_gallery['src'] )
591
				// && ! empty( $this_gallery['type'] )
592
				// && in_array( $this_gallery['type'], array( 'rectangle', 'square', 'circle' ) ) ) {
593
				// $this_gallery['src'] = array_map( 'jetpack_photon_url', $this_gallery['src'] );
594
			}
595
		}
596
		unset( $this_gallery ); // break the reference.
597
598
		return $galleries;
599
	}
600
601
602
	/**
603
	 * Runs the image widget through photon.
604
	 *
605
	 * @param array $instance Image widget instance data.
606
	 * @return array
607
	 */
608
	public static function filter_the_image_widget( $instance ) {
609
		if ( Jetpack::is_module_active( 'photon' ) && ! $instance['attachment_id'] && $instance['url'] ) {
610
			jetpack_photon_url(
611
				$instance['url'],
612
				array(
613
					'w' => $instance['width'],
614
					'h' => $instance['height'],
615
				)
616
			);
617
		}
618
619
		return $instance;
620
	}
621
622
	/**
623
	 * * CORE IMAGE RETRIEVAL
624
	 **/
625
626
	/**
627
	 * Filter post thumbnail image retrieval, passing images through Photon
628
	 *
629
	 * @param string|bool  $image
630
	 * @param int          $attachment_id
631
	 * @param string|array $size
632
	 * @uses is_admin, apply_filters, wp_get_attachment_url, self::validate_image_url, this::image_sizes, jetpack_photon_url
633
	 * @filter image_downsize
634
	 * @return string|bool
635
	 */
636
	public function filter_image_downsize( $image, $attachment_id, $size ) {
637
		// Don't foul up the admin side of things, unless a plugin wants to.
638
		if ( is_admin() &&
639
			/**
640
			 * Provide plugins a way of running Photon for images in the WordPress Dashboard (wp-admin).
641
			 *
642
			 * 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.
643
			 *
644
			 * @module photon
645
			 *
646
			 * @since 4.8.0
647
			 *
648
			 * @param bool false Stop Photon from being run on the Dashboard. Default to false.
649
			 * @param array $args {
650
			 *   Array of image details.
651
			 *
652
			 *   @type $image Image URL.
653
			 *   @type $attachment_id Attachment ID of the image.
654
			 *   @type $size Image size. Can be a string (name of the image size, e.g. full) or an array of width and height.
655
			 * }
656
			 */
657
			false === apply_filters( 'jetpack_photon_admin_allow_image_downsize', false, compact( 'image', 'attachment_id', 'size' ) )
658
		) {
659
			return $image;
660
		}
661
662
		/**
663
		 * Provide plugins a way of preventing Photon from being applied to images retrieved from WordPress Core.
664
		 *
665
		 * @module photon
666
		 *
667
		 * @since 2.0.0
668
		 *
669
		 * @param bool false Stop Photon from being applied to the image. Default to false.
670
		 * @param array $args {
671
		 *   Array of image details.
672
		 *
673
		 *   @type $image Image URL.
674
		 *   @type $attachment_id Attachment ID of the image.
675
		 *   @type $size Image size. Can be a string (name of the image size, e.g. full) or an array of width and height.
676
		 * }
677
		 */
678
		if ( apply_filters( 'jetpack_photon_override_image_downsize', false, compact( 'image', 'attachment_id', 'size' ) ) ) {
679
			return $image;
680
		}
681
682
		// Get the image URL and proceed with Photon-ification if successful
683
		$image_url = wp_get_attachment_url( $attachment_id );
684
685
		// Set this to true later when we know we have size meta.
686
		$has_size_meta = false;
687
688
		if ( $image_url ) {
689
			// Check if image URL should be used with Photon
690
			if ( ! self::validate_image_url( $image_url ) ) {
691
				return $image;
692
			}
693
694
			$intermediate = true; // For the fourth array item returned by the image_downsize filter.
695
696
			// If an image is requested with a size known to WordPress, use that size's settings with Photon.
697
			// WP states that `add_image_size()` should use a string for the name, but doesn't enforce that.
698
			// Due to differences in how Core and Photon check for the registered image size, we check both types.
699
			if ( ( is_string( $size ) || is_int( $size ) ) && array_key_exists( $size, self::image_sizes() ) ) {
700
				$image_args = self::image_sizes();
701
				$image_args = $image_args[ $size ];
702
703
				$photon_args = array();
704
705
				$image_meta = image_get_intermediate_size( $attachment_id, $size );
706
707
				// 'full' is a special case: We need consistent data regardless of the requested size.
708
				if ( 'full' == $size ) {
709
					$image_meta   = wp_get_attachment_metadata( $attachment_id );
710
					$intermediate = false;
711
				} elseif ( ! $image_meta ) {
712
					// If we still don't have any image meta at this point, it's probably from a custom thumbnail size
713
					// for an image that was uploaded before the custom image was added to the theme.  Try to determine the size manually.
714
					$image_meta = wp_get_attachment_metadata( $attachment_id );
715
716
					if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
717
						$image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $image_args['width'], $image_args['height'], $image_args['crop'] );
718
						if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
719
							$image_meta['width']  = $image_resized[6];
720
							$image_meta['height'] = $image_resized[7];
721
						}
722
					}
723
				}
724
725
				if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
726
					$image_args['width']  = $image_meta['width'];
727
					$image_args['height'] = $image_meta['height'];
728
729
					list( $image_args['width'], $image_args['height'] ) = image_constrain_size_for_editor( $image_args['width'], $image_args['height'], $size, 'display' );
730
					$has_size_meta                                      = true;
731
				}
732
733
				// Expose determined arguments to a filter before passing to Photon
734
				$transform = $image_args['crop'] ? 'resize' : 'fit';
735
736
				// Check specified image dimensions and account for possible zero values; photon fails to resize if a dimension is zero.
737
				if ( 0 == $image_args['width'] || 0 == $image_args['height'] ) {
738
					if ( 0 == $image_args['width'] && 0 < $image_args['height'] ) {
739
						$photon_args['h'] = $image_args['height'];
740
					} elseif ( 0 == $image_args['height'] && 0 < $image_args['width'] ) {
741
						$photon_args['w'] = $image_args['width'];
742
					}
743
				} else {
744
					if ( ( 'resize' === $transform ) && $image_meta = wp_get_attachment_metadata( $attachment_id ) ) {
745
						if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
746
							// Lets make sure that we don't upscale images since wp never upscales them as well
747
							$smaller_width  = ( ( $image_meta['width'] < $image_args['width'] ) ? $image_meta['width'] : $image_args['width'] );
748
							$smaller_height = ( ( $image_meta['height'] < $image_args['height'] ) ? $image_meta['height'] : $image_args['height'] );
749
750
							$photon_args[ $transform ] = $smaller_width . ',' . $smaller_height;
751
						}
752
					} else {
753
						$photon_args[ $transform ] = $image_args['width'] . ',' . $image_args['height'];
754
					}
755
				}
756
757
				/**
758
				 * Filter the Photon Arguments added to an image when going through Photon, when that image size is a string.
759
				 * Image size will be a string (e.g. "full", "medium") when it is known to WordPress.
760
				 *
761
				 * @module photon
762
				 *
763
				 * @since 2.0.0
764
				 *
765
				 * @param array $photon_args Array of Photon arguments.
766
				 * @param array $args {
767
				 *   Array of image details.
768
				 *
769
				 *   @type $image_args Array of Image arguments (width, height, crop).
770
				 *   @type $image_url Image URL.
771
				 *   @type $attachment_id Attachment ID of the image.
772
				 *   @type $size Image size. Can be a string (name of the image size, e.g. full) or an integer.
773
				 *   @type $transform Value can be resize or fit.
774
				 *                    @see https://developer.wordpress.com/docs/photon/api
775
				 * }
776
				 */
777
				$photon_args = apply_filters( 'jetpack_photon_image_downsize_string', $photon_args, compact( 'image_args', 'image_url', 'attachment_id', 'size', 'transform' ) );
778
779
				// Generate Photon URL
780
				$image = array(
781
					jetpack_photon_url( $image_url, $photon_args ),
782
					$has_size_meta ? $image_args['width'] : false,
783
					$has_size_meta ? $image_args['height'] : false,
784
					$intermediate,
785
				);
786
			} elseif ( is_array( $size ) ) {
787
				// Pull width and height values from the provided array, if possible
788
				$width  = isset( $size[0] ) ? (int) $size[0] : false;
789
				$height = isset( $size[1] ) ? (int) $size[1] : false;
790
791
				// Don't bother if necessary parameters aren't passed.
792
				if ( ! $width || ! $height ) {
793
					return $image;
794
				}
795
796
				$image_meta = wp_get_attachment_metadata( $attachment_id );
797
				if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
798
					$image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $width, $height );
799
800
					if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
801
						$width  = $image_resized[6];
802
						$height = $image_resized[7];
803
					} else {
804
						$width  = $image_meta['width'];
805
						$height = $image_meta['height'];
806
					}
807
808
					$has_size_meta = true;
809
				}
810
811
				list( $width, $height ) = image_constrain_size_for_editor( $width, $height, $size );
812
813
				// Expose arguments to a filter before passing to Photon
814
				$photon_args = array(
815
					'fit' => $width . ',' . $height,
816
				);
817
818
				/**
819
				 * Filter the Photon Arguments added to an image when going through Photon,
820
				 * when the image size is an array of height and width values.
821
				 *
822
				 * @module photon
823
				 *
824
				 * @since 2.0.0
825
				 *
826
				 * @param array $photon_args Array of Photon arguments.
827
				 * @param array $args {
828
				 *   Array of image details.
829
				 *
830
				 *   @type $width Image width.
831
				 *   @type height Image height.
832
				 *   @type $image_url Image URL.
833
				 *   @type $attachment_id Attachment ID of the image.
834
				 * }
835
				 */
836
				$photon_args = apply_filters( 'jetpack_photon_image_downsize_array', $photon_args, compact( 'width', 'height', 'image_url', 'attachment_id' ) );
837
838
				// Generate Photon URL
839
				$image = array(
840
					jetpack_photon_url( $image_url, $photon_args ),
841
					$has_size_meta ? $width : false,
842
					$has_size_meta ? $height : false,
843
					$intermediate,
844
				);
845
			}
846
		}
847
848
		return $image;
849
	}
850
851
	/**
852
	 * Filters an array of image `srcset` values, replacing each URL with its Photon equivalent.
853
	 *
854
	 * @since 3.8.0
855
	 * @since 4.0.4 Added automatically additional sizes beyond declared image sizes.
856
	 * @param array $sources An array of image urls and widths.
857
	 * @uses self::validate_image_url, jetpack_photon_url, Jetpack_Photon::parse_from_filename
858
	 * @uses Jetpack_Photon::strip_image_dimensions_maybe, Jetpack::get_content_width
859
	 * @return array An array of Photon image urls and widths.
860
	 */
861
	public function filter_srcset_array( $sources = array(), $size_array = array(), $image_src = array(), $image_meta = array(), $attachment_id = 0 ) {
862
		if ( ! is_array( $sources ) ) {
863
			return $sources;
864
		}
865
		$upload_dir = wp_get_upload_dir();
866
867
		foreach ( $sources as $i => $source ) {
868
			if ( ! self::validate_image_url( $source['url'] ) ) {
869
				continue;
870
			}
871
872
			/** This filter is already documented in class.photon.php */
873
			if ( apply_filters( 'jetpack_photon_skip_image', false, $source['url'], $source ) ) {
874
				continue;
875
			}
876
877
			$url                    = $source['url'];
878
			list( $width, $height ) = self::parse_dimensions_from_filename( $url );
879
880
			// It's quicker to get the full size with the data we have already, if available
881
			if ( ! empty( $attachment_id ) ) {
882
				$url = wp_get_attachment_url( $attachment_id );
883
			} else {
884
				$url = self::strip_image_dimensions_maybe( $url );
885
			}
886
887
			$args = array();
888
			if ( 'w' === $source['descriptor'] ) {
889
				if ( $height && ( $source['value'] == $width ) ) {
890
					$args['resize'] = $width . ',' . $height;
891
				} else {
892
					$args['w'] = $source['value'];
893
				}
894
			}
895
896
			$sources[ $i ]['url'] = jetpack_photon_url( $url, $args );
897
		}
898
899
		/**
900
		 * At this point, $sources is the original srcset with Photonized URLs.
901
		 * Now, we're going to construct additional sizes based on multiples of the content_width.
902
		 * This will reduce the gap between the largest defined size and the original image.
903
		 */
904
905
		/**
906
		 * Filter the multiplier Photon uses to create new srcset items.
907
		 * Return false to short-circuit and bypass auto-generation.
908
		 *
909
		 * @module photon
910
		 *
911
		 * @since 4.0.4
912
		 *
913
		 * @param array|bool $multipliers Array of multipliers to use or false to bypass.
914
		 */
915
		$multipliers = apply_filters( 'jetpack_photon_srcset_multipliers', array( 2, 3 ) );
916
		$url         = trailingslashit( $upload_dir['baseurl'] ) . $image_meta['file'];
917
918
		if (
919
			/** Short-circuit via jetpack_photon_srcset_multipliers filter. */
920
			is_array( $multipliers )
921
			/** This filter is already documented in class.photon.php */
922
			&& ! apply_filters( 'jetpack_photon_skip_image', false, $url, null )
923
			/** Verify basic meta is intact. */
924
			&& isset( $image_meta['width'] ) && isset( $image_meta['height'] ) && isset( $image_meta['file'] )
925
			/** Verify we have the requested width/height. */
926
			&& isset( $size_array[0] ) && isset( $size_array[1] )
927
			) {
928
929
			$fullwidth  = $image_meta['width'];
930
			$fullheight = $image_meta['height'];
931
			$reqwidth   = $size_array[0];
932
			$reqheight  = $size_array[1];
933
934
			$constrained_size = wp_constrain_dimensions( $fullwidth, $fullheight, $reqwidth );
935
			$expected_size    = array( $reqwidth, $reqheight );
936
937
			if ( abs( $constrained_size[0] - $expected_size[0] ) <= 1 && abs( $constrained_size[1] - $expected_size[1] ) <= 1 ) {
938
				$crop = 'soft';
939
				$base = Jetpack::get_content_width() ? Jetpack::get_content_width() : 1000; // Provide a default width if none set by the theme.
940
			} else {
941
				$crop = 'hard';
942
				$base = $reqwidth;
943
			}
944
945
			$currentwidths = array_keys( $sources );
946
			$newsources    = null;
947
948
			foreach ( $multipliers as $multiplier ) {
949
950
				$newwidth = $base * $multiplier;
951
				foreach ( $currentwidths as $currentwidth ) {
952
					// If a new width would be within 100 pixes of an existing one or larger than the full size image, skip.
953
					if ( abs( $currentwidth - $newwidth ) < 50 || ( $newwidth > $fullwidth ) ) {
954
						continue 2; // Back to the foreach ( $multipliers as $multiplier )
955
					}
956
				} // foreach ( $currentwidths as $currentwidth ){
957
958
				if ( 'soft' == $crop ) {
959
					$args = array(
960
						'w' => $newwidth,
961
					);
962
				} else { // hard crop, e.g. add_image_size( 'example', 200, 200, true );
963
					$args = array(
964
						'zoom'   => $multiplier,
965
						'resize' => $reqwidth . ',' . $reqheight,
966
					);
967
				}
968
969
				$newsources[ $newwidth ] = array(
970
					'url'        => jetpack_photon_url( $url, $args ),
971
					'descriptor' => 'w',
972
					'value'      => $newwidth,
973
				);
974
			} // foreach ( $multipliers as $multiplier )
975
			if ( is_array( $newsources ) ) {
976
				$sources = array_replace( $sources, $newsources );
977
			}
978
		} // if ( isset( $image_meta['width'] ) && isset( $image_meta['file'] ) )
979
980
		return $sources;
981
	}
982
983
	/**
984
	 * Filters an array of image `sizes` values, using $content_width instead of image's full size.
985
	 *
986
	 * @since 4.0.4
987
	 * @since 4.1.0 Returns early for images not within the_content.
988
	 * @param array $sizes An array of media query breakpoints.
989
	 * @param array $size  Width and height of the image
990
	 * @uses Jetpack::get_content_width
991
	 * @return array An array of media query breakpoints.
992
	 */
993
	public function filter_sizes( $sizes, $size ) {
994
		if ( ! doing_filter( 'the_content' ) ) {
995
			return $sizes;
996
		}
997
		$content_width = Jetpack::get_content_width();
998
		if ( ! $content_width ) {
999
			$content_width = 1000;
1000
		}
1001
1002
		if ( ( is_array( $size ) && $size[0] < $content_width ) ) {
1003
			return $sizes;
1004
		}
1005
1006
		return sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $content_width );
1007
	}
1008
1009
	/**
1010
	 * * GENERAL FUNCTIONS
1011
	 **/
1012
1013
	/**
1014
	 * Ensure image URL is valid for Photon.
1015
	 * 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.
1016
	 *
1017
	 * @param string $url
1018
	 * @uses wp_parse_args
1019
	 * @return bool
1020
	 */
1021
	protected static function validate_image_url( $url ) {
1022
		$parsed_url = wp_parse_url( $url );
1023
1024
		if ( ! $parsed_url ) {
1025
			return false;
1026
		}
1027
1028
		// Parse URL and ensure needed keys exist, since the array returned by `wp_parse_url` only includes the URL components it finds.
1029
		$url_info = wp_parse_args(
1030
			$parsed_url,
1031
			array(
1032
				'scheme' => null,
1033
				'host'   => null,
1034
				'port'   => null,
1035
				'path'   => null,
1036
			)
1037
		);
1038
1039
		// Bail if scheme isn't http or port is set that isn't port 80
1040
		if (
1041
			( 'http' != $url_info['scheme'] || ! in_array( $url_info['port'], array( 80, null ) ) ) &&
1042
			/**
1043
			 * Allow Photon to fetch images that are served via HTTPS.
1044
			 *
1045
			 * @module photon
1046
			 *
1047
			 * @since 2.4.0
1048
			 * @since 3.9.0 Default to false.
1049
			 *
1050
			 * @param bool $reject_https Should Photon ignore images using the HTTPS scheme. Default to false.
1051
			 */
1052
			apply_filters( 'jetpack_photon_reject_https', false )
1053
		) {
1054
			return false;
1055
		}
1056
1057
		// Bail if no host is found
1058
		if ( is_null( $url_info['host'] ) ) {
1059
			return false;
1060
		}
1061
1062
		// Bail if the image alredy went through Photon
1063
		if ( preg_match( '#^i[\d]{1}.wp.com$#i', $url_info['host'] ) ) {
1064
			return false;
1065
		}
1066
1067
		// Bail if no path is found
1068
		if ( is_null( $url_info['path'] ) ) {
1069
			return false;
1070
		}
1071
1072
		// Ensure image extension is acceptable
1073
		if ( ! in_array( strtolower( pathinfo( $url_info['path'], PATHINFO_EXTENSION ) ), self::$extensions ) ) {
1074
			return false;
1075
		}
1076
1077
		// If we got this far, we should have an acceptable image URL
1078
		// But let folks filter to decline if they prefer.
1079
		/**
1080
		 * Overwrite the results of the validation steps an image goes through before to be considered valid to be used by Photon.
1081
		 *
1082
		 * @module photon
1083
		 *
1084
		 * @since 3.0.0
1085
		 *
1086
		 * @param bool true Is the image URL valid and can it be used by Photon. Default to true.
1087
		 * @param string $url Image URL.
1088
		 * @param array $parsed_url Array of information about the image.
1089
		 */
1090
		return apply_filters( 'photon_validate_image_url', true, $url, $parsed_url );
1091
	}
1092
1093
	/**
1094
	 * Checks if the file exists before it passes the file to photon
1095
	 *
1096
	 * @param string $src The image URL
1097
	 * @return string
1098
	 **/
1099
	public static function strip_image_dimensions_maybe( $src ) {
1100
		$stripped_src = $src;
1101
1102
		// Build URL, first removing WP's resized string so we pass the original image to Photon
1103
		if ( preg_match( '#(-\d+x\d+)\.(' . implode( '|', self::$extensions ) . '){1}$#i', $src, $src_parts ) ) {
1104
			$stripped_src = str_replace( $src_parts[1], '', $src );
1105
			$upload_dir   = wp_get_upload_dir();
1106
1107
			// Extracts the file path to the image minus the base url
1108
			$file_path = substr( $stripped_src, strlen( $upload_dir['baseurl'] ) );
1109
1110
			if ( file_exists( $upload_dir['basedir'] . $file_path ) ) {
1111
				$src = $stripped_src;
1112
			}
1113
		}
1114
1115
		return $src;
1116
	}
1117
1118
	/**
1119
	 * Provide an array of available image sizes and corresponding dimensions.
1120
	 * Similar to get_intermediate_image_sizes() except that it includes image sizes' dimensions, not just their names.
1121
	 *
1122
	 * @global $wp_additional_image_sizes
1123
	 * @uses get_option
1124
	 * @return array
1125
	 */
1126
	protected static function image_sizes() {
1127
		if ( null == self::$image_sizes ) {
1128
			global $_wp_additional_image_sizes;
1129
1130
			// Populate an array matching the data structure of $_wp_additional_image_sizes so we have a consistent structure for image sizes
1131
			$images = array(
1132
				'thumb'        => array(
1133
					'width'  => intval( get_option( 'thumbnail_size_w' ) ),
1134
					'height' => intval( get_option( 'thumbnail_size_h' ) ),
1135
					'crop'   => (bool) get_option( 'thumbnail_crop' ),
1136
				),
1137
				'medium'       => array(
1138
					'width'  => intval( get_option( 'medium_size_w' ) ),
1139
					'height' => intval( get_option( 'medium_size_h' ) ),
1140
					'crop'   => false,
1141
				),
1142
				'medium_large' => array(
1143
					'width'  => intval( get_option( 'medium_large_size_w' ) ),
1144
					'height' => intval( get_option( 'medium_large_size_h' ) ),
1145
					'crop'   => false,
1146
				),
1147
				'large'        => array(
1148
					'width'  => intval( get_option( 'large_size_w' ) ),
1149
					'height' => intval( get_option( 'large_size_h' ) ),
1150
					'crop'   => false,
1151
				),
1152
				'full'         => array(
1153
					'width'  => null,
1154
					'height' => null,
1155
					'crop'   => false,
1156
				),
1157
			);
1158
1159
			// Compatibility mapping as found in wp-includes/media.php
1160
			$images['thumbnail'] = $images['thumb'];
1161
1162
			// Update class variable, merging in $_wp_additional_image_sizes if any are set
1163
			if ( is_array( $_wp_additional_image_sizes ) && ! empty( $_wp_additional_image_sizes ) ) {
1164
				self::$image_sizes = array_merge( $images, $_wp_additional_image_sizes );
1165
			} else {
1166
				self::$image_sizes = $images;
1167
			}
1168
		}
1169
1170
		return is_array( self::$image_sizes ) ? self::$image_sizes : array();
1171
	}
1172
1173
	/**
1174
	 * Pass og:image URLs through Photon
1175
	 *
1176
	 * @param array $tags
1177
	 * @param array $parameters
1178
	 * @uses jetpack_photon_url
1179
	 * @return array
1180
	 */
1181
	function filter_open_graph_tags( $tags, $parameters ) {
1182
		if ( empty( $tags['og:image'] ) ) {
1183
			return $tags;
1184
		}
1185
1186
		$photon_args = array(
1187
			'fit' => sprintf( '%d,%d', 2 * $parameters['image_width'], 2 * $parameters['image_height'] ),
1188
		);
1189
1190
		if ( is_array( $tags['og:image'] ) ) {
1191
			$images = array();
1192
			foreach ( $tags['og:image'] as $image ) {
1193
				$images[] = jetpack_photon_url( $image, $photon_args );
1194
			}
1195
			$tags['og:image'] = $images;
1196
		} else {
1197
			$tags['og:image'] = jetpack_photon_url( $tags['og:image'], $photon_args );
1198
		}
1199
1200
		return $tags;
1201
	}
1202
1203
	public function noresize_intermediate_sizes( $sizes ) {
1204
		return __return_empty_array();
1205
	}
1206
1207
	/**
1208
	 * Enqueue Photon helper script
1209
	 *
1210
	 * @uses wp_enqueue_script, plugins_url
1211
	 * @action wp_enqueue_script
1212
	 * @return null
1213
	 */
1214
	public function action_wp_enqueue_scripts() {
1215
		if ( self::is_amp_endpoint() ) {
1216
			return;
1217
		}
1218
		wp_enqueue_script(
1219
			'jetpack-photon',
1220
			Assets::get_file_url_for_environment(
1221
				'_inc/build/photon/photon.min.js',
1222
				'modules/photon/photon.js'
1223
			),
1224
			array(),
1225
			20191001,
1226
			true
1227
		);
1228
	}
1229
1230
	/**
1231
	 * Determine if image_downsize should utilize Photon via REST API.
1232
	 *
1233
	 * The WordPress Block Editor (Gutenberg) and other REST API consumers using the wp/v2/media endpoint, especially in the "edit"
1234
	 * context is more akin to the is_admin usage of Photon (see filter_image_downsize). Since consumers are trying to edit content in posts,
1235
	 * Photon should not fire as it will fire later on display. By aborting an attempt to Photonize an image here, we
1236
	 * prevents issues like https://github.com/Automattic/jetpack/issues/10580 .
1237
	 *
1238
	 * To determine if we're using the wp/v2/media endpoint, we hook onto the `rest_request_before_callbacks` filter and
1239
	 * if determined we are using it in the edit context, we'll false out the `jetpack_photon_override_image_downsize` filter.
1240
	 *
1241
	 * @see Jetpack_Photon::filter_image_downsize()
1242
	 *
1243
	 * @param null|WP_Error   $response
1244
	 * @param array           $endpoint_data
1245
	 * @param WP_REST_Request $request  Request used to generate the response.
1246
	 *
1247
	 * @return null|WP_Error The original response object without modification.
1248
	 */
1249
	public function should_rest_photon_image_downsize( $response, $endpoint_data, $request ) {
1250
		if ( ! is_a( $request, 'WP_REST_Request' ) ) {
1251
			return $response; // Something odd is happening. Do nothing and return the response.
1252
		}
1253
1254
		if ( is_wp_error( $response ) ) {
1255
			// If we're going to return an error, we don't need to do anything with Photon.
1256
			return $response;
1257
		}
1258
1259
		$route = $request->get_route();
1260
1261
		if ( false !== strpos( $route, 'wp/v2/media' ) && 'edit' === $request['context'] ) {
1262
			// Don't use `__return_true()`: Use something unique. See ::_override_image_downsize_in_rest_edit_context()
1263
			// Late execution to avoid conflict with other plugins as we really don't want to run in this situation.
1264
			add_filter( 'jetpack_photon_override_image_downsize', array( $this, '_override_image_downsize_in_rest_edit_context' ), 999999 );
1265
		}
1266
1267
		return $response;
1268
1269
	}
1270
1271
	/**
1272
	 * Remove the override we may have added in ::should_rest_photon_image_downsize()
1273
	 * Since ::_override_image_downsize_in_rest_edit_context() is only
1274
	 * every used here, we can always remove it without ever worrying
1275
	 * about breaking any other configuration.
1276
	 *
1277
	 * @param mixed $response
1278
	 * @return mixed Unchanged $response
1279
	 */
1280
	public function cleanup_rest_photon_image_downsize( $response ) {
1281
		remove_filter( 'jetpack_photon_override_image_downsize', array( $this, '_override_image_downsize_in_rest_edit_context' ), 999999 );
1282
		return $response;
1283
	}
1284
1285
	/**
1286
	 * Used internally by ::should_rest_photon_image_downsize() to not photonize
1287
	 * image URLs in ?context=edit REST requests.
1288
	 * MUST NOT be used anywhere else.
1289
	 * We use a unique function instead of __return_true so that we can clean up
1290
	 * after ourselves without breaking anyone else's filters.
1291
	 *
1292
	 * @internal
1293
	 * @return true
1294
	 */
1295
	public function _override_image_downsize_in_rest_edit_context() {
1296
		return true;
1297
	}
1298
1299
	/**
1300
	 * Return whether the current page is AMP.
1301
	 *
1302
	 * This is only present for the sake of WordPress.com where the Jetpack_AMP_Support
1303
	 * class does not yet exist. This mehod may only be called at the wp action or later.
1304
	 *
1305
	 * @return bool Whether AMP page.
1306
	 */
1307
	private static function is_amp_endpoint() {
1308
		return class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request();
1309
	}
1310
}
1311