Completed
Push — update/react-dash-wordpress-i1... ( 3ef406...9a5312 )
by Jeremy
45:47 queued 38:34
created

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

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
171
		}
172
173
		return $sizes;
174
	}
175
176
	/**
177
	 * Inject image sizes to attachment metadata.
178
	 *
179
	 * @param array $data          Attachment metadata.
180
	 * @param int   $attachment_id Attachment's post ID.
181
	 *
182
	 * @return array Attachment metadata.
183
	 */
184
	public static function filter_photon_norezise_maybe_inject_sizes( $data, $attachment_id ) {
185
		// Can't do much if data is empty.
186
		if ( empty( $data ) ) {
187
			return $data;
188
		}
189
		$sizes_already_exist = (
190
			true === is_array( $data )
191
			&& true === array_key_exists( 'sizes', $data )
192
			&& true === is_array( $data['sizes'] )
193
			&& false === empty( $data['sizes'] )
194
		);
195
		if ( $sizes_already_exist ) {
196
			return $data;
197
		}
198
		// Missing some critical data we need to determine sizes, not processing.
199
		if ( ! isset( $data['file'] )
200
			|| ! isset( $data['width'] )
201
			|| ! isset( $data['height'] )
202
		) {
203
			return $data;
204
		}
205
206
		$mime_type           = get_post_mime_type( $attachment_id );
207
		$attachment_is_image = preg_match( '!^image/!', $mime_type );
208
209
		if ( 1 === $attachment_is_image ) {
210
			$image_sizes   = new Jetpack_Photon_ImageSizes( $attachment_id, $data );
211
			$data['sizes'] = $image_sizes->generate_sizes_meta();
212
		}
213
		return $data;
214
	}
215
216
	/**
217
	 * Inject image sizes to Jetpack REST API responses. This wraps the filter_photon_norezise_maybe_inject_sizes function.
218
	 *
219
	 * @param array $data          Attachment sizes data.
0 ignored issues
show
There is no parameter named $data. Was it maybe removed?

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

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

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

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

Loading history...
220
	 * @param int   $attachment_id Attachment's post ID.
221
	 *
222
	 * @return array Attachment sizes array.
223
	 */
224
	public static function filter_photon_norezise_maybe_inject_sizes_api( $sizes, $attachment_id ) {
225
		return self::filter_photon_norezise_maybe_inject_sizes( wp_get_attachment_metadata( $attachment_id ), $attachment_id );
226
	}
227
228
	/**
229
	 * * IN-CONTENT IMAGE MANIPULATION FUNCTIONS
230
	 **/
231
232
	/**
233
	 * Match all images and any relevant <a> tags in a block of HTML.
234
	 *
235
	 * @param string $content Some HTML.
236
	 * @return array An array of $images matches, where $images[0] is
237
	 *         an array of full matches, and the link_url, img_tag,
238
	 *         and img_url keys are arrays of those matches.
239
	 */
240
	public static function parse_images_from_html( $content ) {
241
		$images = array();
242
243
		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 ) ) {
244
			foreach ( $images as $key => $unused ) {
245
				// Simplify the output as much as possible, mostly for confirming test results.
246
				if ( is_numeric( $key ) && $key > 0 ) {
247
					unset( $images[ $key ] );
248
				}
249
			}
250
251
			return $images;
252
		}
253
254
		return array();
255
	}
256
257
	/**
258
	 * Try to determine height and width from strings WP appends to resized image filenames.
259
	 *
260
	 * @param string $src The image URL.
261
	 * @return array An array consisting of width and height.
262
	 */
263
	public static function parse_dimensions_from_filename( $src ) {
264
		$width_height_string = array();
265
266
		if ( preg_match( '#-(\d+)x(\d+)\.(?:' . implode( '|', self::$extensions ) . '){1}$#i', $src, $width_height_string ) ) {
267
			$width  = (int) $width_height_string[1];
268
			$height = (int) $width_height_string[2];
269
270
			if ( $width && $height ) {
271
				return array( $width, $height );
272
			}
273
		}
274
275
		return array( false, false );
276
	}
277
278
	/**
279
	 * Identify images in post content, and if images are local (uploaded to the current site), pass through Photon.
280
	 *
281
	 * @param string $content
282
	 * @uses self::validate_image_url, apply_filters, jetpack_photon_url, esc_url
283
	 * @filter the_content
284
	 * @return string
285
	 */
