Completed
Push — fix/untranslated-module-names ( 4226a9...c5f2cb )
by
unknown
11:13
created

class.photon.php (5 issues)

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
class Jetpack_Photon {
4
	/**
5
	 * Class variables
6
	 */
7
	// Oh look, a singleton
8
	private static $__instance = null;
9
10
	// Allowed extensions must match http://code.trac.wordpress.org/browser/photon/index.php#L31
11
	protected static $extensions = array(
12
		'gif',
13
		'jpg',
14
		'jpeg',
15
		'png'
16
	);
17
18
	// Don't access this directly. Instead, use self::image_sizes() so it's actually populated with something.
19
	protected static $image_sizes = null;
20
21
	/**
22
	 * Singleton implementation
23
	 *
24
	 * @return object
25
	 */
26
	public static function instance() {
27
		if ( ! is_a( self::$__instance, 'Jetpack_Photon' ) ) {
28
			self::$__instance = new Jetpack_Photon;
29
			self::$__instance->setup();
30
		}
31
32
		return self::$__instance;
33
	}
34
35
	/**
36
	 * Silence is golden.
37
	 */
38
	private function __construct() {}
39
40
	/**
41
	 * Register actions and filters, but only if basic Photon functions are available.
42
	 * The basic functions are found in ./functions.photon.php.
43
	 *
44
	 * @uses add_action, add_filter
45
	 * @return null
46
	 */
47
	private function setup() {
48
		// Display warning if site is private
49
		add_action( 'jetpack_activate_module_photon', array( $this, 'action_jetpack_activate_module_photon' ) );
50
51
		if ( ! function_exists( 'jetpack_photon_url' ) )
52
			return;
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
58
		// Core image retrieval
59
		add_filter( 'image_downsize', array( $this, 'filter_image_downsize' ), 10, 3 );
60
61
		// Responsive image srcset substitution
62
		add_filter( 'wp_calculate_image_srcset', array( $this, 'filter_srcset_array' ), 10, 4 );
63
64
		// Helpers for maniuplated images
65
		add_action( 'wp_enqueue_scripts', array( $this, 'action_wp_enqueue_scripts' ), 9 );
66
	}
67
68
	/**
69
	 * Check if site is private and warn user if it is
70
	 *
71
	 * @uses Jetpack::check_privacy
72
	 * @action jetpack_activate_module_photon
73
	 * @return null
74
	 */
75
	public function action_jetpack_activate_module_photon() {
76
		Jetpack::check_privacy( __FILE__ );
77
	}
78
79
	/**
80
	 ** IN-CONTENT IMAGE MANIPULATION FUNCTIONS
81
	 **/
82
83
	/**
84
	 * Match all images and any relevant <a> tags in a block of HTML.
85
	 *
86
	 * @param string $content Some HTML.
87
	 * @return array An array of $images matches, where $images[0] is
88
	 *         an array of full matches, and the link_url, img_tag,
89
	 *         and img_url keys are arrays of those matches.
90
	 */
91
	public static function parse_images_from_html( $content ) {
92
		$images = array();
93
94
		if ( preg_match_all( '#(?:<a[^>]+?href=["|\'](?P<link_url>[^\s]+?)["|\'][^>]*?>\s*)?(?P<img_tag><img[^>]+?src=["|\'](?P<img_url>[^\s]+?)["|\'].*?>){1}(?:\s*</a>)?#is', $content, $images ) ) {
95
			foreach ( $images as $key => $unused ) {
96
				// Simplify the output as much as possible, mostly for confirming test results.
97
				if ( is_numeric( $key ) && $key > 0 )
98
					unset( $images[$key] );
99
			}
100
101
			return $images;
102
		}
103
104
		return array();
105
	}
106
107
	/**
108
	 * Try to determine height and width from strings WP appends to resized image filenames.
109
	 *
110
	 * @param string $src The image URL.
111
	 * @return array An array consisting of width and height.
112
	 */
113
	public static function parse_dimensions_from_filename( $src ) {
114
		$width_height_string = array();
115
116
		if ( preg_match( '#-(\d+)x(\d+)\.(?:' . implode('|', self::$extensions ) . '){1}$#i', $src, $width_height_string ) ) {
117
			$width = (int) $width_height_string[1];
118
			$height = (int) $width_height_string[2];
119
120
			if ( $width && $height )
121
				return array( $width, $height );
122
		}
123
124
		return array( false, false );
125
	}
126
127
	/**
128
	 * Identify images in post content, and if images are local (uploaded to the current site), pass through Photon.
129
	 *
130
	 * @param string $content
131
	 * @uses self::validate_image_url, apply_filters, jetpack_photon_url, esc_url
132
	 * @filter the_content
133
	 * @return string
134
	 */
135
	public static function filter_the_content( $content ) {
136
		$images = Jetpack_Photon::parse_images_from_html( $content );
137
138
		if ( ! empty( $images ) ) {
139
			$content_width = Jetpack::get_content_width();
140
141
			$image_sizes = self::image_sizes();
142
			$upload_dir = wp_upload_dir();
143
144
			foreach ( $images[0] as $index => $tag ) {
145
				// Default to resize, though fit may be used in certain cases where a dimension cannot be ascertained
146
				$transform = 'resize';
147
148
				// Start with a clean attachment ID each time
149
				$attachment_id = false;
150
151
				// Flag if we need to munge a fullsize URL
152
				$fullsize_url = false;
153
154
				// Identify image source
155
				$src = $src_orig = $images['img_url'][ $index ];
156
157
				/**
158
				 * Allow specific images to be skipped by Photon.
159
				 *
160
				 * @module photon
161
				 *
162
				 * @since 2.0.3
163
				 *
164
				 * @param bool false Should Photon ignore this image. Default to false.
165
				 * @param string $src Image URL.
166
				 * @param string $tag Image Tag (Image HTML output).
167
				 */
168
				if ( apply_filters( 'jetpack_photon_skip_image', false, $src, $tag ) )
169
					continue;
170
171
				// Support Automattic's Lazy Load plugin
172
				// Can't modify $tag yet as we need unadulterated version later
173
				if ( preg_match( '#data-lazy-src=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
174
					$placeholder_src = $placeholder_src_orig = $src;
175
					$src = $src_orig = $lazy_load_src[1];
176
				} elseif ( preg_match( '#data-lazy-original=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
177
					$placeholder_src = $placeholder_src_orig = $src;
178
					$src = $src_orig = $lazy_load_src[1];
179
				}
180
181
				// Check if image URL should be used with Photon
182
				if ( self::validate_image_url( $src ) ) {
183
					// Find the width and height attributes
184
					$width = $height = false;
185
186
					// First, check the image tag
187
					if ( preg_match( '#width=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $width_string ) )
188
						$width = $width_string[1];
189
190
					if ( preg_match( '#height=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $height_string ) )
191
						$height = $height_string[1];
192
193
					// Can't pass both a relative width and height, so unset the height in favor of not breaking the horizontal layout.
194
					if ( false !== strpos( $width, '%' ) && false !== strpos( $height, '%' ) )
195
						$width = $height = false;
196
197
					// Detect WP registered image size from HTML class
198
					if ( preg_match( '#class=["|\']?[^"\']*size-([^"\'\s]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $size ) ) {
199
						$size = array_pop( $size );
200
201
						if ( false === $width && false === $height && 'full' != $size && array_key_exists( $size, $image_sizes ) ) {
202
							$width = (int) $image_sizes[ $size ]['width'];
203
							$height = (int) $image_sizes[ $size ]['height'];
204
							$transform = $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
205
						}
206
					} else {
207
						unset( $size );
208
					}
209
210
					// WP Attachment ID, if uploaded to this site
211
					if (
212
						preg_match( '#class=["|\']?[^"\']*wp-image-([\d]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $attachment_id ) &&
213
						(
214
							0 === strpos( $src, $upload_dir['baseurl'] ) ||
215
							/**
216
							 * Filter whether an image using an attachment ID in its class has to be uploaded to the local site to go through Photon.
217
							 *
218
							 * @module photon
219
							 *
220
							 * @since 2.0.3
221
							 *
222
							 * @param bool false Was the image uploaded to the local site. Default to false.
223
							 * @param array $args {
224
							 * 	 Array of image details.
225
							 *
226
							 * 	 @type $src Image URL.
227
							 * 	 @type tag Image tag (Image HTML output).
228
							 * 	 @type $images Array of information about the image.
229
							 * 	 @type $index Image index.
230
							 * }
231
							 */
232
							apply_filters( 'jetpack_photon_image_is_local', false, compact( 'src', 'tag', 'images', 'index' ) )
233
						)
234
					) {
235
						$attachment_id = intval( array_pop( $attachment_id ) );
236
237
						if ( $attachment_id ) {
238
							$attachment = get_post( $attachment_id );
239
240
							// Basic check on returned post object
241
							if ( is_object( $attachment ) && ! is_wp_error( $attachment ) && 'attachment' == $attachment->post_type ) {
242
								$src_per_wp = wp_get_attachment_image_src( $attachment_id, isset( $size ) ? $size : 'full' );
243
244
								if ( self::validate_image_url( $src_per_wp[0] ) ) {
245
									$src = $src_per_wp[0];
246
									$fullsize_url = true;
247
248
									// Prevent image distortion if a detected dimension exceeds the image's natural dimensions
249
									if ( ( false !== $width && $width > $src_per_wp[1] ) || ( false !== $height && $height > $src_per_wp[2] ) ) {
250
										$width = false == $width ? false : min( $width, $src_per_wp[1] );
251
										$height = false == $height ? false : min( $height, $src_per_wp[2] );
252
									}
253
254
									// If no width and height are found, max out at source image's natural dimensions
255
									// Otherwise, respect registered image sizes' cropping setting
256
									if ( false == $width && false == $height ) {
257
										$width = $src_per_wp[1];
258
										$height = $src_per_wp[2];
259
										$transform = 'fit';
260
									} elseif ( isset( $size ) && array_key_exists( $size, $image_sizes ) && isset( $image_sizes[ $size ]['crop'] ) ) {
261
										$transform = (bool) $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
262
									}
263
								}
264
							} else {
265
								unset( $attachment_id );
266
								unset( $attachment );
267
							}
268
						}
269
					}
270
271
					// If image tag lacks width and height arguments, try to determine from strings WP appends to resized image filenames.
272
					if ( false === $width && false === $height ) {
273
						list( $width, $height ) = Jetpack_Photon::parse_dimensions_from_filename( $src );
274
					}
275
276
					// If width is available, constrain to $content_width
277
					if ( false !== $width && false === strpos( $width, '%' ) && is_numeric( $content_width ) ) {
278
						if ( $width > $content_width && false !== $height && false === strpos( $height, '%' ) ) {
279
							$height = round( ( $content_width * $height ) / $width );
280
							$width = $content_width;
281
						} elseif ( $width > $content_width ) {
282
							$width = $content_width;
283
						}
284
					}
285
286
					// Set a width if none is found and $content_width is available
287
					// If width is set in this manner and height is available, use `fit` instead of `resize` to prevent skewing
288
					if ( false === $width && is_numeric( $content_width ) ) {
289
						$width = (int) $content_width;
290
291
						if ( false !== $height )
292
							$transform = 'fit';
293
					}
294
295
					// Detect if image source is for a custom-cropped thumbnail and prevent further URL manipulation.
296
					if ( ! $fullsize_url && preg_match_all( '#-e[a-z0-9]+(-\d+x\d+)?\.(' . implode('|', self::$extensions ) . '){1}$#i', basename( $src ), $filename ) )
297
						$fullsize_url = true;
298
299
					// Build URL, first maybe removing WP's resized string so we pass the original image to Photon
300
					if ( ! $fullsize_url ) {
301
						$src = self::strip_image_dimensions_maybe( $src );
302
					}
303
304
					// Build array of Photon args and expose to filter before passing to Photon URL function
305
					$args = array();
306
307
					if ( false !== $width && false !== $height && false === strpos( $width, '%' ) && false === strpos( $height, '%' ) )
308
						$args[ $transform ] = $width . ',' . $height;
309
					elseif ( false !== $width )
310
						$args['w'] = $width;
311
					elseif ( false !== $height )
312
						$args['h'] = $height;
313
314
					/**
315
					 * Filter the array of Photon arguments added to an image when it goes through Photon.
316
					 * By default, only includes width and height values.
317
					 * @see https://developer.wordpress.com/docs/photon/api/
318
					 *
319
					 * @module photon
320
					 *
321
					 * @since 2.0.0
322
					 *
323
					 * @param array $args Array of Photon Arguments.
324
					 * @param array $args {
325
					 * 	 Array of image details.
326
					 *
327
					 * 	 @type $tag Image tag (Image HTML output).
328
					 * 	 @type $src Image URL.
329
					 * 	 @type $src_orig Original Image URL.
330
					 * 	 @type $width Image width.
331
					 * 	 @type $height Image height.
332
					 * }
333
					 */
334
					$args = apply_filters( 'jetpack_photon_post_image_args', $args, compact( 'tag', 'src', 'src_orig', 'width', 'height' ) );
335
336
					$photon_url = jetpack_photon_url( $src, $args );
337
338
					// Modify image tag if Photon function provides a URL
339
					// 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.
340
					if ( $src != $photon_url ) {
341
						$new_tag = $tag;
342
343
						// If present, replace the link href with a Photoned URL for the full-size image.
344
						if ( ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) )
345
							$new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . jetpack_photon_url( $images['link_url'][ $index ] ) . '\2', $new_tag, 1 );
346
347
						// Supplant the original source value with our Photon URL
348
						$photon_url = esc_url( $photon_url );
349
						$new_tag = str_replace( $src_orig, $photon_url, $new_tag );
350
351
						// If Lazy Load is in use, pass placeholder image through Photon
352
						if ( isset( $placeholder_src ) && self::validate_image_url( $placeholder_src ) ) {
353
							$placeholder_src = jetpack_photon_url( $placeholder_src );
354
355
							if ( $placeholder_src != $placeholder_src_orig )
356
								$new_tag = str_replace( $placeholder_src_orig, esc_url( $placeholder_src ), $new_tag );
357
358
							unset( $placeholder_src );
359
						}
360
361
						// Remove the width and height arguments from the tag to prevent distortion
362
						$new_tag = preg_replace( '#(?<=\s)(width|height)=["|\']?[\d%]+["|\']?\s?#i', '', $new_tag );
363
364
						// Tag an image for dimension checking
365
						$new_tag = preg_replace( '#(\s?/)?>(\s*</a>)?$#i', ' data-recalc-dims="1"\1>\2', $new_tag );
366
367
						// Replace original tag with modified version
368
						$content = str_replace( $tag, $new_tag, $content );
369
					}
370
				} elseif ( preg_match( '#^http(s)?://i[\d]{1}.wp.com#', $src ) && ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
371
					$new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . jetpack_photon_url( $images['link_url'][ $index ] ) . '\2', $tag, 1 );
372
373
					$content = str_replace( $tag, $new_tag, $content );
374
				}
375
			}
376
		}
377
378
		return $content;
379
	}
380
381
	public static function filter_the_galleries( $galleries ) {
382
		if ( empty( $galleries ) || ! is_array( $galleries ) ) {
383
			return $galleries;
384
		}
385
386
		// Pass by reference, so we can modify them in place.
387
		foreach ( $galleries as &$this_gallery ) {
388
			if ( is_string( $this_gallery ) ) {
389
				$this_gallery = self::filter_the_content( $this_gallery );
390
		// LEAVING COMMENTED OUT as for the moment it doesn't seem
391
		// necessary and I'm not sure how it would propagate through.
392
		//	} elseif ( is_array( $this_gallery )
0 ignored issues
show
Unused Code Comprehensibility introduced by
47% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
393
		//	           && ! empty( $this_gallery['src'] )
0 ignored issues
show
Unused Code Comprehensibility introduced by
65% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
394
		//	           && ! empty( $this_gallery['type'] )
0 ignored issues
show
Unused Code Comprehensibility introduced by
65% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
395
		//	           && in_array( $this_gallery['type'], array( 'rectangle', 'square', 'circle' ) ) ) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
396
		//		$this_gallery['src'] = array_map( 'jetpack_photon_url', $this_gallery['src'] );
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
397
			}
398
		}
399
		unset( $this_gallery ); // break the reference.
400
401
		return $galleries;
402
	}
403
404
	/**
405
	 ** CORE IMAGE RETRIEVAL
406
	 **/
407
408
	/**
409
	 * Filter post thumbnail image retrieval, passing images through Photon
410
	 *
411
	 * @param string|bool $image
412
	 * @param int $attachment_id
413
	 * @param string|array $size
414
	 * @uses is_admin, apply_filters, wp_get_attachment_url, self::validate_image_url, this::image_sizes, jetpack_photon_url
415
	 * @filter image_downsize
416
	 * @return string|bool
417
	 */
418
	public function filter_image_downsize( $image, $attachment_id, $size ) {
419
		// Don't foul up the admin side of things, and provide plugins a way of preventing Photon from being applied to images.
420
		if (
421
			is_admin() ||
422
			/**
423
			 * Provide plugins a way of preventing Photon from being applied to images retrieved from WordPress Core.
424
			 *
425
			 * @module photon
426
			 *
427
			 * @since 2.0.0
428
			 *
429
			 * @param bool false Stop Photon from being applied to the image. Default to false.
430
			 * @param array $args {
431
			 * 	 Array of image details.
432
			 *
433
			 * 	 @type $image Image URL.
434
			 * 	 @type $attachment_id Attachment ID of the image.
435
			 * 	 @type $size Image size. Can be a string (name of the image size, e.g. full) or an integer.
436
			 * }
437
			 */
438
			apply_filters( 'jetpack_photon_override_image_downsize', false, compact( 'image', 'attachment_id', 'size' ) )
439
		)
440
			return $image;
441
442
		// Get the image URL and proceed with Photon-ification if successful
443
		$image_url = wp_get_attachment_url( $attachment_id );
444
445
		// Set this to true later when we know we have size meta.
446
		$has_size_meta = false;
447
448
		if ( $image_url ) {
449
			// Check if image URL should be used with Photon
450
			if ( ! self::validate_image_url( $image_url ) )
451
				return $image;
452
453
			$intermediate = true; // For the fourth array item returned by the image_downsize filter.
454
455
			// If an image is requested with a size known to WordPress, use that size's settings with Photon
456
			if ( ( is_string( $size ) || is_int( $size ) ) && array_key_exists( $size, self::image_sizes() ) ) {
457
				$image_args = self::image_sizes();
458
				$image_args = $image_args[ $size ];
459
460
				$photon_args = array();
461
462
				$image_meta = image_get_intermediate_size( $attachment_id, $size );
463
464
				// 'full' is a special case: We need consistent data regardless of the requested size.
465
				if ( 'full' == $size ) {
466
					$image_meta = wp_get_attachment_metadata( $attachment_id );
467
					$intermediate = false;
468
				} elseif ( ! $image_meta ) {
469
					// If we still don't have any image meta at this point, it's probably from a custom thumbnail size
470
					// for an image that was uploaded before the custom image was added to the theme.  Try to determine the size manually.
471
					$image_meta = wp_get_attachment_metadata( $attachment_id );
472
473 View Code Duplication
					if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
474
						$image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $image_args['width'], $image_args['height'], $image_args['crop'] );
475
						if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
476
							$image_meta['width'] = $image_resized[6];
477
							$image_meta['height'] = $image_resized[7];
478
						}
479
					}
480
				}
481
482 View Code Duplication
				if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
483
					$image_args['width']  = $image_meta['width'];
484
					$image_args['height'] = $image_meta['height'];
485
486
					list( $image_args['width'], $image_args['height'] ) = image_constrain_size_for_editor( $image_args['width'], $image_args['height'], $size, 'display' );
487
					$has_size_meta = true;
488
				}
489
490
				// Expose determined arguments to a filter before passing to Photon
491
				$transform = $image_args['crop'] ? 'resize' : 'fit';
492
493
				// Check specified image dimensions and account for possible zero values; photon fails to resize if a dimension is zero.
494
				if ( 0 == $image_args['width'] || 0 == $image_args['height'] ) {
495
					if ( 0 == $image_args['width'] && 0 < $image_args['height'] ) {
496
						$photon_args['h'] = $image_args['height'];
497
					} elseif ( 0 == $image_args['height'] && 0 < $image_args['width'] ) {
498
						$photon_args['w'] = $image_args['width'];
499
					}
500
				} else {
501
					if ( ( 'resize' === $transform ) && $image_meta = wp_get_attachment_metadata( $attachment_id ) ) {
502
						if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
503
							// Lets make sure that we don't upscale images since wp never upscales them as well
504
							$smaller_width  = ( ( $image_meta['width']  < $image_args['width']  ) ? $image_meta['width']  : $image_args['width']  );
505
							$smaller_height = ( ( $image_meta['height'] < $image_args['height'] ) ? $image_meta['height'] : $image_args['height'] );
506
507
							$photon_args[ $transform ] = $smaller_width . ',' . $smaller_height;
508
						}
509
					} else {
510
						$photon_args[ $transform ] = $image_args['width'] . ',' . $image_args['height'];
511
					}
512
513
				}
514
515
516
				/**
517
				 * Filter the Photon Arguments added to an image when going through Photon, when that image size is a string.
518
				 * Image size will be a string (e.g. "full", "medium") when it is known to WordPress.
519
				 *
520
				 * @module photon
521
				 *
522
				 * @since 2.0.0
523
				 *
524
				 * @param array $photon_args Array of Photon arguments.
525
				 * @param array $args {
526
				 * 	 Array of image details.
527
				 *
528
				 * 	 @type $image_args Array of Image arguments (width, height, crop).
529
				 * 	 @type $image_url Image URL.
530
				 * 	 @type $attachment_id Attachment ID of the image.
531
				 * 	 @type $size Image size. Can be a string (name of the image size, e.g. full) or an integer.
532
				 * 	 @type $transform Value can be resize or fit.
533
				 *                    @see https://developer.wordpress.com/docs/photon/api
534
				 * }
535
				 */
536
				$photon_args = apply_filters( 'jetpack_photon_image_downsize_string', $photon_args, compact( 'image_args', 'image_url', 'attachment_id', 'size', 'transform' ) );
537
538
				// Generate Photon URL
539
				$image = array(
540
					jetpack_photon_url( $image_url, $photon_args ),
541
					$has_size_meta ? $image_args['width'] : false,
542
					$has_size_meta ? $image_args['height'] : false,
543
					$intermediate
544
				);
545
			} elseif ( is_array( $size ) ) {
546
				// Pull width and height values from the provided array, if possible
547
				$width = isset( $size[0] ) ? (int) $size[0] : false;
548
				$height = isset( $size[1] ) ? (int) $size[1] : false;
549
550
				// Don't bother if necessary parameters aren't passed.
551
				if ( ! $width || ! $height ) {
552
					return $image;
553
				}
554
555
				$image_meta = wp_get_attachment_metadata( $attachment_id );
556
				if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
557
					$image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $width, $height );
558
559
					if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
560
						$width = $image_resized[6];
561
						$height = $image_resized[7];
562
					} else {
563
						$width = $image_meta['width'];
564
						$height = $image_meta['height'];
565
					}
566
567
					$has_size_meta = true;
568
				}
569
570
				list( $width, $height ) = image_constrain_size_for_editor( $width, $height, $size );
571
572
				// Expose arguments to a filter before passing to Photon
573
				$photon_args = array(
574
					'fit' => $width . ',' . $height
575
				);
576
577
				/**
578
				 * Filter the Photon Arguments added to an image when going through Photon,
579
				 * when the image size is an array of height and width values.
580
				 *
581
				 * @module photon
582
				 *
583
				 * @since 2.0.0
584
				 *
585
				 * @param array $photon_args Array of Photon arguments.
586
				 * @param array $args {
587
				 * 	 Array of image details.
588
				 *
589
				 * 	 @type $width Image width.
590
				 * 	 @type height Image height.
591
				 * 	 @type $image_url Image URL.
592
				 * 	 @type $attachment_id Attachment ID of the image.
593
				 * }
594
				 */
595
				$photon_args = apply_filters( 'jetpack_photon_image_downsize_array', $photon_args, compact( 'width', 'height', 'image_url', 'attachment_id' ) );
596
597
				// Generate Photon URL
598
				$image = array(
599
					jetpack_photon_url( $image_url, $photon_args ),
600
					$has_size_meta ? $width : false,
601
					$has_size_meta ? $height : false,
602
					$intermediate
603
				);
604
			}
