PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 4.2.5
Jetpack – WP Security, Backup, Speed, & Growth v4.2.5
16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 All 501 releases
jetpack / class.photon.php
class.photon.php
948 lines 33.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 class Jetpack_Photon {
4 /**
5 * Class variables
6 */
7 // Oh look, a singleton
8 private static $__instance = null;
9
10 // Allowed extensions must match http://code.trac.wordpress.org/browser/photon/index.php#L31
11 protected static $extensions = array(
12 'gif',
13 'jpg',
14 'jpeg',
15 'png'
16 );
17
18 // Don't access this directly. Instead, use self::image_sizes() so it's actually populated with something.
19 protected static $image_sizes = null;
20
21 /**
22 * Singleton implementation
23 *
24 * @return object
25 */
26 public static function instance() {
27 if ( ! is_a( self::$__instance, 'Jetpack_Photon' ) ) {
28 self::$__instance = new Jetpack_Photon;
29 self::$__instance->setup();
30 }
31
32 return self::$__instance;
33 }
34
35 /**
36 * Silence is golden.
37 */
38 private function __construct() {}
39
40 /**
41 * Register actions and filters, but only if basic Photon functions are available.
42 * The basic functions are found in ./functions.photon.php.
43 *
44 * @uses add_action, add_filter
45 * @return null
46 */
47 private function setup() {
48 // Display warning if site is private
49 add_action( 'jetpack_activate_module_photon', array( $this, 'action_jetpack_activate_module_photon' ) );
50
51 if ( ! function_exists( 'jetpack_photon_url' ) )
52 return;
53
54 // Images in post content and galleries
55 add_filter( 'the_content', array( __CLASS__, 'filter_the_content' ), 999999 );
56 add_filter( 'get_post_galleries', array( __CLASS__, 'filter_the_galleries' ), 999999 );
57
58 // Core image retrieval
59 add_filter( 'image_downsize', array( $this, 'filter_image_downsize' ), 10, 3 );
60
61 // Responsive image srcset substitution
62 add_filter( 'wp_calculate_image_srcset', array( $this, 'filter_srcset_array' ), 10, 4 );
63 add_filter( 'wp_calculate_image_sizes', array( $this, 'filter_sizes' ), 1, 2 ); // Early so themes can still easily filter.
64
65 // Helpers for maniuplated images
66 add_action( 'wp_enqueue_scripts', array( $this, 'action_wp_enqueue_scripts' ), 9 );
67 }
68
69 /**
70 * Check if site is private and warn user if it is
71 *
72 * @uses Jetpack::check_privacy
73 * @action jetpack_activate_module_photon
74 * @return null
75 */
76 public function action_jetpack_activate_module_photon() {
77 Jetpack::check_privacy( __FILE__ );
78 }
79
80 /**
81 ** IN-CONTENT IMAGE MANIPULATION FUNCTIONS
82 **/
83
84 /**
85 * Match all images and any relevant <a> tags in a block of HTML.
86 *
87 * @param string $content Some HTML.
88 * @return array An array of $images matches, where $images[0] is
89 * an array of full matches, and the link_url, img_tag,
90 * and img_url keys are arrays of those matches.
91 */
92 public static function parse_images_from_html( $content ) {
93 $images = array();
94
95 if ( preg_match_all( '#(?:<a[^>]+?href=["|\'](?P<link_url>[^\s]+?)["|\'][^>]*?>\s*)?(?P<img_tag><img[^>]*?\s+?src=["|\'](?P<img_url>[^\s]+?)["|\'].*?>){1}(?:\s*</a>)?#is', $content, $images ) ) {
96 foreach ( $images as $key => $unused ) {
97 // Simplify the output as much as possible, mostly for confirming test results.
98 if ( is_numeric( $key ) && $key > 0 )
99 unset( $images[$key] );
100 }
101
102 return $images;
103 }
104
105 return array();
106 }
107
108 /**
109 * Try to determine height and width from strings WP appends to resized image filenames.
110 *
111 * @param string $src The image URL.
112 * @return array An array consisting of width and height.
113 */
114 public static function parse_dimensions_from_filename( $src ) {
115 $width_height_string = array();
116
117 if ( preg_match( '#-(\d+)x(\d+)\.(?:' . implode('|', self::$extensions ) . '){1}$#i', $src, $width_height_string ) ) {
118 $width = (int) $width_height_string[1];
119 $height = (int) $width_height_string[2];
120
121 if ( $width && $height )
122 return array( $width, $height );
123 }
124
125 return array( false, false );
126 }
127
128 /**
129 * Identify images in post content, and if images are local (uploaded to the current site), pass through Photon.
130 *
131 * @param string $content
132 * @uses self::validate_image_url, apply_filters, jetpack_photon_url, esc_url
133 * @filter the_content
134 * @return string
135 */
136 public static function filter_the_content( $content ) {
137 $images = Jetpack_Photon::parse_images_from_html( $content );
138
139 if ( ! empty( $images ) ) {
140 $content_width = Jetpack::get_content_width();
141
142 $image_sizes = self::image_sizes();
143 $upload_dir = wp_upload_dir();
144
145 foreach ( $images[0] as $index => $tag ) {
146 // Default to resize, though fit may be used in certain cases where a dimension cannot be ascertained
147 $transform = 'resize';
148
149 // Start with a clean attachment ID each time
150 $attachment_id = false;
151
152 // Flag if we need to munge a fullsize URL
153 $fullsize_url = false;
154
155 // Identify image source
156 $src = $src_orig = $images['img_url'][ $index ];
157
158 /**
159 * Allow specific images to be skipped by Photon.
160 *
161 * @module photon
162 *
163 * @since 2.0.3
164 *
165 * @param bool false Should Photon ignore this image. Default to false.
166 * @param string $src Image URL.
167 * @param string $tag Image Tag (Image HTML output).
168 */
169 if ( apply_filters( 'jetpack_photon_skip_image', false, $src, $tag ) )
170 continue;
171
172 // Support Automattic's Lazy Load plugin
173 // Can't modify $tag yet as we need unadulterated version later
174 if ( preg_match( '#data-lazy-src=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
175 $placeholder_src = $placeholder_src_orig = $src;
176 $src = $src_orig = $lazy_load_src[1];
177 } elseif ( preg_match( '#data-lazy-original=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
178 $placeholder_src = $placeholder_src_orig = $src;
179 $src = $src_orig = $lazy_load_src[1];
180 }
181
182 // Check if image URL should be used with Photon
183 if ( self::validate_image_url( $src ) ) {
184 // Find the width and height attributes
185 $width = $height = false;
186
187 // First, check the image tag
188 if ( preg_match( '#width=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $width_string ) )
189 $width = $width_string[1];
190
191 if ( preg_match( '#height=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $height_string ) )
192 $height = $height_string[1];
193
194 // Can't pass both a relative width and height, so unset the height in favor of not breaking the horizontal layout.
195 if ( false !== strpos( $width, '%' ) && false !== strpos( $height, '%' ) )
196 $width = $height = false;
197
198 // Detect WP registered image size from HTML class
199 if ( preg_match( '#class=["|\']?[^"\']*size-([^"\'\s]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $size ) ) {
200 $size = array_pop( $size );
201
202 if ( false === $width && false === $height && 'full' != $size && array_key_exists( $size, $image_sizes ) ) {
203 $width = (int) $image_sizes[ $size ]['width'];
204 $height = (int) $image_sizes[ $size ]['height'];
205 $transform = $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
206 }
207 } else {
208 unset( $size );
209 }
210
211 // WP Attachment ID, if uploaded to this site
212 if (
213 preg_match( '#class=["|\']?[^"\']*wp-image-([\d]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $attachment_id ) &&
214 (
215 0 === strpos( $src, $upload_dir['baseurl'] ) ||
216 /**
217 * Filter whether an image using an attachment ID in its class has to be uploaded to the local site to go through Photon.
218 *
219 * @module photon
220 *
221 * @since 2.0.3
222 *
223 * @param bool false Was the image uploaded to the local site. Default to false.
224 * @param array $args {
225 * Array of image details.
226 *
227 * @type $src Image URL.
228 * @type tag Image tag (Image HTML output).
229 * @type $images Array of information about the image.
230 * @type $index Image index.
231 * }
232 */
233 apply_filters( 'jetpack_photon_image_is_local', false, compact( 'src', 'tag', 'images', 'index' ) )
234 )
235 ) {
236 $attachment_id = intval( array_pop( $attachment_id ) );
237
238 if ( $attachment_id ) {
239 $attachment = get_post( $attachment_id );
240
241 // Basic check on returned post object
242 if ( is_object( $attachment ) && ! is_wp_error( $attachment ) && 'attachment' == $attachment->post_type ) {
243 $src_per_wp = wp_get_attachment_image_src( $attachment_id, isset( $size ) ? $size : 'full' );
244
245 if ( self::validate_image_url( $src_per_wp[0] ) ) {
246 $src = $src_per_wp[0];
247 $fullsize_url = true;
248
249 // Prevent image distortion if a detected dimension exceeds the image's natural dimensions
250 if ( ( false !== $width && $width > $src_per_wp[1] ) || ( false !== $height && $height > $src_per_wp[2] ) ) {
251 $width = false == $width ? false : min( $width, $src_per_wp[1] );
252 $height = false == $height ? false : min( $height, $src_per_wp[2] );
253 }
254
255 // If no width and height are found, max out at source image's natural dimensions
256 // Otherwise, respect registered image sizes' cropping setting
257 if ( false == $width && false == $height ) {
258 $width = $src_per_wp[1];
259 $height = $src_per_wp[2];
260 $transform = 'fit';
261 } elseif ( isset( $size ) && array_key_exists( $size, $image_sizes ) && isset( $image_sizes[ $size ]['crop'] ) ) {
262 $transform = (bool) $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
263 }
264 }
265 } else {
266 unset( $attachment_id );
267 unset( $attachment );
268 }
269 }
270 }
271
272 // If image tag lacks width and height arguments, try to determine from strings WP appends to resized image filenames.
273 if ( false === $width && false === $height ) {
274 list( $width, $height ) = Jetpack_Photon::parse_dimensions_from_filename( $src );
275 }
276
277 // If width is available, constrain to $content_width
278 if ( false !== $width && false === strpos( $width, '%' ) && is_numeric( $content_width ) ) {
279 if ( $width > $content_width && false !== $height && false === strpos( $height, '%' ) ) {
280 $height = round( ( $content_width * $height ) / $width );
281 $width = $content_width;
282 } elseif ( $width > $content_width ) {
283 $width = $content_width;
284 }
285 }
286
287 // Set a width if none is found and $content_width is available
288 // If width is set in this manner and height is available, use `fit` instead of `resize` to prevent skewing
289 if ( false === $width && is_numeric( $content_width ) ) {
290 $width = (int) $content_width;
291
292 if ( false !== $height )
293 $transform = 'fit';
294 }
295
296 // Detect if image source is for a custom-cropped thumbnail and prevent further URL manipulation.
297 if ( ! $fullsize_url && preg_match_all( '#-e[a-z0-9]+(-\d+x\d+)?\.(' . implode('|', self::$extensions ) . '){1}$#i', basename( $src ), $filename ) )
298 $fullsize_url = true;
299
300 // Build URL, first maybe removing WP's resized string so we pass the original image to Photon
301 if ( ! $fullsize_url ) {
302 $src = self::strip_image_dimensions_maybe( $src );
303 }
304
305 // Build array of Photon args and expose to filter before passing to Photon URL function
306 $args = array();
307
308 if ( false !== $width && false !== $height && false === strpos( $width, '%' ) && false === strpos( $height, '%' ) )
309 $args[ $transform ] = $width . ',' . $height;
310 elseif ( false !== $width )
311 $args['w'] = $width;
312 elseif ( false !== $height )
313 $args['h'] = $height;
314
315 /**
316 * Filter the array of Photon arguments added to an image when it goes through Photon.
317 * By default, only includes width and height values.
318 * @see https://developer.wordpress.com/docs/photon/api/
319 *
320 * @module photon
321 *
322 * @since 2.0.0
323 *
324 * @param array $args Array of Photon Arguments.
325 * @param array $args {
326 * Array of image details.
327 *
328 * @type $tag Image tag (Image HTML output).
329 * @type $src Image URL.
330 * @type $src_orig Original Image URL.
331 * @type $width Image width.
332 * @type $height Image height.
333 * }
334 */
335 $args = apply_filters( 'jetpack_photon_post_image_args', $args, compact( 'tag', 'src', 'src_orig', 'width', 'height' ) );
336
337 $photon_url = jetpack_photon_url( $src, $args );
338
339 // Modify image tag if Photon function provides a URL
340 // 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.
341 if ( $src != $photon_url ) {
342 $new_tag = $tag;
343
344 // If present, replace the link href with a Photoned URL for the full-size image.
345 if ( ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) )
346 $new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . jetpack_photon_url( $images['link_url'][ $index ] ) . '\2', $new_tag, 1 );
347
348 // Supplant the original source value with our Photon URL
349 $photon_url = esc_url( $photon_url );
350 $new_tag = str_replace( $src_orig, $photon_url, $new_tag );
351
352 // If Lazy Load is in use, pass placeholder image through Photon
353 if ( isset( $placeholder_src ) && self::validate_image_url( $placeholder_src ) ) {
354 $placeholder_src = jetpack_photon_url( $placeholder_src );
355
356 if ( $placeholder_src != $placeholder_src_orig )
357 $new_tag = str_replace( $placeholder_src_orig, esc_url( $placeholder_src ), $new_tag );
358
359 unset( $placeholder_src );
360 }
361
362 // Remove the width and height arguments from the tag to prevent distortion
363 $new_tag = preg_replace( '#(?<=\s)(width|height)=["|\']?[\d%]+["|\']?\s?#i', '', $new_tag );
364
365 // Tag an image for dimension checking
366 $new_tag = preg_replace( '#(\s?/)?>(\s*</a>)?$#i', ' data-recalc-dims="1"\1>\2', $new_tag );
367
368 // Replace original tag with modified version
369 $content = str_replace( $tag, $new_tag, $content );
370 }
371 } elseif ( preg_match( '#^http(s)?://i[\d]{1}.wp.com#', $src ) && ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
372 $new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . jetpack_photon_url( $images['link_url'][ $index ] ) . '\2', $tag, 1 );
373
374 $content = str_replace( $tag, $new_tag, $content );
375 }
376 }
377 }
378
379 return $content;
380 }
381
382 public static function filter_the_galleries( $galleries ) {
383 if ( empty( $galleries ) || ! is_array( $galleries ) ) {
384 return $galleries;
385 }
386
387 // Pass by reference, so we can modify them in place.
388 foreach ( $galleries as &$this_gallery ) {
389 if ( is_string( $this_gallery ) ) {
390 $this_gallery = self::filter_the_content( $this_gallery );
391 // LEAVING COMMENTED OUT as for the moment it doesn't seem
392 // necessary and I'm not sure how it would propagate through.
393 // } elseif ( is_array( $this_gallery )
394 // && ! empty( $this_gallery['src'] )
395 // && ! empty( $this_gallery['type'] )
396 // && in_array( $this_gallery['type'], array( 'rectangle', 'square', 'circle' ) ) ) {
397 // $this_gallery['src'] = array_map( 'jetpack_photon_url', $this_gallery['src'] );
398 }
399 }
400 unset( $this_gallery ); // break the reference.
401
402 return $galleries;
403 }
404
405 /**
406 ** CORE IMAGE RETRIEVAL
407 **/
408
409 /**
410 * Filter post thumbnail image retrieval, passing images through Photon
411 *
412 * @param string|bool $image
413 * @param int $attachment_id
414 * @param string|array $size
415 * @uses is_admin, apply_filters, wp_get_attachment_url, self::validate_image_url, this::image_sizes, jetpack_photon_url
416 * @filter image_downsize
417 * @return string|bool
418 */
419 public function filter_image_downsize( $image, $attachment_id, $size ) {
420 // Don't foul up the admin side of things, and provide plugins a way of preventing Photon from being applied to images.
421 if (
422 is_admin() ||
423 /**
424 * Provide plugins a way of preventing Photon from being applied to images retrieved from WordPress Core.
425 *
426 * @module photon
427 *
428 * @since 2.0.0
429 *
430 * @param bool false Stop Photon from being applied to the image. Default to false.
431 * @param array $args {
432 * Array of image details.
433 *
434 * @type $image Image URL.
435 * @type $attachment_id Attachment ID of the image.
436 * @type $size Image size. Can be a string (name of the image size, e.g. full) or an integer.
437 * }
438 */
439 apply_filters( 'jetpack_photon_override_image_downsize', false, compact( 'image', 'attachment_id', 'size' ) )
440 )
441 return $image;
442
443 // Get the image URL and proceed with Photon-ification if successful
444 $image_url = wp_get_attachment_url( $attachment_id );
445
446 // Set this to true later when we know we have size meta.
447 $has_size_meta = false;
448
449 if ( $image_url ) {
450 // Check if image URL should be used with Photon
451 if ( ! self::validate_image_url( $image_url ) )
452 return $image;
453
454 $intermediate = true; // For the fourth array item returned by the image_downsize filter.
455
456 // If an image is requested with a size known to WordPress, use that size's settings with Photon
457 if ( ( is_string( $size ) || is_int( $size ) ) && array_key_exists( $size, self::image_sizes() ) ) {
458 $image_args = self::image_sizes();
459 $image_args = $image_args[ $size ];
460
461 $photon_args = array();
462
463 $image_meta = image_get_intermediate_size( $attachment_id, $size );
464
465 // 'full' is a special case: We need consistent data regardless of the requested size.
466 if ( 'full' == $size ) {
467 $image_meta = wp_get_attachment_metadata( $attachment_id );
468 $intermediate = false;
469 } elseif ( ! $image_meta ) {
470 // If we still don't have any image meta at this point, it's probably from a custom thumbnail size
471 // for an image that was uploaded before the custom image was added to the theme. Try to determine the size manually.
472 $image_meta = wp_get_attachment_metadata( $attachment_id );
473
474 if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
475 $image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $image_args['width'], $image_args['height'], $image_args['crop'] );
476 if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
477 $image_meta['width'] = $image_resized[6];
478 $image_meta['height'] = $image_resized[7];
479 }
480 }
481 }
482
483 if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
484 $image_args['width'] = $image_meta['width'];
485 $image_args['height'] = $image_meta['height'];
486
487 list( $image_args['width'], $image_args['height'] ) = image_constrain_size_for_editor( $image_args['width'], $image_args['height'], $size, 'display' );
488 $has_size_meta = true;
489 }
490
491 // Expose determined arguments to a filter before passing to Photon
492 $transform = $image_args['crop'] ? 'resize' : 'fit';
493
494 // Check specified image dimensions and account for possible zero values; photon fails to resize if a dimension is zero.
495 if ( 0 == $image_args['width'] || 0 == $image_args['height'] ) {
496 if ( 0 == $image_args['width'] && 0 < $image_args['height'] ) {
497 $photon_args['h'] = $image_args['height'];
498 } elseif ( 0 == $image_args['height'] && 0 < $image_args['width'] ) {
499 $photon_args['w'] = $image_args['width'];
500 }
501 } else {
502 if ( ( 'resize' === $transform ) && $image_meta = wp_get_attachment_metadata( $attachment_id ) ) {
503 if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
504 // Lets make sure that we don't upscale images since wp never upscales them as well
505 $smaller_width = ( ( $image_meta['width'] < $image_args['width'] ) ? $image_meta['width'] : $image_args['width'] );
506 $smaller_height = ( ( $image_meta['height'] < $image_args['height'] ) ? $image_meta['height'] : $image_args['height'] );
507
508 $photon_args[ $transform ] = $smaller_width . ',' . $smaller_height;
509 }
510 } else {
511 $photon_args[ $transform ] = $image_args['width'] . ',' . $image_args['height'];
512 }
513
514 }
515
516
517 /**
518 * Filter the Photon Arguments added to an image when going through Photon, when that image size is a string.
519 * Image size will be a string (e.g. "full", "medium") when it is known to WordPress.
520 *
521 * @module photon
522 *
523 * @since 2.0.0
524 *
525 * @param array $photon_args Array of Photon arguments.
526 * @param array $args {
527 * Array of image details.
528 *
529 * @type $image_args Array of Image arguments (width, height, crop).
530 * @type $image_url Image URL.
531 * @type $attachment_id Attachment ID of the image.
532 * @type $size Image size. Can be a string (name of the image size, e.g. full) or an integer.
533 * @type $transform Value can be resize or fit.
534 * @see https://developer.wordpress.com/docs/photon/api
535 * }
536 */
537 $photon_args = apply_filters( 'jetpack_photon_image_downsize_string', $photon_args, compact( 'image_args', 'image_url', 'attachment_id', 'size', 'transform' ) );
538
539 // Generate Photon URL
540 $image = array(
541 jetpack_photon_url( $image_url, $photon_args ),
542 $has_size_meta ? $image_args['width'] : false,
543 $has_size_meta ? $image_args['height'] : false,
544 $intermediate
545 );
546 } elseif ( is_array( $size ) ) {
547 // Pull width and height values from the provided array, if possible
548 $width = isset( $size[0] ) ? (int) $size[0] : false;
549 $height = isset( $size[1] ) ? (int) $size[1] : false;
550
551 // Don't bother if necessary parameters aren't passed.
552 if ( ! $width || ! $height ) {
553 return $image;
554 }
555
556 $image_meta = wp_get_attachment_metadata( $attachment_id );
557 if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
558 $image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $width, $height );
559
560 if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
561 $width = $image_resized[6];
562 $height = $image_resized[7];
563 } else {
564 $width = $image_meta['width'];
565 $height = $image_meta['height'];
566 }
567
568 $has_size_meta = true;
569 }
570
571 list( $width, $height ) = image_constrain_size_for_editor( $width, $height, $size );
572
573 // Expose arguments to a filter before passing to Photon
574 $photon_args = array(
575 'fit' => $width . ',' . $height
576 );
577
578 /**
579 * Filter the Photon Arguments added to an image when going through Photon,
580 * when the image size is an array of height and width values.
581 *
582 * @module photon
583 *
584 * @since 2.0.0
585 *
586 * @param array $photon_args Array of Photon arguments.
587 * @param array $args {
588 * Array of image details.
589 *
590 * @type $width Image width.
591 * @type height Image height.
592 * @type $image_url Image URL.
593 * @type $attachment_id Attachment ID of the image.
594 * }
595 */
596 $photon_args = apply_filters( 'jetpack_photon_image_downsize_array', $photon_args, compact( 'width', 'height', 'image_url', 'attachment_id' ) );
597
598 // Generate Photon URL
599 $image = array(
600 jetpack_photon_url( $image_url, $photon_args ),
601 $has_size_meta ? $width : false,
602 $has_size_meta ? $height : false,
603 $intermediate
604 );
605 }
606 }
607
608 return $image;
609 }
610
611 /**
612 * Filters an array of image `srcset` values, replacing each URL with its Photon equivalent.
613 *
614 * @since 3.8.0
615 * @since 4.0.4 Added automatically additional sizes beyond declared image sizes.
616 * @param array $sources An array of image urls and widths.
617 * @uses self::validate_image_url, jetpack_photon_url, Jetpack_Photon::parse_from_filename
618 * @uses Jetpack_Photon::strip_image_dimensions_maybe, Jetpack::get_content_width
619 * @return array An array of Photon image urls and widths.
620 */
621 public function filter_srcset_array( $sources, $size_array, $image_src, $image_meta ) {
622 $upload_dir = wp_upload_dir();
623
624 foreach ( $sources as $i => $source ) {
625 if ( ! self::validate_image_url( $source['url'] ) ) {
626 continue;
627 }
628
629 $url = $source['url'];
630 list( $width, $height ) = Jetpack_Photon::parse_dimensions_from_filename( $url );
631
632 // It's quicker to get the full size with the data we have already, if available
633 if ( isset( $image_meta['file'] ) ) {
634 $url = trailingslashit( $upload_dir['baseurl'] ) . $image_meta['file'];
635 } else {
636 $url = Jetpack_Photon::strip_image_dimensions_maybe( $url );
637 }
638
639 $args = array();
640 if ( 'w' === $source['descriptor'] ) {
641 if ( $height && ( $source['value'] == $width ) ) {
642 $args['resize'] = $width . ',' . $height;
643 } else {
644 $args['w'] = $source['value'];
645 }
646
647 }
648
649 $sources[ $i ]['url'] = jetpack_photon_url( $url, $args );
650 }
651
652 /**
653 * At this point, $sources is the original srcset with Photonized URLs.
654 * Now, we're going to construct additional sizes based on multiples of the content_width.
655 * This will reduce the gap between the largest defined size and the original image.
656 */
657
658 /**
659 * Filter the multiplier Photon uses to create new srcset items.
660 * Return false to short-circuit and bypass auto-generation.
661 *
662 * @module photon
663 *
664 * @since 4.0.4
665 *
666 * @param array|bool $multipliers Array of multipliers to use or false to bypass.
667 */
668 $multipliers = apply_filters( 'jetpack_photon_srcset_multipliers', array( 2, 3 ) );
669
670 if ( is_array( $multipliers ) // Short-circuit via jetpack_photon_srcset_multipliers filter.
671 && isset( $image_meta['width'] ) && isset( $image_meta['height'] ) && isset( $image_meta['file'] ) // Verify basic meta is intact.
672 && isset( $size_array[0] ) && isset( $size_array[1] ) // Verify we have the requested width/height.
673 ) {
674
675 $url = trailingslashit( $upload_dir['baseurl'] ) . $image_meta['file'];
676 $fullwidth = $image_meta['width'];
677 $fullheight = $image_meta['height'];
678 $reqwidth = $size_array[0];
679 $reqheight = $size_array[1];
680
681 $constrained_size = wp_constrain_dimensions( $fullwidth, $fullheight, $reqwidth );
682 $expected_size = array( $reqwidth, $reqheight );
683
684 if ( abs( $constrained_size[0] - $expected_size[0] ) <= 1 && abs( $constrained_size[1] - $expected_size[1] ) <= 1 ) {
685 $crop = 'soft';
686 $base = Jetpack::get_content_width() ? Jetpack::get_content_width() : 1000; // Provide a default width if none set by the theme.
687 }
688 else {
689 $crop = 'hard';
690 $base = $reqwidth;
691 }
692
693
694 $currentwidths = array_keys( $sources );
695 $newsources = null;
696
697 foreach ( $multipliers as $multiplier ) {
698 $newwidth = $base * $multiplier;
699 foreach ( $currentwidths as $currentwidth ){
700 // If a new width would be within 100 pixes of an existing one or larger than the full size image, skip.
701 if ( abs( $currentwidth - $newwidth ) < 50 || ( $newwidth > $fullwidth ) ) {
702 continue 2; // Back to the foreach ( $multipliers as $multiplier )
703 }
704 } // foreach ( $currentwidths as $currentwidth ){
705
706 if ( 'soft' == $crop ) {
707 $args = array(
708 'w' => $newwidth,
709 );
710 }
711 else { // hard crop, e.g. add_image_size( 'example', 200, 200, true );
712 $args = array(
713 'zoom' => $multiplier,
714 'resize' => $reqwidth . ',' . $reqheight,
715 );
716 }
717
718 $newsources[ $newwidth ] = array(
719 'url' => jetpack_photon_url( $url, $args ),
720 'descriptor' => 'w',
721 'value' => $newwidth,
722 );
723 } // foreach ( $multipliers as $multiplier )
724 if ( is_array( $newsources ) ) {
725 $sources = array_merge( $sources, $newsources );
726 }
727 } // if ( isset( $image_meta['width'] ) && isset( $image_meta['file'] ) )
728
729 return $sources;
730 }
731
732 /**
733 * Filters an array of image `sizes` values, using $content_width instead of image's full size.
734 *
735 * @since 4.0.4
736 * @since 4.1.0 Returns early for images not within the_content.
737 * @param array $sizes An array of media query breakpoints.
738 * @param array $size Width and height of the image
739 * @uses Jetpack::get_content_width
740 * @return array An array of media query breakpoints.
741 */
742 public function filter_sizes( $sizes, $size ) {
743 if ( ! doing_filter( 'the_content' ) ){
744 return $sizes;
745 }
746 $content_width = Jetpack::get_content_width();
747 if ( ! $content_width ) {
748 $content_width = 1000;
749 }
750
751 if ( ( is_array( $size ) && $size[0] < $content_width ) ) {
752 return $sizes;
753 }
754
755 return sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $content_width );
756 }
757
758 /**
759 ** GENERAL FUNCTIONS
760 **/
761
762 /**
763 * Ensure image URL is valid for Photon.
764 * 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.
765 *
766 * @param string $url
767 * @uses wp_parse_args
768 * @return bool
769 */
770 protected static function validate_image_url( $url ) {
771 $parsed_url = @parse_url( $url );
772
773 if ( ! $parsed_url )
774 return false;
775
776 // Parse URL and ensure needed keys exist, since the array returned by `parse_url` only includes the URL components it finds.
777 $url_info = wp_parse_args( $parsed_url, array(
778 'scheme' => null,
779 'host' => null,
780 'port' => null,
781 'path' => null
782 ) );
783
784 // Bail if scheme isn't http or port is set that isn't port 80
785 if (
786 ( 'http' != $url_info['scheme'] || ! in_array( $url_info['port'], array( 80, null ) ) ) &&
787 /**
788 * Allow Photon to fetch images that are served via HTTPS.
789 *
790 * @module photon
791 *
792 * @since 2.4.0
793 * @since 3.9.0 Default to false.
794 *
795 * @param bool $reject_https Should Photon ignore images using the HTTPS scheme. Default to false.
796 */
797 apply_filters( 'jetpack_photon_reject_https', false )
798 ) {
799 return false;
800 }
801
802 // Bail if no host is found
803 if ( is_null( $url_info['host'] ) )
804 return false;
805
806 // Bail if the image alredy went through Photon
807 if ( preg_match( '#^i[\d]{1}.wp.com$#i', $url_info['host'] ) )
808 return false;
809
810 // Bail if no path is found
811 if ( is_null( $url_info['path'] ) )
812 return false;
813
814 // Ensure image extension is acceptable
815 if ( ! in_array( strtolower( pathinfo( $url_info['path'], PATHINFO_EXTENSION ) ), self::$extensions ) )
816 return false;
817
818 // If we got this far, we should have an acceptable image URL
819 // But let folks filter to decline if they prefer.
820 /**
821 * Overwrite the results of the validation steps an image goes through before to be considered valid to be used by Photon.
822 *
823 * @module photon
824 *
825 * @since 3.0.0
826 *
827 * @param bool true Is the image URL valid and can it be used by Photon. Default to true.
828 * @param string $url Image URL.
829 * @param array $parsed_url Array of information about the image.
830 */
831 return apply_filters( 'photon_validate_image_url', true, $url, $parsed_url );
832 }
833
834 /**
835 * Checks if the file exists before it passes the file to photon
836 *
837 * @param string $src The image URL
838 * @return string
839 **/
840 protected static function strip_image_dimensions_maybe( $src ){
841 $stripped_src = $src;
842
843 // Build URL, first removing WP's resized string so we pass the original image to Photon
844 if ( preg_match( '#(-\d+x\d+)\.(' . implode('|', self::$extensions ) . '){1}$#i', $src, $src_parts ) ) {
845 $stripped_src = str_replace( $src_parts[1], '', $src );
846 $upload_dir = wp_upload_dir();
847
848 // Extracts the file path to the image minus the base url
849 $file_path = substr( $stripped_src, strlen ( $upload_dir['baseurl'] ) );
850
851 if( file_exists( $upload_dir["basedir"] . $file_path ) )
852 $src = $stripped_src;
853 }
854
855 return $src;
856 }
857
858 /**
859 * Provide an array of available image sizes and corresponding dimensions.
860 * Similar to get_intermediate_image_sizes() except that it includes image sizes' dimensions, not just their names.
861 *
862 * @global $wp_additional_image_sizes
863 * @uses get_option
864 * @return array
865 */
866 protected static function image_sizes() {
867 if ( null == self::$image_sizes ) {
868 global $_wp_additional_image_sizes;
869
870 // Populate an array matching the data structure of $_wp_additional_image_sizes so we have a consistent structure for image sizes
871 $images = array(
872 'thumb' => array(
873 'width' => intval( get_option( 'thumbnail_size_w' ) ),
874 'height' => intval( get_option( 'thumbnail_size_h' ) ),
875 'crop' => (bool) get_option( 'thumbnail_crop' )
876 ),
877 'medium' => array(
878 'width' => intval( get_option( 'medium_size_w' ) ),
879 'height' => intval( get_option( 'medium_size_h' ) ),
880 'crop' => false
881 ),
882 'large' => array(
883 'width' => intval( get_option( 'large_size_w' ) ),
884 'height' => intval( get_option( 'large_size_h' ) ),
885 'crop' => false
886 ),
887 'full' => array(
888 'width' => null,
889 'height' => null,
890 'crop' => false
891 )
892 );
893
894 // Compatibility mapping as found in wp-includes/media.php
895 $images['thumbnail'] = $images['thumb'];
896
897 // Update class variable, merging in $_wp_additional_image_sizes if any are set
898 if ( is_array( $_wp_additional_image_sizes ) && ! empty( $_wp_additional_image_sizes ) )
899 self::$image_sizes = array_merge( $images, $_wp_additional_image_sizes );
900 else
901 self::$image_sizes = $images;
902 }
903
904 return is_array( self::$image_sizes ) ? self::$image_sizes : array();
905 }
906
907 /**
908 * Pass og:image URLs through Photon
909 *
910 * @param array $tags
911 * @param array $parameters
912 * @uses jetpack_photon_url
913 * @return array
914 */
915 function filter_open_graph_tags( $tags, $parameters ) {
916 if ( empty( $tags['og:image'] ) ) {
917 return $tags;
918 }
919
920 $photon_args = array(
921 'fit' => sprintf( '%d,%d', 2 * $parameters['image_width'], 2 * $parameters['image_height'] ),
922 );
923
924 if ( is_array( $tags['og:image'] ) ) {
925 $images = array();
926 foreach ( $tags['og:image'] as $image ) {
927 $images[] = jetpack_photon_url( $image, $photon_args );
928 }
929 $tags['og:image'] = $images;
930 } else {
931 $tags['og:image'] = jetpack_photon_url( $tags['og:image'], $photon_args );
932 }
933
934 return $tags;
935 }
936
937 /**
938 * Enqueue Photon helper script
939 *
940 * @uses wp_enqueue_script, plugins_url
941 * @action wp_enqueue_script
942 * @return null
943 */
944 public function action_wp_enqueue_scripts() {
945 wp_enqueue_script( 'jetpack-photon', plugins_url( 'modules/photon/photon.js', JETPACK__PLUGIN_FILE ), array( 'jquery' ), 20130122, true );
946 }
947 }
948