286
	public static function filter_the_content( $content ) {
287
		$images = self::parse_images_from_html( $content );
288
289
		if ( ! empty( $images ) ) {
290
			$content_width = Jetpack::get_content_width();
291
292
			$image_sizes = self::image_sizes();
293
294
			$upload_dir = wp_get_upload_dir();
295
296
			foreach ( $images[0] as $index => $tag ) {
297
				// Default to resize, though fit may be used in certain cases where a dimension cannot be ascertained
298
				$transform = 'resize';
299
300
				// Start with a clean attachment ID each time
301
				$attachment_id = false;
302
303
				// Flag if we need to munge a fullsize URL
304
				$fullsize_url = false;
305
306
				// Identify image source
307
				$src = $src_orig = $images['img_url'][ $index ];
308
309
				/**
310
				 * Allow specific images to be skipped by Photon.
311
				 *
312
				 * @module photon
313
				 *
314
				 * @since 2.0.3
315
				 *
316
				 * @param bool false Should Photon ignore this image. Default to false.
317
				 * @param string $src Image URL.
318
				 * @param string $tag Image Tag (Image HTML output).
319
				 */
320
				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...
321
					continue;
322
				}
323
324
				// Support Automattic's Lazy Load plugin
325
				// Can't modify $tag yet as we need unadulterated version later
326
				if ( preg_match( '#data-lazy-src=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
327
					$placeholder_src = $placeholder_src_orig = $src;
328
					$src             = $src_orig = $lazy_load_src[1];
329
				} elseif ( preg_match( '#data-lazy-original=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
330
					$placeholder_src = $placeholder_src_orig = $src;
331
					$src             = $src_orig = $lazy_load_src[1];
332
				}
333
334
				// Check if image URL should be used with Photon
335
				if ( self::validate_image_url( $src ) ) {
336
					// Find the width and height attributes
337
					$width = $height = false;
338
339
					// First, check the image tag
340
					if ( preg_match( '#[\s|"|\']width=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $width_string ) ) {
341
						$width = $width_string[1];
342
					}
343
344
					if ( preg_match( '#[\s|"|\']height=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $height_string ) ) {
345
						$height = $height_string[1];
346
					}
347
348
					// Can't pass both a relative width and height, so unset the height in favor of not breaking the horizontal layout.
349
					if ( false !== strpos( $width, '%' ) && false !== strpos( $height, '%' ) ) {
350
						$width = $height = false;
351
					}
352
353
					// Detect WP registered image size from HTML class
354
					if ( preg_match( '#class=["|\']?[^"\']*size-([^"\'\s]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $size ) ) {
355
						$size = array_pop( $size );
356
357
						if ( false === $width && false === $height && 'full' != $size && array_key_exists( $size, $image_sizes ) ) {
358
							$width     = (int) $image_sizes[ $size ]['width'];
359
							$height    = (int) $image_sizes[ $size ]['height'];
360
							$transform = $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
361
						}
362
					} else {
363
						unset( $size );
364
					}
365
366
					// WP Attachment ID, if uploaded to this site
367
					if (
368
						preg_match( '#class=["|\']?[^"\']*wp-image-([\d]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $attachment_id ) &&
369
						0 === strpos( $src, $upload_dir['baseurl'] ) &&
370
						/**
371
						 * Filter whether an image using an attachment ID in its class has to be uploaded to the local site to go through Photon.
372
						 *
373
						 * @module photon
374
						 *
375
						 * @since 2.0.3
376
						 *
377
						 * @param bool false Was the image uploaded to the local site. Default to false.
378
						 * @param array $args {
379
						 *   Array of image details.
380
						 *
381
						 *   @type $src Image URL.
382
						 *   @type tag Image tag (Image HTML output).
383
						 *   @type $images Array of information about the image.
384
						 *   @type $index Image index.
385
						 * }
386
						 */
387
						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...
388
					) {
389
						$attachment_id = intval( array_pop( $attachment_id ) );
390
391
						if ( $attachment_id ) {
392
							$attachment = get_post( $attachment_id );
393
394
							// Basic check on returned post object
395
							if ( is_object( $attachment ) && ! is_wp_error( $attachment ) && 'attachment' == $attachment->post_type ) {
396
								$src_per_wp = wp_get_attachment_image_src( $attachment_id, isset( $size ) ? $size : 'full' );
397
398
								if ( self::validate_image_url( $src_per_wp[0] ) ) {
399
									$src          = $src_per_wp[0];
400
									$fullsize_url = true;
401
402
									// Prevent image distortion if a detected dimension exceeds the image's natural dimensions
403
									if ( ( false !== $width && $width > $src_per_wp[1] ) || ( false !== $height && $height > $src_per_wp[2] ) ) {
404
										$width  = false === $width ? false : min( $width, $src_per_wp[1] );
405
										$height = false === $height ? false : min( $height, $src_per_wp[2] );
406
									}
407
408
									// If no width and height are found, max out at source image's natural dimensions
409
									// Otherwise, respect registered image sizes' cropping setting
410
									if ( false === $width && false === $height ) {
411
										$width     = $src_per_wp[1];
412
										$height    = $src_per_wp[2];
413
										$transform = 'fit';
414
									} elseif ( isset( $size ) && array_key_exists( $size, $image_sizes ) && isset( $image_sizes[ $size ]['crop'] ) ) {
415
										$transform = (bool) $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
416
									}
417
								}
418
							} else {
419
								unset( $attachment_id );
420
								unset( $attachment );
421
							}
422
						}
423
					}
424
425
					// If image tag lacks width and height arguments, try to determine from strings WP appends to resized image filenames.
426
					if ( false === $width && false === $height ) {
427
						list( $width, $height ) = self::parse_dimensions_from_filename( $src );
428
					}
429
430
					$width_orig     = $width;
431
					$height_orig    = $height;
432
					$transform_orig = $transform;
433
434
					// If width is available, constrain to $content_width
435
					if ( false !== $width && false === strpos( $width, '%' ) && is_numeric( $content_width ) ) {
436
						if ( $width > $content_width && false !== $height && false === strpos( $height, '%' ) ) {
437
							$height = round( ( $content_width * $height ) / $width );
438
							$width  = $content_width;
439
						} elseif ( $width > $content_width ) {
440
							$width = $content_width;
441
						}
442
					}
443
444
					// Set a width if none is found and $content_width is available
445
					// If width is set in this manner and height is available, use `fit` instead of `resize` to prevent skewing
446
					if ( false === $width && is_numeric( $content_width ) ) {
447
						$width = (int) $content_width;
448
449
						if ( false !== $height ) {
450
							$transform = 'fit';
451
						}
452
					}
453
454
					// Detect if image source is for a custom-cropped thumbnail and prevent further URL manipulation.
455
					if ( ! $fullsize_url && preg_match_all( '#-e[a-z0-9]+(-\d+x\d+)?\.(' . implode( '|', self::$extensions ) . '){1}$#i', basename( $src ), $filename ) ) {
456
						$fullsize_url = true;
457
					}
458
459
					// Build URL, first maybe removing WP's resized string so we pass the original image to Photon
460
					if ( ! $fullsize_url && 0 === strpos( $src, $upload_dir['baseurl'] ) ) {
461
						$src = self::strip_image_dimensions_maybe( $src );
462
					}
463
464
					// Build array of Photon args and expose to filter before passing to Photon URL function
465
					$args = array();
466
467
					if ( false !== $width && false !== $height && false === strpos( $width, '%' ) && false === strpos( $height, '%' ) ) {
468
						$args[ $transform ] = $width . ',' . $height;
469
					} elseif ( false !== $width ) {
470
						$args['w'] = $width;
471
					} elseif ( false !== $height ) {
472
						$args['h'] = $height;
473
					}
474
475
					/**
476
					 * Filter the array of Photon arguments added to an image when it goes through Photon.
477
					 * By default, only includes width and height values.
478
					 *
479
					 * @see https://developer.wordpress.com/docs/photon/api/
480
					 *
481
					 * @module photon
482
					 *
483
					 * @since 2.0.0
484
					 *
485
					 * @param array $args Array of Photon Arguments.
486
					 * @param array $details {
487
					 *     Array of image details.
488
					 *
489
					 *     @type string    $tag            Image tag (Image HTML output).
490
					 *     @type string    $src            Image URL.
491
					 *     @type string    $src_orig       Original Image URL.
492
					 *     @type int|false $width          Image width.
493
					 *     @type int|false $height         Image height.
494
					 *     @type int|false $width_orig     Original image width before constrained by content_width.
495
					 *     @type int|false $height_orig    Original Image height before constrained by content_width.
496
					 *     @type string    $transform      Transform.
497
					 *     @type string    $transform_orig Original transform before constrained by content_width.
498
					 * }
499
					 */
500
					$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...
501
502
					$photon_url = jetpack_photon_url( $src, $args );
503
504
					// Modify image tag if Photon function provides a URL
505
					// 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.
506
					if ( $src != $photon_url ) {
507
						$new_tag = $tag;
508
509
						// If present, replace the link href with a Photoned URL for the full-size image.
510
						if ( ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
511
							$new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . jetpack_photon_url( $images['link_url'][ $index ] ) . '\2', $new_tag, 1 );
512
						}
513
514
						// Supplant the original source value with our Photon URL
515
						$photon_url = esc_url( $photon_url );
516
						$new_tag    = str_replace( $src_orig, $photon_url, $new_tag );
517
518
						// If Lazy Load is in use, pass placeholder image through Photon
519
						if ( isset( $placeholder_src ) && self::validate_image_url( $placeholder_src ) ) {
520
							$placeholder_src = jetpack_photon_url( $placeholder_src );
521
522
							if ( $placeholder_src != $placeholder_src_orig ) {
523
								$new_tag = str_replace( $placeholder_src_orig, esc_url( $placeholder_src ), $new_tag );
0 ignored issues
show
The variable $placeholder_src_orig does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
524
							}
525
526
							unset( $placeholder_src );
527
						}
528
529
						// If we are not transforming the image with resize, fit, or letterbox (lb), then we should remove
530
						// the width and height arguments from the image to prevent distortion. Even if $args['w'] and $args['h']
531
						// are present, Photon does not crop to those dimensions. Instead, it appears to favor height.
532
						//
533
						// If we are transforming the image via one of those methods, let's update the width and height attributes.
534
						if ( empty( $args['resize'] ) && empty( $args['fit'] ) && empty( $args['lb'] ) ) {
535
							$new_tag = preg_replace( '#(?<=\s)(width|height)=["|\']?[\d%]+["|\']?\s?#i', '', $new_tag );
536
						} else {
537
							$resize_args = isset( $args['resize'] ) ? $args['resize'] : false;
538 View Code Duplication
							if ( false == $resize_args ) {
539
								$resize_args = ( ! $resize_args && isset( $args['fit'] ) )
540
									? $args['fit']
541
									: false;
542
							}
543 View Code Duplication
							if ( false == $resize_args ) {
544
								$resize_args = ( ! $resize_args && isset( $args['lb'] ) )
545
									? $args['lb']
546
									: false;
547
							}
548
549
							$resize_args = array_map( 'trim', explode( ',', $resize_args ) );
550
551
							// (?<=\s)         - Ensure width or height attribute is preceded by a space
552
							// (width=["|\']?) - Matches, and captures, width=, width=", or width='
553
							// [\d%]+          - Matches 1 or more digits
554
							// (["|\']?)       - Matches, and captures, ", ', or empty string
555
							// \s              - Ensures there's a space after the attribute
556
							$new_tag = preg_replace( '#(?<=\s)(width=["|\']?)[\d%]+(["|\']?)\s?#i', sprintf( '${1}%d${2} ', $resize_args[0] ), $new_tag );
557
							$new_tag = preg_replace( '#(?<=\s)(height=["|\']?)[\d%]+(["|\']?)\s?#i', sprintf( '${1}%d${2} ', $resize_args[1] ), $new_tag );
558
						}
559
560
						// Tag an image for dimension checking
561
						if ( ! self::is_amp_endpoint() ) {
562
							$new_tag = preg_replace( '#(\s?/)?>(\s*</a>)?$#i', ' data-recalc-dims="1"\1>\2', $new_tag );
563
						}
564
565
						// Replace original tag with modified version
566
						$content = str_replace( $tag, $new_tag, $content );
567
					}
568
				} elseif ( preg_match( '#^http(s)?://i[\d]{1}.wp.com#', $src ) && ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
569
					$new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . jetpack_photon_url( $images['link_url'][ $index ] ) . '\2', $tag, 1 );
570
571
					$content = str_replace( $tag, $new_tag, $content );
572
				}
573
			}
574
		}
575
576
		return $content;
577
	}
578
579
	public static function filter_the_galleries( $galleries ) {
580
		if ( empty( $galleries ) || ! is_array( $galleries ) ) {
581
			return $galleries;
582
		}
583
584
		// Pass by reference, so we can modify them in place.
585
		foreach ( $galleries as &$this_gallery ) {
586
			if ( is_string( $this_gallery ) ) {
587
				$this_gallery = self::filter_the_content( $this_gallery );
588
				// LEAVING COMMENTED OUT as for the moment it doesn't seem
589
				// necessary and I'm not sure how it would propagate through.
590
				// } elseif ( is_array( $this_gallery )
591
				// && ! empty( $this_gallery['src'] )
592
				// && ! empty( $this_gallery['type'] )
593
				// && in_array( $this_gallery['type'], array( 'rectangle', 'square', 'circle' ) ) ) {
594
				// $this_gallery['src'] = array_map( 'jetpack_photon_url', $this_gallery['src'] );
595
			}
596
		}
597
		unset( $this_gallery ); // break the reference.
598
599
		return $galleries;
600
	}
601
602
603
	/**
604
	 * Runs the image widget through photon.
605
	 *
606
	 * @param array $instance Image widget instance data.
607
	 * @return array
608
	 */
609
	public static function filter_the_image_widget( $instance ) {
610
		if ( Jetpack::is_module_active( 'photon' ) && ! $instance['attachment_id'] && $instance['url'] ) {
611
			jetpack_photon_url(
612
				$instance['url'],
613
				array(
614
					'w' => $instance['width'],
615
					'h' => $instance['height'],
616
				)
617
			);
618
		}
619
620
		return $instance;
621
	}
622
623
	/**
624
	 * * CORE IMAGE RETRIEVAL
625
	 **/
626
627
	/**
628
	 * Filter post thumbnail image retrieval, passing images through Photon
629
	 *
630
	 * @param string|bool  $image
631
	 * @param int          $attachment_id
632
	 * @param string|array $size
633
	 * @uses is_admin, apply_filters, wp_get_attachment_url, self::validate_image_url, this::image_sizes, jetpack_photon_url
634
	 * @filter image_downsize
635
	 * @return string|bool
636
	 */
637
	public function filter_image_downsize( $image, $attachment_id, $size ) {
638
		// Don't foul up the admin side of things, unless a plugin wants to.
639
		if ( is_admin() &&
640
			/**
641
			 * Provide plugins a way of running Photon for images in the WordPress Dashboard (wp-admin).
642
			 *
643
			 * 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.
644
			 *
645
			 * @module photon
646
			 *
647
			 * @since 4.8.0
648
			 *
649
			 * @param bool false Stop Photon from being run on the Dashboard. Default to false.
650
			 * @param array $args {
651
			 *   Array of image details.
652
			 *
653
			 *   @type $image Image URL.
654
			 *   @type $attachment_id Attachment ID of the image.
655
			 *   @type $size Image size. Can be a string (name of the image size, e.g. full) or an array of width and height.
656
			 * }
657
			 */
658
			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...
659
		) {
660
			return $image;
661
		}
662
663
		/**
664
		 * Provide plugins a way of preventing Photon from being applied to images retrieved from WordPress Core.
665
		 *
666
		 * @module photon
667
		 *
668
		 * @since 2.0.0
669
		 *
670
		 * @param bool false Stop Photon from being applied to the image. Default to false.
671
		 * @param array $args {
672
		 *   Array of image details.
673
		 *
674
		 *   @type $image Image URL.
675
		 *   @type $attachment_id Attachment ID of the image.
676
		 *   @type $size Image size. Can be a string (name of the image size, e.g. full) or an array of width and height.
677
		 * }
678
		 */
679
		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...
680
			return $image;
681
		}
682
683
		// Get the image URL and proceed with Photon-ification if successful
684
		$image_url = wp_get_attachment_url( $attachment_id );
685
686
		// Set this to true later when we know we have size meta.
687
		$has_size_meta = false;
688
689
		if ( $image_url ) {
690
			// Check if image URL should be used with Photon
691
			if ( ! self::validate_image_url( $image_url ) ) {
692
				return $image;
693
			}
694
695
			$intermediate = true; // For the fourth array item returned by the image_downsize filter.
696
697
			// If an image is requested with a size known to WordPress, use that size's settings with Photon.
698
			// WP states that `add_image_size()` should use a string for the name, but doesn't enforce that.
699
			// Due to differences in how Core and Photon check for the registered image size, we check both types.
700
			if ( ( is_string( $size ) || is_int( $size ) ) && array_key_exists( $size, self::image_sizes() ) ) {
701
				$image_args = self::image_sizes();
702
				$image_args = $image_args[ $size ];
703
704
				$photon_args = array();
705
706
				$image_meta = image_get_intermediate_size( $attachment_id, $size );
707
708
				// 'full' is a special case: We need consistent data regardless of the requested size.
709
				if ( 'full' == $size ) {
710
					$image_meta   = wp_get_attachment_metadata( $attachment_id );
711
					$intermediate = false;
712
				} elseif ( ! $image_meta ) {
713
					// If we still don't have any image meta at this point, it's probably from a custom thumbnail size
714
					// for an image that was uploaded before the custom image was added to the theme.  Try to determine the size manually.
715
					$image_meta = wp_get_attachment_metadata( $attachment_id );
716
717
					if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
718
						$image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $image_args['width'], $image_args['height'], $image_args['crop'] );
719
						if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
720
							$image_meta['width']  = $image_resized[6];
721
							$image_meta['height'] = $image_resized[7];
722
						}
723
					}
724
				}
725
726
				if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
727
					$image_args['width']  = $image_meta['width'];
728
					$image_args['height'] = $image_meta['height'];
729
730
					list( $image_args['width'], $image_args['height'] ) = image_constrain_size_for_editor( $image_args['width'], $image_args['height'], $size, 'display' );
731
					$has_size_meta                                      = true;
732
				}
733
734
				// Expose determined arguments to a filter before passing to Photon
735
				$transform = $image_args['crop'] ? 'resize' : 'fit';
736
737
				// Check specified image dimensions and account for possible zero values; photon fails to resize if a dimension is zero.
738
				if ( 0 == $image_args['width'] || 0 == $image_args['height'] ) {
739
					if ( 0 == $image_args['width'] && 0 < $image_args['height'] ) {
740
						$photon_args['h'] = $image_args['height'];
741
					} elseif ( 0 == $image_args['height'] && 0 < $image_args['width'] ) {
742
						$photon_args['w'] = $image_args['width'];
743
					}
744
				} else {
745
					if ( ( 'resize' === $transform ) && $image_meta = wp_get_attachment_metadata( $attachment_id ) ) {
746
						if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
747
							// Lets make sure that we don't upscale images since wp never upscales them as well
748
							$smaller_width  = ( ( $image_meta['width'] < $image_args['width'] ) ? $image_meta['width'] : $image_args['width'] );
749
							$smaller_height = ( ( $image_meta['height'] < $image_args['height'] ) ? $image_meta['height'] : $image_args['height'] );
750
751
							$photon_args[ $transform ] = $smaller_width . ',' . $smaller_height;
752
						}
753
					} else {
754
						$photon_args[ $transform ] = $image_args['width'] . ',' . $image_args['height'];
755
					}
756
				}