605
		}
606
607
		return $image;
608
	}
609
610
	/**
611
	 * Filters an array of image `srcset` values, replacing each URL with its Photon equivalent.
612
	 *
613
	 * @since 3.8.0
614
	 * @param array $sources An array of image urls and widths.
615
	 * @uses self::validate_image_url, jetpack_photon_url
616
	 * @return array An array of Photon image urls and widths.
617
	 */
618
	public function filter_srcset_array( $sources, $size_array, $image_src, $image_meta ) {
619
		$upload_dir = wp_upload_dir();
620
621
		foreach ( $sources as $i => $source ) {
622
			if ( ! self::validate_image_url( $source['url'] ) ) {
623
				continue;
624
			}
625
626
			$url = $source['url'];
627
			list( $width, $height ) = Jetpack_Photon::parse_dimensions_from_filename( $url );
628
629
			// It's quicker to get the full size with the data we have already, if available
630
			if ( isset( $image_meta['file'] ) ) {
631
				$url = trailingslashit( $upload_dir['baseurl'] ) . $image_meta['file'];
632
			} else {
633
				$url = Jetpack_Photon::strip_image_dimensions_maybe( $url );
634
			}
635
636
			$args = array();
637
			if ( 'w' === $source['descriptor'] ) {
638
				if ( $height && ( $source['value'] == $width ) ) {
639
					$args['resize'] = $width . ',' . $height;
640
				} else {
641
					$args['w'] = $source['value'];
642
				}
643
644
			}
645
646
			$sources[ $i ]['url'] = jetpack_photon_url( $url, $args );
647
		}
648
649
		return $sources;
650
	}
