Completed
Push — start/92-cycle ( 3fe444 )
by Jeremy
07:10
created

class.photon.php (11 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 // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
/**
3
 * Class for photon functionality.
4
 *
5
 * @package Jetpack.
6
 */
7
8
use Automattic\Jetpack\Assets;
9
10
/**
11
 * Class Jetpack_Photon
12
 */
13
class Jetpack_Photon {
14
	/**
15
	 * Singleton.
16
	 *
17
	 * @var null
18
	 */
19
	private static $instance = null;
20
21
	/**
22
	 * Allowed extensions.
23
	 *
24
	 * @var string[] Allowed extensions must match https://code.trac.wordpress.org/browser/photon/index.php#L31
25
	 */
26
	protected static $extensions = array(
27
		'gif',
28
		'jpg',
29
		'jpeg',
30
		'png',
31
	);
32
33
	/**
34
	 * Image sizes.
35
	 *
36
	 * Don't access this directly. Instead, use self::image_sizes() so it's actually populated with something.
37
	 *
38
	 * @var array Image sizes.
39
	 */
40
	protected static $image_sizes = null;
41
42
	/**
43
	 * Singleton implementation
44
	 *
45
	 * @return object
46
	 */
47
	public static function instance() {
48
		if ( ! is_a( self::$instance, 'Jetpack_Photon' ) ) {
49
			self::$instance = new Jetpack_Photon();
50
			self::$instance->setup();
51
		}
52
53
		return self::$instance;
54
	}
55
56
	/**
57
	 * Silence is golden.
58
	 */
59
	private function __construct() {}
60
61
	/**
62
	 * Register actions and filters, but only if basic Photon functions are available.
63
	 * The basic functions are found in ./functions.photon.php.
64
	 *
65
	 * @uses add_action, add_filter
66
	 * @return null
67
	 */
68
	private function setup() {
69
		if ( ! function_exists( 'jetpack_photon_url' ) ) {
70
			return;
71
		}
72
73
		// Images in post content and galleries.
74
		add_filter( 'the_content', array( __CLASS__, 'filter_the_content' ), 999999 );
75
		add_filter( 'get_post_galleries', array( __CLASS__, 'filter_the_galleries' ), 999999 );
76
		add_filter( 'widget_media_image_instance', array( __CLASS__, 'filter_the_image_widget' ), 999999 );
77
78
		// Core image retrieval.
79
		add_filter( 'image_downsize', array( $this, 'filter_image_downsize' ), 10, 3 );
80
		add_filter( 'rest_request_before_callbacks', array( $this, 'should_rest_photon_image_downsize' ), 10, 3 );
81
		add_action( 'rest_after_insert_attachment', array( $this, 'should_rest_photon_image_downsize_insert_attachment' ), 10, 2 );
82
		add_filter( 'rest_request_after_callbacks', array( $this, 'cleanup_rest_photon_image_downsize' ) );
83
84
		// Responsive image srcset substitution.
85
		add_filter( 'wp_calculate_image_srcset', array( $this, 'filter_srcset_array' ), 10, 5 );
86
		add_filter( 'wp_calculate_image_sizes', array( $this, 'filter_sizes' ), 1, 2 ); // Early so themes can still easily filter.
87
88
		// Helpers for maniuplated images.
89
		add_action( 'wp_enqueue_scripts', array( $this, 'action_wp_enqueue_scripts' ), 9 );
90
91
		/**
92
		 * Allow Photon to disable uploaded images resizing and use its own resize capabilities instead.
93
		 *
94
		 * @module photon
95
		 *
96
		 * @since 7.1.0
97
		 *
98
		 * @param bool false Should Photon enable noresize mode. Default to false.
99
		 */
100
		if ( apply_filters( 'jetpack_photon_noresize_mode', false ) ) {
101
			$this->enable_noresize_mode();
102
		}
103
	}
104
105
	/**
106
	 * Enables the noresize mode for Photon, allowing to avoid intermediate size files generation.
107
	 */
108
	private function enable_noresize_mode() {
109
		jetpack_require_lib( 'class.jetpack-photon-image-sizes' );
110
111
		// The main objective of noresize mode is to disable additional resized image versions creation.
112
		// This filter handles removal of additional sizes.
113
		add_filter( 'intermediate_image_sizes_advanced', array( __CLASS__, 'filter_photon_noresize_intermediate_sizes' ) );
114
115
		// Load the noresize srcset solution on priority of 20, allowing other plugins to set sizes earlier.
116
		add_filter( 'wp_get_attachment_metadata', array( __CLASS__, 'filter_photon_norezise_maybe_inject_sizes' ), 20, 2 );
117
118
		// Photonize thumbnail URLs in the API response.
119
		add_filter( 'rest_api_thumbnail_size_urls', array( __CLASS__, 'filter_photon_noresize_thumbnail_urls' ) );
120
121
		// This allows to assign the Photon domain to images that normally use the home URL as base.
122
		add_filter( 'jetpack_photon_domain', array( __CLASS__, 'filter_photon_norezise_domain' ), 10, 2 );
123
124
		add_filter( 'the_content', array( __CLASS__, 'filter_content_add' ), 0 );
125
126
		// Jetpack hooks in at six nines (999999) so this filter does at seven.
127
		add_filter( 'the_content', array( __CLASS__, 'filter_content_remove' ), 9999999 );
128
129
		// Regular Photon operation mode filter doesn't run when is_admin(), so we need an additional filter.
130
		// This is temporary until Jetpack allows more easily running these filters for is_admin().
131
		if ( is_admin() ) {
132
			add_filter( 'image_downsize', array( $this, 'filter_image_downsize' ), 5, 3 );
133
134
			// Allows any image that gets passed to Photon to be resized via Photon.
135
			add_filter( 'jetpack_photon_admin_allow_image_downsize', '__return_true' );
136
		}
137
	}
138
139
	/**
140
	 * This is our catch-all to strip dimensions from intermediate images in content.
141
	 * Since this primarily only impacts post_content we do a little dance to add the filter early
142
	 * to `the_content` and then remove it later on in the same hook.
143
	 *
144
	 * @param String $content the post content.
145
	 * @return String the post content unchanged.
146
	 */
147
	public static function filter_content_add( $content ) {
148
		add_filter( 'jetpack_photon_pre_image_url', array( __CLASS__, 'strip_image_dimensions_maybe' ) );
149
		return $content;
150
	}
151
152
	/**
153
	 * Removing the content filter that was set previously.
154
	 *
155
	 * @param String $content the post content.
156
	 * @return String the post content unchanged.
157
	 */
158
	public static function filter_content_remove( $content ) {
159
		remove_filter( 'jetpack_photon_pre_image_url', array( __CLASS__, 'strip_image_dimensions_maybe' ) );
160
		return $content;
161
	}
162
163
	/**
164
	 * Short circuits the Photon filter to enable Photon processing for any URL.
165
	 *
166
	 * @param String $photon_url a proposed Photon URL for the media file.
167
	 *
168
	 * @return String an URL to be used for the media file.
169
	 */
170
	public static function filter_photon_norezise_domain( $photon_url ) {
171
		return $photon_url;
172
	}
173
174
	/**
175
	 * Disables intermediate sizes to disallow resizing.
176
	 *
177
	 * @return array Empty array.
178
	 */
179
	public static function filter_photon_noresize_intermediate_sizes() {
180
		return array();
181
	}
182
183
	/**
184
	 * Filter thumbnail URLS to not generate.
185
	 *
186
	 * @param array $sizes Image sizes.
187
	 *
188
	 * @return mixed
189
	 */
190
	public static function filter_photon_noresize_thumbnail_urls( $sizes ) {
191
		foreach ( $sizes as $size => $url ) {
192
			$parts     = explode( '?', $url );
193
			$arguments = isset( $parts[1] ) ? $parts[1] : array();
194
195
			$sizes[ $size ] = jetpack_photon_url( $url, wp_parse_args( $arguments ) );
196
		}
197
198
		return $sizes;
199
	}
200
201
	/**
202
	 * Inject image sizes to attachment metadata.
203
	 *
204
	 * @param array $data          Attachment metadata.
205
	 * @param int   $attachment_id Attachment's post ID.
206
	 *
207
	 * @return array Attachment metadata.
208
	 */
209
	public static function filter_photon_norezise_maybe_inject_sizes( $data, $attachment_id ) {
210
		// Can't do much if data is empty.
211
		if ( empty( $data ) ) {
212
			return $data;
213
		}
214
		$sizes_already_exist = (
215
			true === is_array( $data )
216
			&& true === array_key_exists( 'sizes', $data )
217
			&& true === is_array( $data['sizes'] )
218
			&& false === empty( $data['sizes'] )
219
		);
220
		if ( $sizes_already_exist ) {
221
			return $data;
222
		}
223
		// Missing some critical data we need to determine sizes, not processing.
224
		if ( ! isset( $data['file'] )
225
			|| ! isset( $data['width'] )
226
			|| ! isset( $data['height'] )
227
		) {
228
			return $data;
229
		}
230
231
		$mime_type           = get_post_mime_type( $attachment_id );
232
		$attachment_is_image = preg_match( '!^image/!', $mime_type );
233
234
		if ( 1 === $attachment_is_image ) {
235
			$image_sizes   = new Jetpack_Photon_ImageSizes( $attachment_id, $data );
236
			$data['sizes'] = $image_sizes->generate_sizes_meta();
237
		}
238
		return $data;
239
	}
240
241
	/**
242
	 * Inject image sizes to Jetpack REST API responses. This wraps the filter_photon_norezise_maybe_inject_sizes function.
243
	 *
244
	 * @param array $sizes Attachment sizes data.
245
	 * @param int   $attachment_id Attachment's post ID.
246
	 *
247
	 * @return array Attachment sizes array.
248
	 */
249
	public static function filter_photon_norezise_maybe_inject_sizes_api( $sizes, $attachment_id ) {
250
		return self::filter_photon_norezise_maybe_inject_sizes( wp_get_attachment_metadata( $attachment_id ), $attachment_id );
251
	}
252
253
	/**
254
	 * * IN-CONTENT IMAGE MANIPULATION FUNCTIONS
255
	 **/
256
257
	/**
258
	 * Match all images and any relevant <a> tags in a block of HTML.
259
	 *
260
	 * @param string $content Some HTML.
261
	 * @return array An array of $images matches, where $images[0] is
262
	 *         an array of full matches, and the link_url, img_tag,
263
	 *         and img_url keys are arrays of those matches.
264
	 */
265
	public static function parse_images_from_html( $content ) {
266
		$images = array();
267
268
		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 ) ) {
269
			foreach ( $images as $key => $unused ) {
270
				// Simplify the output as much as possible, mostly for confirming test results.
271
				if ( is_numeric( $key ) && $key > 0 ) {
272
					unset( $images[ $key ] );
273
				}
274
			}
275
276
			return $images;
277
		}
278
279
		return array();
280
	}