757
758
				/**
759
				 * Filter the Photon Arguments added to an image when going through Photon, when that image size is a string.
760
				 * Image size will be a string (e.g. "full", "medium") when it is known to WordPress.
761
				 *
762
				 * @module photon
763
				 *
764
				 * @since 2.0.0
765
				 *
766
				 * @param array $photon_args Array of Photon arguments.
767
				 * @param array $args {
768
				 *   Array of image details.
769
				 *
770
				 *   @type $image_args Array of Image arguments (width, height, crop).
771
				 *   @type $image_url Image URL.
772
				 *   @type $attachment_id Attachment ID of the image.
773
				 *   @type $size Image size. Can be a string (name of the image size, e.g. full) or an integer.
774
				 *   @type $transform Value can be resize or fit.
775
				 *                    @see https://developer.wordpress.com/docs/photon/api
776
				 * }
777
				 */
778
				$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...
779
780
				// Generate Photon URL
781
				$image = array(
782
					jetpack_photon_url( $image_url, $photon_args ),
783
					$has_size_meta ? $image_args['width'] : false,
784
					$has_size_meta ? $image_args['height'] : false,
785
					$intermediate,
786
				);
787
			} elseif ( is_array( $size ) ) {
788
				// Pull width and height values from the provided array, if possible
789
				$width  = isset( $size[0] ) ? (int) $size[0] : false;
790
				$height = isset( $size[1] ) ? (int) $size[1] : false;
791
792
				// Don't bother if necessary parameters aren't passed.
793
				if ( ! $width || ! $height ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $width of type integer|false is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Bug Best Practice introduced by
The expression $height of type integer|false is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
794
					return $image;
795
				}
796
797
				$image_meta = wp_get_attachment_metadata( $attachment_id );
798
				if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
799
					$image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $width, $height );