651
652
	/**
653
	 ** GENERAL FUNCTIONS
654
	 **/
655
656
	/**
657
	 * Ensure image URL is valid for Photon.
658
	 * 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.
659
	 *
660
	 * @param string $url
661
	 * @uses wp_parse_args
662
	 * @return bool
663
	 */
664
	protected static function validate_image_url( $url ) {
665
		$parsed_url = @parse_url( $url );
666
667
		if ( ! $parsed_url )
668
			return false;
669
670
		// Parse URL and ensure needed keys exist, since the array returned by `parse_url` only includes the URL components it finds.
671
		$url_info = wp_parse_args( $parsed_url, array(
672
			'scheme' => null,
673
			'host'   => null,
674
			'port'   => null,
675
			'path'   => null
676
		) );
677
678
		// Bail if scheme isn't http or port is set that isn't port 80
679
		if (
680
			( 'http' != $url_info['scheme'] || ! in_array( $url_info['port'], array( 80, null ) ) ) &&
681
			/**
682
			 * Allow Photon to fetch images that are served via HTTPS.
683
			 *
684
			 * @module photon
685
			 *
686
			 * @since 2.4.0
687
			 * @since 3.9.0 Default to false.
688
			 *
689
			 * @param bool $reject_https Should Photon ignore images using the HTTPS scheme. Default to false.
690
			 */
691
			apply_filters( 'jetpack_photon_reject_https', false )
692
		) {
693
			return false;
694
		}
695
696
		// Bail if no host is found
697
		if ( is_null( $url_info['host'] ) )
698
			return false;
699
700
		// Bail if the image alredy went through Photon
701
		if ( preg_match( '#^i[\d]{1}.wp.com$#i', $url_info['host'] ) )
702
			return false;
703
704
		// Bail if no path is found
705
		if ( is_null( $url_info['path'] ) )
706
			return false;
707
708
		// Ensure image extension is acceptable
709
		if ( ! in_array( strtolower( pathinfo( $url_info['path'], PATHINFO_EXTENSION ) ), self::$extensions ) )
710
			return false;
711
712
		// If we got this far, we should have an acceptable image URL
713
		// But let folks filter to decline if they prefer.
714
		/**
715
		 * Overwrite the results of the validation steps an image goes through before to be considered valid to be used by Photon.
716
		 *
717
		 * @module photon
718
		 *
719
		 * @since 3.0.0
720
		 *
721
		 * @param bool true Is the image URL valid and can it be used by Photon. Default to true.
722
		 * @param string $url Image URL.
723
		 * @param array $parsed_url Array of information about the image.
724
		 */
725
		return apply_filters( 'photon_validate_image_url', true, $url, $parsed_url );
726
	}
