PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 10.7.2
Jetpack – WP Security, Backup, Speed, & Growth v10.7.2
15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / class.photon.php
jetpack Last commit date
3rd-party 4 years ago _inc 4 years ago css 4 years ago extensions 4 years ago images 4 years ago jetpack_vendor 4 years ago json-endpoints 3 years ago modules 1 year ago sal 4 years ago src 4 years ago vendor 4 years ago views 4 years ago CHANGELOG.md 4 years ago LICENSE.txt 5 years ago SECURITY.md 5 years ago class-jetpack-connection-status.php 5 years ago class-jetpack-pre-connection-jitms.php 4 years ago class-jetpack-recommendations-banner.php 4 years ago class-jetpack-stats-dashboard-widget.php 4 years ago class-jetpack-wizard-banner.php 5 years ago class-jetpack-xmlrpc-methods.php 5 years ago class.frame-nonce-preview.php 4 years ago class.jetpack-admin.php 4 years ago class.jetpack-affiliate.php 4 years ago class.jetpack-autoupdate.php 4 years ago class.jetpack-bbpress-json-api.compat.php 5 years ago class.jetpack-cli.php 4 years ago class.jetpack-client-server.php 4 years ago class.jetpack-connection-banner.php 4 years ago class.jetpack-data.php 5 years ago class.jetpack-gutenberg.php 4 years ago class.jetpack-heartbeat.php 4 years ago class.jetpack-idc.php 4 years ago class.jetpack-modules-list-table.php 4 years ago class.jetpack-network-sites-list-table.php 4 years ago class.jetpack-network.php 4 years ago class.jetpack-plan.php 4 years ago class.jetpack-post-images.php 4 years ago class.jetpack-twitter-cards.php 5 years ago class.jetpack-user-agent.php 4 years ago class.jetpack.php 4 years ago class.json-api-endpoints.php 3 years ago class.json-api.php 4 years ago class.photon.php 4 years ago composer.json 4 years ago functions.compat.php 5 years ago functions.cookies.php 5 years ago functions.gallery.php 6 years ago functions.global.php 4 years ago functions.opengraph.php 4 years ago functions.photon.php 4 years ago jest.config.js 4 years ago jetpack.php 1 year ago json-api-config.php 5 years ago json-endpoints.php 7 years ago load-jetpack.php 4 years ago locales.php 7 years ago readme.txt 1 year ago require-lib.php 5 years ago uninstall.php 5 years ago wpml-config.xml 10 years ago
class.photon.php
1391 lines
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 /**
3 * Class for photon functionality.
4 *
5 * @package automattic/jetpack
6 */
7
8 use Automattic\Jetpack\Assets;
9
10 /**
11 * Class Jetpack_Photon
12 */
13 class Jetpack_Photon {
14 /**
15 * Singleton.
16 *
17 * @var null
18 */
19 private static $instance = null;
20
21 /**
22 * Allowed extensions.
23 *
24 * @var string[] Allowed extensions must match https://code.trac.wordpress.org/browser/photon/index.php#L41
25 */
26 protected static $extensions = array(
27 'gif',
28 'jpg',
29 'jpeg',
30 'png',
31 'webp', // Jetpack assumes Photon_OpenCV backend class is being used on the server. See link in docblock.
32 );
33
34 /**
35 * Image sizes.
36 *
37 * Don't access this directly. Instead, use self::image_sizes() so it's actually populated with something.
38 *
39 * @var array Image sizes.
40 */
41 protected static $image_sizes = null;
42
43 /**
44 * Singleton implementation
45 *
46 * @return object
47 */
48 public static function instance() {
49 if ( ! is_a( self::$instance, 'Jetpack_Photon' ) ) {
50 self::$instance = new Jetpack_Photon();
51 self::$instance->setup();
52 }
53
54 return self::$instance;
55 }
56
57 /**
58 * Silence is golden.
59 */
60 private function __construct() {}
61
62 /**
63 * Register actions and filters, but only if basic Photon functions are available.
64 * The basic functions are found in ./functions.photon.php.
65 *
66 * @uses add_action, add_filter
67 * @return null
68 */
69 private function setup() {
70 if ( ! function_exists( 'jetpack_photon_url' ) ) {
71 return;
72 }
73
74 // Images in post content and galleries.
75 add_filter( 'the_content', array( __CLASS__, 'filter_the_content' ), 999999 );
76 add_filter( 'get_post_galleries', array( __CLASS__, 'filter_the_galleries' ), 999999 );
77 add_filter( 'widget_media_image_instance', array( __CLASS__, 'filter_the_image_widget' ), 999999 );
78
79 // Core image retrieval.
80 add_filter( 'image_downsize', array( $this, 'filter_image_downsize' ), 10, 3 );
81 add_filter( 'rest_request_before_callbacks', array( $this, 'should_rest_photon_image_downsize' ), 10, 3 );
82 add_action( 'rest_after_insert_attachment', array( $this, 'should_rest_photon_image_downsize_insert_attachment' ), 10, 2 );
83 add_filter( 'rest_request_after_callbacks', array( $this, 'cleanup_rest_photon_image_downsize' ) );
84
85 // Responsive image srcset substitution.
86 add_filter( 'wp_calculate_image_srcset', array( $this, 'filter_srcset_array' ), 10, 5 );
87 add_filter( 'wp_calculate_image_sizes', array( $this, 'filter_sizes' ), 1, 2 ); // Early so themes can still easily filter.
88
89 // Helpers for maniuplated images.
90 add_action( 'wp_enqueue_scripts', array( $this, 'action_wp_enqueue_scripts' ), 9 );
91
92 /**
93 * Allow Photon to disable uploaded images resizing and use its own resize capabilities instead.
94 *
95 * @module photon
96 *
97 * @since 7.1.0
98 *
99 * @param bool false Should Photon enable noresize mode. Default to false.
100 */
101 if ( apply_filters( 'jetpack_photon_noresize_mode', false ) ) {
102 $this->enable_noresize_mode();
103 }
104 }
105
106 /**
107 * Enables the noresize mode for Photon, allowing to avoid intermediate size files generation.
108 */
109 private function enable_noresize_mode() {
110 jetpack_require_lib( 'class.jetpack-photon-image-sizes' );
111
112 // The main objective of noresize mode is to disable additional resized image versions creation.
113 // This filter handles removal of additional sizes.
114 add_filter( 'intermediate_image_sizes_advanced', array( __CLASS__, 'filter_photon_noresize_intermediate_sizes' ) );
115
116 // Load the noresize srcset solution on priority of 20, allowing other plugins to set sizes earlier.
117 add_filter( 'wp_get_attachment_metadata', array( __CLASS__, 'filter_photon_norezise_maybe_inject_sizes' ), 20, 2 );
118
119 // Photonize thumbnail URLs in the API response.
120 add_filter( 'rest_api_thumbnail_size_urls', array( __CLASS__, 'filter_photon_noresize_thumbnail_urls' ) );
121
122 // This allows to assign the Photon domain to images that normally use the home URL as base.
123 add_filter( 'jetpack_photon_domain', array( __CLASS__, 'filter_photon_norezise_domain' ), 10, 2 );
124
125 add_filter( 'the_content', array( __CLASS__, 'filter_content_add' ), 0 );
126
127 // Jetpack hooks in at six nines (999999) so this filter does at seven.
128 add_filter( 'the_content', array( __CLASS__, 'filter_content_remove' ), 9999999 );
129
130 // Regular Photon operation mode filter doesn't run when is_admin(), so we need an additional filter.
131 // This is temporary until Jetpack allows more easily running these filters for is_admin().
132 if ( is_admin() ) {
133 add_filter( 'image_downsize', array( $this, 'filter_image_downsize' ), 5, 3 );
134
135 // Allows any image that gets passed to Photon to be resized via Photon.
136 add_filter( 'jetpack_photon_admin_allow_image_downsize', '__return_true' );
137 }
138 }
139
140 /**
141 * This is our catch-all to strip dimensions from intermediate images in content.
142 * Since this primarily only impacts post_content we do a little dance to add the filter early
143 * to `the_content` and then remove it later on in the same hook.
144 *
145 * @param String $content the post content.
146 * @return String the post content unchanged.
147 */
148 public static function filter_content_add( $content ) {
149 add_filter( 'jetpack_photon_pre_image_url', array( __CLASS__, 'strip_image_dimensions_maybe' ) );
150 return $content;
151 }
152
153 /**
154 * Removing the content filter that was set previously.
155 *
156 * @param String $content the post content.
157 * @return String the post content unchanged.
158 */
159 public static function filter_content_remove( $content ) {
160 remove_filter( 'jetpack_photon_pre_image_url', array( __CLASS__, 'strip_image_dimensions_maybe' ) );
161 return $content;
162 }
163
164 /**
165 * Short circuits the Photon filter to enable Photon processing for any URL.
166 *
167 * @param String $photon_url a proposed Photon URL for the media file.
168 *
169 * @return String an URL to be used for the media file.
170 */
171 public static function filter_photon_norezise_domain( $photon_url ) {
172 return $photon_url;
173 }
174
175 /**
176 * Disables intermediate sizes to disallow resizing.
177 *
178 * @return array Empty array.
179 */
180 public static function filter_photon_noresize_intermediate_sizes() {
181 return array();
182 }
183
184 /**
185 * Filter thumbnail URLS to not generate.
186 *
187 * @param array $sizes Image sizes.
188 *
189 * @return mixed
190 */
191 public static function filter_photon_noresize_thumbnail_urls( $sizes ) {
192 foreach ( $sizes as $size => $url ) {
193 $parts = explode( '?', $url );
194 $arguments = isset( $parts[1] ) ? $parts[1] : array();
195
196 $sizes[ $size ] = jetpack_photon_url( $url, wp_parse_args( $arguments ) );
197 }
198
199 return $sizes;
200 }
201
202 /**
203 * Inject image sizes to attachment metadata.
204 *
205 * @param array $data Attachment metadata.
206 * @param int $attachment_id Attachment's post ID.
207 *
208 * @return array Attachment metadata.
209 */
210 public static function filter_photon_norezise_maybe_inject_sizes( $data, $attachment_id ) {
211 // Can't do much if data is empty.
212 if ( empty( $data ) ) {
213 return $data;
214 }
215 $sizes_already_exist = (
216 true === is_array( $data )
217 && true === array_key_exists( 'sizes', $data )
218 && true === is_array( $data['sizes'] )
219 && false === empty( $data['sizes'] )
220 );
221 if ( $sizes_already_exist ) {
222 return $data;
223 }
224 // Missing some critical data we need to determine sizes, not processing.
225 if ( ! isset( $data['file'] )
226 || ! isset( $data['width'] )
227 || ! isset( $data['height'] )
228 ) {
229 return $data;
230 }
231
232 $mime_type = get_post_mime_type( $attachment_id );
233 $attachment_is_image = preg_match( '!^image/!', $mime_type );
234
235 if ( 1 === $attachment_is_image ) {
236 $image_sizes = new Jetpack_Photon_ImageSizes( $attachment_id, $data );
237 $data['sizes'] = $image_sizes->generate_sizes_meta();
238 }
239 return $data;
240 }
241
242 /**
243 * Inject image sizes to Jetpack REST API responses. This wraps the filter_photon_norezise_maybe_inject_sizes function.
244 *
245 * @param array $sizes Attachment sizes data.
246 * @param int $attachment_id Attachment's post ID.
247 *
248 * @return array Attachment sizes array.
249 */
250 public static function filter_photon_norezise_maybe_inject_sizes_api( $sizes, $attachment_id ) {
251 return self::filter_photon_norezise_maybe_inject_sizes( wp_get_attachment_metadata( $attachment_id ), $attachment_id );
252 }
253
254 /**
255 * * IN-CONTENT IMAGE MANIPULATION FUNCTIONS
256 **/
257
258 /**
259 * Match all images and any relevant <a> tags in a block of HTML.
260 *
261 * @param string $content Some HTML.
262 * @return array An array of $images matches, where $images[0] is
263 * an array of full matches, and the link_url, img_tag,
264 * and img_url keys are arrays of those matches.
265 */
266 public static function parse_images_from_html( $content ) {
267 $images = array();
268
269 if ( preg_match_all( '#(?:<a[^>]+?href=["|\'](?P<link_url>[^\s]+?)["|\'][^>]*?>\s*)?(?P<img_tag><(?:img|amp-img|amp-anim)[^>]*?\s+?src=["|\'](?P<img_url>[^\s]+?)["|\'].*?>){1}(?:\s*</a>)?#is', $content, $images ) ) {
270 foreach ( $images as $key => $unused ) {
271 // Simplify the output as much as possible, mostly for confirming test results.
272 if ( is_numeric( $key ) && $key > 0 ) {
273 unset( $images[ $key ] );
274 }
275 }
276
277 return $images;
278 }
279
280 return array();
281 }
282
283 /**
284 * Try to determine height and width from strings WP appends to resized image filenames.
285 *
286 * @param string $src The image URL.
287 * @return array An array consisting of width and height.
288 */
289 public static function parse_dimensions_from_filename( $src ) {
290 $width_height_string = array();
291
292 if ( preg_match( '#-(\d+)x(\d+)\.(?:' . implode( '|', self::$extensions ) . '){1}$#i', $src, $width_height_string ) ) {
293 $width = (int) $width_height_string[1];
294 $height = (int) $width_height_string[2];
295
296 if ( $width && $height ) {
297 return array( $width, $height );
298 }
299 }
300
301 return array( false, false );
302 }
303
304 /**
305 * Identify images in post content, and if images are local (uploaded to the current site), pass through Photon.
306 *
307 * @param string $content The content.
308 *
309 * @uses self::validate_image_url, apply_filters, jetpack_photon_url, esc_url
310 * @filter the_content
311 *
312 * @return string
313 */
314 public static function filter_the_content( $content ) {
315 $images = self::parse_images_from_html( $content );
316
317 if ( ! empty( $images ) ) {
318 $content_width = Jetpack::get_content_width();
319
320 $image_sizes = self::image_sizes();
321
322 $upload_dir = wp_get_upload_dir();
323
324 foreach ( $images[0] as $index => $tag ) {
325 // Default to resize, though fit may be used in certain cases where a dimension cannot be ascertained.
326 $transform = 'resize';
327
328 // Start with a clean attachment ID each time.
329 $attachment_id = false;
330
331 // Flag if we need to munge a fullsize URL.
332 $fullsize_url = false;
333
334 // Identify image source.
335 $src_orig = $images['img_url'][ $index ];
336 $src = $src_orig;
337
338 /**
339 * Allow specific images to be skipped by Photon.
340 *
341 * @module photon
342 *
343 * @since 2.0.3
344 *
345 * @param bool false Should Photon ignore this image. Default to false.
346 * @param string $src Image URL.
347 * @param string $tag Image Tag (Image HTML output).
348 */
349 if ( apply_filters( 'jetpack_photon_skip_image', false, $src, $tag ) ) {
350 continue;
351 }
352
353 // Support Automattic's Lazy Load plugin.
354 // Can't modify $tag yet as we need unadulterated version later.
355 if ( preg_match( '#data-lazy-src=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
356 $placeholder_src_orig = $src;
357 $placeholder_src = $placeholder_src_orig;
358 $src_orig = $lazy_load_src[1];
359 $src = $src_orig;
360 } elseif ( preg_match( '#data-lazy-original=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
361 $placeholder_src_orig = $src;
362 $placeholder_src = $placeholder_src_orig;
363 $src_orig = $lazy_load_src[1];
364 $src = $src_orig;
365 }
366
367 // Check if image URL should be used with Photon.
368 if ( self::validate_image_url( $src ) ) {
369 // Find the width and height attributes.
370 $width = false;
371 $height = false;
372
373 // First, check the image tag. Note we only check for pixel sizes now; HTML4 percentages have never been correctly
374 // supported, so we stopped pretending to support them in JP 9.1.0.
375 if ( preg_match( '#[\s|"|\']width=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $width_string ) ) {
376 $width = false === strpos( $width_string[1], '%' ) ? $width_string[1] : false;
377 }
378
379 if ( preg_match( '#[\s|"|\']height=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $height_string ) ) {
380 $height = false === strpos( $height_string[1], '%' ) ? $height_string[1] : false;
381 }
382
383 // Detect WP registered image size from HTML class.
384 if ( preg_match( '#class=["|\']?[^"\']*size-([^"\'\s]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $size ) ) {
385 $size = array_pop( $size );
386
387 if ( false === $width && false === $height && 'full' !== $size && array_key_exists( $size, $image_sizes ) ) {
388 $width = (int) $image_sizes[ $size ]['width'];
389 $height = (int) $image_sizes[ $size ]['height'];
390 $transform = $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
391 }
392 } else {
393 unset( $size );
394 }
395
396 // WP Attachment ID, if uploaded to this site.
397 if (
398 preg_match( '#class=["|\']?[^"\']*wp-image-([\d]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $attachment_id ) &&
399 0 === strpos( $src, $upload_dir['baseurl'] ) &&
400 /**
401 * Filter whether an image using an attachment ID in its class has to be uploaded to the local site to go through Photon.
402 *
403 * @module photon
404 *
405 * @since 2.0.3
406 *
407 * @param bool false Was the image uploaded to the local site. Default to false.
408 * @param array $args {
409 * Array of image details.
410 *
411 * @type $src Image URL.
412 * @type tag Image tag (Image HTML output).
413 * @type $images Array of information about the image.
414 * @type $index Image index.
415 * }
416 */
417 apply_filters( 'jetpack_photon_image_is_local', false, compact( 'src', 'tag', 'images', 'index' ) )
418 ) {
419 $attachment_id = (int) array_pop( $attachment_id );
420
421 if ( $attachment_id ) {
422 $attachment = get_post( $attachment_id );
423
424 // Basic check on returned post object.
425 if ( is_object( $attachment ) && ! is_wp_error( $attachment ) && 'attachment' === $attachment->post_type ) {
426 $src_per_wp = wp_get_attachment_image_src( $attachment_id, isset( $size ) ? $size : 'full' );
427
428 if ( self::validate_image_url( $src_per_wp[0] ) ) {
429 $src = $src_per_wp[0];
430 $fullsize_url = true;
431
432 // Prevent image distortion if a detected dimension exceeds the image's natural dimensions.
433 if ( ( false !== $width && $width > $src_per_wp[1] ) || ( false !== $height && $height > $src_per_wp[2] ) ) {
434 $width = false === $width ? false : min( $width, $src_per_wp[1] );
435 $height = false === $height ? false : min( $height, $src_per_wp[2] );
436 }
437
438 // If no width and height are found, max out at source image's natural dimensions.
439 // Otherwise, respect registered image sizes' cropping setting.
440 if ( false === $width && false === $height ) {
441 $width = $src_per_wp[1];
442 $height = $src_per_wp[2];
443 $transform = 'fit';
444 } elseif ( isset( $size ) && array_key_exists( $size, $image_sizes ) && isset( $image_sizes[ $size ]['crop'] ) ) {
445 $transform = (bool) $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
446 }
447 }
448 } else {
449 unset( $attachment_id );
450 unset( $attachment );
451 }
452 }
453 }
454
455 // If image tag lacks width and height arguments, try to determine from strings WP appends to resized image filenames.
456 if ( false === $width && false === $height ) {
457 list( $width, $height ) = self::parse_dimensions_from_filename( $src );
458 }
459
460 $width_orig = $width;
461 $height_orig = $height;
462 $transform_orig = $transform;
463
464 // If width is available, constrain to $content_width.
465 if ( false !== $width && is_numeric( $content_width ) && $width > $content_width ) {
466 if ( false !== $height ) {
467 $height = round( ( $content_width * $height ) / $width );
468 }
469 $width = $content_width;
470 }
471
472 // Set a width if none is found and $content_width is available.
473 // If width is set in this manner and height is available, use `fit` instead of `resize` to prevent skewing.
474 if ( false === $width && is_numeric( $content_width ) ) {
475 $width = (int) $content_width;
476
477 if ( false !== $height ) {
478 $transform = 'fit';
479 }
480 }
481
482 // Detect if image source is for a custom-cropped thumbnail and prevent further URL manipulation.
483 if ( ! $fullsize_url && preg_match_all( '#-e[a-z0-9]+(-\d+x\d+)?\.(' . implode( '|', self::$extensions ) . '){1}$#i', basename( $src ), $filename ) ) {
484 $fullsize_url = true;
485 }
486
487 // Build URL, first maybe removing WP's resized string so we pass the original image to Photon.
488 if ( ! $fullsize_url && 0 === strpos( $src, $upload_dir['baseurl'] ) ) {
489 $src = self::strip_image_dimensions_maybe( $src );
490 }
491
492 // Build array of Photon args and expose to filter before passing to Photon URL function.
493 $args = array();
494
495 if ( false !== $width && false !== $height ) {
496 $args[ $transform ] = $width . ',' . $height;
497 } elseif ( false !== $width ) {
498 $args['w'] = $width;
499 } elseif ( false !== $height ) {
500 $args['h'] = $height;
501 }
502
503 /**
504 * Filter the array of Photon arguments added to an image when it goes through Photon.
505 * By default, only includes width and height values.
506 *
507 * @see https://developer.wordpress.com/docs/photon/api/
508 *
509 * @module photon
510 *
511 * @since 2.0.0
512 *
513 * @param array $args Array of Photon Arguments.
514 * @param array $details {
515 * Array of image details.
516 *
517 * @type string $tag Image tag (Image HTML output).
518 * @type string $src Image URL.
519 * @type string $src_orig Original Image URL.
520 * @type int|false $width Image width.
521 * @type int|false $height Image height.
522 * @type int|false $width_orig Original image width before constrained by content_width.
523 * @type int|false $height_orig Original Image height before constrained by content_width.
524 * @type string $transform Transform.
525 * @type string $transform_orig Original transform before constrained by content_width.
526 * }
527 */
528 $args = apply_filters( 'jetpack_photon_post_image_args', $args, compact( 'tag', 'src', 'src_orig', 'width', 'height', 'width_orig', 'height_orig', 'transform', 'transform_orig' ) );
529
530 $photon_url = jetpack_photon_url( $src, $args );
531
532 // Modify image tag if Photon function provides a URL
533 // 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.
534 if ( $src !== $photon_url ) {
535 $new_tag = $tag;
536
537 // If present, replace the link href with a Photoned URL for the full-size image.
538 if ( ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
539 $new_tag = preg_replace( '#(href=["|\'])' . preg_quote( $images['link_url'][ $index ], '#' ) . '(["|\'])#i', '\1' . jetpack_photon_url( $images['link_url'][ $index ] ) . '\2', $new_tag, 1 );
540 }
541
542 // Supplant the original source value with our Photon URL.
543 $photon_url = esc_url( $photon_url );
544 $new_tag = str_replace( $src_orig, $photon_url, $new_tag );
545
546 // If Lazy Load is in use, pass placeholder image through Photon.
547 if ( isset( $placeholder_src ) && self::validate_image_url( $placeholder_src ) ) {
548 $placeholder_src = jetpack_photon_url( $placeholder_src );
549
550 if ( $placeholder_src !== $placeholder_src_orig ) {
551 $new_tag = str_replace( $placeholder_src_orig, esc_url( $placeholder_src ), $new_tag );
552 }
553
554 unset( $placeholder_src );
555 }
556
557 // If we are not transforming the image with resize, fit, or letterbox (lb), then we should remove
558 // the width and height arguments (including HTML4 percentages) from the image to prevent distortion.
559 // Even if $args['w'] and $args['h'] are present, Photon does not crop to those dimensions. Instead,
560 // it appears to favor height.
561 //
562 // If we are transforming the image via one of those methods, let's update the width and height attributes.
563 if ( empty( $args['resize'] ) && empty( $args['fit'] ) && empty( $args['lb'] ) ) {
564 $new_tag = preg_replace( '#(?<=\s)(width|height)=["|\']?[\d%]+["|\']?\s?#i', '', $new_tag );
565 } else {
566 $resize_args = isset( $args['resize'] ) ? $args['resize'] : false;
567 if ( false === $resize_args ) {
568 $resize_args = ( ! $resize_args && isset( $args['fit'] ) )
569 ? $args['fit']
570 : false;
571 }
572 if ( false === $resize_args ) {
573 $resize_args = ( ! $resize_args && isset( $args['lb'] ) )
574 ? $args['lb']
575 : false;
576 }
577
578 $resize_args = array_map( 'trim', explode( ',', $resize_args ) );
579
580 // (?<=\s) - Ensure width or height attribute is preceded by a space
581 // (width=["|\']?) - Matches, and captures, width=, width=", or width='
582 // [\d%]+ - Matches 1 or more digits or percent signs
583 // (["|\']?) - Matches, and captures, ", ', or empty string
584 // \s - Ensures there's a space after the attribute
585 $new_tag = preg_replace( '#(?<=\s)(width=["|\']?)[\d%]+(["|\']?)\s?#i', sprintf( '${1}%d${2} ', $resize_args[0] ), $new_tag );
586 $new_tag = preg_replace( '#(?<=\s)(height=["|\']?)[\d%]+(["|\']?)\s?#i', sprintf( '${1}%d${2} ', $resize_args[1] ), $new_tag );
587 }
588
589 // Tag an image for dimension checking.
590 if ( ! self::is_amp_endpoint() ) {
591 $new_tag = preg_replace( '#(\s?/)?>(\s*</a>)?$#i', ' data-recalc-dims="1"\1>\2', $new_tag );
592 }
593
594 // Replace original tag with modified version.
595 $content = str_replace( $tag, $new_tag, $content );
596 }
597 } elseif ( preg_match( '#^http(s)?://i[\d]{1}.wp.com#', $src ) && ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
598 $new_tag = preg_replace( '#(href=["|\'])' . preg_quote( $images['link_url'][ $index ], '#' ) . '(["|\'])#i', '\1' . jetpack_photon_url( $images['link_url'][ $index ] ) . '\2', $tag, 1 );
599
600 $content = str_replace( $tag, $new_tag, $content );
601 }
602 }
603 }
604
605 return $content;
606 }
607
608 /**
609 * Filter Core galleries
610 *
611 * @param array $galleries Gallery array.
612 *
613 * @return array
614 */
615 public static function filter_the_galleries( $galleries ) {
616 if ( empty( $galleries ) || ! is_array( $galleries ) ) {
617 return $galleries;
618 }
619
620 // Pass by reference, so we can modify them in place.
621 foreach ( $galleries as &$this_gallery ) {
622 if ( is_string( $this_gallery ) ) {
623 $this_gallery = self::filter_the_content( $this_gallery );
624 }
625 }
626 unset( $this_gallery ); // break the reference.
627
628 return $galleries;
629 }
630
631 /**
632 * Runs the image widget through photon.
633 *
634 * @param array $instance Image widget instance data.
635 * @return array
636 */
637 public static function filter_the_image_widget( $instance ) {
638 if ( Jetpack::is_module_active( 'photon' ) && ! $instance['attachment_id'] && $instance['url'] ) {
639 jetpack_photon_url(
640 $instance['url'],
641 array(
642 'w' => $instance['width'],
643 'h' => $instance['height'],
644 )
645 );
646 }
647
648 return $instance;
649 }
650
651 /**
652 * * CORE IMAGE RETRIEVAL
653 **/
654
655 /**
656 * Filter post thumbnail image retrieval, passing images through Photon
657 *
658 * @param string|bool $image Image URL.
659 * @param int $attachment_id Attachment ID.
660 * @param string|array $size Declared size or a size array.
661 * @uses is_admin, apply_filters, wp_get_attachment_url, self::validate_image_url, this::image_sizes, jetpack_photon_url
662 * @filter image_downsize
663 * @return string|bool
664 */
665 public function filter_image_downsize( $image, $attachment_id, $size ) {
666 // Don't foul up the admin side of things, unless a plugin wants to.
667 if ( is_admin() &&
668 /**
669 * Provide plugins a way of running Photon for images in the WordPress Dashboard (wp-admin).
670 *
671 * Note: enabling this will result in Photon URLs added to your post content, which could make migrations across domains (and off Photon) a bit more challenging.
672 *
673 * @module photon
674 *
675 * @since 4.8.0
676 *
677 * @param bool false Stop Photon from being run on the Dashboard. Default to false.
678 * @param array $args {
679 * Array of image details.
680 *
681 * @type $image Image URL.
682 * @type $attachment_id Attachment ID of the image.
683 * @type $size Image size. Can be a string (name of the image size, e.g. full) or an array of width and height.
684 * }
685 */
686 false === apply_filters( 'jetpack_photon_admin_allow_image_downsize', false, compact( 'image', 'attachment_id', 'size' ) )
687 ) {
688 return $image;
689 }
690
691 /**
692 * Provide plugins a way of preventing Photon from being applied to images retrieved from WordPress Core.
693 *
694 * @module photon
695 *
696 * @since 2.0.0
697 *
698 * @param bool false Stop Photon from being applied to the image. Default to false.
699 * @param array $args {
700 * Array of image details.
701 *
702 * @type $image Image URL.
703 * @type $attachment_id Attachment ID of the image.
704 * @type $size Image size. Can be a string (name of the image size, e.g. full) or an array of width and height.
705 * }
706 */
707 if ( apply_filters( 'jetpack_photon_override_image_downsize', false, compact( 'image', 'attachment_id', 'size' ) ) ) {
708 return $image;
709 }
710
711 // Get the image URL and proceed with Photon-ification if successful.
712 $image_url = wp_get_attachment_url( $attachment_id );
713
714 // Set this to true later when we know we have size meta.
715 $has_size_meta = false;
716
717 if ( $image_url ) {
718 // Check if image URL should be used with Photon.
719 if ( ! self::validate_image_url( $image_url ) ) {
720 return $image;
721 }
722
723 $intermediate = true; // For the fourth array item returned by the image_downsize filter.
724
725 // If an image is requested with a size known to WordPress, use that size's settings with Photon.
726 // WP states that `add_image_size()` should use a string for the name, but doesn't enforce that.
727 // Due to differences in how Core and Photon check for the registered image size, we check both types.
728 if ( ( is_string( $size ) || is_int( $size ) ) && array_key_exists( $size, self::image_sizes() ) ) {
729 $image_args = self::image_sizes();
730 $image_args = $image_args[ $size ];
731
732 $photon_args = array();
733
734 $image_meta = image_get_intermediate_size( $attachment_id, $size );
735
736 // 'full' is a special case: We need consistent data regardless of the requested size.
737 if ( 'full' === $size ) {
738 $image_meta = wp_get_attachment_metadata( $attachment_id );
739 $intermediate = false;
740 } elseif ( ! $image_meta ) {
741 // If we still don't have any image meta at this point, it's probably from a custom thumbnail size
742 // for an image that was uploaded before the custom image was added to the theme. Try to determine the size manually.
743 $image_meta = wp_get_attachment_metadata( $attachment_id );
744
745 if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
746 $image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $image_args['width'], $image_args['height'], $image_args['crop'] );
747 if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
748 $image_meta['width'] = $image_resized[6];
749 $image_meta['height'] = $image_resized[7];
750 }
751 }
752 }
753
754 if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
755 $image_args['width'] = (int) $image_meta['width'];
756 $image_args['height'] = (int) $image_meta['height'];
757
758 list( $image_args['width'], $image_args['height'] ) = image_constrain_size_for_editor( $image_args['width'], $image_args['height'], $size, 'display' );
759 $has_size_meta = true;
760 }
761
762 // Expose determined arguments to a filter before passing to Photon.
763 $transform = $image_args['crop'] ? 'resize' : 'fit';
764
765 // Check specified image dimensions and account for possible zero values; photon fails to resize if a dimension is zero.
766 if ( 0 === $image_args['width'] || 0 === $image_args['height'] ) {
767 if ( 0 === $image_args['width'] && 0 < $image_args['height'] ) {
768 $photon_args['h'] = $image_args['height'];
769 } elseif ( 0 === $image_args['height'] && 0 < $image_args['width'] ) {
770 $photon_args['w'] = $image_args['width'];
771 }
772 } else {
773 $image_meta = wp_get_attachment_metadata( $attachment_id );
774 if ( ( 'resize' === $transform ) && $image_meta ) {
775 if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
776 // Lets make sure that we don't upscale images since wp never upscales them as well.
777 $smaller_width = ( ( $image_meta['width'] < $image_args['width'] ) ? $image_meta['width'] : $image_args['width'] );
778 $smaller_height = ( ( $image_meta['height'] < $image_args['height'] ) ? $image_meta['height'] : $image_args['height'] );
779
780 $photon_args[ $transform ] = $smaller_width . ',' . $smaller_height;
781 }
782 } else {
783 $photon_args[ $transform ] = $image_args['width'] . ',' . $image_args['height'];
784 }
785 }
786
787 /**
788 * Filter the Photon Arguments added to an image when going through Photon, when that image size is a string.
789 * Image size will be a string (e.g. "full", "medium") when it is known to WordPress.
790 *
791 * @module photon
792 *
793 * @since 2.0.0
794 *
795 * @param array $photon_args Array of Photon arguments.
796 * @param array $args {
797 * Array of image details.
798 *
799 * @type array $image_args Array of Image arguments (width, height, crop).
800 * @type string $image_url Image URL.
801 * @type int $attachment_id Attachment ID of the image.
802 * @type string|int $size Image size. Can be a string (name of the image size, e.g. full) or an integer.
803 * @type string $transform Value can be resize or fit.
804 * @see https://developer.wordpress.com/docs/photon/api
805 * }
806 */
807 $photon_args = apply_filters( 'jetpack_photon_image_downsize_string', $photon_args, compact( 'image_args', 'image_url', 'attachment_id', 'size', 'transform' ) );
808
809 // Generate Photon URL.
810 $image = array(
811 jetpack_photon_url( $image_url, $photon_args ),
812 $has_size_meta ? $image_args['width'] : false,
813 $has_size_meta ? $image_args['height'] : false,
814 $intermediate,
815 );
816 } elseif ( is_array( $size ) ) {
817 // Pull width and height values from the provided array, if possible.
818 $width = isset( $size[0] ) ? (int) $size[0] : false;
819 $height = isset( $size[1] ) ? (int) $size[1] : false;
820
821 // Don't bother if necessary parameters aren't passed.
822 if ( ! $width || ! $height ) {
823 return $image;
824 }
825
826 $image_meta = wp_get_attachment_metadata( $attachment_id );
827 if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
828 $image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $width, $height );
829
830 if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
831 $width = $image_resized[6];
832 $height = $image_resized[7];
833 } else {
834 $width = $image_meta['width'];
835 $height = $image_meta['height'];
836 }
837
838 $has_size_meta = true;
839 }
840
841 list( $width, $height ) = image_constrain_size_for_editor( $width, $height, $size );
842
843 // Expose arguments to a filter before passing to Photon.
844 $photon_args = array(
845 'fit' => $width . ',' . $height,
846 );
847
848 /**
849 * Filter the Photon Arguments added to an image when going through Photon,
850 * when the image size is an array of height and width values.
851 *
852 * @module photon
853 *
854 * @since 2.0.0
855 *
856 * @param array $photon_args Array of Photon arguments.
857 * @param array $args {
858 * Array of image details.
859 *
860 * @type $width Image width.
861 * @type height Image height.
862 * @type $image_url Image URL.
863 * @type $attachment_id Attachment ID of the image.
864 * }
865 */
866 $photon_args = apply_filters( 'jetpack_photon_image_downsize_array', $photon_args, compact( 'width', 'height', 'image_url', 'attachment_id' ) );
867
868 // Generate Photon URL.
869 $image = array(
870 jetpack_photon_url( $image_url, $photon_args ),
871 $has_size_meta ? $width : false,
872 $has_size_meta ? $height : false,
873 $intermediate,
874 );
875 }
876 }
877
878 return $image;
879 }
880
881 /**
882 * Filters an array of image `srcset` values, replacing each URL with its Photon equivalent.
883 *
884 * @since 3.8.0
885 * @since 4.0.4 Added automatically additional sizes beyond declared image sizes.
886 *
887 * @param array $sources An array of image urls and widths.
888 * @param array $size_array The size array for srcset.
889 * @param array $image_src The image srcs.
890 * @param array $image_meta The image meta.
891 * @param int $attachment_id Attachment ID.
892 *
893 * @uses self::validate_image_url, jetpack_photon_url, Jetpack_Photon::parse_from_filename
894 * @uses Jetpack_Photon::strip_image_dimensions_maybe, Jetpack::get_content_width
895 *
896 * @return array An array of Photon image urls and widths.
897 */
898 public function filter_srcset_array( $sources = array(), $size_array = array(), $image_src = array(), $image_meta = array(), $attachment_id = 0 ) {
899 if ( ! is_array( $sources ) || array() === $sources ) {
900 return $sources;
901 }
902 $upload_dir = wp_get_upload_dir();
903
904 foreach ( $sources as $i => $source ) {
905 if ( ! self::validate_image_url( $source['url'] ) ) {
906 continue;
907 }
908
909 /** This filter is already documented in class.photon.php */
910 if ( apply_filters( 'jetpack_photon_skip_image', false, $source['url'], $source ) ) {
911 continue;
912 }
913
914 $url = $source['url'];
915 list( $width, $height ) = self::parse_dimensions_from_filename( $url );
916
917 // It's quicker to get the full size with the data we have already, if available.
918 if ( ! empty( $attachment_id ) ) {
919 $url = wp_get_attachment_url( $attachment_id );
920 } else {
921 $url = self::strip_image_dimensions_maybe( $url );
922 }
923
924 $args = array();
925 if ( 'w' === $source['descriptor'] ) {
926 if ( $height && ( (int) $source['value'] === $width ) ) {
927 $args['resize'] = $width . ',' . $height;
928 } else {
929 $args['w'] = $source['value'];
930 }
931 }
932
933 $sources[ $i ]['url'] = jetpack_photon_url( $url, $args );
934 }
935
936 /**
937 * At this point, $sources is the original srcset with Photonized URLs.
938 * Now, we're going to construct additional sizes based on multiples of the content_width.
939 * This will reduce the gap between the largest defined size and the original image.
940 */
941
942 /**
943 * Filter the multiplier Photon uses to create new srcset items.
944 * Return false to short-circuit and bypass auto-generation.
945 *
946 * @module photon
947 *
948 * @since 4.0.4
949 *
950 * @param array|bool $multipliers Array of multipliers to use or false to bypass.
951 */
952 $multipliers = apply_filters( 'jetpack_photon_srcset_multipliers', array( 2, 3 ) );
953 $url = trailingslashit( $upload_dir['baseurl'] ) . $image_meta['file'];
954
955 if (
956 /** Short-circuit via jetpack_photon_srcset_multipliers filter. */
957 is_array( $multipliers )
958 /** This filter is already documented in class.photon.php */
959 && ! apply_filters( 'jetpack_photon_skip_image', false, $url, null )
960 /** Verify basic meta is intact. */
961 && isset( $image_meta['width'] ) && isset( $image_meta['height'] ) && isset( $image_meta['file'] )
962 /** Verify we have the requested width/height. */
963 && isset( $size_array[0] ) && isset( $size_array[1] )
964 ) {
965
966 $fullwidth = $image_meta['width'];
967 $fullheight = $image_meta['height'];
968 $reqwidth = $size_array[0];
969 $reqheight = $size_array[1];
970
971 $constrained_size = wp_constrain_dimensions( $fullwidth, $fullheight, $reqwidth );
972 $expected_size = array( $reqwidth, $reqheight );
973
974 if ( abs( $constrained_size[0] - $expected_size[0] ) <= 1 && abs( $constrained_size[1] - $expected_size[1] ) <= 1 ) {
975 $crop = 'soft';
976 $base = Jetpack::get_content_width() ? Jetpack::get_content_width() : 1000; // Provide a default width if none set by the theme.
977 } else {
978 $crop = 'hard';
979 $base = $reqwidth;
980 }
981
982 $currentwidths = array_keys( $sources );
983 $newsources = null;
984
985 foreach ( $multipliers as $multiplier ) {
986
987 $newwidth = $base * $multiplier;
988 foreach ( $currentwidths as $currentwidth ) {
989 // If a new width would be within 100 pixes of an existing one or larger than the full size image, skip.
990 if ( abs( $currentwidth - $newwidth ) < 50 || ( $newwidth > $fullwidth ) ) {
991 continue 2; // Bump out back to the $multipliers as $multiplier.
992 }
993 } //end foreach ( $currentwidths as $currentwidth ){
994
995 if ( 'soft' === $crop ) {
996 $args = array(
997 'w' => $newwidth,
998 );
999 } else { // hard crop, e.g. add_image_size( 'example', 200, 200, true ).
1000 $args = array(
1001 'zoom' => $multiplier,
1002 'resize' => $reqwidth . ',' . $reqheight,
1003 );
1004 }
1005
1006 $newsources[ $newwidth ] = array(
1007 'url' => jetpack_photon_url( $url, $args ),
1008 'descriptor' => 'w',
1009 'value' => $newwidth,
1010 );
1011 } //end foreach ( $multipliers as $multiplier )
1012 if ( is_array( $newsources ) ) {
1013 $sources = array_replace( $sources, $newsources );
1014 }
1015 } //end if isset( $image_meta['width'] ) && isset( $image_meta['file'] ) )
1016
1017 return $sources;
1018 }
1019
1020 /**
1021 * Filters an array of image `sizes` values, using $content_width instead of image's full size.
1022 *
1023 * @since 4.0.4
1024 * @since 4.1.0 Returns early for images not within the_content.
1025 * @param array $sizes An array of media query breakpoints.
1026 * @param array $size Width and height of the image.
1027 * @uses Jetpack::get_content_width
1028 * @return array An array of media query breakpoints.
1029 */
1030 public function filter_sizes( $sizes, $size ) {
1031 if ( ! doing_filter( 'the_content' ) ) {
1032 return $sizes;
1033 }
1034 $content_width = Jetpack::get_content_width();
1035 if ( ! $content_width ) {
1036 $content_width = 1000;
1037 }
1038
1039 if ( ( is_array( $size ) && $size[0] < $content_width ) ) {
1040 return $sizes;
1041 }
1042
1043 return sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $content_width );
1044 }
1045
1046 /**
1047 * * GENERAL FUNCTIONS
1048 **/
1049
1050 /**
1051 * Ensure image URL is valid for Photon.
1052 * Though Photon functions address some of the URL issues, we should avoid unnecessary processing if we know early on that the image isn't supported.
1053 *
1054 * @param string $url Image URL.
1055 * @uses wp_parse_args
1056 * @return bool
1057 */
1058 protected static function validate_image_url( $url ) {
1059 $parsed_url = wp_parse_url( $url );
1060
1061 if ( ! $parsed_url ) {
1062 return false;
1063 }
1064
1065 // Parse URL and ensure needed keys exist, since the array returned by `wp_parse_url` only includes the URL components it finds.
1066 $url_info = wp_parse_args(
1067 $parsed_url,
1068 array(
1069 'scheme' => null,
1070 'host' => null,
1071 'port' => null,
1072 'path' => null,
1073 )
1074 );
1075
1076 // Bail if scheme isn't http or port is set that isn't port 80.
1077 if (
1078 ( 'http' !== $url_info['scheme'] || ! in_array( $url_info['port'], array( 80, null ), true ) ) &&
1079 /**
1080 * Allow Photon to fetch images that are served via HTTPS.
1081 *
1082 * @module photon
1083 *
1084 * @since 2.4.0
1085 * @since 3.9.0 Default to false.
1086 *
1087 * @param bool $reject_https Should Photon ignore images using the HTTPS scheme. Default to false.
1088 */
1089 apply_filters( 'jetpack_photon_reject_https', false )
1090 ) {
1091 return false;
1092 }
1093
1094 // Bail if no host is found.
1095 if ( is_null( $url_info['host'] ) ) {
1096 return false;
1097 }
1098
1099 // Bail if the image already went through Photon.
1100 if ( preg_match( '#^i[\d]{1}.wp.com$#i', $url_info['host'] ) ) {
1101 return false;
1102 }
1103
1104 // Bail if no path is found.
1105 if ( is_null( $url_info['path'] ) ) {
1106 return false;
1107 }
1108
1109 // Ensure image extension is acceptable.
1110 if ( ! in_array( strtolower( pathinfo( $url_info['path'], PATHINFO_EXTENSION ) ), self::$extensions, true ) ) {
1111 return false;
1112 }
1113
1114 // If we got this far, we should have an acceptable image URL
1115 // But let folks filter to decline if they prefer.
1116 /**
1117 * Overwrite the results of the validation steps an image goes through before to be considered valid to be used by Photon.
1118 *
1119 * @module photon
1120 *
1121 * @since 3.0.0
1122 *
1123 * @param bool true Is the image URL valid and can it be used by Photon. Default to true.
1124 * @param string $url Image URL.
1125 * @param array $parsed_url Array of information about the image.
1126 */
1127 return apply_filters( 'photon_validate_image_url', true, $url, $parsed_url );
1128 }
1129
1130 /**
1131 * Checks if the file exists before it passes the file to photon.
1132 *
1133 * @param string $src The image URL.
1134 * @return string
1135 **/
1136 public static function strip_image_dimensions_maybe( $src ) {
1137 $stripped_src = $src;
1138
1139 // Build URL, first removing WP's resized string so we pass the original image to Photon.
1140 if ( preg_match( '#(-\d+x\d+)\.(' . implode( '|', self::$extensions ) . '){1}$#i', $src, $src_parts ) ) {
1141 $stripped_src = str_replace( $src_parts[1], '', $src );
1142 $upload_dir = wp_get_upload_dir();
1143
1144 // Extracts the file path to the image minus the base url.
1145 $file_path = substr( $stripped_src, strlen( $upload_dir['baseurl'] ) );
1146
1147 if ( file_exists( $upload_dir['basedir'] . $file_path ) ) {
1148 $src = $stripped_src;
1149 }
1150 }
1151
1152 return $src;
1153 }
1154
1155 /**
1156 * Provide an array of available image sizes and corresponding dimensions.
1157 * Similar to get_intermediate_image_sizes() except that it includes image sizes' dimensions, not just their names.
1158 *
1159 * @global $wp_additional_image_sizes
1160 * @uses get_option
1161 * @return array
1162 */
1163 protected static function image_sizes() {
1164 if ( null === self::$image_sizes ) {
1165 global $_wp_additional_image_sizes;
1166
1167 // Populate an array matching the data structure of $_wp_additional_image_sizes so we have a consistent structure for image sizes.
1168 $images = array(
1169 'thumb' => array(
1170 'width' => (int) get_option( 'thumbnail_size_w' ),
1171 'height' => (int) get_option( 'thumbnail_size_h' ),
1172 'crop' => (bool) get_option( 'thumbnail_crop' ),
1173 ),
1174 'medium' => array(
1175 'width' => (int) get_option( 'medium_size_w' ),
1176 'height' => (int) get_option( 'medium_size_h' ),
1177 'crop' => false,
1178 ),
1179 'medium_large' => array(
1180 'width' => (int) get_option( 'medium_large_size_w' ),
1181 'height' => (int) get_option( 'medium_large_size_h' ),
1182 'crop' => false,
1183 ),
1184 'large' => array(
1185 'width' => (int) get_option( 'large_size_w' ),
1186 'height' => (int) get_option( 'large_size_h' ),
1187 'crop' => false,
1188 ),
1189 'full' => array(
1190 'width' => null,
1191 'height' => null,
1192 'crop' => false,
1193 ),
1194 );
1195
1196 // Compatibility mapping as found in wp-includes/media.php.
1197 $images['thumbnail'] = $images['thumb'];
1198
1199 // Update class variable, merging in $_wp_additional_image_sizes if any are set.
1200 if ( is_array( $_wp_additional_image_sizes ) && ! empty( $_wp_additional_image_sizes ) ) {
1201 self::$image_sizes = array_merge( $images, $_wp_additional_image_sizes );
1202 } else {
1203 self::$image_sizes = $images;
1204 }
1205 }
1206
1207 return is_array( self::$image_sizes ) ? self::$image_sizes : array();
1208 }
1209
1210 /**
1211 * Pass og:image URLs through Photon
1212 *
1213 * @param array $tags Open graph tags.
1214 * @param array $parameters Image parameters.
1215 * @uses jetpack_photon_url
1216 * @return array Open graph tags.
1217 */
1218 public function filter_open_graph_tags( $tags, $parameters ) {
1219 if ( empty( $tags['og:image'] ) ) {
1220 return $tags;
1221 }
1222
1223 $photon_args = array(
1224 'fit' => sprintf( '%d,%d', 2 * $parameters['image_width'], 2 * $parameters['image_height'] ),
1225 );
1226
1227 if ( is_array( $tags['og:image'] ) ) {
1228 $images = array();
1229 foreach ( $tags['og:image'] as $image ) {
1230 $images[] = jetpack_photon_url( $image, $photon_args );
1231 }
1232 $tags['og:image'] = $images;
1233 } else {
1234 $tags['og:image'] = jetpack_photon_url( $tags['og:image'], $photon_args );
1235 }
1236
1237 return $tags;
1238 }
1239
1240 /**
1241 * Enqueue Photon helper script
1242 *
1243 * @uses wp_enqueue_script, plugins_url
1244 * @action wp_enqueue_script
1245 * @return null
1246 */
1247 public function action_wp_enqueue_scripts() {
1248 if ( self::is_amp_endpoint() ) {
1249 return;
1250 }
1251 wp_enqueue_script(
1252 'jetpack-photon',
1253 Assets::get_file_url_for_environment(
1254 '_inc/build/photon/photon.min.js',
1255 'modules/photon/photon.js'
1256 ),
1257 array(),
1258 20191001,
1259 true
1260 );
1261 }
1262
1263 /**
1264 * Determine if image_downsize should utilize Photon via REST API.
1265 *
1266 * The WordPress Block Editor (Gutenberg) and other REST API consumers using the wp/v2/media endpoint, especially in the "edit"
1267 * context is more akin to the is_admin usage of Photon (see filter_image_downsize). Since consumers are trying to edit content in posts,
1268 * Photon should not fire as it will fire later on display. By aborting an attempt to Photonize an image here, we
1269 * prevents issues like https://github.com/Automattic/jetpack/issues/10580 .
1270 *
1271 * To determine if we're using the wp/v2/media endpoint, we hook onto the `rest_request_before_callbacks` filter and
1272 * if determined we are using it in the edit context, we'll false out the `jetpack_photon_override_image_downsize` filter.
1273 *
1274 * @see Jetpack_Photon::filter_image_downsize()
1275 *
1276 * @param null|WP_Error $response REST API response.
1277 * @param array $endpoint_data Endpoint data. Not used, but part of the filter.
1278 * @param WP_REST_Request $request Request used to generate the response.
1279 *
1280 * @return null|WP_Error The original response object without modification.
1281 */
1282 public function should_rest_photon_image_downsize( $response, $endpoint_data, $request ) {
1283 if ( ! is_a( $request, 'WP_REST_Request' ) ) {
1284 return $response; // Something odd is happening. Do nothing and return the response.
1285 }
1286
1287 if ( is_wp_error( $response ) ) {
1288 // If we're going to return an error, we don't need to do anything with Photon.
1289 return $response;
1290 }
1291
1292 $this->should_rest_photon_image_downsize_override( $request );
1293
1294 return $response;
1295
1296 }
1297
1298 /**
1299 * Helper function to check if a WP_REST_Request is the media endpoint in the edit context.
1300 *
1301 * @param WP_REST_Request $request The current REST request.
1302 */
1303 private function should_rest_photon_image_downsize_override( WP_REST_Request $request ) {
1304 $route = $request->get_route();
1305
1306 if (
1307 (
1308 false !== strpos( $route, 'wp/v2/media' )
1309 && 'edit' === $request->get_param( 'context' )
1310 )
1311 || false !== strpos( $route, 'wpcom/v2/external-media/copy' )
1312 ) {
1313 // Don't use `__return_true()`: Use something unique. See ::_override_image_downsize_in_rest_edit_context()
1314 // Late execution to avoid conflict with other plugins as we really don't want to run in this situation.
1315 add_filter(
1316 'jetpack_photon_override_image_downsize',
1317 array(
1318 $this,
1319 'override_image_downsize_in_rest_edit_context',
1320 ),
1321 999999
1322 );
1323 }
1324 }
1325
1326 /**
1327 * Brings in should_rest_photon_image_downsize for the rest_after_insert_attachment hook.
1328 *
1329 * @since 8.7.0
1330 *
1331 * @param WP_Post $attachment Inserted or updated attachment object.
1332 * @param WP_REST_Request $request Request object.
1333 */
1334 public function should_rest_photon_image_downsize_insert_attachment( WP_Post $attachment, WP_REST_Request $request ) {
1335 if ( ! is_a( $request, 'WP_REST_Request' ) ) {
1336 // Something odd is happening.
1337 return;
1338 }
1339
1340 $this->should_rest_photon_image_downsize_override( $request );
1341
1342 }
1343
1344 /**
1345 * Remove the override we may have added in ::should_rest_photon_image_downsize()
1346 * Since ::_override_image_downsize_in_rest_edit_context() is only
1347 * every used here, we can always remove it without ever worrying
1348 * about breaking any other configuration.
1349 *
1350 * @param mixed $response REST API Response.
1351 * @return mixed Unchanged $response
1352 */
1353 public function cleanup_rest_photon_image_downsize( $response ) {
1354 remove_filter(
1355 'jetpack_photon_override_image_downsize',
1356 array(
1357 $this,
1358 'override_image_downsize_in_rest_edit_context',
1359 ),
1360 999999
1361 );
1362 return $response;
1363 }
1364
1365 /**
1366 * Used internally by ::should_rest_photon_image_downsize() to not photonize
1367 * image URLs in ?context=edit REST requests.
1368 * MUST NOT be used anywhere else.
1369 * We use a unique function instead of __return_true so that we can clean up
1370 * after ourselves without breaking anyone else's filters.
1371 *
1372 * @internal
1373 * @return true
1374 */
1375 public function override_image_downsize_in_rest_edit_context() {
1376 return true;
1377 }
1378
1379 /**
1380 * Return whether the current page is AMP.
1381 *
1382 * This is only present for the sake of WordPress.com where the Jetpack_AMP_Support
1383 * class does not yet exist. This mehod may only be called at the wp action or later.
1384 *
1385 * @return bool Whether AMP page.
1386 */
1387 private static function is_amp_endpoint() {
1388 return class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request();
1389 }
1390 }
1391