800
801
					if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
802
						$width  = $image_resized[6];
803
						$height = $image_resized[7];
804
					} else {
805
						$width  = $image_meta['width'];
806
						$height = $image_meta['height'];
807
					}
808
809
					$has_size_meta = true;
810
				}
811
812
				list( $width, $height ) = image_constrain_size_for_editor( $width, $height, $size );
813
814
				// Expose arguments to a filter before passing to Photon
815
				$photon_args = array(
816
					'fit' => $width . ',' . $height,
817
				);
818
819
				/**
820
				 * Filter the Photon Arguments added to an image when going through Photon,
821
				 * when the image size is an array of height and width values.
822
				 *
823
				 * @module photon
824
				 *
825
				 * @since 2.0.0
826
				 *
827
				 * @param array $photon_args Array of Photon arguments.
828
				 * @param array $args {
829
				 *   Array of image details.
830
				 *
831
				 *   @type $width Image width.
832
				 *   @type height Image height.
833
				 *   @type $image_url Image URL.
834
				 *   @type $attachment_id Attachment ID of the image.
835
				 * }
836
				 */
837
				$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...
838
839
				// Generate Photon URL
840
				$image = array(
841
					jetpack_photon_url( $image_url, $photon_args ),
842
					$has_size_meta ? $width : false,
843
					$has_size_meta ? $height : false,
844
					$intermediate,
845
				);