727
728
	/**
729
	 * Checks if the file exists before it passes the file to photon
730
	 *
731
	 * @param string $src The image URL
732
	 * @return string
733
	 **/
734
	protected static function strip_image_dimensions_maybe( $src ){
735
		$stripped_src = $src;
736
737
		// Build URL, first removing WP's resized string so we pass the original image to Photon
738
		if ( preg_match( '#(-\d+x\d+)\.(' . implode('|', self::$extensions ) . '){1}$#i', $src, $src_parts ) ) {
739
			$stripped_src = str_replace( $src_parts[1], '', $src );
740
			$upload_dir = wp_upload_dir();
741
742
			// Extracts the file path to the image minus the base url
743
			$file_path = substr( $stripped_src, strlen ( $upload_dir['baseurl'] ) );
744
745
			if( file_exists( $upload_dir["basedir"] . $file_path ) )
746
				$src = $stripped_src;
747
		}
748
749
		return $src;
750
	}
751
752
	/**
753
	 * Provide an array of available image sizes and corresponding dimensions.
754
	 * Similar to get_intermediate_image_sizes() except that it includes image sizes' dimensions, not just their names.
755
	 *
756
	 * @global $wp_additional_image_sizes
757
	 * @uses get_option
758
	 * @return array
759
	 */