281
282
	/**
283
	 * Try to determine height and width from strings WP appends to resized image filenames.
284
	 *
285
	 * @param string $src The image URL.
286
	 * @return array An array consisting of width and height.
287
	 */
288
	public static function parse_dimensions_from_filename( $src ) {
289
		$width_height_string = array();
290
291
		if ( preg_match( '#-(\d+)x(\d+)\.(?:' . implode( '|', self::$extensions ) . '){1}$#i', $src, $width_height_string ) ) {
292
			$width  = (int) $width_height_string[1];
293
			$height = (int) $width_height_string[2];
294
295
			if ( $width && $height ) {
296
				return array( $width, $height );
297
			}
298
		}
299
300
		return array( false, false );
301
	}
302
303
	/**
304
	 * Identify images in post content, and if images are local (uploaded to the current site), pass through Photon.
305
	 *
306
	 * @param string $content The content.
307
	 *
308
	 * @uses self::validate_image_url, apply_filters, jetpack_photon_url, esc_url
309
	 * @filter the_content
310
	 *
311
	 * @return string
312
	 */
313
	public static function filter_the_content( $content ) {
314
		$images = self::parse_images_from_html( $content );
315
316
		if ( ! empty( $images ) ) {
317
			$content_width = Jetpack::get_content_width();
318
319
			$image_sizes = self::image_sizes();
320
321
			$upload_dir = wp_get_upload_dir();
322
323
			foreach ( $images[0] as $index => $tag ) {
324
				// Default to resize, though fit may be used in certain cases where a dimension cannot be ascertained.
325
				$transform = 'resize';
326
327
				// Start with a clean attachment ID each time.
328
				$attachment_id = false;
329
330
				// Flag if we need to munge a fullsize URL.
331
				$fullsize_url = false;
332
333
				// Identify image source.
334
				$src_orig = $images['img_url'][ $index ];
335
				$src      = $src_orig;
336
337
				/**
338
				 * Allow specific images to be skipped by Photon.
339
				 *
340
				 * @module photon
341
				 *
342
				 * @since 2.0.3
343
				 *
344
				 * @param bool false Should Photon ignore this image. Default to false.
345
				 * @param string $src Image URL.
346
				 * @param string $tag Image Tag (Image HTML output).
347
				 */
348
				if ( apply_filters( 'jetpack_photon_skip_image', false, $src, $tag ) ) {
0 ignored issues
show
The call to apply_filters() has too many arguments starting with $src.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
349
					continue;
350
				}
351
352
				// Support Automattic's Lazy Load plugin.
353
				// Can't modify $tag yet as we need unadulterated version later.
354
				if ( preg_match( '#data-lazy-src=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
355
					$placeholder_src_orig = $src;
356
					$placeholder_src      = $placeholder_src_orig;
357
					$src_orig             = $lazy_load_src[1];
358
					$src                  = $src_orig;
359
				} elseif ( preg_match( '#data-lazy-original=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
360
					$placeholder_src_orig = $src;
361
					$placeholder_src      = $placeholder_src_orig;
362
					$src_orig             = $lazy_load_src[1];
363
					$src                  = $src_orig;
364
				}
365
366
				// Check if image URL should be used with Photon.
367
				if ( self::validate_image_url( $src ) ) {
368
					// Find the width and height attributes.
369
					$width  = false;
370
					$height = false;
371
372
					// First, check the image tag. Note we only check for pixel sizes now; HTML4 percentages have never been correctly
373
					// supported, so we stopped pretending to support them in JP 9.1.0.
374 View Code Duplication
					if ( preg_match( '#[\s|"|\']width=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $width_string ) ) {
375
						$width = false === strpos( $width_string[1], '%' ) ? $width_string[1] : false;
376
					}
377
378 View Code Duplication
					if ( preg_match( '#[\s|"|\']height=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $height_string ) ) {
379
						$height = false === strpos( $height_string[1], '%' ) ? $height_string[1] : false;
380
					}
381
382
					// Detect WP registered image size from HTML class.
383
					if ( preg_match( '#class=["|\']?[^"\']*size-([^"\'\s]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $size ) ) {
384
						$size = array_pop( $size );
385
386
						if ( false === $width && false === $height && 'full' !== $size && array_key_exists( $size, $image_sizes ) ) {
387
							$width     = (int) $image_sizes[ $size ]['width'];
388
							$height    = (int) $image_sizes[ $size ]['height'];
389
							$transform = $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
390
						}
391
					} else {
392
						unset( $size );
393
					}
394
395
					// WP Attachment ID, if uploaded to this site.
396
					if (
397
						preg_match( '#class=["|\']?[^"\']*wp-image-([\d]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $attachment_id ) &&
398
						0 === strpos( $src, $upload_dir['baseurl'] ) &&
399
						/**
400
						 * Filter whether an image using an attachment ID in its class has to be uploaded to the local site to go through Photon.
401
						 *
402
						 * @module photon
403
						 *
404
						 * @since 2.0.3
405
						 *
406
						 * @param bool false Was the image uploaded to the local site. Default to false.
407
						 * @param array $args {
408
						 *   Array of image details.
409
						 *
410
						 *   @type $src Image URL.
411
						 *   @type tag Image tag (Image HTML output).
412
						 *   @type $images Array of information about the image.
413
						 *   @type $index Image index.
414
						 * }
415
						 */
416
						apply_filters( 'jetpack_photon_image_is_local', false, compact( 'src', 'tag', 'images', 'index' ) )
0 ignored issues
show
The call to apply_filters() has too many arguments starting with compact('src', 'tag', 'images', 'index').

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
417
					) {
418
						$attachment_id = (int) array_pop( $attachment_id );
419
420
						if ( $attachment_id ) {
421
							$attachment = get_post( $attachment_id );
422
423
							// Basic check on returned post object.
424
							if ( is_object( $attachment ) && ! is_wp_error( $attachment ) && 'attachment' === $attachment->post_type ) {
425
								$src_per_wp = wp_get_attachment_image_src( $attachment_id, isset( $size ) ? $size : 'full' );
426
427
								if ( self::validate_image_url( $src_per_wp[0] ) ) {
428
									$src          = $src_per_wp[0];
429
									$fullsize_url = true;
430
431
									// Prevent image distortion if a detected dimension exceeds the image's natural dimensions.
432
									if ( ( false !== $width && $width > $src_per_wp[1] ) || ( false !== $height && $height > $src_per_wp[2] ) ) {
433
										$width  = false === $width ? false : min( $width, $src_per_wp[1] );
434
										$height = false === $height ? false : min( $height, $src_per_wp[2] );
435
									}
436
437
									// If no width and height are found, max out at source image's natural dimensions.
438
									// Otherwise, respect registered image sizes' cropping setting.
439
									if ( false === $width && false === $height ) {
440
										$width     = $src_per_wp[1];
441
										$height    = $src_per_wp[2];
442
										$transform = 'fit';
443
									} elseif ( isset( $size ) && array_key_exists( $size, $image_sizes ) && isset( $image_sizes[ $size ]['crop'] ) ) {
444
										$transform = (bool) $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
445
									}
446
								}
447
							} else {
448
								unset( $attachment_id );
449
								unset( $attachment );
450
							}
451
						}
452
					}
453
454
					// If image tag lacks width and height arguments, try to determine from strings WP appends to resized image filenames.
455
					if ( false === $width && false === $height ) {
456
						list( $width, $height ) = self::parse_dimensions_from_filename( $src );
457
					}
458
459
					$width_orig     = $width;
460
					$height_orig    = $height;
461
					$transform_orig = $transform;
462
463
					// If width is available, constrain to $content_width.
464
					if ( false !== $width && is_numeric( $content_width ) && $width > $content_width ) {
465
						if ( false !== $height ) {
466
							$height = round( ( $content_width * $height ) / $width );
467
						}
468
						$width = $content_width;
469
					}
470
471
					// Set a width if none is found and $content_width is available.
472
					// If width is set in this manner and height is available, use `fit` instead of `resize` to prevent skewing.
473
					if ( false === $width && is_numeric( $content_width ) ) {
474
						$width = (int) $content_width;
475
476
						if ( false !== $height ) {
477
							$transform = 'fit';
478
						}
479
					}
480
481
					// Detect if image source is for a custom-cropped thumbnail and prevent further URL manipulation.
482
					if ( ! $fullsize_url && preg_match_all( '#-e[a-z0-9]+(-\d+x\d+)?\.(' . implode( '|', self::$extensions ) . '){1}$#i', basename( $src ), $filename ) ) {
483
						$fullsize_url = true;
484
					}
485
486
					// Build URL, first maybe removing WP's resized string so we pass the original image to Photon.
487
					if ( ! $fullsize_url && 0 === strpos( $src, $upload_dir['baseurl'] ) ) {
488
						$src = self::strip_image_dimensions_maybe( $src );
489
					}
490
491
					// Build array of Photon args and expose to filter before passing to Photon URL function.
492
					$args = array();
493
494 View Code Duplication
					if ( false !== $width && false !== $height ) {
495
						$args[ $transform ] = $width . ',' . $height;
496
					} elseif ( false !== $width ) {
497
						$args['w'] = $width;
498
					} elseif ( false !== $height ) {
499
						$args['h'] = $height;
500
					}
501
502
					/**
503
					 * Filter the array of Photon arguments added to an image when it goes through Photon.
504
					 * By default, only includes width and height values.
505
					 *
506
					 * @see https://developer.wordpress.com/docs/photon/api/
507
					 *
508
					 * @module photon
509
					 *
510
					 * @since 2.0.0
511
					 *
512
					 * @param array $args Array of Photon Arguments.
513
					 * @param array $details {
514
					 *     Array of image details.
515
					 *
516
					 *     @type string    $tag            Image tag (Image HTML output).
517
					 *     @type string    $src            Image URL.
518
					 *     @type string    $src_orig       Original Image URL.
519
					 *     @type int|false $width          Image width.
520
					 *     @type int|false $height         Image height.
521
					 *     @type int|false $width_orig     Original image width before constrained by content_width.
522
					 *     @type int|false $height_orig    Original Image height before constrained by content_width.
523
					 *     @type string    $transform      Transform.
524
					 *     @type string    $transform_orig Original transform before constrained by content_width.
525
					 * }
526
					 */
527
					$args = apply_filters( 'jetpack_photon_post_image_args', $args, compact( 'tag', 'src', 'src_orig', 'width', 'height', 'width_orig', 'height_orig', 'transform', 'transform_orig' ) );
0 ignored issues
show
The call to apply_filters() has too many arguments starting with compact('tag', 'src', 's...orm', 'transform_orig').

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
528
529
					$photon_url = jetpack_photon_url( $src, $args );
530
531
					// Modify image tag if Photon function provides a URL
532
					// 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.
533
					if ( $src !== $photon_url ) {
534
						$new_tag = $tag;
535
536
						// If present, replace the link href with a Photoned URL for the full-size image.
537
						if ( ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
538
							$new_tag = preg_replace( '#(href=["|\'])' . preg_quote( $images['link_url'][ $index ], '#' ) . '(["|\'])#i', '\1' . jetpack_photon_url( $images['link_url'][ $index ] ) . '\2', $new_tag, 1 );
539
						}
540
541
						// Supplant the original source value with our Photon URL.
542
						$photon_url = esc_url( $photon_url );
543
						$new_tag    = str_replace( $src_orig, $photon_url, $new_tag );
544
545
						// If Lazy Load is in use, pass placeholder image through Photon.
546
						if ( isset( $placeholder_src ) && self::validate_image_url( $placeholder_src ) ) {
547
							$placeholder_src = jetpack_photon_url( $placeholder_src );
548
549
							if ( $placeholder_src !== $placeholder_src_orig ) {
550
								$new_tag = str_replace( $placeholder_src_orig, esc_url( $placeholder_src ), $new_tag );
551
							}
552
553
							unset( $placeholder_src );
554
						}
555
556
						// If we are not transforming the image with resize, fit, or letterbox (lb), then we should remove
557
						// the width and height arguments (including HTML4 percentages) from the image to prevent distortion.
558
						// Even if $args['w'] and $args['h'] are present, Photon does not crop to those dimensions. Instead,
559
						// it appears to favor height.
560
						//
561
						// If we are transforming the image via one of those methods, let's update the width and height attributes.
562
						if ( empty( $args['resize'] ) && empty( $args['fit'] ) && empty( $args['lb'] ) ) {
563
							$new_tag = preg_replace( '#(?<=\s)(width|height)=["|\']?[\d%]+["|\']?\s?#i', '', $new_tag );
564
						} else {
565
							$resize_args = isset( $args['resize'] ) ? $args['resize'] : false;
566 View Code Duplication
							if ( false === $resize_args ) {
567
								$resize_args = ( ! $resize_args && isset( $args['fit'] ) )
568
									? $args['fit']
569
									: false;
570
							}
571 View Code Duplication
							if ( false === $resize_args ) {
572
								$resize_args = ( ! $resize_args && isset( $args['lb'] ) )
573
									? $args['lb']
574
									: false;
575
							}
576
577
							$resize_args = array_map( 'trim', explode( ',', $resize_args ) );
578
579
							// (?<=\s)         - Ensure width or height attribute is preceded by a space
580
							// (width=["|\']?) - Matches, and captures, width=, width=", or width='
581
							// [\d%]+          - Matches 1 or more digits or percent signs
582
							// (["|\']?)       - Matches, and captures, ", ', or empty string
583
							// \s              - Ensures there's a space after the attribute
584
							$new_tag = preg_replace( '#(?<=\s)(width=["|\']?)[\d%]+(["|\']?)\s?#i', sprintf( '${1}%d${2} ', $resize_args[0] ), $new_tag );
585
							$new_tag = preg_replace( '#(?<=\s)(height=["|\']?)[\d%]+(["|\']?)\s?#i', sprintf( '${1}%d${2} ', $resize_args[1] ), $new_tag );
586
						}
587
588
						// Tag an image for dimension checking.
589
						if ( ! self::is_amp_endpoint() ) {
590
							$new_tag = preg_replace( '#(\s?/)?>(\s*</a>)?$#i', ' data-recalc-dims="1"\1>\2', $new_tag );
591
						}
592
593
						// Replace original tag with modified version.
594
						$content = str_replace( $tag, $new_tag, $content );
595
					}
596
				} elseif ( preg_match( '#^http(s)?://i[\d]{1}.wp.com#', $src ) && ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
597
					$new_tag = preg_replace( '#(href=["|\'])' . preg_quote( $images['link_url'][ $index ], '#' ) . '(["|\'])#i', '\1' . jetpack_photon_url( $images['link_url'][ $index ] ) . '\2', $tag, 1 );
598
599
					$content = str_replace( $tag, $new_tag, $content );
600
				}
601
			}
602
		}
603
604
		return $content;
605
	}
606
607
	/**
608
	 * Filter Core galleries
609
	 *
610
	 * @param array $galleries Gallery array.
611
	 *
612
	 * @return array
613
	 */
614
	public static function filter_the_galleries( $galleries ) {
615
		if ( empty( $galleries ) || ! is_array( $galleries ) ) {
616
			return $galleries;
617
		}
618
619
		// Pass by reference, so we can modify them in place.
620
		foreach ( $galleries as &$this_gallery ) {
621
			if ( is_string( $this_gallery ) ) {
622
				$this_gallery = self::filter_the_content( $this_gallery );
623
			}
624
		}
625
		unset( $this_gallery ); // break the reference.
626
627
		return $galleries;
628
	}
629
630
	/**
631
	 * Runs the image widget through photon.
632
	 *
633
	 * @param array $instance Image widget instance data.
634
	 * @return array
635
	 */
636
	public static function filter_the_image_widget( $instance ) {
637
		if ( Jetpack::is_module_active( 'photon' ) && ! $instance['attachment_id'] && $instance['url'] ) {
638
			jetpack_photon_url(
639
				$instance['url'],
640
				array(
641
					'w' => $instance['width'],
642
					'h' => $instance['height'],
643
				)
644
			);
645
		}
646
647
		return $instance;
648
	}
649
650
	/**
651
	 * * CORE IMAGE RETRIEVAL
652
	 **/
653
654
	/**
655
	 * Filter post thumbnail image retrieval, passing images through Photon
656
	 *
657
	 * @param string|bool  $image Image URL.
658
	 * @param int          $attachment_id Attachment ID.
659
	 * @param string|array $size Declared size or a size array.
660
	 * @uses is_admin, apply_filters, wp_get_attachment_url, self::validate_image_url, this::image_sizes, jetpack_photon_url
661
	 * @filter image_downsize
662
	 * @return string|bool
663
	 */
664
	public function filter_image_downsize( $image, $attachment_id, $size ) {
665
		// Don't foul up the admin side of things, unless a plugin wants to.
666
		if ( is_admin() &&
667
			/**
668
			 * Provide plugins a way of running Photon for images in the WordPress Dashboard (wp-admin).
669
			 *
670
			 * 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.
671
			 *
672
			 * @module photon
673
			 *
674
			 * @since 4.8.0
675
			 *
676
			 * @param bool false Stop Photon from being run on the Dashboard. Default to false.
677
			 * @param array $args {
678
			 *   Array of image details.
679
			 *
680
			 *   @type $image Image URL.
681
			 *   @type $attachment_id Attachment ID of the image.
682
			 *   @type $size Image size. Can be a string (name of the image size, e.g. full) or an array of width and height.
683
			 * }
684
			 */
685
			false === apply_filters( 'jetpack_photon_admin_allow_image_downsize', false, compact( 'image', 'attachment_id', 'size' ) )
0 ignored issues
show
The call to apply_filters() has too many arguments starting with compact('image', 'attachment_id', 'size').

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
686
		) {
687
			return $image;
688
		}
689
690
		/**
691
		 * Provide plugins a way of preventing Photon from being applied to images retrieved from WordPress Core.
692
		 *
693
		 * @module photon
694
		 *
695
		 * @since 2.0.0
696
		 *
697
		 * @param bool false Stop Photon from being applied to the image. Default to false.
698
		 * @param array $args {
699
		 *   Array of image details.
700
		 *
701
		 *   @type $image Image URL.
702
		 *   @type $attachment_id Attachment ID of the image.
703
		 *   @type $size Image size. Can be a string (name of the image size, e.g. full) or an array of width and height.
704
		 * }
705
		 */
706
		if ( apply_filters( 'jetpack_photon_override_image_downsize', false, compact( 'image', 'attachment_id', 'size' ) ) ) {
0 ignored issues
show
The call to apply_filters() has too many arguments starting with compact('image', 'attachment_id', 'size').

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
707
			return $image;
708
		}
709
710
		// Get the image URL and proceed with Photon-ification if successful.
711
		$image_url = wp_get_attachment_url( $attachment_id );
712
713
		// Set this to true later when we know we have size meta.
714
		$has_size_meta = false;
715
716
		if ( $image_url ) {
717
			// Check if image URL should be used with Photon.
718
			if ( ! self::validate_image_url( $image_url ) ) {
719
				return $image;
720
			}
721
722
			$intermediate = true; // For the fourth array item returned by the image_downsize filter.
723
724
			// If an image is requested with a size known to WordPress, use that size's settings with Photon.
725
			// WP states that `add_image_size()` should use a string for the name, but doesn't enforce that.
726
			// Due to differences in how Core and Photon check for the registered image size, we check both types.
727
			if ( ( is_string( $size ) || is_int( $size ) ) && array_key_exists( $size, self::image_sizes() ) ) {
728
				$image_args = self::image_sizes();
729
				$image_args = $image_args[ $size ];
730
731
				$photon_args = array();
732
733
				$image_meta = image_get_intermediate_size( $attachment_id, $size );
734
735
				// 'full' is a special case: We need consistent data regardless of the requested size.
736
				if ( 'full' === $size ) {
737
					$image_meta   = wp_get_attachment_metadata( $attachment_id );
738
					$intermediate = false;
739
				} elseif ( ! $image_meta ) {
740
					// If we still don't have any image meta at this point, it's probably from a custom thumbnail size
741
					// for an image that was uploaded before the custom image was added to the theme.  Try to determine the size manually.
742
					$image_meta = wp_get_attachment_metadata( $attachment_id );
743
744
					if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
745
						$image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $image_args['width'], $image_args['height'], $image_args['crop'] );
746
						if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
747
							$image_meta['width']  = $image_resized[6];
748
							$image_meta['height'] = $image_resized[7];
749
						}
750
					}
751
				}
752
753
				if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
754
					$image_args['width']  = $image_meta['width'];
755
					$image_args['height'] = $image_meta['height'];
756
757
					list( $image_args['width'], $image_args['height'] ) = image_constrain_size_for_editor( $image_args['width'], $image_args['height'], $size, 'display' );
758
					$has_size_meta                                      = true;
759
				}
760
761
				// Expose determined arguments to a filter before passing to Photon.
762
				$transform = $image_args['crop'] ? 'resize' : 'fit';
763
764
				// Check specified image dimensions and account for possible zero values; photon fails to resize if a dimension is zero.
765
				if ( 0 === $image_args['width'] || 0 === $image_args['height'] ) {
766
					if ( 0 === $image_args['width'] && 0 < $image_args['height'] ) {
767
						$photon_args['h'] = $image_args['height'];
768
					} elseif ( 0 === $image_args['height'] && 0 < $image_args['width'] ) {
769
						$photon_args['w'] = $image_args['width'];
770
					}
771
				} else {
772
					$image_meta = wp_get_attachment_metadata( $attachment_id );
773
					if ( ( 'resize' === $transform ) && $image_meta ) {
774
						if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
775
							// Lets make sure that we don't upscale images since wp never upscales them as well.
776
							$smaller_width  = ( ( $image_meta['width'] < $image_args['width'] ) ? $image_meta['width'] : $image_args['width'] );
777
							$smaller_height = ( ( $image_meta['height'] < $image_args['height'] ) ? $image_meta['height'] : $image_args['height'] );
778
779
							$photon_args[ $transform ] = $smaller_width . ',' . $smaller_height;
780
						}
781
					} else {
782
						$photon_args[ $transform ] = $image_args['width'] . ',' . $image_args['height'];
783
					}
784
				}
785
786
				/**
787
				 * Filter the Photon Arguments added to an image when going through Photon, when that image size is a string.
788
				 * Image size will be a string (e.g. "full", "medium") when it is known to WordPress.
789
				 *
790
				 * @module photon
791
				 *
792
				 * @since 2.0.0
793
				 *
794
				 * @param array $photon_args Array of Photon arguments.
795
				 * @param array $args {
796
				 *   Array of image details.
797
				 *
798
				 *   @type array $image_args Array of Image arguments (width, height, crop).
799
				 *   @type string $image_url Image URL.
800
				 *   @type int $attachment_id Attachment ID of the image.
801
				 *   @type string|int $size Image size. Can be a string (name of the image size, e.g. full) or an integer.
802
				 *   @type string $transform Value can be resize or fit.
803
				 *                    @see https://developer.wordpress.com/docs/photon/api
804
				 * }
805
				 */
806
				$photon_args = apply_filters( 'jetpack_photon_image_downsize_string', $photon_args, compact( 'image_args', 'image_url', 'attachment_id', 'size', 'transform' ) );
0 ignored issues
show
The call to apply_filters() has too many arguments starting with compact('image_args', 'i...', 'size', 'transform').

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
807
808
				// Generate Photon URL.
809
				$image = array(
810
					jetpack_photon_url( $image_url, $photon_args ),
811
					$has_size_meta ? $image_args['width'] : false,
812
					$has_size_meta ? $image_args['height'] : false,
813
					$intermediate,
814
				);
815
			} elseif ( is_array( $size ) ) {
816
				// Pull width and height values from the provided array, if possible.
817
				$width  = isset( $size[0] ) ? (int) $size[0] : false;
818
				$height = isset( $size[1] ) ? (int) $size[1] : false;
819
820
				// Don't bother if necessary parameters aren't passed.
821
				if ( ! $width || ! $height ) {
822
					return $image;
823
				}
824
825
				$image_meta = wp_get_attachment_metadata( $attachment_id );
826
				if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
827
					$image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $width, $height );
828
829
					if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
830
						$width  = $image_resized[6];
831
						$height = $image_resized[7];
832
					} else {
833
						$width  = $image_meta['width'];
834
						$height = $image_meta['height'];
835
					}
836
837
					$has_size_meta = true;
838
				}
839
840
				list( $width, $height ) = image_constrain_size_for_editor( $width, $height, $size );
841
842
				// Expose arguments to a filter before passing to Photon.
843
				$photon_args = array(
844
					'fit' => $width . ',' . $height,
845
				);
846
847
				/**
848
				 * Filter the Photon Arguments added to an image when going through Photon,
849
				 * when the image size is an array of height and width values.
850
				 *
851
				 * @module photon
852
				 *
853
				 * @since 2.0.0
854
				 *
855
				 * @param array $photon_args Array of Photon arguments.
856
				 * @param array $args {
857
				 *   Array of image details.
858
				 *
859
				 *   @type $width Image width.
860
				 *   @type height Image height.
861
				 *   @type $image_url Image URL.
862
				 *   @type $attachment_id Attachment ID of the image.
863
				 * }
864
				 */
865
				$photon_args = apply_filters( 'jetpack_photon_image_downsize_array', $photon_args, compact( 'width', 'height', 'image_url', 'attachment_id' ) );
0 ignored issues
show
The call to apply_filters() has too many arguments starting with compact('width', 'height..._url', 'attachment_id').

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
866
867
				// Generate Photon URL.
868
				$image = array(
869
					jetpack_photon_url( $image_url, $photon_args ),
870
					$has_size_meta ? $width : false,
871
					$has_size_meta ? $height : false,
872
					$intermediate,
873
				);
874
			}