846
			}
847
		}
848
849
		return $image;
850
	}
851
852
	/**
853
	 * Filters an array of image `srcset` values, replacing each URL with its Photon equivalent.
854
	 *
855
	 * @since 3.8.0
856
	 * @since 4.0.4 Added automatically additional sizes beyond declared image sizes.
857
	 * @param array $sources An array of image urls and widths.
858
	 * @uses self::validate_image_url, jetpack_photon_url, Jetpack_Photon::parse_from_filename
859
	 * @uses Jetpack_Photon::strip_image_dimensions_maybe, Jetpack::get_content_width
860
	 * @return array An array of Photon image urls and widths.
861
	 */
862
	public function filter_srcset_array( $sources = array(), $size_array = array(), $image_src = array(), $image_meta = array(), $attachment_id = 0 ) {
863
		if ( ! is_array( $sources ) ) {
864
			return $sources;
865
		}
866
		$upload_dir = wp_get_upload_dir();
867
868
		foreach ( $sources as $i => $source ) {
869
			if ( ! self::validate_image_url( $source['url'] ) ) {
870
				continue;
871
			}
872
873
			/** This filter is already documented in class.photon.php */
874
			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...
875
				continue;
876
			}
877
878
			$url                    = $source['url'];
879
			list( $width, $height ) = self::parse_dimensions_from_filename( $url );
880
881
			// It's quicker to get the full size with the data we have already, if available
882
			if ( ! empty( $attachment_id ) ) {
883
				$url = wp_get_attachment_url( $attachment_id );
884
			} else {
885
				$url = self::strip_image_dimensions_maybe( $url );
886
			}
887
888
			$args = array();
889
			if ( 'w' === $source['descriptor'] ) {
890
				if ( $height && ( $source['value'] == $width ) ) {
891
					$args['resize'] = $width . ',' . $height;
892
				} else {
893
					$args['w'] = $source['value'];
894
				}
895
			}
896
897
			$sources[ $i ]['url'] = jetpack_photon_url( $url, $args );
898
		}
