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 +392 -283 13.2.4 → 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.3.2';
15 + const PACKAGE_VERSION = '0.8.1';
18 16
19 17 /**
20 18 * Singleton.
21 19 *
@@ -61,9 +59,9 @@
61 59 *
62 60 * @return object
63 61 */
64 62 public static function instance() {
65 - if ( ! is_a( self::$instance, self::class ) ) {
63 + if ( ! self::$instance instanceof self ) {
66 64 self::$instance = new self();
67 65 self::$instance->setup();
68 66 self::$is_enabled = true;
69 67 }
@@ -111,13 +109,10 @@
111 109 add_filter( 'rest_request_after_callbacks', array( $this, 'cleanup_rest_photon_image_downsize' ) );
112 110
113 111 // Responsive image srcset substitution.
114 112 add_filter( 'wp_calculate_image_srcset', array( $this, 'filter_srcset_array' ), 10, 5 );
115 - 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.
116 114
117 - // Helpers for maniuplated images.
118 - add_action( 'wp_enqueue_scripts', array( $this, 'action_wp_enqueue_scripts' ), 9 );
119 -
120 115 /**
121 116 * Allow Photon to disable uploaded images resizing and use its own resize capabilities instead.
122 117 *
123 118 * @module photon
@@ -216,9 +211,9 @@
216 211 */
217 212 public static function filter_photon_noresize_thumbnail_urls( $sizes ) {
218 213 foreach ( $sizes as $size => $url ) {
219 214 $parts = explode( '?', $url );
220 - $arguments = isset( $parts[1] ) ? $parts[1] : array();
215 + $arguments = $parts[1] ?? array();
221 216
222 217 $sizes[ $size ] = Image_CDN_Core::cdn_url( $url, wp_parse_args( $arguments ) );
223 218 }
224 219
@@ -238,12 +233,12 @@
238 233 if ( empty( $data ) ) {
239 234 return $data;
240 235 }
241 236 $sizes_already_exist = (
242 - true === is_array( $data )
243 - && true === array_key_exists( 'sizes', $data )
244 - && true === is_array( $data['sizes'] )
245 - && false === empty( $data['sizes'] )
237 + is_array( $data )
238 + && array_key_exists( 'sizes', $data )
239 + && is_array( $data['sizes'] )
240 + && ! empty( $data['sizes'] )
246 241 );
247 242 if ( $sizes_already_exist ) {
248 243 return $data;
249 244 }
@@ -314,9 +309,9 @@
314 309 */
315 310 public static function parse_dimensions_from_filename( $src ) {
316 311 $width_height_string = array();
317 312
318 - 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 ) ) {
319 314 $width = (int) $width_height_string[1];
320 315 $height = (int) $width_height_string[2];
321 316
322 317 if ( $width && $height ) {
@@ -329,307 +324,385 @@
329 324
330 325 /**
331 326 * Identify images in post content, and if images are local (uploaded to the current site), pass through Photon.
332 327 *
333 - * @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.
334 329 *
335 330 * @uses self::validate_image_url, apply_filters, Image_CDN_Core::cdn_url, esc_url
336 331 * @filter the_content
337 332 *
338 - * @return string
333 + * @return string The content.
339 334 */
340 335 public static function filter_the_content( $content ) {
341 - $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 + }
342 340
343 - if ( ! empty( $images ) ) {
344 - $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;
345 347
346 - $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 + }
347 364
348 - $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;
349 370
350 - foreach ( $images[0] as $index => $tag ) {
351 - // Default to resize, though fit may be used in certain cases where a dimension cannot be ascertained.
352 - $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 + }
353 384
354 - // Start with a clean attachment ID each time.
355 - $attachment_id = false;
385 + $processor->set_bookmark( 'image' );
356 386
357 - // Flag if we need to munge a fullsize URL.
358 - $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 + }
359 396
360 - // Identify image source.
361 - $src_orig = $images['img_url'][ $index ];
362 - $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();
363 410
364 - /**
365 - * Allow specific images to be skipped by Photon.
366 - *
367 - * @module photon
368 - *
369 - * @since 2.0.3
370 - *
371 - * @param bool false Should Photon ignore this image. Default to false.
372 - * @param string $src Image URL.
373 - * @param string $tag Image Tag (Image HTML output).
374 - */
375 - if ( apply_filters( 'jetpack_photon_skip_image', false, $src, $tag ) ) {
376 - continue;
377 - }
411 + // Default to resize, though fit may be used in certain cases where a dimension cannot be ascertained.
412 + $transform = 'resize';
378 413
379 - // Support Automattic's Lazy Load plugin.
380 - // Can't modify $tag yet as we need unadulterated version later.
381 - if ( preg_match( '#data-lazy-src=["\'](.+?)["\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
382 - $placeholder_src_orig = $src;
383 - $placeholder_src = $placeholder_src_orig;
384 - $src_orig = $lazy_load_src[1];
385 - $src = $src_orig;
386 - } elseif ( preg_match( '#data-lazy-original=["\'](.+?)["\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
387 - $placeholder_src_orig = $src;
388 - $placeholder_src = $placeholder_src_orig;
389 - $src_orig = $lazy_load_src[1];
390 - $src = $src_orig;
391 - }
414 + // Flag if we need to munge a fullsize URL.
415 + $fullsize_url = false;
392 416
393 - // Check if image URL should be used with Photon.
394 - if ( self::validate_image_url( $src ) ) {
395 - // Find the width and height attributes.
396 - $width = false;
397 - $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 + }
398 431
399 - // First, check the image tag. Note we only check for pixel sizes now; HTML4 percentages have never been correctly
400 - // supported, so we stopped pretending to support them in JP 9.1.0.
401 - if ( preg_match( '#[\s"\']width=["\']?([\d%]+)["\']?#i', $images['img_tag'][ $index ], $width_string ) ) {
402 - $width = str_contains( $width_string[1], '%' ) ? false : $width_string[1];
403 - }
432 + $data_lazy_src = $processor->get_attribute( 'data-lazy-src' );
433 + $data_lazy_original = $processor->get_attribute( 'data-lazy-original' );
404 434
405 - if ( preg_match( '#[\s"\']height=["\']?([\d%]+)["\']?#i', $images['img_tag'][ $index ], $height_string ) ) {
406 - $height = str_contains( $height_string[1], '%' ) ? false : $height_string[1];
407 - }
435 + $source_type = 'src';
436 + $chosen_data_src = null;
408 437
409 - // Detect WP registered image size from HTML class.
410 - if ( preg_match( '#class=["\']?[^"\']*size-([^"\'\s]+)[^"\']*["\']?#i', $images['img_tag'][ $index ], $size ) ) {
411 - $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 + }
412 443
413 - if ( false === $width && false === $height && 'full' !== $size && array_key_exists( $size, $image_sizes ) ) {
414 - $width = (int) $image_sizes[ $size ]['width'];
415 - $height = (int) $image_sizes[ $size ]['height'];
416 - $transform = $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
417 - }
418 - } else {
419 - unset( $size );
420 - }
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 + }
421 449
422 - // WP Attachment ID, if uploaded to this site.
423 - if (
424 - preg_match( '#class=["\']?[^"\']*wp-image-([\d]+)[^"\']*["\']?#i', $images['img_tag'][ $index ], $attachment_id ) &&
425 - str_starts_with( $src, $upload_dir['baseurl'] ) &&
426 - /**
427 - * Filter whether an image using an attachment ID in its class has to be uploaded to the local site to go through Photon.
428 - *
429 - * @module photon
430 - *
431 - * @since 2.0.3
432 - *
433 - * @param bool false Was the image uploaded to the local site. Default to false.
434 - * @param array $args {
435 - * Array of image details.
436 - *
437 - * @type $src Image URL.
438 - * @type tag Image tag (Image HTML output).
439 - * @type $images Array of information about the image.
440 - * @type $index Image index.
441 - * }
442 - */
443 - apply_filters( 'jetpack_photon_image_is_local', false, compact( 'src', 'tag', 'images', 'index' ) )
444 - ) {
445 - $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 + }
446 457
447 - if ( $attachment_id ) {
448 - $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' );
449 462
450 - // Basic check on returned post object.
451 - if ( is_object( $attachment ) && ! is_wp_error( $attachment ) && 'attachment' === $attachment->post_type ) {
452 - $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 + }
453 468
454 - if ( self::validate_image_url( $src_per_wp[0] ) ) {
455 - $src = $src_per_wp[0];
456 - $fullsize_url = true;
469 + if ( ! is_string( $height ) || ! ctype_digit( $height ) ) {
470 + $height = false;
471 + }
457 472
458 - // Prevent image distortion if a detected dimension exceeds the image's natural dimensions.
459 - if ( ( false !== $width && $width > $src_per_wp[1] ) || ( false !== $height && $height > $src_per_wp[2] ) ) {
460 - $width = false === $width ? false : min( $width, $src_per_wp[1] );
461 - $height = false === $height ? false : min( $height, $src_per_wp[2] );
462 - }
473 + $needs_sizing = false === $width && false === $height;
474 + $size = null;
463 475
464 - // If no width and height are found, max out at source image's natural dimensions.
465 - // Otherwise, respect registered image sizes' cropping setting.
466 - if ( false === $width && false === $height ) {
467 - $width = $src_per_wp[1];
468 - $height = $src_per_wp[2];
469 - $transform = 'fit';
470 - } elseif ( isset( $size ) && array_key_exists( $size, $image_sizes ) && isset( $image_sizes[ $size ]['crop'] ) ) {
471 - $transform = (bool) $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
472 - }
473 - }
474 - } else {
475 - unset( $attachment_id );
476 - unset( $attachment );
477 - }
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;
478 482 }
479 483 }
484 + }
480 485
481 - // If image tag lacks width and height arguments, try to determine from strings WP appends to resized image filenames.
482 - if ( false === $width && false === $height ) {
483 - 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;
484 500 }
501 + }
485 502
486 - $width_orig = $width;
487 - $height_orig = $height;
488 - $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 );
489 530
490 - // If width is available, constrain to $content_width.
491 - if ( false !== $width && is_numeric( $content_width ) && $width > $content_width ) {
492 - if ( false !== $height ) {
493 - $height = round( ( $content_width * $height ) / $width );
494 - }
495 - $width = $content_width;
496 - }
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' );
497 534
498 - // Set a width if none is found and $content_width is available.
499 - // If width is set in this manner and height is available, use `fit` instead of `resize` to prevent skewing.
500 - if ( false === $width && is_numeric( $content_width ) ) {
501 - $width = (int) $content_width;
535 + if ( self::validate_image_url( $src_per_wp[0] ) ) {
536 + $src = $src_per_wp[0];
537 + $fullsize_url = true;
502 538
503 - if ( false !== $height ) {
504 - $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 + }
505 554 }
555 + } else {
556 + unset( $attachment_id );
557 + unset( $attachment );
506 558 }
559 + }
507 560
508 - // Detect if image source is for a custom-cropped thumbnail and prevent further URL manipulation.
509 - if ( ! $fullsize_url && preg_match_all( '#-e[a-z0-9]+(-\d+x\d+)?\.(' . implode( '|', self::$extensions ) . '){1}$#i', basename( $src ), $filename ) ) {
510 - $fullsize_url = true;
511 - }
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 + }
512 565
513 - // Build URL, first maybe removing WP's resized string so we pass the original image to Photon.
514 - if ( ! $fullsize_url && str_starts_with( $src, $upload_dir['baseurl'] ) ) {
515 - $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 );
516 574 }
575 + $width = $content_width;
576 + }
517 577
518 - // Build array of Photon args and expose to filter before passing to Photon URL function.
519 - $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;
520 582
521 - if ( false !== $width && false !== $height ) {
522 - $args[ $transform ] = $width . ',' . $height;
523 - } elseif ( false !== $width ) {
524 - $args['w'] = $width;
525 - } elseif ( false !== $height ) {
526 - $args['h'] = $height;
583 + if ( false !== $height ) {
584 + $transform = 'fit';
527 585 }
586 + }
528 587
529 - /**
530 - * Filter the array of Photon arguments added to an image when it goes through Photon.
531 - * By default, only includes width and height values.
532 - *
533 - * @see https://developer.wordpress.com/docs/photon/api/
534 - *
535 - * @module photon
536 - *
537 - * @since 2.0.0
538 - *
539 - * @param array $args Array of Photon Arguments.
540 - * @param array $details {
541 - * Array of image details.
542 - *
543 - * @type string $tag Image tag (Image HTML output).
544 - * @type string $src Image URL.
545 - * @type string $src_orig Original Image URL.
546 - * @type int|false $width Image width.
547 - * @type int|false $height Image height.
548 - * @type int|false $width_orig Original image width before constrained by content_width.
549 - * @type int|false $height_orig Original Image height before constrained by content_width.
550 - * @type string $transform Transform.
551 - * @type string $transform_orig Original transform before constrained by content_width.
552 - * }
553 - */
554 - $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 + }
555 592
556 - $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 + }
557 597
558 - // Modify image tag if Photon function provides a URL
559 - // 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.
560 - if ( $src !== $photon_url ) {
561 - $new_tag = $tag;
598 + // Build array of Photon args and expose to filter before passing to Photon URL function.
599 + $args = array();
562 600
563 - // If present, replace the link href with a Photoned URL for the full-size image.
564 - if ( ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
565 - $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 );
566 - }
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 + }
567 608
568 - // Supplant the original source value with our Photon URL.
569 - $photon_url = esc_url( $photon_url );
570 - $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' ) );
571 636
572 - // If Lazy Load is in use, pass placeholder image through Photon.
573 - if ( isset( $placeholder_src ) && self::validate_image_url( $placeholder_src ) ) {
574 - $placeholder_src = Image_CDN_Core::cdn_url( $placeholder_src );
637 + $photon_url = Image_CDN_Core::cdn_url( $src, $args );
575 638
576 - if ( $placeholder_src !== $placeholder_src_orig ) {
577 - $new_tag = str_replace( $placeholder_src_orig, esc_url( $placeholder_src ), $new_tag );
578 - }
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 + }
579 648
580 - unset( $placeholder_src );
581 - }
649 + // Supplant the original source value with our Photon URL.
650 + $processor->set_attribute( 'src', $photon_url );
582 651
583 - // If we are not transforming the image with resize, fit, or letterbox (lb), then we should remove
584 - // the width and height arguments (including HTML4 percentages) from the image to prevent distortion.
585 - // Even if $args['w'] and $args['h'] are present, Photon does not crop to those dimensions. Instead,
586 - // it appears to favor height.
587 - //
588 - // If we are transforming the image via one of those methods, let's update the width and height attributes.
589 - if ( empty( $args['resize'] ) && empty( $args['fit'] ) && empty( $args['lb'] ) ) {
590 - $new_tag = preg_replace( '#(?<=\s)(width|height)=["\']?[\d%]+["\']?\s?#i', '', $new_tag );
591 - } else {
592 - $resize_args = isset( $args['resize'] ) ? $args['resize'] : false;
593 - if ( false === $resize_args ) {
594 - $resize_args = ( ! $resize_args && isset( $args['fit'] ) )
595 - ? $args['fit']
596 - : false;
597 - }
598 - if ( false === $resize_args ) {
599 - $resize_args = ( ! $resize_args && isset( $args['lb'] ) )
600 - ? $args['lb']
601 - : false;
602 - }
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 );
603 655
604 - $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 + }
605 660
606 - // (?<=\s) - Ensure width or height attribute is preceded by a space
607 - // (width=["\']?) - Matches, and captures, width=, width=", or width='
608 - // [\d%]+ - Matches 1 or more digits or percent signs
609 - // (["\']?) - Matches, and captures, ", ', or empty string
610 - // \s - Ensures there's a space after the attribute
611 - $new_tag = preg_replace( '#(?<=\s)(width=["\']?)[\d%]+(["\']?)\s?#i', sprintf( '${1}%d${2} ', $resize_args[0] ), $new_tag );
612 - $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;
613 676 }
614 -
615 - // Tag an image for dimension checking.
616 - if ( ! self::is_amp_endpoint() ) {
617 - $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;
618 681 }
619 682
620 - // Replace original tag with modified version.
621 - $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 ) );
622 686 }
623 - } elseif ( preg_match( '#^http(s)?://i[\d]{1}.wp.com#', $src ) && ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
624 - $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 );
625 687
626 - $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 + }
627 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' );
628 701 }
629 702 }
630 703
631 - return $content;
704 + return $processor->get_updated_html();
632 705 }
633 706
634 707 /**
635 708 * Filter Core galleries
@@ -747,12 +820,13 @@
747 820 }
748 821
749 822 $intermediate = true; // For the fourth array item returned by the image_downsize filter.
750 823
824 + $registered_size = self::find_registered_image_size( $size );
825 +
751 826 // If an image is requested with a size known to WordPress, use that size's settings with Photon.
752 - // WP states that `add_image_size()` should use a string for the name, but doesn't enforce that.
753 - // Due to differences in how Core and Photon check for the registered image size, we check both types.
754 - if ( ( is_string( $size ) || is_int( $size ) ) && array_key_exists( $size, self::image_sizes() ) ) {
827 + if ( $registered_size ) {
828 + $size = $registered_size;
755 829 $image_args = self::image_sizes();
756 830 $image_args = $image_args[ $size ];
757 831
758 832 $photon_args = array();
@@ -769,9 +843,13 @@
769 843 $image_meta = wp_get_attachment_metadata( $attachment_id );
770 844
771 845 if ( isset( $image_meta['width'] ) && isset( $image_meta['height'] ) ) {
772 846 $image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $image_args['width'], $image_args['height'], $image_args['crop'] );
773 - 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 + ) {
774 852 $image_meta['width'] = $image_resized[6];
775 853 $image_meta['height'] = $image_resized[7];
776 854 }
777 855 }
@@ -906,13 +984,13 @@
906 984
907 985 /**
908 986 * Filters an array of image `srcset` values, replacing each URL with its Photon equivalent.
909 987 *
910 - * @param array $sources An array of image urls and widths.
911 - * @param array $size_array The size array for srcset.
912 - * @param array $image_src The image srcs.
913 - * @param array $image_meta The image meta.
914 - * @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.
915 993 *
916 994 * @uses self::validate_image_url, Image_CDN_Core::cdn_url
917 995 * @uses Image_CDN::strip_image_dimensions_maybe, Image_CDN_Core::get_jetpack_content_width
918 996 *
@@ -917,9 +995,14 @@
917 995 * @uses Image_CDN::strip_image_dimensions_maybe, Image_CDN_Core::get_jetpack_content_width
918 996 *
919 997 * @return array An array of Photon image urls and widths.
920 998 */
921 - 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 +
922 1005 if ( ! is_array( $sources ) || array() === $sources ) {
923 1006 return $sources;
924 1007 }
925 1008 $upload_dir = wp_get_upload_dir();
@@ -1006,9 +1089,9 @@
1006 1089 $newsources = null;
1007 1090
1008 1091 foreach ( $multipliers as $multiplier ) {
1009 1092
1010 - $newwidth = $base * $multiplier;
1093 + $newwidth = (int) round( $base * $multiplier );
1011 1094 foreach ( $currentwidths as $currentwidth ) {
1012 1095 // If a new width would be within 100 pixes of an existing one or larger than the full size image, skip.
1013 1096 if ( abs( $currentwidth - $newwidth ) < 50 || ( $newwidth > $fullwidth ) ) {
1014 1097 continue 2; // Bump out back to the $multipliers as $multiplier.
@@ -1042,14 +1125,20 @@
1042 1125
1043 1126 /**
1044 1127 * Filters an array of image `sizes` values, using $content_width instead of image's full size.
1045 1128 *
1046 - * @param array $sizes An array of media query breakpoints.
1047 - * @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 + *
1048 1133 * @uses Jetpack::get_content_width
1049 1134 * @return array An array of media query breakpoints.
1050 1135 */
1051 - 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 +
1052 1141 if ( ! doing_filter( 'the_content' ) ) {
1053 1142 return $sizes;
1054 1143 }
1055 1144 $content_width = Image_CDN_Core::get_jetpack_content_width();
@@ -1064,8 +1153,19 @@
1064 1153 return sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $content_width );
1065 1154 }
1066 1155
1067 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 + /**
1068 1168 * * GENERAL FUNCTIONS
1069 1169 **/
1070 1170
1071 1171 /**
@@ -1228,29 +1328,29 @@
1228 1328 return is_array( self::$image_sizes ) ? self::$image_sizes : array();
1229 1329 }
1230 1330
1231 1331 /**
1232 - * Enqueue Photon helper script
1332 + * Find registered image size name if it exists.
1233 1333 *
1234 - * @uses wp_enqueue_script, plugins_url
1235 - * @action wp_enqueue_script
1236 - * @return null
1334 + * @param string|int|int[] $size Image size name if registered, or false if not.
1237 1335 */
1238 - public function action_wp_enqueue_scripts() {
1239 - if ( self::is_amp_endpoint() ) {
1240 - 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;
1241 1342 }
1242 1343
1243 - Assets::register_script(
1244 - 'jetpack-photon',
1245 - '../dist/image-cdn.js',
1246 - __FILE__,
1247 - array(
1248 - 'enqueue' => true,
1249 - 'nonminpath' => 'js/image-cdn.js',
1250 - 'in_footer' => true,
1251 - )
1252 - );
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;
1253 1353 }
1254 1354
1255 1355 /**
1256 1356 * Determine if image_downsize should utilize Photon via REST API.
@@ -1374,6 +1474,15 @@
1374 1474 * @return bool Whether AMP page.
1375 1475 */
1376 1476 private static function is_amp_endpoint() {
1377 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;
1378 1487 }
1379 1488 }