PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 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 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-image-cdn/src/class-image-cdn.php +397 -286 12.2.3 → 16.3 View file →
@@ -1,5 +1,5 @@
1 -<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
1 +<?php
2 2 /**
3 3 * Class for photon functionality.
4 4 *
5 5 * @package automattic/jetpack-image-cdn
@@ -6,16 +6,14 @@
6 6 */
7 7
8 8 namespace Automattic\Jetpack\Image_CDN;
9 9
10 -use Automattic\Jetpack\Assets;
11 -
12 10 /**
13 11 * Class Image_CDN
14 12 */
15 13 final class Image_CDN {
16 14
17 - const PACKAGE_VERSION = '0.2.2';
15 + const PACKAGE_VERSION = '0.8.1';
18 16
19 17 /**
20 18 * Singleton.
21 19 *
@@ -32,9 +30,11 @@
32 30 'gif',
33 31 'jpg',
34 32 'jpeg',
35 33 'png',
36 - 'webp', // Jetpack assumes Photon_OpenCV backend class is being used on the server. See link in docblock.
34 + // Jetpack assumes Photon_OpenCV backend class is being used on the server. See link in docblock.
35 + 'webp', // Photon_OpenCV supports webp with libwebp-*, getimageformat() returns webp
36 + 'heic', // Photon_OpenCV supports webp with libheif-*, getimageformat() returns jpeg so does not match docblock
37 37 );
38 38
39 39 /**
40 40 * Image sizes.
@@ -59,9 +59,9 @@
59 59 *
60 60 * @return object
61 61 */
62 62 public static function instance() {
63 - if ( ! is_a( self::$instance, self::class ) ) {
63 + if ( ! self::$instance instanceof self ) {
64 64 self::$instance = new self();
65 65 self::$instance->setup();
66 66 self::$is_enabled = true;
67 67 }
@@ -109,13 +109,10 @@
109 109 add_filter( 'rest_request_after_callbacks', array( $this, 'cleanup_rest_photon_image_downsize' ) );
110 110
111 111 // Responsive image srcset substitution.
112 112 add_filter( 'wp_calculate_image_srcset', array( $this, 'filter_srcset_array' ), 10, 5 );
113 - add_filter( 'wp_calculate_image_sizes', array( $this, 'filter_sizes' ), 1, 2 ); // Early so themes can still easily filter.
113 + add_filter( 'wp_calculate_image_sizes', array( $this, 'filter_sizes' ), 1, 3 ); // Early so themes can still easily filter.
114 114
115 - // Helpers for maniuplated images.
116 - add_action( 'wp_enqueue_scripts', array( $this, 'action_wp_enqueue_scripts' ), 9 );
117 -
118 115 /**
119 116 * Allow Photon to disable uploaded images resizing and use its own resize capabilities instead.
120 117 *
121 118 * @module photon
@@ -214,9 +211,9 @@
214 211 */
215 212 public static function filter_photon_noresize_thumbnail_urls( $sizes ) {
216 213 foreach ( $sizes as $size => $url ) {
217 214 $parts = explode( '?', $url );
218 - $arguments = isset( $parts[1] ) ? $parts[1] : array();
215 + $arguments = $parts[1] ?? array();
219 216
220 217 $sizes[ $size ] = Image_CDN_Core::cdn_url( $url, wp_parse_args( $arguments ) );
221 218 }
222 219
@@ -236,12 +233,12 @@
236 233 if ( empty( $data ) ) {
237 234 return $data;
238 235 }
239 236 $sizes_already_exist = (
240 - true === is_array( $data )
241 - && true === array_key_exists( 'sizes', $data )
242 - && true === is_array( $data['sizes'] )
243 - && false === empty( $data['sizes'] )
237 + is_array( $data )
238 + && array_key_exists( 'sizes', $data )
239 + && is_array( $data['sizes'] )
240 + && ! empty( $data['sizes'] )
244 241 );
245 242 if ( $sizes_already_exist ) {
246 243 return $data;
247 244 }
@@ -312,9 +309,9 @@
312 309 */
313 310 public static function parse_dimensions_from_filename( $src ) {
314 311 $width_height_string = array();
315 312
316 - if ( preg_match( '#-(\d+)x(\d+)\.(?:' . implode( '|', self::$extensions ) . '){1}$#i', $src, $width_height_string ) ) {
313 + if ( preg_match( '#-(\d+)x(\d+)\.(?:' . implode( '|', self::$extensions ) . '){1}(?:\?.*)?$#i', $src, $width_height_string ) ) {
317 314 $width = (int) $width_height_string[1];
318 315 $height = (int) $width_height_string[2];
319 316
320 317 if ( $width && $height ) {
@@ -327,307 +324,385 @@
327 324
328 325 /**
329 326 * Identify images in post content, and if images are local (uploaded to the current site), pass through Photon.
330 327 *
331 - * @param string $content The content.
328 + * @param string|mixed $content The content; should be a string but will convert to an empty string if not.
332 329 *
333 330 * @uses self::validate_image_url, apply_filters, Image_CDN_Core::cdn_url, esc_url
334 331 * @filter the_content
335 332 *
336 - * @return string
333 + * @return string The content.
337 334 */
338 335 public static function filter_the_content( $content ) {
339 - $images = self::parse_images_from_html( $content );
336 + // Early return if content is empty or not a string.
337 + if ( ! is_string( $content ) || '' === $content ) {
338 + return '';
339 + }
340 340
341 - if ( ! empty( $images ) ) {
342 - $content_width = Image_CDN_Core::get_jetpack_content_width();
341 + static $image_tags = array( 'IMG', 'AMP-IMG', 'AMP-ANIM' );
342 + $content_width = null;
343 + $image_sizes = null;
344 + $upload_dir = null;
345 + $processor = new \WP_HTML_Tag_Processor( $content );
346 + $nearest_preceding_href = null;
343 347
344 - $image_sizes = self::image_sizes();
348 + // Visit every image-containing tag in the document.
349 + while ( $processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
350 + /*
351 + * When an image is wrapped by an A element, the goal is to modify
352 + * both elements. Thus it's important to track links that may be
353 + * opened before reaching any image. In normative HTML this detection
354 + * is reliable enough, but it could be confused in cases where other
355 + * tags implicitly close the A. If this additional reliability is
356 + * required, replace the Tag Processor with the HTML Processor.
357 + */
358 + if ( 'A' === $processor->get_tag() ) {
359 + // If this is a closing tag the attribute will be `null`.
360 + $nearest_preceding_href = $processor->get_attribute( 'href' );
361 + $processor->set_bookmark( 'link' );
362 + continue;
363 + }
345 364
346 - $upload_dir = wp_get_upload_dir();
365 + // Identify image source.
366 + $src_orig = $processor->get_attribute( 'src' );
367 + $src = $src_orig;
368 + $placeholder_src = null;
369 + $placeholder_src_orig = null;
347 370
348 - foreach ( $images[0] as $index => $tag ) {
349 - // Default to resize, though fit may be used in certain cases where a dimension cannot be ascertained.
350 - $transform = 'resize';
371 + /*
372 + * Only examine tags that are considered an image,
373 + * with a valid src attribute.
374 + * If encountering a closing tag then this is not the image being sought.
375 + */
376 + if (
377 + $processor->is_tag_closer()
378 + || ! in_array( $processor->get_tag(), $image_tags, true )
379 + || ! is_string( $src )
380 + || $src === ''
381 + ) {
382 + continue;
383 + }
351 384
352 - // Start with a clean attachment ID each time.
353 - $attachment_id = false;
385 + $processor->set_bookmark( 'image' );
354 386
355 - // Flag if we need to munge a fullsize URL.
356 - $fullsize_url = false;
387 + /*
388 + * At this point a target image has been found. Initialize the
389 + * shared data and then process each image as it appears.
390 + */
391 + if ( null === $content_width ) {
392 + $content_width = Image_CDN_Core::get_jetpack_content_width();
393 + $image_sizes = self::image_sizes();
394 + $upload_dir = wp_get_upload_dir();
395 + }
357 396
358 - // Identify image source.
359 - $src_orig = $images['img_url'][ $index ];
360 - $src = $src_orig;
397 + /*
398 + * To preserve legacy behaviors for filtering by third-party plugins,
399 + * create a normalized HTML string representing the tag. This will
400 + * present all attributes as double-quoted attributes and include at
401 + * most one copy of each attribute, escaping all values appropriately.
402 + */
403 + $tag_name = strtolower( (string) $processor->get_tag() );
404 + $tag = new \WP_HTML_Tag_Processor( "<{$tag_name}>" );
405 + $tag->next_tag();
406 + foreach ( $processor->get_attribute_names_with_prefix( '' ) ?? array() as $name ) {
407 + $tag->set_attribute( $name, $processor->get_attribute( $name ) );
408 + }
409 + $tag = $tag->get_updated_html();
361 410
362 - /**
363 - * Allow specific images to be skipped by Photon.
364 - *
365 - * @module photon
366 - *
367 - * @since 2.0.3
368 - *
369 - * @param bool false Should Photon ignore this image. Default to false.
370 - * @param string $src Image URL.
371 - * @param string $tag Image Tag (Image HTML output).
372 - */
373 - if ( apply_filters( 'jetpack_photon_skip_image', false, $src, $tag ) ) {
374 - continue;
375 - }
411 + // Default to resize, though fit may be used in certain cases where a dimension cannot be ascertained.
412 + $transform = 'resize';
376 413
377 - // Support Automattic's Lazy Load plugin.
378 - // Can't modify $tag yet as we need unadulterated version later.
379 - if ( preg_match( '#data-lazy-src=["\'](.+?)["\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
380 - $placeholder_src_orig = $src;
381 - $placeholder_src = $placeholder_src_orig;
382 - $src_orig = $lazy_load_src[1];
383 - $src = $src_orig;
384 - } elseif ( preg_match( '#data-lazy-original=["\'](.+?)["\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
385 - $placeholder_src_orig = $src;
386 - $placeholder_src = $placeholder_src_orig;
387 - $src_orig = $lazy_load_src[1];
388 - $src = $src_orig;
389 - }
414 + // Flag if we need to munge a fullsize URL.
415 + $fullsize_url = false;
390 416
391 - // Check if image URL should be used with Photon.
392 - if ( self::validate_image_url( $src ) ) {
393 - // Find the width and height attributes.
394 - $width = false;
395 - $height = false;
417 + /**
418 + * Allow specific images to be skipped by Photon.
419 + *
420 + * @module photon
421 + *
422 + * @since 2.0.3
423 + *
424 + * @param bool false Should Photon ignore this image. Default to false.
425 + * @param string $src Image URL.
426 + * @param string|array|null $tag Image Tag (Image HTML output) or array of image details for srcset.
427 + */
428 + if ( apply_filters( 'jetpack_photon_skip_image', false, $src, $tag ) ) {
429 + continue;
430 + }
396 431
397 - // First, check the image tag. Note we only check for pixel sizes now; HTML4 percentages have never been correctly
398 - // supported, so we stopped pretending to support them in JP 9.1.0.
399 - if ( preg_match( '#[\s"\']width=["\']?([\d%]+)["\']?#i', $images['img_tag'][ $index ], $width_string ) ) {
400 - $width = false === strpos( $width_string[1], '%' ) ? $width_string[1] : false;
401 - }
432 + $data_lazy_src = $processor->get_attribute( 'data-lazy-src' );
433 + $data_lazy_original = $processor->get_attribute( 'data-lazy-original' );
402 434
403 - if ( preg_match( '#[\s"\']height=["\']?([\d%]+)["\']?#i', $images['img_tag'][ $index ], $height_string ) ) {
404 - $height = false === strpos( $height_string[1], '%' ) ? $height_string[1] : false;
405 - }
435 + $source_type = 'src';
436 + $chosen_data_src = null;
406 437
407 - // Detect WP registered image size from HTML class.
408 - if ( preg_match( '#class=["\']?[^"\']*size-([^"\'\s]+)[^"\']*["\']?#i', $images['img_tag'][ $index ], $size ) ) {
409 - $size = array_pop( $size );
438 + // Prefer a URL from the `data-lazy-src` attribute.
439 + if ( null === $chosen_data_src && is_string( $data_lazy_src ) && ! empty( $data_lazy_src ) ) {
440 + $source_type = 'data-lazy-src';
441 + $chosen_data_src = $data_lazy_src;
442 + }
410 443
411 - if ( false === $width && false === $height && 'full' !== $size && array_key_exists( $size, $image_sizes ) ) {
412 - $width = (int) $image_sizes[ $size ]['width'];
413 - $height = (int) $image_sizes[ $size ]['height'];
414 - $transform = $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
415 - }
416 - } else {
417 - unset( $size );
418 - }
444 + // Fall back to a URL from the `data-lazy-original` attribute.
445 + if ( null === $chosen_data_src && is_string( $data_lazy_original ) && ! empty( $data_lazy_original ) ) {
446 + $source_type = 'data-lazy-original';
447 + $chosen_data_src = $data_lazy_original;
448 + }
419 449
420 - // WP Attachment ID, if uploaded to this site.
421 - if (
422 - preg_match( '#class=["\']?[^"\']*wp-image-([\d]+)[^"\']*["\']?#i', $images['img_tag'][ $index ], $attachment_id ) &&
423 - 0 === strpos( $src, $upload_dir['baseurl'] ) &&
424 - /**
425 - * Filter whether an image using an attachment ID in its class has to be uploaded to the local site to go through Photon.
426 - *
427 - * @module photon
428 - *
429 - * @since 2.0.3
430 - *
431 - * @param bool false Was the image uploaded to the local site. Default to false.
432 - * @param array $args {
433 - * Array of image details.
434 - *
435 - * @type $src Image URL.
436 - * @type tag Image tag (Image HTML output).
437 - * @type $images Array of information about the image.
438 - * @type $index Image index.
439 - * }
440 - */
441 - apply_filters( 'jetpack_photon_image_is_local', false, compact( 'src', 'tag', 'images', 'index' ) )
442 - ) {
443 - $attachment_id = (int) array_pop( $attachment_id );
450 + // Update the src if one was provided in the `data-lazy-` attributes.
451 + if ( 'src' !== $source_type ) {
452 + $placeholder_src_orig = $src;
453 + $placeholder_src = $placeholder_src_orig;
454 + $src_orig = $chosen_data_src;
455 + $src = $src_orig;
456 + }
444 457
445 - if ( $attachment_id ) {
446 - $attachment = get_post( $attachment_id );
458 + // Check if image URL should be used with Photon.
459 + if ( self::validate_image_url( $src ) ) {
460 + $width = $processor->get_attribute( 'width' );
461 + $height = $processor->get_attribute( 'height' );
447 462
448 - // Basic check on returned post object.
449 - if ( is_object( $attachment ) && ! is_wp_error( $attachment ) && 'attachment' === $attachment->post_type ) {
450 - $src_per_wp = wp_get_attachment_image_src( $attachment_id, isset( $size ) ? $size : 'full' );
463 + // First, check the image tag. Note we only check for pixel sizes now; HTML4 percentages have never been correctly
464 + // supported, so we stopped pretending to support them in JP 9.1.0.
465 + if ( ! is_string( $width ) || ! ctype_digit( $width ) ) {
466 + $width = false;
467 + }
451 468
452 - if ( self::validate_image_url( $src_per_wp[0] ) ) {
453 - $src = $src_per_wp[0];
454 - $fullsize_url = true;
469 + if ( ! is_string( $height ) || ! ctype_digit( $height ) ) {
470 + $height = false;
471 + }
455 472
456 - // Prevent image distortion if a detected dimension exceeds the image's natural dimensions.
457 - if ( ( false !== $width && $width > $src_per_wp[1] ) || ( false !== $height && $height > $src_per_wp[2] ) ) {
458 - $width = false === $width ? false : min( $width, $src_per_wp[1] );
459 - $height = false === $height ? false : min( $height, $src_per_wp[2] );
460 - }
473 + $needs_sizing = false === $width && false === $height;
474 + $size = null;
461 475
462 - // If no width and height are found, max out at source image's natural dimensions.
463 - // Otherwise, respect registered image sizes' cropping setting.
464 - if ( false === $width && false === $height ) {
465 - $width = $src_per_wp[1];
466 - $height = $src_per_wp[2];
467 - $transform = 'fit';
468 - } elseif ( isset( $size ) && array_key_exists( $size, $image_sizes ) && isset( $image_sizes[ $size ]['crop'] ) ) {
469 - $transform = (bool) $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
470 - }
471 - }
472 - } else {
473 - unset( $attachment_id );
474 - unset( $attachment );
475 - }
476 + if ( $needs_sizing ) {
477 + // Find the first CSS class listed with a prefix of `size-`, e.g. `size-full-width`
478 + foreach ( $processor->class_list() ?? array() as $class_name ) {
479 + if ( str_starts_with( $class_name, 'size-' ) ) {
480 + $size = substr( $class_name, strlen( 'size-' ) );
481 + break;
476 482 }
477 483 }
484 + }
478 485
479 - // If image tag lacks width and height arguments, try to determine from strings WP appends to resized image filenames.
480 - if ( false === $width && false === $height ) {
481 - list( $width, $height ) = self::parse_dimensions_from_filename( $src );
486 + if ( $needs_sizing && 'full' !== $size && is_string( $size ) && isset( $image_sizes[ $size ] ) && is_array( $image_sizes[ $size ] ) ) {
487 + $width = (int) $image_sizes[ $size ]['width'];
488 + $height = (int) $image_sizes[ $size ]['height'];
489 + $transform = $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
490 + } else {
491 + unset( $size );
492 + }
493 +
494 + // WP Attachment ID, if uploaded to this site.
495 + $attachment_id = null;
496 + foreach ( $processor->class_list() ?? array() as $class_name ) {
497 + if ( str_starts_with( $class_name, 'wp-image-' ) ) {
498 + $attachment_id = substr( $class_name, strlen( 'wp-image-' ) );
499 + break;
482 500 }
501 + }
483 502
484 - $width_orig = $width;
485 - $height_orig = $height;
486 - $transform_orig = $transform;
503 + // These values have not been used for a very long time, but removing them could break something.
504 + $images = array();
505 + $index = 0;
506 + if (
507 + $attachment_id &&
508 + preg_match( '#^[1-9][0-9]*$#', $attachment_id ) &&
509 + is_array( $upload_dir ) &&
510 + str_starts_with( $src, $upload_dir['baseurl'] ) &&
511 + /**
512 + * Filter whether an image using an attachment ID in its class has to be uploaded to the local site to go through Photon.
513 + *
514 + * @module photon
515 + *
516 + * @since 2.0.3
517 + *
518 + * @param bool false Was the image uploaded to the local site. Default to false.
519 + * @param array $args {
520 + * Array of image details.
521 + *
522 + * @type $src Image URL.
523 + * @type tag Image tag (Image HTML output).
524 + * }
525 + */
526 + apply_filters( 'jetpack_photon_image_is_local', false, compact( 'src', 'tag', 'images', 'index' ) )
527 + ) {
528 + $attachment_id = (int) $attachment_id;
529 + $attachment = get_post( $attachment_id );
487 530
488 - // If width is available, constrain to $content_width.
489 - if ( false !== $width && is_numeric( $content_width ) && $width > $content_width ) {
490 - if ( false !== $height ) {
491 - $height = round( ( $content_width * $height ) / $width );
492 - }
493 - $width = $content_width;
494 - }
531 + // Basic check on returned post object.
532 + if ( is_object( $attachment ) && ! is_wp_error( $attachment ) && 'attachment' === $attachment->post_type ) {
533 + $src_per_wp = wp_get_attachment_image_src( $attachment_id, $size ?? 'full' );
495 534
496 - // Set a width if none is found and $content_width is available.
497 - // If width is set in this manner and height is available, use `fit` instead of `resize` to prevent skewing.
498 - if ( false === $width && is_numeric( $content_width ) ) {
499 - $width = (int) $content_width;
535 + if ( self::validate_image_url( $src_per_wp[0] ) ) {
536 + $src = $src_per_wp[0];
537 + $fullsize_url = true;
500 538
501 - if ( false !== $height ) {
502 - $transform = 'fit';
539 + // Prevent image distortion if a detected dimension exceeds the image's natural dimensions.
540 + if ( ( false !== $width && $width > $src_per_wp[1] ) || ( false !== $height && $height > $src_per_wp[2] ) ) {
541 + $width = false === $width ? false : min( $width, $src_per_wp[1] );
542 + $height = false === $height ? false : min( $height, $src_per_wp[2] );
543 + }
544 +
545 + // If no width and height are found, max out at source image's natural dimensions.
546 + // Otherwise, respect registered image sizes' cropping setting.
547 + if ( false === $width && false === $height ) {
548 + $width = $src_per_wp[1];
549 + $height = $src_per_wp[2];
550 + $transform = 'fit';
551 + } elseif ( isset( $size ) && is_array( $image_sizes ) && array_key_exists( $size, $image_sizes ) && isset( $image_sizes[ $size ]['crop'] ) ) {
552 + $transform = $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
553 + }
503 554 }
555 + } else {
556 + unset( $attachment_id );
557 + unset( $attachment );
504 558 }
559 + }
505 560
506 - // Detect if image source is for a custom-cropped thumbnail and prevent further URL manipulation.
507 - if ( ! $fullsize_url && preg_match_all( '#-e[a-z0-9]+(-\d+x\d+)?\.(' . implode( '|', self::$extensions ) . '){1}$#i', basename( $src ), $filename ) ) {
508 - $fullsize_url = true;
509 - }
561 + // If image tag lacks width and height arguments, try to determine from strings WP appends to resized image filenames.
562 + if ( false === $width && false === $height ) {
563 + list( $width, $height ) = self::parse_dimensions_from_filename( $src );
564 + }
510 565
511 - // Build URL, first maybe removing WP's resized string so we pass the original image to Photon.
512 - if ( ! $fullsize_url && 0 === strpos( $src, $upload_dir['baseurl'] ) ) {
513 - $src = self::strip_image_dimensions_maybe( $src );
566 + $width_orig = $width;
567 + $height_orig = $height;
568 + $transform_orig = $transform;
569 +
570 + // If width is available, constrain to $content_width.
571 + if ( false !== $width && is_numeric( $content_width ) && $width > $content_width ) {
572 + if ( false !== $height ) {
573 + $height = round( ( $content_width * $height ) / $width );
514 574 }
575 + $width = $content_width;
576 + }
515 577
516 - // Build array of Photon args and expose to filter before passing to Photon URL function.
517 - $args = array();
578 + // Set a width if none is found and $content_width is available.
579 + // If width is set in this manner and height is available, use `fit` instead of `resize` to prevent skewing.
580 + if ( false === $width && is_numeric( $content_width ) ) {
581 + $width = (int) $content_width;
518 582
519 - if ( false !== $width && false !== $height ) {
520 - $args[ $transform ] = $width . ',' . $height;
521 - } elseif ( false !== $width ) {
522 - $args['w'] = $width;
523 - } elseif ( false !== $height ) {
524 - $args['h'] = $height;
583 + if ( false !== $height ) {
584 + $transform = 'fit';
525 585 }
586 + }
526 587
527 - /**
528 - * Filter the array of Photon arguments added to an image when it goes through Photon.
529 - * By default, only includes width and height values.
530 - *
531 - * @see https://developer.wordpress.com/docs/photon/api/
532 - *
533 - * @module photon
534 - *
535 - * @since 2.0.0
536 - *
537 - * @param array $args Array of Photon Arguments.
538 - * @param array $details {
539 - * Array of image details.
540 - *
541 - * @type string $tag Image tag (Image HTML output).
542 - * @type string $src Image URL.
543 - * @type string $src_orig Original Image URL.
544 - * @type int|false $width Image width.
545 - * @type int|false $height Image height.
546 - * @type int|false $width_orig Original image width before constrained by content_width.
547 - * @type int|false $height_orig Original Image height before constrained by content_width.
548 - * @type string $transform Transform.
549 - * @type string $transform_orig Original transform before constrained by content_width.
550 - * }
551 - */
552 - $args = apply_filters( 'jetpack_photon_post_image_args', $args, compact( 'tag', 'src', 'src_orig', 'width', 'height', 'width_orig', 'height_orig', 'transform', 'transform_orig' ) );
588 + // Detect if image source is for a custom-cropped thumbnail and prevent further URL manipulation.
589 + if ( ! $fullsize_url && preg_match_all( '#-e[a-z0-9]+(-\d+x\d+)?\.(' . implode( '|', self::$extensions ) . '){1}$#i', basename( $src ), $filename ) ) {
590 + $fullsize_url = true;
591 + }
553 592
554 - $photon_url = Image_CDN_Core::cdn_url( $src, $args );
593 + // Build URL, first maybe removing WP's resized string so we pass the original image to Photon.
594 + if ( ! $fullsize_url && is_array( $upload_dir ) && str_starts_with( $src, $upload_dir['baseurl'] ) ) {
595 + $src = self::strip_image_dimensions_maybe( $src );
596 + }
555 597
556 - // Modify image tag if Photon function provides a URL
557 - // 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.
558 - if ( $src !== $photon_url ) {
559 - $new_tag = $tag;
598 + // Build array of Photon args and expose to filter before passing to Photon URL function.
599 + $args = array();
560 600
561 - // If present, replace the link href with a Photoned URL for the full-size image.
562 - if ( ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
563 - $new_tag = preg_replace( '#(href=["|\'])' . preg_quote( $images['link_url'][ $index ], '#' ) . '(["|\'])#i', '\1' . Image_CDN_Core::cdn_url( $images['link_url'][ $index ] ) . '\2', $new_tag, 1 );
564 - }
601 + if ( false !== $width && false !== $height ) {
602 + $args[ $transform ] = $width . ',' . $height;
603 + } elseif ( false !== $width ) {
604 + $args['w'] = $width;
605 + } elseif ( false !== $height ) {
606 + $args['h'] = $height;
607 + }
565 608
566 - // Supplant the original source value with our Photon URL.
567 - $photon_url = esc_url( $photon_url );
568 - $new_tag = str_replace( $src_orig, $photon_url, $new_tag );
609 + /**
610 + * Filter the array of Photon arguments added to an image when it goes through Photon.
611 + * By default, only includes width and height values.
612 + *
613 + * @see https://developer.wordpress.com/docs/photon/api/
614 + *
615 + * @module photon
616 + *
617 + * @since 2.0.0
618 + * @since 0.4.7 Passes image tag name instead of full HTML of tag.
619 + *
620 + * @param array $args Array of Photon Arguments.
621 + * @param array $details {
622 + * Array of image details.
623 + *
624 + * @type string $tag Image tag (Image HTML output).
625 + * @type string $src Image URL.
626 + * @type string $src_orig Original Image URL.
627 + * @type int|false $width Image width.
628 + * @type int|false $height Image height.
629 + * @type int|false $width_orig Original image width before constrained by content_width.
630 + * @type int|false $height_orig Original Image height before constrained by content_width.
631 + * @type string $transform Transform.
632 + * @type string $transform_orig Original transform before constrained by content_width.
633 + * }
634 + */
635 + $args = apply_filters( 'jetpack_photon_post_image_args', $args, compact( 'tag', 'src', 'src_orig', 'width', 'height', 'width_orig', 'height_orig', 'transform', 'transform_orig' ) );
569 636
570 - // If Lazy Load is in use, pass placeholder image through Photon.
571 - if ( isset( $placeholder_src ) && self::validate_image_url( $placeholder_src ) ) {
572 - $placeholder_src = Image_CDN_Core::cdn_url( $placeholder_src );
637 + $photon_url = Image_CDN_Core::cdn_url( $src, $args );
573 638
574 - if ( $placeholder_src !== $placeholder_src_orig ) {
575 - $new_tag = str_replace( $placeholder_src_orig, esc_url( $placeholder_src ), $new_tag );
576 - }
639 + // Modify image tag if Photon function provides a URL
640 + // 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.
641 + if ( $src !== $photon_url ) {
642 + // If present, replace the link href with a Photoned URL for the full-size image.
643 + if ( is_string( $nearest_preceding_href ) && self::validate_image_url( $nearest_preceding_href ) ) {
644 + $processor->seek( 'link' );
645 + $processor->set_attribute( 'href', Image_CDN_Core::cdn_url( $nearest_preceding_href ) );
646 + $processor->seek( 'image' );
647 + }
577 648
578 - unset( $placeholder_src );
579 - }
649 + // Supplant the original source value with our Photon URL.
650 + $processor->set_attribute( 'src', $photon_url );
580 651
581 - // If we are not transforming the image with resize, fit, or letterbox (lb), then we should remove
582 - // the width and height arguments (including HTML4 percentages) from the image to prevent distortion.
583 - // Even if $args['w'] and $args['h'] are present, Photon does not crop to those dimensions. Instead,
584 - // it appears to favor height.
585 - //
586 - // If we are transforming the image via one of those methods, let's update the width and height attributes.
587 - if ( empty( $args['resize'] ) && empty( $args['fit'] ) && empty( $args['lb'] ) ) {
588 - $new_tag = preg_replace( '#(?<=\s)(width|height)=["\']?[\d%]+["\']?\s?#i', '', $new_tag );
589 - } else {
590 - $resize_args = isset( $args['resize'] ) ? $args['resize'] : false;
591 - if ( false === $resize_args ) {
592 - $resize_args = ( ! $resize_args && isset( $args['fit'] ) )
593 - ? $args['fit']
594 - : false;
595 - }
596 - if ( false === $resize_args ) {
597 - $resize_args = ( ! $resize_args && isset( $args['lb'] ) )
598 - ? $args['lb']
599 - : false;
600 - }
652 + // If Lazy Load is in use, pass placeholder image through Photon.
653 + if ( $placeholder_src !== null && self::validate_image_url( $placeholder_src ) ) {
654 + $placeholder_src = Image_CDN_Core::cdn_url( $placeholder_src );
601 655
602 - $resize_args = array_map( 'trim', explode( ',', $resize_args ) );
656 + if ( $placeholder_src !== $placeholder_src_orig ) {
657 + $processor->set_attribute( $source_type, $placeholder_src );
658 + }
659 + }
603 660
604 - // (?<=\s) - Ensure width or height attribute is preceded by a space
605 - // (width=["\']?) - Matches, and captures, width=, width=", or width='
606 - // [\d%]+ - Matches 1 or more digits or percent signs
607 - // (["\']?) - Matches, and captures, ", ', or empty string
608 - // \s - Ensures there's a space after the attribute
609 - $new_tag = preg_replace( '#(?<=\s)(width=["\']?)[\d%]+(["\']?)\s?#i', sprintf( '${1}%d${2} ', $resize_args[0] ), $new_tag );
610 - $new_tag = preg_replace( '#(?<=\s)(height=["\']?)[\d%]+(["\']?)\s?#i', sprintf( '${1}%d${2} ', $resize_args[1] ), $new_tag );
661 + // If we are not transforming the image with resize, fit, or letterbox (lb), then we should remove
662 + // the width and height arguments (including HTML4 percentages) from the image to prevent distortion.
663 + // Even if $args['w'] and $args['h'] are present, Photon does not crop to those dimensions. Instead,
664 + // it appears to favor height.
665 + //
666 + // If we are transforming the image via one of those methods, let's update the width and height attributes.
667 + if ( empty( $args['resize'] ) && empty( $args['fit'] ) && empty( $args['lb'] ) ) {
668 + $processor->remove_attribute( 'width' );
669 + $processor->remove_attribute( 'height' );
670 + } else {
671 + $resize_args = $args['resize'] ?? false;
672 + if ( false === $resize_args ) {
673 + $resize_args = ( ! $resize_args && isset( $args['fit'] ) )
674 + ? $args['fit']
675 + : false;
611 676 }
612 -
613 - // Tag an image for dimension checking.
614 - if ( ! self::is_amp_endpoint() ) {
615 - $new_tag = preg_replace( '#(\s?/)?>(\s*</a>)?$#i', ' data-recalc-dims="1"\1>\2', $new_tag );
677 + if ( false === $resize_args ) {
678 + $resize_args = ( ! $resize_args && isset( $args['lb'] ) )
679 + ? $args['lb']
680 + : false;
616 681 }
617 682
618 - // Replace original tag with modified version.
619 - $content = str_replace( $tag, $new_tag, $content );
683 + list( $resize_width, $resize_height ) = explode( ',', $resize_args );
684 + $processor->set_attribute( 'width', trim( $resize_width ) );
685 + $processor->set_attribute( 'height', trim( $resize_height ) );
620 686 }
621 - } elseif ( preg_match( '#^http(s)?://i[\d]{1}.wp.com#', $src ) && ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
622 - $new_tag = preg_replace( '#(href=["\'])' . preg_quote( $images['link_url'][ $index ], '#' ) . '(["\'])#i', '\1' . Image_CDN_Core::cdn_url( $images['link_url'][ $index ] ) . '\2', $tag, 1 );
623 687
624 - $content = str_replace( $tag, $new_tag, $content );
688 + // Tag an image for dimension checking.
689 + if ( ! self::is_amp_endpoint() ) {
690 + $processor->set_attribute( 'data-recalc-dims', '1' );
691 + }
625 692 }
693 + } elseif (
694 + preg_match( '#^http(s)?://i[\d]{1}.wp.com#', $src )
695 + && is_string( $nearest_preceding_href )
696 + && self::validate_image_url( $nearest_preceding_href )
697 + ) {
698 + $processor->seek( 'link' );
699 + $processor->set_attribute( 'href', Image_CDN_Core::cdn_url( $nearest_preceding_href ) );
700 + $processor->seek( 'image' );
626 701 }
627 702 }
628 703
629 - return $content;
704 + return $processor->get_updated_html();
630 705 }
631 706
632 707 /**
633 708 * Filter Core galleries
@@ -745,12 +820,13 @@
745 820 }
746 821
747 822 $intermediate = true; // For the fourth array item returned by the image_downsize filter.
748 823
824 + $registered_size = self::find_registered_image_size( $size );
825 +
749 826 // If an image is requested with a size known to WordPress, use that size's settings with Photon.
750 - // WP states that `add_image_size()` should use a string for the name, but doesn't enforce that.
751 - // Due to differences in how Core and Photon check for the registered image size, we check both types.
752 - if ( ( is_string( $size ) || is_int( $size ) ) && array_key_exists( $size, self::image_sizes() ) ) {
827 + if ( $registered_size ) {
828 + $size = $registered_size;
753 829 $image_args = self::image_sizes();
754 830 $image_args = $image_args[ $size ];
755 831
756 832 $photon_args = array();
@@ -767,9 +843,13 @@
767 843 $image_meta = wp_get_attachment_metadata( $attachment_id );
768 844
769 845 if ( isset( $image_meta['width'] ) && isset( $image_meta['height'] ) ) {
770 846 $image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $image_args['width'], $image_args['height'], $image_args['crop'] );
771 - if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
847 + if (
848 + $image_resized // This could be false when the requested image size is larger than the full-size image.
849 + && isset( $image_resized[6] ) // This key can be manipulated by the `image_resize_dimensions` filter
850 + && isset( $image_resized[7] ) // This key can be manipulated by the `image_resize_dimensions` filter
851 + ) {
772 852 $image_meta['width'] = $image_resized[6];
773 853 $image_meta['height'] = $image_resized[7];
774 854 }
775 855 }
@@ -904,13 +984,13 @@
904 984
905 985 /**
906 986 * Filters an array of image `srcset` values, replacing each URL with its Photon equivalent.
907 987 *
908 - * @param array $sources An array of image urls and widths.
909 - * @param array $size_array The size array for srcset.
910 - * @param array $image_src The image srcs.
911 - * @param array $image_meta The image meta.
912 - * @param int $attachment_id Attachment ID.
988 + * @param array $sources An array of image urls and widths.
989 + * @param array $size_array The size array for srcset.
990 + * @param string $image_src The image src attribute.
991 + * @param array $image_meta The image meta.
992 + * @param int $attachment_id Attachment ID.
913 993 *
914 994 * @uses self::validate_image_url, Image_CDN_Core::cdn_url
915 995 * @uses Image_CDN::strip_image_dimensions_maybe, Image_CDN_Core::get_jetpack_content_width
916 996 *
@@ -915,9 +995,14 @@
915 995 * @uses Image_CDN::strip_image_dimensions_maybe, Image_CDN_Core::get_jetpack_content_width
916 996 *
917 997 * @return array An array of Photon image urls and widths.
918 998 */
919 - public function filter_srcset_array( $sources = array(), $size_array = array(), $image_src = array(), $image_meta = array(), $attachment_id = 0 ) {
999 + public function filter_srcset_array( $sources = array(), $size_array = array(), $image_src = '', $image_meta = array(), $attachment_id = 0 ) {
1000 + // Check if we are supposed to skip the main image.
1001 + if ( $this->photon_should_skip_image( $image_src ) ) {
1002 + return $sources;
1003 + }
1004 +
920 1005 if ( ! is_array( $sources ) || array() === $sources ) {
921 1006 return $sources;
922 1007 }
923 1008 $upload_dir = wp_get_upload_dir();
@@ -1004,9 +1089,9 @@
1004 1089 $newsources = null;
1005 1090
1006 1091 foreach ( $multipliers as $multiplier ) {
1007 1092
1008 - $newwidth = $base * $multiplier;
1093 + $newwidth = (int) round( $base * $multiplier );
1009 1094 foreach ( $currentwidths as $currentwidth ) {
1010 1095 // If a new width would be within 100 pixes of an existing one or larger than the full size image, skip.
1011 1096 if ( abs( $currentwidth - $newwidth ) < 50 || ( $newwidth > $fullwidth ) ) {
1012 1097 continue 2; // Bump out back to the $multipliers as $multiplier.
@@ -1040,14 +1125,20 @@
1040 1125
1041 1126 /**
1042 1127 * Filters an array of image `sizes` values, using $content_width instead of image's full size.
1043 1128 *
1044 - * @param array $sizes An array of media query breakpoints.
1045 - * @param array $size Width and height of the image.
1129 + * @param array $sizes An array of media query breakpoints.
1130 + * @param array $size Width and height of the image.
1131 + * @param string $image_url The image URL.
1132 + *
1046 1133 * @uses Jetpack::get_content_width
1047 1134 * @return array An array of media query breakpoints.
1048 1135 */
1049 - public function filter_sizes( $sizes, $size ) {
1136 + public function filter_sizes( $sizes, $size, $image_url ) {
1137 + if ( $this->photon_should_skip_image( $image_url ) ) {
1138 + return $sizes;
1139 + }
1140 +
1050 1141 if ( ! doing_filter( 'the_content' ) ) {
1051 1142 return $sizes;
1052 1143 }
1053 1144 $content_width = Image_CDN_Core::get_jetpack_content_width();
@@ -1062,8 +1153,19 @@
1062 1153 return sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $content_width );
1063 1154 }
1064 1155
1065 1156 /**
1157 + * Whether to skip the image from being processed by Photon.
1158 + *
1159 + * @param string $image_url The image URL.
1160 + *
1161 + * @return bool Whether to skip the image.
1162 + */
1163 + private function photon_should_skip_image( $image_url ) {
1164 + return apply_filters( 'jetpack_photon_skip_image', false, $image_url, null );
1165 + }
1166 +
1167 + /**
1066 1168 * * GENERAL FUNCTIONS
1067 1169 **/
1068 1170
1069 1171 /**
@@ -1226,29 +1328,29 @@
1226 1328 return is_array( self::$image_sizes ) ? self::$image_sizes : array();
1227 1329 }
1228 1330
1229 1331 /**
1230 - * Enqueue Photon helper script
1332 + * Find registered image size name if it exists.
1231 1333 *
1232 - * @uses wp_enqueue_script, plugins_url
1233 - * @action wp_enqueue_script
1234 - * @return null
1334 + * @param string|int|int[] $size Image size name if registered, or false if not.
1235 1335 */
1236 - public function action_wp_enqueue_scripts() {
1237 - if ( self::is_amp_endpoint() ) {
1238 - return;
1336 + protected static function find_registered_image_size( $size ) {
1337 + $sizes = self::image_sizes();
1338 +
1339 + // WP states that `add_image_size()` should use a string for the name, but doesn't enforce that.
1340 + if ( ( is_string( $size ) || is_int( $size ) ) && array_key_exists( $size, self::image_sizes() ) ) {
1341 + return $size;
1239 1342 }
1240 1343
1241 - Assets::register_script(
1242 - 'jetpack-photon',
1243 - '../dist/image-cdn.js',
1244 - __FILE__,
1245 - array(
1246 - 'enqueue' => true,
1247 - 'nonminpath' => 'js/image-cdn.js',
1248 - 'in_footer' => true,
1249 - )
1250 - );
1344 + if ( is_array( $size ) && isset( $size[0] ) && isset( $size[1] ) ) {
1345 + foreach ( $sizes as $name => $args ) {
1346 + if ( $args['width'] === $size[0] && $args['height'] === $size[1] ) {
1347 + return $name;
1348 + }
1349 + }
1350 + }
1351 +
1352 + return false;
1251 1353 }
1252 1354
1253 1355 /**
1254 1356 * Determine if image_downsize should utilize Photon via REST API.
@@ -1293,12 +1395,12 @@
1293 1395 $route = $request->get_route();
1294 1396
1295 1397 if (
1296 1398 (
1297 - false !== strpos( $route, 'wp/v2/media' )
1399 + str_contains( $route, 'wp/v2/media' )
1298 1400 && 'edit' === $request->get_param( 'context' )
1299 1401 )
1300 - || false !== strpos( $route, 'wpcom/v2/external-media/copy' )
1402 + || str_contains( $route, 'wpcom/v2/external-media/copy' )
1301 1403 || (bool) $request->get_header( 'x-wp-api-fetch-from-editor' )
1302 1404 ) {
1303 1405 // Don't use `__return_true()`: Use something unique. See ::_override_image_downsize_in_rest_edit_context()
1304 1406 // Late execution to avoid conflict with other plugins as we really don't want to run in this situation.
@@ -1372,6 +1474,15 @@
1372 1474 * @return bool Whether AMP page.
1373 1475 */
1374 1476 private static function is_amp_endpoint() {
1375 1477 return class_exists( '\Jetpack_AMP_Support' ) && \Jetpack_AMP_Support::is_amp_request();
1478 + }
1479 +
1480 + /**
1481 + * Get the list of supported image extensions
1482 + *
1483 + * @return string[] Array of supported extensions
1484 + */
1485 + public static function get_supported_extensions() {
1486 + return self::$extensions;
1376 1487 }
1377 1488 }