899
900
		/**
901
		 * At this point, $sources is the original srcset with Photonized URLs.
902
		 * Now, we're going to construct additional sizes based on multiples of the content_width.
903
		 * This will reduce the gap between the largest defined size and the original image.
904
		 */
905
906
		/**
907
		 * Filter the multiplier Photon uses to create new srcset items.
908
		 * Return false to short-circuit and bypass auto-generation.
909
		 *
910
		 * @module photon
911
		 *
912
		 * @since 4.0.4
913
		 *
914
		 * @param array|bool $multipliers Array of multipliers to use or false to bypass.
915
		 */
916
		$multipliers = apply_filters( 'jetpack_photon_srcset_multipliers', array( 2, 3 ) );
917
		$url         = trailingslashit( $upload_dir['baseurl'] ) . $image_meta['file'];
918
919
		if (
920
			/** Short-circuit via jetpack_photon_srcset_multipliers filter. */
921
			is_array( $multipliers )
922
			/** This filter is already documented in class.photon.php */
923
			&& ! 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...
924
			/** Verify basic meta is intact. */
925
			&& isset( $image_meta['width'] ) && isset( $image_meta['height'] ) && isset( $image_meta['file'] )
926
			/** Verify we have the requested width/height. */
927
			&& isset( $size_array[0] ) && isset( $size_array[1] )
928
			) {
929
930
			$fullwidth  = $image_meta['width'];
931
			$fullheight = $image_meta['height'];
932
			$reqwidth   = $size_array[0];
933
			$reqheight  = $size_array[1];
934
935
			$constrained_size = wp_constrain_dimensions( $fullwidth, $fullheight, $reqwidth );
936
			$expected_size    = array( $reqwidth, $reqheight );
937
938
			if ( abs( $constrained_size[0] - $expected_size[0] ) <= 1 && abs( $constrained_size[1] - $expected_size[1] ) <= 1 ) {
939
				$crop = 'soft';
940
				$base = Jetpack::get_content_width() ? Jetpack::get_content_width() : 1000; // Provide a default width if none set by the theme.
941
			} else {
942
				$crop = 'hard';
943
				$base = $reqwidth;
944
			}
945
946
			$currentwidths = array_keys( $sources );
947
			$newsources    = null;
948
949
			foreach ( $multipliers as $multiplier ) {
950
951
				$newwidth = $base * $multiplier;
952
				foreach ( $currentwidths as $currentwidth ) {
953
					// If a new width would be within 100 pixes of an existing one or larger than the full size image, skip.
954
					if ( abs( $currentwidth - $newwidth ) < 50 || ( $newwidth > $fullwidth ) ) {
955
						continue 2; // Back to the foreach ( $multipliers as $multiplier )
956
					}
957
				} // foreach ( $currentwidths as $currentwidth ){
958
959
				if ( 'soft' == $crop ) {
960
					$args = array(
961
						'w' => $newwidth,
962
					);
963
				} else { // hard crop, e.g. add_image_size( 'example', 200, 200, true );
964
					$args = array(
965
						'zoom'   => $multiplier,
966
						'resize' => $reqwidth . ',' . $reqheight,
967
					);
968
				}
969
970
				$newsources[ $newwidth ] = array(
971
					'url'        => jetpack_photon_url( $url, $args ),
972
					'descriptor' => 'w',
973
					'value'      => $newwidth,
974
				);
975
			} // foreach ( $multipliers as $multiplier )
976
			if ( is_array( $newsources ) ) {
977
				$sources = array_replace( $sources, $newsources );
978
			}
979
		} // if ( isset( $image_meta['width'] ) && isset( $image_meta['file'] ) )