875
		}
876
877
		return $image;
878
	}
879
880
	/**
881
	 * Filters an array of image `srcset` values, replacing each URL with its Photon equivalent.
882
	 *
883
	 * @since 3.8.0
884
	 * @since 4.0.4 Added automatically additional sizes beyond declared image sizes.
885
	 *
886
	 * @param array $sources An array of image urls and widths.
887
	 * @param array $size_array The size array for srcset.
888
	 * @param array $image_src The image srcs.
889
	 * @param array $image_meta The image meta.
890
	 * @param int   $attachment_id Attachment ID.
891
	 *
892
	 * @uses self::validate_image_url, jetpack_photon_url, Jetpack_Photon::parse_from_filename
893
	 * @uses Jetpack_Photon::strip_image_dimensions_maybe, Jetpack::get_content_width
894
	 *
895
	 * @return array An array of Photon image urls and widths.
896
	 */
897
	public function filter_srcset_array( $sources = array(), $size_array = array(), $image_src = array(), $image_meta = array(), $attachment_id = 0 ) {
898
		if ( ! is_array( $sources ) ) {
899
			return $sources;
900
		}
901
		$upload_dir = wp_get_upload_dir();
902
903
		foreach ( $sources as $i => $source ) {
904
			if ( ! self::validate_image_url( $source['url'] ) ) {
905
				continue;
906
			}
907
908
			/** This filter is already documented in class.photon.php */
909
			if ( apply_filters( 'jetpack_photon_skip_image', false, $source['url'], $source ) ) {
0 ignored issues
show
The call to apply_filters() has too many arguments starting with $source['url'].

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
910
				continue;
911
			}
912
913
			$url                    = $source['url'];
914
			list( $width, $height ) = self::parse_dimensions_from_filename( $url );
915
916
			// It's quicker to get the full size with the data we have already, if available.
917
			if ( ! empty( $attachment_id ) ) {
918
				$url = wp_get_attachment_url( $attachment_id );
919
			} else {
920
				$url = self::strip_image_dimensions_maybe( $url );
921
			}
922
923
			$args = array();
924
			if ( 'w' === $source['descriptor'] ) {
925
				if ( $height && ( $source['value'] === $width ) ) {
926
					$args['resize'] = $width . ',' . $height;
927
				} else {
928
					$args['w'] = $source['value'];
929
				}
930
			}
931
932
			$sources[ $i ]['url'] = jetpack_photon_url( $url, $args );
933
		}
934
935
		/**
936
		 * At this point, $sources is the original srcset with Photonized URLs.
937
		 * Now, we're going to construct additional sizes based on multiples of the content_width.
938
		 * This will reduce the gap between the largest defined size and the original image.
939
		 */
940
941
		/**
942
		 * Filter the multiplier Photon uses to create new srcset items.
943
		 * Return false to short-circuit and bypass auto-generation.
944
		 *
945
		 * @module photon
946
		 *
947
		 * @since 4.0.4
948
		 *
949
		 * @param array|bool $multipliers Array of multipliers to use or false to bypass.
950
		 */
951
		$multipliers = apply_filters( 'jetpack_photon_srcset_multipliers', array( 2, 3 ) );
952
		$url         = trailingslashit( $upload_dir['baseurl'] ) . $image_meta['file'];
953
954
		if (
955
			/** Short-circuit via jetpack_photon_srcset_multipliers filter. */
956
			is_array( $multipliers )
957
			/** This filter is already documented in class.photon.php */
958
			&& ! apply_filters( 'jetpack_photon_skip_image', false, $url, null )
0 ignored issues
show
The call to apply_filters() has too many arguments starting with $url.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
959
			/** Verify basic meta is intact. */
960
			&& isset( $image_meta['width'] ) && isset( $image_meta['height'] ) && isset( $image_meta['file'] )
961
			/** Verify we have the requested width/height. */
962
			&& isset( $size_array[0] ) && isset( $size_array[1] )
963
			) {
964
965
			$fullwidth  = $image_meta['width'];
966
			$fullheight = $image_meta['height'];
967
			$reqwidth   = $size_array[0];
968
			$reqheight  = $size_array[1];
969
970
			$constrained_size = wp_constrain_dimensions( $fullwidth, $fullheight, $reqwidth );
971
			$expected_size    = array( $reqwidth, $reqheight );
972
973
			if ( abs( $constrained_size[0] - $expected_size[0] ) <= 1 && abs( $constrained_size[1] - $expected_size[1] ) <= 1 ) {
974
				$crop = 'soft';
975
				$base = Jetpack::get_content_width() ? Jetpack::get_content_width() : 1000; // Provide a default width if none set by the theme.
976
			} else {
977
				$crop = 'hard';
978
				$base = $reqwidth;
979
			}
980
981
			$currentwidths = array_keys( $sources );
982
			$newsources    = null;
983
984
			foreach ( $multipliers as $multiplier ) {
985
986
				$newwidth = $base * $multiplier;
987
				foreach ( $currentwidths as $currentwidth ) {
988
					// If a new width would be within 100 pixes of an existing one or larger than the full size image, skip.
989
					if ( abs( $currentwidth - $newwidth ) < 50 || ( $newwidth > $fullwidth ) ) {
990
						continue 2; // Bump out back to the $multipliers as $multiplier.
991
					}
992
				} //end foreach ( $currentwidths as $currentwidth ){
993
994
				if ( 'soft' === $crop ) {
995
					$args = array(
996
						'w' => $newwidth,
997
					);
998
				} else { // hard crop, e.g. add_image_size( 'example', 200, 200, true ).
999
					$args = array(
1000
						'zoom'   => $multiplier,
1001
						'resize' => $reqwidth . ',' . $reqheight,
1002
					);
1003
				}
1004
1005
				$newsources[ $newwidth ] = array(
1006
					'url'        => jetpack_photon_url( $url, $args ),
1007
					'descriptor' => 'w',
1008
					'value'      => $newwidth,
1009
				);
1010
			} //end foreach ( $multipliers as $multiplier )
1011
			if ( is_array( $newsources ) ) {
1012
				$sources = array_replace( $sources, $newsources );
1013
			}
1014
		} //end if isset( $image_meta['width'] ) && isset( $image_meta['file'] ) )
1015
1016
		return $sources;
1017
	}