760
	protected static function image_sizes() {
761
		if ( null == self::$image_sizes ) {
762
			global $_wp_additional_image_sizes;
763
764
			// Populate an array matching the data structure of $_wp_additional_image_sizes so we have a consistent structure for image sizes
765
			$images = array(
766
				'thumb'  => array(
767
					'width'  => intval( get_option( 'thumbnail_size_w' ) ),
768
					'height' => intval( get_option( 'thumbnail_size_h' ) ),
769
					'crop'   => (bool) get_option( 'thumbnail_crop' )
770
				),
771
				'medium' => array(
772
					'width'  => intval( get_option( 'medium_size_w' ) ),
773
					'height' => intval( get_option( 'medium_size_h' ) ),
774
					'crop'   => false
775
				),
776
				'large'  => array(
777
					'width'  => intval( get_option( 'large_size_w' ) ),
778
					'height' => intval( get_option( 'large_size_h' ) ),
779
					'crop'   => false
780
				),
781
				'full'   => array(
782
					'width'  => null,
783
					'height' => null,
784
					'crop'   => false
785
				)
786
			);
787
788
			// Compatibility mapping as found in wp-includes/media.php
789
			$images['thumbnail'] = $images['thumb'];
790
791
			// Update class variable, merging in $_wp_additional_image_sizes if any are set
792
			if ( is_array( $_wp_additional_image_sizes ) && ! empty( $_wp_additional_image_sizes ) )
793
				self::$image_sizes = array_merge( $images, $_wp_additional_image_sizes );
794
			else
795
				self::$image_sizes = $images;
796
		}
797
798
		return is_array( self::$image_sizes ) ? self::$image_sizes : array();
799
	}