980
981
		return $sources;
982
	}
983
984
	/**
985
	 * Filters an array of image `sizes` values, using $content_width instead of image's full size.
986
	 *
987
	 * @since 4.0.4
988
	 * @since 4.1.0 Returns early for images not within the_content.
989
	 * @param array $sizes An array of media query breakpoints.
990
	 * @param array $size  Width and height of the image
991
	 * @uses Jetpack::get_content_width
992
	 * @return array An array of media query breakpoints.
993
	 */
994
	public function filter_sizes( $sizes, $size ) {
995
		if ( ! doing_filter( 'the_content' ) ) {
996
			return $sizes;
997
		}
998
		$content_width = Jetpack::get_content_width();
999
		if ( ! $content_width ) {
1000
			$content_width = 1000;
1001
		}
1002
1003
		if ( ( is_array( $size ) && $size[0] < $content_width ) ) {
1004
			return $sizes;
1005
		}
1006
1007
		return sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $content_width );
1008
	}
1009
1010
	/**
1011
	 * * GENERAL FUNCTIONS
1012
	 **/
1013
1014
	/**
1015
	 * Ensure image URL is valid for Photon.
1016
	 * 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.
1017
	 *
1018
	 * @param string $url
1019
	 * @uses wp_parse_args
1020
	 * @return bool
1021
	 */