1018
1019
	/**
1020
	 * Filters an array of image `sizes` values, using $content_width instead of image's full size.
1021
	 *
1022
	 * @since 4.0.4
1023
	 * @since 4.1.0 Returns early for images not within the_content.
1024
	 * @param array $sizes An array of media query breakpoints.
1025
	 * @param array $size  Width and height of the image.
1026
	 * @uses Jetpack::get_content_width
1027
	 * @return array An array of media query breakpoints.
1028
	 */
1029
	public function filter_sizes( $sizes, $size ) {
1030
		if ( ! doing_filter( 'the_content' ) ) {
1031
			return $sizes;
1032
		}
1033
		$content_width = Jetpack::get_content_width();
1034
		if ( ! $content_width ) {
1035
			$content_width = 1000;
1036
		}
1037
1038
		if ( ( is_array( $size ) && $size[0] < $content_width ) ) {
1039
			return $sizes;
1040
		}
1041
1042
		return sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $content_width );
1043
	}
1044
1045
	/**
1046
	 * * GENERAL FUNCTIONS
1047
	 **/
1048
1049
	/**
1050
	 * Ensure image URL is valid for Photon.
1051
	 * 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.
1052
	 *
1053
	 * @param string $url Image URL.
1054
	 * @uses wp_parse_args
1055
	 * @return bool
1056
	 */