800
801
	/**
802
	 * Pass og:image URLs through Photon
803
	 *
804
	 * @param array $tags
805
	 * @param array $parameters
806
	 * @uses jetpack_photon_url
807
	 * @return array
808
	 */
809
	function filter_open_graph_tags( $tags, $parameters ) {
810
		if ( empty( $tags['og:image'] ) ) {
811
			return $tags;
812
		}
813
814
		$photon_args = array(
815
			'fit' => sprintf( '%d,%d', 2 * $parameters['image_width'], 2 * $parameters['image_height'] ),
816
		);
817
818
		if ( is_array( $tags['og:image'] ) ) {
819
			$images = array();
820
			foreach ( $tags['og:image'] as $image ) {
821
				$images[] = jetpack_photon_url( $image, $photon_args );
822
			}
823
			$tags['og:image'] = $images;
824
		} else {
825
			$tags['og:image'] = jetpack_photon_url( $tags['og:image'], $photon_args );
826
		}
827
828
		return $tags;
829
	}
830
831
	/**
832
	 * Enqueue Photon helper script
833
	 *
834
	 * @uses wp_enqueue_script, plugins_url
835
	 * @action wp_enqueue_script
836
	 * @return null
837
	 */
838
	public function action_wp_enqueue_scripts() {
839
		wp_enqueue_script( 'jetpack-photon', plugins_url( 'modules/photon/photon.js', JETPACK__PLUGIN_FILE ), array( 'jquery' ), 20130122, true );
840
	}
841
}
842