1022
	protected static function validate_image_url( $url ) {
1023
		$parsed_url = wp_parse_url( $url );
1024
1025
		if ( ! $parsed_url ) {
1026
			return false;
1027
		}
1028
1029
		// Parse URL and ensure needed keys exist, since the array returned by `wp_parse_url` only includes the URL components it finds.
1030
		$url_info = wp_parse_args(
1031
			$parsed_url,
1032
			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...
1033
				'scheme' => null,
1034
				'host'   => null,
1035
				'port'   => null,
1036
				'path'   => null,
1037
			)
1038
		);
1039
1040
		// Bail if scheme isn't http or port is set that isn't port 80
1041
		if (
1042
			( 'http' != $url_info['scheme'] || ! in_array( $url_info['port'], array( 80, null ) ) ) &&
1043
			/**
1044
			 * Allow Photon to fetch images that are served via HTTPS.
1045
			 *
1046
			 * @module photon
1047
			 *
1048
			 * @since 2.4.0
1049
			 * @since 3.9.0 Default to false.
1050
			 *
1051
			 * @param bool $reject_https Should Photon ignore images using the HTTPS scheme. Default to false.
1052
			 */
1053
			apply_filters( 'jetpack_photon_reject_https', false )
1054
		) {
1055
			return false;
1056
		}
1057
1058
		// Bail if no host is found
1059
		if ( is_null( $url_info['host'] ) ) {
1060
			return false;
1061
		}
1062
1063
		// Bail if the image alredy went through Photon
1064
		if ( preg_match( '#^i[\d]{1}.wp.com$#i', $url_info['host'] ) ) {
1065
			return false;
1066
		}
1067
1068
		// Bail if no path is found
1069
		if ( is_null( $url_info['path'] ) ) {
1070
			return false;
1071
		}
1072
1073
		// Ensure image extension is acceptable
1074
		if ( ! in_array( strtolower( pathinfo( $url_info['path'], PATHINFO_EXTENSION ) ), self::$extensions ) ) {
1075
			return false;
1076
		}
1077
1078
		// If we got this far, we should have an acceptable image URL
1079
		// But let folks filter to decline if they prefer.
1080
		/**
1081
		 * Overwrite the results of the validation steps an image goes through before to be considered valid to be used by Photon.
1082
		 *
1083
		 * @module photon
1084
		 *
1085
		 * @since 3.0.0
1086
		 *
1087
		 * @param bool true Is the image URL valid and can it be used by Photon. Default to true.
1088
		 * @param string $url Image URL.
1089
		 * @param array $parsed_url Array of information about the image.
1090
		 */
1091
		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...
1092
	}
1093
1094
	/**
1095
	 * Checks if the file exists before it passes the file to photon
1096
	 *
1097
	 * @param string $src The image URL
1098
	 * @return string
1099
	 **/
1100
	public static function strip_image_dimensions_maybe( $src ) {
1101
		$stripped_src = $src;
0 ignored issues
show
$stripped_src is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

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