1057
	protected static function validate_image_url( $url ) {
1058
		$parsed_url = wp_parse_url( $url );
1059
1060
		if ( ! $parsed_url ) {
1061
			return false;
1062
		}
1063
1064
		// Parse URL and ensure needed keys exist, since the array returned by `wp_parse_url` only includes the URL components it finds.
1065
		$url_info = wp_parse_args(
1066
			$parsed_url,
1067
			array(
0 ignored issues
show
array('scheme' => null, ...> null, 'path' => null) is of type array<string,null,{"sche...:"null","path":"null"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1068
				'scheme' => null,
1069
				'host'   => null,
1070
				'port'   => null,
1071
				'path'   => null,
1072
			)
1073
		);
1074
1075
		// Bail if scheme isn't http or port is set that isn't port 80.
1076
		if (
1077
			( 'http' !== $url_info['scheme'] || ! in_array( $url_info['port'], array( 80, null ), true ) ) &&
1078
			/**
1079
			 * Allow Photon to fetch images that are served via HTTPS.
1080
			 *
1081
			 * @module photon
1082
			 *
1083
			 * @since 2.4.0
1084
			 * @since 3.9.0 Default to false.
1085
			 *
1086
			 * @param bool $reject_https Should Photon ignore images using the HTTPS scheme. Default to false.
1087
			 */
1088
			apply_filters( 'jetpack_photon_reject_https', false )
1089
		) {
1090
			return false;
1091
		}
1092
1093
		// Bail if no host is found.
1094
		if ( is_null( $url_info['host'] ) ) {
1095
			return false;
1096
		}
1097
1098
		// Bail if the image already went through Photon.
1099
		if ( preg_match( '#^i[\d]{1}.wp.com$#i', $url_info['host'] ) ) {
1100
			return false;
1101
		}
1102
1103
		// Bail if no path is found.
1104
		if ( is_null( $url_info['path'] ) ) {
1105
			return false;
1106
		}
1107
1108
		// Ensure image extension is acceptable.
1109
		if ( ! in_array( strtolower( pathinfo( $url_info['path'], PATHINFO_EXTENSION ) ), self::$extensions, true ) ) {
1110
			return false;
1111
		}
1112
1113
		// If we got this far, we should have an acceptable image URL
1114
		// But let folks filter to decline if they prefer.
1115
		/**
1116
		 * Overwrite the results of the validation steps an image goes through before to be considered valid to be used by Photon.
1117
		 *
1118
		 * @module photon
1119
		 *
1120
		 * @since 3.0.0
1121
		 *
1122
		 * @param bool true Is the image URL valid and can it be used by Photon. Default to true.
1123
		 * @param string $url Image URL.
1124
		 * @param array $parsed_url Array of information about the image.
1125
		 */
1126
		return apply_filters( 'photon_validate_image_url', true, $url, $parsed_url );
0 ignored issues
show
The call to apply_filters() has too many arguments starting with $url.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1127
	}
1128
1129
	/**
1130
	 * Checks if the file exists before it passes the file to photon.
1131
	 *
1132
	 * @param string $src The image URL.
1133
	 * @return string
1134
	 **/
1135
	public static function strip_image_dimensions_maybe( $src ) {
1136
		$stripped_src = $src;
1137
1138
		// Build URL, first removing WP's resized string so we pass the original image to Photon.
1139
		if ( preg_match( '#(-\d+x\d+)\.(' . implode( '|', self::$extensions ) . '){1}$#i', $src, $src_parts ) ) {
1140
			$stripped_src = str_replace( $src_parts[1], '', $src );
1141
			$upload_dir   = wp_get_upload_dir();
1142
1143
			// Extracts the file path to the image minus the base url.
1144
			$file_path = substr( $stripped_src, strlen( $upload_dir['baseurl'] ) );
1145
1146
			if ( file_exists( $upload_dir['basedir'] . $file_path ) ) {
1147
				$src = $stripped_src;
1148
			}
1149
		}
1150
1151
		return $src;
1152
	}
1153
1154
	/**
1155
	 * Provide an array of available image sizes and corresponding dimensions.
1156
	 * Similar to get_intermediate_image_sizes() except that it includes image sizes' dimensions, not just their names.
1157
	 *
1158
	 * @global $wp_additional_image_sizes
1159
	 * @uses get_option
1160
	 * @return array
1161
	 */
1162
	protected static function image_sizes() {
1163
		if ( null === self::$image_sizes ) {
1164
			global $_wp_additional_image_sizes;
1165
1166
			// Populate an array matching the data structure of $_wp_additional_image_sizes so we have a consistent structure for image sizes.
1167
			$images = array(
1168
				'thumb'        => array(
1169
					'width'  => (int) get_option( 'thumbnail_size_w' ),
1170
					'height' => (int) get_option( 'thumbnail_size_h' ),
1171
					'crop'   => (bool) get_option( 'thumbnail_crop' ),
1172
				),
1173
				'medium'       => array(
1174
					'width'  => (int) get_option( 'medium_size_w' ),
1175
					'height' => (int) get_option( 'medium_size_h' ),
1176
					'crop'   => false,
1177
				),
1178
				'medium_large' => array(
1179
					'width'  => (int) get_option( 'medium_large_size_w' ),
1180
					'height' => (int) get_option( 'medium_large_size_h' ),
1181
					'crop'   => false,
1182
				),
1183
				'large'        => array(
1184
					'width'  => (int) get_option( 'large_size_w' ),
1185
					'height' => (int) get_option( 'large_size_h' ),
1186
					'crop'   => false,
1187
				),
1188
				'full'         => array(
1189
					'width'  => null,
1190
					'height' => null,
1191
					'crop'   => false,
1192
				),
1193
			);
1194
1195
			// Compatibility mapping as found in wp-includes/media.php.
1196
			$images['thumbnail'] = $images['thumb'];
1197
1198
			// Update class variable, merging in $_wp_additional_image_sizes if any are set.
1199
			if ( is_array( $_wp_additional_image_sizes ) && ! empty( $_wp_additional_image_sizes ) ) {
1200
				self::$image_sizes = array_merge( $images, $_wp_additional_image_sizes );
1201
			} else {
1202
				self::$image_sizes = $images;
1203
			}
1204
		}
1205
1206
		return is_array( self::$image_sizes ) ? self::$image_sizes : array();
1207
	}
1208
1209
	/**
1210
	 * Pass og:image URLs through Photon
1211
	 *
1212
	 * @param array $tags Open graph tags.
1213
	 * @param array $parameters Image parameters.
1214
	 * @uses jetpack_photon_url
1215
	 * @return array Open graph tags.
1216
	 */
1217
	public function filter_open_graph_tags( $tags, $parameters ) {
1218
		if ( empty( $tags['og:image'] ) ) {
1219
			return $tags;
1220
		}
1221
1222
		$photon_args = array(
1223
			'fit' => sprintf( '%d,%d', 2 * $parameters['image_width'], 2 * $parameters['image_height'] ),
1224
		);
1225
1226
		if ( is_array( $tags['og:image'] ) ) {
1227
			$images = array();
1228
			foreach ( $tags['og:image'] as $image ) {
1229
				$images[] = jetpack_photon_url( $image, $photon_args );
1230
			}
1231
			$tags['og:image'] = $images;
1232
		} else {
1233
			$tags['og:image'] = jetpack_photon_url( $tags['og:image'], $photon_args );
1234
		}
1235
1236
		return $tags;
1237
	}
1238
1239
	/**
1240
	 * Returns empty array.
1241
	 *
1242
	 * @deprecated 8.8.0 Use filter_photon_noresize_intermediate_sizes.
1243
	 *
1244
	 * @return array Empty array.
1245
	 */
1246
	public function noresize_intermediate_sizes() {
1247
		_deprecated_function( __METHOD__, 'jetpack-8.8.0', '::filter_photon_noresize_intermediate_sizes' );
1248
		return __return_empty_array();
1249
	}
1250
1251
	/**
1252
	 * Enqueue Photon helper script
1253
	 *
1254
	 * @uses wp_enqueue_script, plugins_url
1255
	 * @action wp_enqueue_script
1256
	 * @return null
1257
	 */
1258
	public function action_wp_enqueue_scripts() {
1259
		if ( self::is_amp_endpoint() ) {
1260
			return;
1261
		}
1262
		wp_enqueue_script(
1263
			'jetpack-photon',
1264
			Assets::get_file_url_for_environment(
1265
				'_inc/build/photon/photon.min.js',
1266
				'modules/photon/photon.js'
1267
			),
1268
			array(),
1269
			20191001,
1270
			true
1271
		);
1272
	}
1273
1274
	/**
1275
	 * Determine if image_downsize should utilize Photon via REST API.
1276
	 *
1277
	 * The WordPress Block Editor (Gutenberg) and other REST API consumers using the wp/v2/media endpoint, especially in the "edit"
1278
	 * context is more akin to the is_admin usage of Photon (see filter_image_downsize). Since consumers are trying to edit content in posts,
1279
	 * Photon should not fire as it will fire later on display. By aborting an attempt to Photonize an image here, we
1280
	 * prevents issues like https://github.com/Automattic/jetpack/issues/10580 .
1281
	 *
1282
	 * To determine if we're using the wp/v2/media endpoint, we hook onto the `rest_request_before_callbacks` filter and
1283
	 * if determined we are using it in the edit context, we'll false out the `jetpack_photon_override_image_downsize` filter.
1284
	 *
1285
	 * @see Jetpack_Photon::filter_image_downsize()
1286
	 *
1287
	 * @param null|WP_Error   $response REST API response.
1288
	 * @param array           $endpoint_data Endpoint data. Not used, but part of the filter.
1289
	 * @param WP_REST_Request $request  Request used to generate the response.
1290
	 *
1291
	 * @return null|WP_Error The original response object without modification.
1292
	 */
1293
	public function should_rest_photon_image_downsize( $response, $endpoint_data, $request ) {
1294
		if ( ! is_a( $request, 'WP_REST_Request' ) ) {
1295
			return $response; // Something odd is happening. Do nothing and return the response.
1296
		}
1297
1298
		if ( is_wp_error( $response ) ) {
1299
			// If we're going to return an error, we don't need to do anything with Photon.
1300
			return $response;
1301
		}
1302
1303
		$this->should_rest_photon_image_downsize_override( $request );
1304
1305
		return $response;
1306
1307
	}
1308
1309
	/**
1310
	 * Helper function to check if a WP_REST_Request is the media endpoint in the edit context.
1311
	 *
1312
	 * @param WP_REST_Request $request The current REST request.
1313
	 */
1314
	private function should_rest_photon_image_downsize_override( WP_REST_Request $request ) {
1315
		$route = $request->get_route();
1316
1317
		if (
1318
			(
1319
				false !== strpos( $route, 'wp/v2/media' )
1320
				&& 'edit' === $request->get_param( 'context' )
1321
			)
1322
			|| false !== strpos( $route, 'wpcom/v2/external-media/copy' )
1323
		) {
1324
			// Don't use `__return_true()`: Use something unique. See ::_override_image_downsize_in_rest_edit_context()
1325
			// Late execution to avoid conflict with other plugins as we really don't want to run in this situation.
1326
			add_filter(
1327
				'jetpack_photon_override_image_downsize',
1328
				array(
1329
					$this,
1330
					'override_image_downsize_in_rest_edit_context',
1331
				),
1332
				999999
1333
			);
1334
		}
1335
	}
1336
1337
	/**
1338
	 * Brings in should_rest_photon_image_downsize for the rest_after_insert_attachment hook.
1339
	 *
1340
	 * @since 8.7.0
1341
	 *
1342
	 * @param WP_Post         $attachment Inserted or updated attachment object.
1343
	 * @param WP_REST_Request $request    Request object.
1344
	 */
1345
	public function should_rest_photon_image_downsize_insert_attachment( WP_Post $attachment, WP_REST_Request $request ) {
1346
		if ( ! is_a( $request, 'WP_REST_Request' ) ) {
1347
			// Something odd is happening.
1348
			return;
1349
		}
1350
1351
		$this->should_rest_photon_image_downsize_override( $request );
1352
1353
	}
1354
1355
	/**
1356
	 * Remove the override we may have added in ::should_rest_photon_image_downsize()
1357
	 * Since ::_override_image_downsize_in_rest_edit_context() is only
1358
	 * every used here, we can always remove it without ever worrying
1359
	 * about breaking any other configuration.
1360
	 *
1361
	 * @param mixed $response REST API Response.
1362
	 * @return mixed Unchanged $response
1363
	 */
1364
	public function cleanup_rest_photon_image_downsize( $response ) {
1365
		remove_filter(
1366
			'jetpack_photon_override_image_downsize',
1367
			array(
1368
				$this,
1369
				'override_image_downsize_in_rest_edit_context',
1370
			),
1371
			999999
1372
		);
1373
		return $response;
1374
	}
1375
1376
	/**
1377
	 * Used internally by ::should_rest_photon_image_downsize() to not photonize
1378
	 * image URLs in ?context=edit REST requests.
1379
	 * MUST NOT be used anywhere else.
1380
	 * We use a unique function instead of __return_true so that we can clean up
1381
	 * after ourselves without breaking anyone else's filters.
1382
	 *
1383
	 * @internal
1384
	 * @return true
1385
	 */
1386
	public function override_image_downsize_in_rest_edit_context() {
1387
		return true;
1388
	}
1389
1390
	/**
1391
	 * Return whether the current page is AMP.
1392
	 *
1393
	 * This is only present for the sake of WordPress.com where the Jetpack_AMP_Support
1394
	 * class does not yet exist. This mehod may only be called at the wp action or later.
1395
	 *
1396
	 * @return bool Whether AMP page.
1397
	 */
1398
	private static function is_amp_endpoint() {
1399
		return class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request();
1400
	}
1401
}
1402