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