This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -0,0 +1,334 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.7.29] - 2026-06-15
### Changed
- Internal updates.
## [0.7.28] - 2026-06-08
### Changed
- Internal updates.
## [0.7.27] - 2026-05-25
### Fixed
- Phan: Address `PhanPluginDuplicateConditionalNullCoalescing` violations. [#48887]
## [0.7.26] - 2026-05-19
### Changed
- Internal updates.
## [0.7.25] - 2026-05-04
### Changed
- Internal: No longer require automattic/jetpack-changelogger as a per-project dev dependency. [#48225]
## [0.7.24] - 2025-11-24
### Fixed
- Phan: Address `PhanPossiblyUndeclaredVariable` violations. [#45911]
## [0.7.23] - 2025-11-10
### Changed
- Tests: Ensure PHP 8.5 compatibility. [#45769]
## [0.7.22] - 2025-10-20
### Changed
- Internal updates.
## [0.7.21] - 2025-09-19
### Changed
- Internal updates.
## [0.7.20] - 2025-09-15
### Fixed
- Ensure compatibility with images added via Breakdance blocks. [#45059]
## [0.7.19] - 2025-09-08
### Changed
- Internal updates.
## [0.7.18] - 2025-08-25
### Fixed
- Prevent an error when attempting to filter null. [#44874]
## [0.7.17] - 2025-08-18
### Fixed
- Add defensive check for `is_string` before trimming photon url. [#44822]
## [0.7.16] - 2025-08-14
### Changed
- Update package dependencies. [#44725]
## [0.7.15] - 2025-08-05
### Changed
- Add openlibrary.org CDN to ignore list. [#44627]
## [0.7.14] - 2025-07-21
### Changed
- Internal updates. [#39303]
## [0.7.13] - 2025-06-02
### Changed
- Internal updates.
## [0.7.12] - 2025-04-28
### Changed
- Internal updates.
## [0.7.11] - 2025-04-14
### Changed
- Internal updates.
## [0.7.10] - 2025-03-24
### Changed
- Internal updates.
## [0.7.9] - 2025-03-17
### Changed
- Internal updates.
## [0.7.8] - 2025-03-12
### Changed
- Internal updates.
## [0.7.7] - 2025-03-05
### Changed
- Internal updates.
## [0.7.6] - 2025-02-24
### Changed
- Internal updates.
## [0.7.5] - 2025-02-17
### Fixed
- Do not serve images using unsupported extensions with the cdn_url method. [#41760]
## [0.7.4] - 2025-02-03
### Fixed
- Code: Remove extra params on function calls. [#41263]
## [0.7.3] - 2025-01-20
### Fixed
- General: Ensure that double encoding doesn't happen. [#40886]
## [0.7.2] - 2024-12-16
### Changed
- Internal updates.
## [0.7.1] - 2024-11-25
### Changed
- Updated dependencies. [#40286]
## [0.7.0] - 2024-11-18
### Removed
- General: Update minimum PHP version to 7.2. [#40147]
## [0.6.0] - 2024-11-11
### Changed
- Resource Hint: use preconnect instead of dns-prefetch [#39883]
## [0.5.3] - 2024-11-04
### Added
- Enable test coverage. [#39961]
### Fixed
- Fix PHPUnit coverage warnings. [#39989]
## [0.5.2] - 2024-10-29
### Changed
- Internal updates. [#39303]
## [0.5.1] - 2024-10-10
### Fixed
- Avoid deprecation notice when an image URL does not have an expected format. [#39685]
- URL encode path parts of an image [#39560]
## [0.5.0] - 2024-10-07
### Added
- Add a public method to check if a URL is CDN url. [#39635]
## [0.4.9] - 2024-09-09
### Changed
- Update dependencies. [#39260]
## [0.4.8] - 2024-09-03
### Fixed
- Avoid a fatal error if an `<img>` tag has width or height that's not an integer or percentage.
## [0.4.7] - 2024-08-29
### Changed
- Rely on WordPress HTML API to parse HTML instead of Regex [#32700]
## [0.4.6] - 2024-08-26
### Changed
- Updated package dependencies. [#39004]
## [0.4.5] - 2024-08-19
### Fixed
- Lossless image optimization for images (should improve performance with no visible changes). [#38750]
## [0.4.4] - 2024-08-05
### Changed
- Do not serve media from Amazon CDN from Jetpack's CDN. [#38682]
## [0.4.3] - 2024-06-21
### Changed
- Image CDN: Added support for query strings in image URLs [#37931]
- More closely match core behavior while downsizing images [#37946]
## [0.4.2] - 2024-06-11
### Fixed
- Add additional check to avoid PHP deprecation warnings. [#37788]
## [0.4.1] - 2024-05-20
### Changed
- Internal updates.
## [0.4.0] - 2024-05-06
### Removed
- Lazy Loading: Removed compatibility script for Jetpack Lazy Loading module. [#37069]
## [0.3.7] - 2024-04-29
### Changed
- Internal updates.
## [0.3.6] - 2024-04-25
### Changed
- Update dependencies. [#33960]
## [0.3.5] - 2024-04-22
### Fixed
- WP.com: Don't Photonize images on private WordPress.com sites. [#36876]
## [0.3.4] - 2024-04-08
### Changed
- Update filter docblock to match possible types. [#36731]
## [0.3.3] - 2024-03-15
### Changed
- Internal updates.
## [0.3.2] - 2023-12-03
### Changed
- Internal updates.
## [0.3.1] - 2023-11-24
### Changed
- Replaced usage of strpos() with str_contains(). [#34137]
- Replaced usage of substr() with str_starts_with() and str_ends_with(). [#34207]
## [0.3.0] - 2023-11-20
### Changed
- Replaced usage of strpos() with str_starts_with(). [#34135]
- The package now requires PHP >= 7.0. [#34192]
## [0.2.8] - 2023-11-03
### Changed
- Update dependencies. [#33946]
## [0.2.7] - 2023-10-16
### Added
- Added HEIC (`*.heic`) to list of images types allowed to be passed through Photon. [#33494]
## [0.2.6] - 2023-09-28
### Fixed
- Use WordPress `str_ends_with` polyfill. [#33288]
## [0.2.5] - 2023-09-19
- Minor internal updates.
## [0.2.4] - 2023-08-28
### Changed
- Updated package dependencies. [#32605]
## [0.2.3] - 2023-06-06
### Fixed
- Photon: fix potential bug where two "?" characters might be added to a url [#30865]
## [0.2.2] - 2023-05-15
### Added
- Add compatibility layer for the ActivityPub plugin [#30298]
## [0.2.1] - 2023-05-11
### Added
- Added `is_enabled()` method to check if image CDN is enabled by any plugin' [#30582]
## [0.2.0] - 2023-05-08
### Added
- Add Jetpack Autoloader package suggestion. [#29988]
### Changed
- Update photon compatibility to allow using the package in Jetpack [#30050]
### Removed
- Remove unused method used to update images in Open Graph Meta tags. [#30338]
## 0.1.0 - 2023-04-06
### Added
- Add image CDN package. [#29561]
[0.7.29]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.28...v0.7.29
[0.7.28]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.27...v0.7.28
[0.7.27]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.26...v0.7.27
[0.7.26]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.25...v0.7.26
[0.7.25]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.24...v0.7.25
[0.7.24]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.23...v0.7.24
[0.7.23]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.22...v0.7.23
[0.7.22]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.21...v0.7.22
[0.7.21]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.20...v0.7.21
[0.7.20]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.19...v0.7.20
[0.7.19]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.18...v0.7.19
[0.7.18]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.17...v0.7.18
[0.7.17]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.16...v0.7.17
[0.7.16]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.15...v0.7.16
[0.7.15]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.14...v0.7.15
[0.7.14]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.13...v0.7.14
[0.7.13]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.12...v0.7.13
[0.7.12]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.11...v0.7.12
[0.7.11]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.10...v0.7.11
[0.7.10]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.9...v0.7.10
[0.7.9]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.8...v0.7.9
[0.7.8]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.7...v0.7.8
[0.7.7]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.6...v0.7.7
[0.7.6]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.5...v0.7.6
[0.7.5]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.4...v0.7.5
[0.7.4]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.3...v0.7.4
[0.7.3]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.2...v0.7.3
[0.7.2]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.1...v0.7.2
[0.7.1]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.7.0...v0.7.1
[0.7.0]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.6.0...v0.7.0
[0.6.0]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.5.3...v0.6.0
[0.5.3]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.5.2...v0.5.3
[0.5.2]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.5.1...v0.5.2
[0.5.1]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.5.0...v0.5.1
[0.5.0]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.4.9...v0.5.0
[0.4.9]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.4.8...v0.4.9
[0.4.8]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.4.7...v0.4.8
[0.4.7]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.4.6...v0.4.7
[0.4.6]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.4.5...v0.4.6
[0.4.5]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.4.4...v0.4.5
[0.4.4]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.4.3...v0.4.4
[0.4.3]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.4.2...v0.4.3
[0.4.2]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.4.1...v0.4.2
[0.4.1]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.4.0...v0.4.1
[0.4.0]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.3.7...v0.4.0
[0.3.7]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.3.6...v0.3.7
[0.3.6]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.3.5...v0.3.6
[0.3.5]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.3.4...v0.3.5
[0.3.4]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.3.3...v0.3.4
[0.3.3]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.3.2...v0.3.3
[0.3.2]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.3.1...v0.3.2
[0.3.1]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.2.8...v0.3.0
[0.2.8]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.2.7...v0.2.8
[0.2.7]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.2.6...v0.2.7
[0.2.6]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.2.5...v0.2.6
[0.2.5]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.2.4...v0.2.5
[0.2.4]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.2.3...v0.2.4
[0.2.3]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.2.2...v0.2.3
[0.2.2]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.2.1...v0.2.2
[0.2.1]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/Automattic/jetpack-image-cdn/compare/v0.1.0...v0.2.0
@@ -0,0 +1,447 @@
<?php
/**
* Core Image CDN functionality.
*
* It should be available even if Image CDN is not active.
*
* @package automattic/jetpack-image-cdn
*/
namespace Automattic\Jetpack\Image_CDN;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host;
/**
* A static class that provides core Image CDN functionality.
*/
class Image_CDN_Core {
/**
* Register hooks.
*/
public static function setup() {
// Add photon compatibility.
require_once __DIR__ . '/compatibility/photon.php';
// Add ActivityPub compatibility.
require_once __DIR__ . '/compatibility/activitypub.php';
// Add Breakdance compatibility.
require_once __DIR__ . '/compatibility/breakdance.php';
/**
* Add an easy way to photon-ize a URL that is safe to call even if Jetpack isn't active.
*
* See: https://jetpack.com/2013/07/11/photon-and-themes/
*/
add_filter( 'jetpack_photon_url', array( __CLASS__, 'cdn_url' ), 10, 3 );
/**
* WordPress.com
*
* If a cropped WP.com-hosted image is the source image, have Photon replicate the crop.
*/
add_filter( 'jetpack_photon_pre_args', array( __CLASS__, 'parse_wpcom_query_args' ), 10, 2 );
add_filter( 'jetpack_photon_skip_for_url', array( __CLASS__, 'banned_domains' ), 9, 2 );
}
/**
* Generates a Photon URL.
*
* @see https://developer.wordpress.com/docs/photon/
*
* @param string $image_url URL to the publicly accessible image you want to manipulate.
* @param array|string $args An array of arguments, e.g. array( 'w' => '300', 'resize' => array( 123, 456 ) ), or in string form (w=123&h=456).
* @param string|null $scheme URL protocol.
* @return string The raw final URL. You should run this through esc_url() before displaying it.
*/
public static function cdn_url( $image_url, $args = array(), $scheme = null ) {
if ( ! is_string( $image_url ) || empty( $image_url ) ) {
return '';
}
$image_url = trim( $image_url );
if ( ! defined( 'IS_WPCOM' ) || ! \IS_WPCOM ) {
/**
* Disables Photon URL processing for local development
*
* @module photon
*
* @since 4.1.0
*
* @param bool false Result of Automattic\Jetpack\Status->is_offline_mode().
*/
if ( true === apply_filters( 'jetpack_photon_development_mode', ( new Status() )->is_offline_mode() ) ) {
return $image_url;
}
}
/**
* Allow specific image URls to avoid going through Photon.
*
* @module photon
*
* @since 3.2.0
*
* @param bool false Should the image be returned as is, without going through Photon. Default to false.
* @param string $image_url Image URL.
* @param array|string $args Array of Photon arguments.
* @param string|null $scheme Image scheme. Default to null.
*/
if ( false !== apply_filters( 'jetpack_photon_skip_for_url', false, $image_url, $args, $scheme ) ) {
return $image_url;
}
/**
* Filter the original image URL before it goes through Photon.
*
* @module photon
*
* @since 1.9.0
*
* @param string $image_url Image URL.
* @param array|string $args Array of Photon arguments.
* @param string|null $scheme Image scheme. Default to null.
*/
$image_url = apply_filters( 'jetpack_photon_pre_image_url', $image_url, $args, $scheme );
/**
* Filter the original Photon image parameters before Photon is applied to an image.
*
* @module photon
*
* @since 1.9.0
*
* @param array|string $args Array of Photon arguments.
* @param string $image_url Image URL.
* @param string|null $scheme Image scheme. Default to null.
*/
$args = apply_filters( 'jetpack_photon_pre_args', $args, $image_url, $scheme );
if ( empty( $image_url ) ) {
return $image_url;
}
$image_url_parts = wp_parse_url( $image_url );
// Unable to parse.
if ( ! is_array( $image_url_parts ) || empty( $image_url_parts['host'] ) || empty( $image_url_parts['path'] ) ) {
return $image_url;
}
// Ensure image extension is acceptable.
if (
! in_array(
strtolower( pathinfo( $image_url_parts['path'], PATHINFO_EXTENSION ) ),
Image_CDN::get_supported_extensions(),
true
)
) {
return $image_url;
}
if ( is_array( $args ) ) {
// Convert values that are arrays into strings.
foreach ( $args as $arg => $value ) {
if ( is_array( $value ) ) {
$args[ $arg ] = implode( ',', $value );
}
}
// Encode values.
// See https://core.trac.wordpress.org/ticket/17923 .
$args = rawurlencode_deep( $args );
}
// Don't photon-ize WPCOM hosted images -- we can serve them up from wpcom directly.
$is_wpcom_image = false;
if ( self::ends_with( strtolower( $image_url_parts['host'] ), '.files.wordpress.com' ) ) {
$is_wpcom_image = true;
if ( isset( $args['ssl'] ) ) {
// Do not send the ssl argument to prevent caching issues.
unset( $args['ssl'] );
}
}
$is_wpcom_private_site = false;
if ( ( new Host() )->is_wpcom_platform() && ( new Status() )->is_private_site() ) {
$is_wpcom_private_site = true;
if ( isset( $args['ssl'] ) ) {
// Do not send the ssl argument to prevent caching issues.
unset( $args['ssl'] );
}
}
// You can't run a Photon URL through Photon again because query strings are stripped.
// So if the image is already a Photon URL, append the new arguments to the existing URL.
// Alternately, if it's a *.files.wordpress.com url or an image on a private WordPress.com Simple site,
// then keep the domain as is.
if (
self::is_cdn_url( $image_url )
|| $is_wpcom_image
|| $is_wpcom_private_site
) {
$photon_url = add_query_arg( $args, $image_url );
return self::cdn_url_scheme( $photon_url, $scheme );
}
/**
* Allow Photon to use query strings as well.
* By default, Photon doesn't support query strings so we ignore them and look only at the path.
* This setting is Photon Server dependent.
*
* @module photon
*
* @since 1.9.0
*
* @param bool false Should images using query strings go through Photon. Default is false.
* @param string $image_url_parts['host'] Image URL's host.
*/
if ( ! apply_filters( 'jetpack_photon_any_extension_for_domain', false, $image_url_parts['host'] ) ) {
// Photon doesn't support query strings so we ignore them and look only at the path.
// However some source images are served via PHP so check the no-query-string extension.
// For future proofing, this is an excluded list of common issues rather than an allow list.
$extension = pathinfo( $image_url_parts['path'], PATHINFO_EXTENSION );
if ( empty( $extension ) || in_array( $extension, array( 'php', 'ashx' ), true ) ) {
return $image_url;
}
}
$image_host_path = $image_url_parts['host'] . static::escape_path( $image_url_parts['path'] );
/**
* Filters the domain used by the Photon module.
*
* @module photon
*
* @since 3.4.2
*
* @param string https://i0.wp.com Domain used by Photon.
* @param string $image_url URL of the image to be photonized.
*/
$photon_domain = apply_filters( 'jetpack_photon_domain', 'https://i0.wp.com', $image_url );
$photon_domain = trailingslashit( esc_url( $photon_domain ) );
$photon_url = $photon_domain . $image_host_path;
/**
* Add query strings to Photon URL.
* By default, Photon doesn't support query strings so we ignore them.
* This setting is Photon Server dependent.
*
* @module photon
*
* @since 1.9.0
*
* @param bool false Should query strings be added to the image URL. Default is false.
* @param string $image_url_parts['host'] Image URL's host.
*/
if ( isset( $image_url_parts['query'] ) && apply_filters( 'jetpack_photon_add_query_string_to_domain', false, $image_url_parts['host'] ) ) {
$photon_url .= '?q=' . rawurlencode( $image_url_parts['query'] );
}
if ( $args ) {
if ( is_array( $args ) ) {
$photon_url = add_query_arg( $args, $photon_url );
} elseif ( strpos( $photon_url, '?' ) !== false ) {
$photon_url .= '&' . $args;
} else {
$photon_url .= '?' . $args;
}
}
if ( isset( $image_url_parts['scheme'] ) && 'https' === $image_url_parts['scheme'] ) {
$photon_url = add_query_arg( array( 'ssl' => 1 ), $photon_url );
}
return self::cdn_url_scheme( $photon_url, $scheme );
}
/**
* Checks if a given URL is a Photon URL.
*
* @since 0.5.0
* @param string $url The URL to check.
* @return bool True if the URL is a Photon URL, false otherwise.
*/
public static function is_cdn_url( $url ) {
$parsed_url = wp_parse_url( $url );
if ( ! $parsed_url ) {
return false;
}
// See usage in ::cdn_url for documentation of this filter
$custom_photon_url = apply_filters( 'jetpack_photon_domain', '', $url );
$custom_photon_url = esc_url( $custom_photon_url );
return in_array( $parsed_url['host'], array( 'i0.wp.com', 'i1.wp.com', 'i2.wp.com' ), true )
|| wp_parse_url( $custom_photon_url, PHP_URL_HOST ) === $parsed_url['host'];
}
/**
* URL-encodes each path component.
*
* Example:
* Input: "foo/bar baz/baz"
* Output: "foo/bar%20baz/baz"
*
* @param string $path The path to escape.
* @return string The escaped path.
*/
private static function escape_path( $path ) {
$parts = explode( '/', $path );
$parts = array_map( 'rawurldecode', $parts );
$parts = array_map( 'rawurlencode', $parts );
return implode( '/', $parts );
}
/**
* Parses WP.com-hosted image args to replicate the crop.
*
* @param mixed $args Args set during Photon's processing.
* @param string $image_url URL of the image.
* @return array|string Args for Photon to use for the URL.
*/
public static function parse_wpcom_query_args( $args, $image_url ) {
$parsed_url = wp_parse_url( $image_url );
if ( ! $parsed_url ) {
return $args;
}
$image_url_parts = wp_parse_args(
$parsed_url,
array(
'host' => '',
'query' => '',
)
);
if ( ! str_ends_with( $image_url_parts['host'], '.files.wordpress.com' ) ) {
return $args;
}
if ( empty( $image_url_parts['query'] ) ) {
return $args;
}
$wpcom_args = wp_parse_args( $image_url_parts['query'] );
if ( empty( $wpcom_args['w'] ) || empty( $wpcom_args['h'] ) ) {
return $args;
}
// Keep the crop by using "resize".
if ( ! empty( $wpcom_args['crop'] ) ) {
if ( is_array( $args ) ) {
$args = array_merge( array( 'resize' => array( $wpcom_args['w'], $wpcom_args['h'] ) ), $args );
} else {
$args = 'resize=' . rawurlencode( absint( $wpcom_args['w'] ) . ',' . absint( $wpcom_args['h'] ) ) . '&' . $args;
}
} elseif ( is_array( $args ) ) {
$args = array_merge( array( 'fit' => array( $wpcom_args['w'], $wpcom_args['h'] ) ), $args );
} else {
$args = 'fit=' . rawurlencode( absint( $wpcom_args['w'] ) . ',' . absint( $wpcom_args['h'] ) ) . '&' . $args;
}
return $args;
}
/**
* Sets the scheme for a URL
*
* @param string $url URL to set scheme.
* @param string $scheme Scheme to use. Accepts http, https, network_path.
*
* @return string URL.
*/
public static function cdn_url_scheme( $url, $scheme ) {
if ( ! in_array( $scheme, array( 'http', 'https', 'network_path' ), true ) ) {
if ( preg_match( '#^(https?:)?//#', $url ) ) {
return $url;
}
$scheme = 'http';
}
if ( 'network_path' === $scheme ) {
$scheme_slashes = '//';
} else {
$scheme_slashes = "$scheme://";
}
return preg_replace( '#^([a-z:]+)?//#i', $scheme_slashes, $url );
}
/**
* Check to skip Photon for a known domain that shouldn't be Photonized.
*
* @param bool $skip If the image should be skipped by Photon.
* @param string $image_url URL of the image.
*
* @return bool Should the image be skipped by Photon.
*/
public static function banned_domains( $skip, $image_url ) {
$banned_host_patterns = array(
'/^chart\.googleapis\.com$/',
'/^chart\.apis\.google\.com$/',
'/^graph\.facebook\.com$/',
'/\.fbcdn\.net$/',
'/\.paypalobjects\.com$/',
'/\.dropbox\.com$/',
'/\.cdninstagram\.com$/',
'/^(commons|upload)\.wikimedia\.org$/',
'/\.wikipedia\.org$/',
'/^m\.media-amazon\.com$/',
'/^covers\.openlibrary\.org$/',
);
$host = wp_parse_url( $image_url, PHP_URL_HOST );
if ( ! $host ) {
return $skip;
}
foreach ( $banned_host_patterns as $banned_host_pattern ) {
if ( 1 === preg_match( $banned_host_pattern, $host ) ) {
return true;
}
}
return $skip;
}
/**
* Check whether a string ends with a specific substring.
*
* @param string $haystack String we are filtering.
* @param string $needle The substring we are looking for.
* @return bool
*/
public static function ends_with( $haystack, $needle ) {
if ( ! $haystack || ! $needle || ! is_scalar( $haystack ) || ! is_scalar( $needle ) ) {
return false;
}
$haystack = (string) $haystack;
$needle = (string) $needle;
return str_ends_with( $haystack, $needle );
}
/**
* This is a copy of Jetpack::get_content_width()
* for backwards compatibility.
*/
public static function get_jetpack_content_width() {
$content_width = ( isset( $GLOBALS['content_width'] ) && is_numeric( $GLOBALS['content_width'] ) )
? $GLOBALS['content_width']
: false;
/**
* Filter the Content Width value.
*
* @since 2.2.3
*
* @param string $content_width Content Width value.
*/
return apply_filters( 'jetpack_content_width', $content_width );
}
}
@@ -0,0 +1,199 @@
<?php
/**
* The Image Sizes library.
*
* @package automattic/jetpack-image-cdn
*/
namespace Automattic\Jetpack\Image_CDN;
/**
* Class Image_CDN_Image_Sizes
*
* Manages image resizing via Jetpack CDN Service.
*/
class Image_CDN_Image_Sizes {
/**
* Attachment metadata.
*
* @var array
*/
public $data;
/**
* Image to be resized.
*
* @var Image_CDN_Image
*/
public $image;
/**
* Intermediate sizes.
*
* @var null|array
*/
public static $sizes = null;
/**
* Construct new sizes meta
*
* @param int $attachment_id Attachment ID.
* @param array $data Attachment metadata.
*/
public function __construct( $attachment_id, $data ) {
$this->data = $data;
$this->image = new Image_CDN_Image( $data, get_post_mime_type( $attachment_id ) );
$this->generate_sizes();
}
/**
* Generate sizes for attachment.
*
* @return array Array of sizes; empty array as failure fallback.
*/
protected function generate_sizes() {
// There is no need to generate the sizes anew for every single image.
if ( null !== self::$sizes ) {
return self::$sizes;
}
/*
* The following logic is copied over from wp_generate_attachment_metadata
*/
$_wp_additional_image_sizes = wp_get_additional_image_sizes();
$sizes = array();
$intermediate_image_sizes = get_intermediate_image_sizes();
foreach ( $intermediate_image_sizes as $s ) {
$sizes[ $s ] = array(
'width' => '',
'height' => '',
'crop' => false,
);
if ( isset( $_wp_additional_image_sizes[ $s ]['width'] ) ) {
// For theme-added sizes.
$sizes[ $s ]['width'] = (int) $_wp_additional_image_sizes[ $s ]['width'];
} else {
// For default sizes set in options.
$sizes[ $s ]['width'] = get_option( "{$s}_size_w" );
}
if ( isset( $_wp_additional_image_sizes[ $s ]['height'] ) ) {
// For theme-added sizes.
$sizes[ $s ]['height'] = (int) $_wp_additional_image_sizes[ $s ]['height'];
} else {
// For default sizes set in options.
$sizes[ $s ]['height'] = get_option( "{$s}_size_h" );
}
if ( isset( $_wp_additional_image_sizes[ $s ]['crop'] ) ) {
// For theme-added sizes.
$sizes[ $s ]['crop'] = $_wp_additional_image_sizes[ $s ]['crop'];
} else {
// For default sizes set in options.
$sizes[ $s ]['crop'] = get_option( "{$s}_crop" );
}
}
self::$sizes = $sizes;
return $sizes;
}
/**
* Add filtered sizes.
*
* @return array
*/
public function filtered_sizes() {
// Remove filter preventing the creation of advanced sizes.
remove_filter(
'intermediate_image_sizes_advanced',
array( Image_CDN::class, 'filter_photon_noresize_intermediate_sizes' )
);
// Compatibility fix: Ensure that Jetpack's inbuilt Photon does not get in the way.
// No need to re-enable this filter if present, as its functionality will be handled
// by the Image_CDN filter added below.
remove_filter(
'intermediate_image_sizes_advanced',
array( 'Jetpack_Photon', 'filter_photon_noresize_intermediate_sizes' )
);
/** This filter is documented in wp-admin/includes/image.php */
$sizes = apply_filters( 'intermediate_image_sizes_advanced', self::$sizes, $this->data );
// Re-add the filter removed above.
add_filter(
'intermediate_image_sizes_advanced',
array( Image_CDN::class, 'filter_photon_noresize_intermediate_sizes' )
);
return (array) $sizes;
}
/**
* Standardises and validates the size_data array.
*
* @param array $size_data Size data array - at least containing height or width key. Can contain crop as well.
*
* @return array Array with populated width, height and crop keys; empty array if no width and height are provided.
*/
public function standardize_size_data( $size_data ) {
$has_at_least_width_or_height = ( isset( $size_data['width'] ) || isset( $size_data['height'] ) );
if ( ! $has_at_least_width_or_height ) {
return array();
}
$defaults = array(
'width' => null,
'height' => null,
'crop' => false,
);
return array_merge( $defaults, $size_data );
}
/**
* Get sizes for attachment post meta.
*
* @return array ImageSizes for attachment postmeta.
*/
public function generate_sizes_meta() {
$metadata = array();
foreach ( $this->filtered_sizes() as $size => $size_data ) {
$size_data = $this->standardize_size_data( $size_data );
if ( empty( $size_data ) ) {
continue;
}
$resized_image = $this->resize( $size_data );
if ( is_array( $resized_image ) ) {
$metadata[ $size ] = $resized_image;
}
}
return $metadata;
}
/**
* Resize image.
*
* @param array $size_data Resize parameters.
*
* @return array|\WP_Error Array for usage in $metadata['sizes']; WP_Error on failure.
*/
protected function resize( $size_data ) {
return $this->image->get_size( $size_data );
}
}
@@ -0,0 +1,267 @@
<?php
/**
* The Image Class.
*
* @package automattic/jetpack-image-cdn
*/
namespace Automattic\Jetpack\Image_CDN;
use WP_Error;
/**
* Represents a resizable image, exposing properties necessary for properly generating srcset.
*/
class Image_CDN_Image {
/**
* Attachment's Filename.
*
* @var string
*/
public $filename;
/**
* Attachment's mime-type, WP_Error on failure when recalculating the dimensions.
*
* @var string|WP_Error
*/
private $mime_type;
/**
* Image original width.
*
* @var int
*/
private $original_width;
/**
* Image original height.
*
* @var int
*/
private $original_height;
/**
* Current attachment's width.
*
* @var int
*/
private $width;
/**
* Current attachment's height.
*
* @var int
*/
private $height;
/**
* Whether the attachment has been resized yet, or not.
*
* @var bool
*/
private $is_resized = false;
/**
* Constructs the image object.
*
* The $data array should provide at least
* file : string Image file path
* width : int Image width
* height : int Image height
*
* @param array $data Array of attachment metadata, typically value of _wp_attachment_metadata postmeta.
* @param string|WP_Error $mime_type Typically value returned from get_post_mime_type function.
*/
public function __construct( $data, $mime_type ) {
$this->filename = $data['file'];
$this->original_width = $data['width'];
$this->original_height = $data['height'];
$this->width = $this->original_width;
$this->height = $this->original_height;
$this->mime_type = $mime_type;
}
/**
* Resizes the image to given size.
*
* @param array $size_data Array of width, height, and crop properties of a size.
*
* @return bool|WP_Error True if resize was successful, WP_Error on failure.
*/
public function resize( $size_data ) {
$dimensions = $this->image_resize_dimensions( $size_data['width'], $size_data['height'], $size_data['crop'] );
if ( true === is_wp_error( $dimensions ) ) {
return $dimensions; // Returns WP_Error.
}
if ( true === is_wp_error( $this->mime_type ) ) {
return $this->mime_type; // Returns WP_Error.
}
$this->set_width_height( $dimensions );
$this->is_resized = true;
return true;
}
/**
* Generates size data for usage in $metadata['sizes'];.
*
* @param array $size_data Array of width, height, and crop properties of a size.
*
* @return array|WP_Error An array containing file, width, height, and mime-type keys and it's values. WP_Error on failure.
*/
public function get_size( $size_data ) {
$is_resized = $this->resize( $size_data );
if ( true === is_wp_error( $is_resized ) ) {
return $is_resized;
}
return array(
'file' => $this->get_filename(),
'width' => $this->get_width(),
'height' => $this->get_height(),
'mime-type' => $this->get_mime_type(),
);
}
/**
* Resets the image to it's original dimensions.
*
* @return bool True on successful reset to original dimensions.
*/
public function reset_to_original() {
$this->width = $this->original_width;
$this->height = $this->original_height;
$this->is_resized = false;
return true;
}
/**
* Return the basename filename. If the image has been resized, including
* the resizing params for Jetpack CDN.
*
* @return string Basename of the filename.
*/
public function get_filename() {
return wp_basename( $this->get_raw_filename() );
}
/**
* Return the absolute filename. If the image has been resized, including
* the resizing params for Jetpack CDN.
*
* @return string Filename.
*/
public function get_raw_filename() {
return $this->is_resized() ? $this->get_resized_filename() : $this->filename;
}
/**
* Returns current image width. Either original, or after resize.
*
* @return int
*/
public function get_width() {
return (int) $this->width;
}
/**
* Returns current image height. Either original, or after resize.
*
* @return int
*/
public function get_height() {
return (int) $this->height;
}
/**
* Returns image mime type.
*
* @return string|WP_Error Image's mime type or WP_Error if it was not determined.
*/
public function get_mime_type() {
return $this->mime_type;
}
/**
* Checks the resize status of the image.
*
* @return bool If the image has been resized.
*/
public function is_resized() {
return ( true === $this->is_resized );
}
/**
* Get filename with proper args for the Photon service.
*
* @return string Filename with query args for Photon service
*/
protected function get_resized_filename() {
$query_args = array(
'resize' => implode(
',',
array(
$this->get_width(),
$this->get_height(),
)
),
);
return add_query_arg( $query_args, $this->filename );
}
/**
* Get resize dimensions used for the Jetpack CDN service.
*
* Converts the list of values returned from `image_resize_dimensions()` to
* associative array for the sake of more readable code no relying on index
* nor `list`.
*
* @param int $max_width Maximum width.
* @param int $max_height Maximum height.
* @param bool|array $crop Cropping parameters.
*
* @return array|WP_Error Array of dimensions matching the parameters to imagecopyresampled. WP_Error on failure.
*/
protected function image_resize_dimensions( $max_width, $max_height, $crop ) {
$dimensions = image_resize_dimensions( $this->original_width, $this->original_height, $max_width, $max_height, $crop );
if ( ! $dimensions ) {
return new WP_Error( 'error_getting_dimensions', __( 'Could not calculate resized image dimensions', 'jetpack-image-cdn' ), $this->filename );
}
return array_combine(
array(
'dst_x',
'dst_y',
'src_x',
'src_y',
'dst_w',
'dst_h',
'src_w',
'src_h',
),
$dimensions
);
}
/**
* Sets proper width and height from dimensions.
*
* @param array $dimensions an array of image dimensions.
* @return void
*/
protected function set_width_height( $dimensions ) {
$this->width = (int) $dimensions['dst_w'];
$this->height = (int) $dimensions['dst_h'];
}
}
@@ -0,0 +1,28 @@
<?php
/**
* Package description here
*
* @package automattic/jetpack-image-cdn
*/
namespace Automattic\Jetpack\Image_CDN;
/**
* Class description.
*/
class Image_CDN_Setup {
/**
* Initialize Image CDN.
*/
public static function load() {
\Automattic\Jetpack\Assets::add_resource_hint(
array(
'//i0.wp.com',
),
'preconnect'
);
Image_CDN::instance();
}
}
@@ -0,0 +1,62 @@
<?php
/**
* Compatibility functions for the ActivityPub plugin.
*
* @since 0.2.2
*
* @package automattic/jetpack-image-cdn
*/
namespace Automattic\Jetpack\Image_CDN\Compatibility;
use Automattic\Jetpack\Image_CDN\Image_CDN;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Hook the compatibility functions into ActivityPub filters if necessary.
*
* @since 0.2.2
*
* @return void
*/
function load_activitypub_compat() {
if (
/**
* Allow disabling Jetpack's image CDN for ActivityPub requests.
*
* @since 0.2.2
*
* @param bool $should_disable_photon Should the CDN be disabled for that request. Default to false.
*/
apply_filters( 'jetpack_activitypub_post_disable_cdn', false )
) {
add_action( 'activitypub_get_image_pre', __NAMESPACE__ . '\disable_photon' );
add_action( 'activitypub_get_image_post', __NAMESPACE__ . '\enable_photon' );
}
}
add_action( 'plugins_loaded', __NAMESPACE__ . '\load_activitypub_compat' );
/**
* Disable Jetpack's Image CDN processing for this request.
*
* @see https://github.com/pfefferle/wordpress-activitypub/pull/309
*
* @return void
*/
function disable_photon() {
remove_filter( 'image_downsize', array( Image_CDN::instance(), 'filter_image_downsize' ) );
}
/**
* Re-enable Jetpack's Image CDN processing after the request.
*
* @see https://github.com/pfefferle/wordpress-activitypub/pull/309
*
* @return void
*/
function enable_photon() {
add_filter( 'image_downsize', array( Image_CDN::instance(), 'filter_image_downsize' ), 10, 3 );
}
@@ -0,0 +1,42 @@
<?php
/**
* Compatibility functions for the Breakdance plugin.
*
* @since 0.7.20
*
* @package automattic/jetpack-image-cdn
*/
namespace Automattic\Jetpack\Image_CDN\Compatibility;
use Automattic\Jetpack\Image_CDN\Image_CDN;
/**
* Hook the compatibility functions into Breakdance filters.
*
* @since 0.7.20
*
* @return void
*/
function load_breakdance_compat() {
add_filter( 'breakdance_singular_content', __NAMESPACE__ . '\use_image_cdn' );
}
add_action( 'plugins_loaded', __NAMESPACE__ . '\load_breakdance_compat' );
/**
* Unless the 'Apply the_content filter to Breakdance content' option is enabled
* in the Breakdance settings, the content will not be filtered by the_content filter.
* This ensures that images are passed through Image CDN when it's enabled.
*
* @since 0.7.20
*
* @param string $content The content to filter.
* @return string The filtered content.
*/
function use_image_cdn( $content ) {
if ( Image_CDN::is_enabled() ) {
$content = Image_CDN::filter_the_content( $content );
}
return $content;
}
@@ -0,0 +1,53 @@
<?php
/**
* Compatibility for Photon
*
* Replace photon with image-cdn.
*
* @package automattic/jetpack-image-cdn
*/
namespace Automattic\Jetpack\Image_CDN\Compatibility;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Disable the unpackaged photon version living in Jetpack.
*
* Before the package, Jetpack had the photon functionality within the plugin. To avoid double
* activation, we need to disable the photon functionality in Jetpack to avoid potential conflicts.
*
* If this package is installed via a different plugin, e.g. Boost, Image CDN should be served instead
* of the photon module in Jetpack.
*/
function jetpack_image_cdn_photon_compat() {
/*
* Photon used have different functions names. They are later replaced by methods in
* Image_CDN_Core class. And the filters are now handled by the Image_CDN_Core class itself.
*/
remove_filter( 'jetpack_photon_url', 'jetpack_photon_url', 10 );
remove_filter( 'jetpack_photon_pre_args', 'jetpack_photon_parse_wpcom_query_args', 10 );
remove_filter( 'jetpack_photon_skip_for_url', 'jetpack_photon_banned_domains', 9 );
remove_filter( 'widget_text', 'jetpack_photon_support_text_widgets' );
/*
* If photon is active in an old version of Jetpack which is using the unpackaged photon
* fake loading the module in Jetpack itself. This will prevent jetpack from loading
* the photon module, and we will use the image CDN package instead.
*
* When this is happening, the package is likely being loaded from a different plugin, e.g. Boost.
*/
if ( class_exists( 'Jetpack' ) && defined( 'JETPACK__VERSION' ) && ( version_compare( JETPACK__VERSION, '12.1', '<=' ) ) ) {
do_action( 'jetpack_module_loaded_photon' );
}
}
/*
* Remove the hooks that Jetpack uses to load photon and pretend that the module is already loaded.
*
* Jetpack hooks the filters as it loads. So, we can remove the filters when `plugins_loaded` fires.
* This would make sure Jetpack has already loaded and hooked the filters.
*/
add_action( 'plugins_loaded', __NAMESPACE__ . '\jetpack_image_cdn_photon_compat' );