PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 6.7.2
Jetpack – WP Security, Backup, Speed, & Growth v6.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 7 years ago _inc 6 years ago bin 7 years ago css 7 years ago images 6 years ago json-endpoints 7 years ago languages 7 years ago modules 5 years ago sal 7 years ago scss 7 years ago sync 7 years ago views 7 years ago .svnignore 12 years ago CODE-OF-CONDUCT.md 9 years ago changelog.txt 7 years ago class.frame-nonce-preview.php 9 years ago class.jetpack-admin.php 7 years ago class.jetpack-autoupdate.php 8 years ago class.jetpack-bbpress-json-api-compat.php 9 years ago class.jetpack-cli.php 7 years ago class.jetpack-client-server.php 8 years ago class.jetpack-client.php 7 years ago class.jetpack-connection-banner.php 7 years ago class.jetpack-constants.php 8 years ago class.jetpack-data.php 7 years ago class.jetpack-debugger.php 7 years ago class.jetpack-error.php 10 years ago class.jetpack-gutenberg.php 7 years ago class.jetpack-heartbeat.php 7 years ago class.jetpack-idc.php 8 years ago class.jetpack-ixr-client.php 10 years ago class.jetpack-jitm.php 7 years ago class.jetpack-modules-list-table.php 8 years ago class.jetpack-network-sites-list-table.php 9 years ago class.jetpack-network.php 7 years ago class.jetpack-options.php 7 years ago class.jetpack-post-images.php 7 years ago class.jetpack-signature.php 7 years ago class.jetpack-tracks.php 8 years ago class.jetpack-twitter-cards.php 7 years ago class.jetpack-user-agent.php 8 years ago class.jetpack-xmlrpc-server.php 7 years ago class.jetpack.php 7 years ago class.json-api-endpoints.php 7 years ago class.json-api.php 7 years ago class.photon.php 7 years ago composer.json 7 years ago functions.compat.php 7 years ago functions.gallery.php 8 years ago functions.global.php 7 years ago functions.opengraph.php 7 years ago functions.photon.php 8 years ago jetpack.php 5 years ago json-api-config.php 10 years ago json-endpoints.php 7 years ago locales.php 9 years ago readme.txt 5 years ago require-lib.php 7 years ago uninstall.php 8 years ago wpml-config.xml 10 years ago
class.photon.php
1032 lines
1 <?php
2
3 class Jetpack_Photon {
4 /**
5 * Class variables
6 */
7 // Oh look, a singleton
8 private static $__instance = null;
9
10 // Allowed extensions must match http://code.trac.wordpress.org/browser/photon/index.php#L31
11 protected static $extensions = array(
12 'gif',
13 'jpg',
14 'jpeg',
15 'png'
16 );
17
18 // Don't access this directly. Instead, use self::image_sizes() so it's actually populated with something.
19 protected static $image_sizes = null;
20
21 /**
22 * Singleton implementation
23 *
24 * @return object
25 */
26 public static function instance() {
27 if ( ! is_a( self::$__instance, 'Jetpack_Photon' ) ) {
28 self::$__instance = new Jetpack_Photon;
29 self::$__instance->setup();
30 }
31
32 return self::$__instance;
33 }
34
35 /**
36 * Silence is golden.
37 */
38 private function __construct() {}
39
40 /**
41 * Register actions and filters, but only if basic Photon functions are available.
42 * The basic functions are found in ./functions.photon.php.
43 *
44 * @uses add_action, add_filter
45 * @return null
46 */
47 private function setup() {
48 if ( ! function_exists( 'jetpack_photon_url' ) )
49 return;
50
51 // Images in post content and galleries
52 add_filter( 'the_content', array( __CLASS__, 'filter_the_content' ), 999999 );
53 add_filter( 'get_post_galleries', array( __CLASS__, 'filter_the_galleries' ), 999999 );
54 add_filter( 'widget_media_image_instance', array( __CLASS__, 'filter_the_image_widget' ), 999999 );
55
56 // Core image retrieval
57 add_filter( 'image_downsize', array( $this, 'filter_image_downsize' ), 10, 3 );
58
59 // Responsive image srcset substitution
60 add_filter( 'wp_calculate_image_srcset', array( $this, 'filter_srcset_array' ), 10, 5 );
61 add_filter( 'wp_calculate_image_sizes', array( $this, 'filter_sizes' ), 1, 2 ); // Early so themes can still easily filter.
62
63 // Helpers for maniuplated images
64 add_action( 'wp_enqueue_scripts', array( $this, 'action_wp_enqueue_scripts' ), 9 );
65 }
66
67 /**
68 ** IN-CONTENT IMAGE MANIPULATION FUNCTIONS
69 **/
70
71 /**
72 * Match all images and any relevant <a> tags in a block of HTML.
73 *
74 * @param string $content Some HTML.
75 * @return array An array of $images matches, where $images[0] is
76 * an array of full matches, and the link_url, img_tag,
77 * and img_url keys are arrays of those matches.
78 */
79 public static function parse_images_from_html( $content ) {
80 $images = array();
81
82 if ( preg_match_all( '#(?:<a[^>]+?href=["|\'](?P<link_url>[^\s]+?)["|\'][^>]*?>\s*)?(?P<img_tag><img[^>]*?\s+?src=["|\'](?P<img_url>[^\s]+?)["|\'].*?>){1}(?:\s*</a>)?#is', $content, $images ) ) {
83 foreach ( $images as $key => $unused ) {
84 // Simplify the output as much as possible, mostly for confirming test results.
85 if ( is_numeric( $key ) && $key > 0 )
86 unset( $images[$key] );
87 }
88
89 return $images;
90 }
91
92 return array();
93 }
94
95 /**
96 * Try to determine height and width from strings WP appends to resized image filenames.
97 *
98 * @param string $src The image URL.
99 * @return array An array consisting of width and height.
100 */
101 public static function parse_dimensions_from_filename( $src ) {
102 $width_height_string = array();
103
104 if ( preg_match( '#-(\d+)x(\d+)\.(?:' . implode('|', self::$extensions ) . '){1}$#i', $src, $width_height_string ) ) {
105 $width = (int) $width_height_string[1];
106 $height = (int) $width_height_string[2];
107
108 if ( $width && $height )
109 return array( $width, $height );
110 }
111
112 return array( false, false );
113 }
114
115 /**
116 * Identify images in post content, and if images are local (uploaded to the current site), pass through Photon.
117 *
118 * @param string $content
119 * @uses self::validate_image_url, apply_filters, jetpack_photon_url, esc_url
120 * @filter the_content
121 * @return string
122 */
123 public static function filter_the_content( $content ) {
124 $images = Jetpack_Photon::parse_images_from_html( $content );
125
126 if ( ! empty( $images ) ) {
127 $content_width = Jetpack::get_content_width();
128
129 $image_sizes = self::image_sizes();
130 $upload_dir = wp_get_upload_dir();
131
132 foreach ( $images[0] as $index => $tag ) {
133 // Default to resize, though fit may be used in certain cases where a dimension cannot be ascertained
134 $transform = 'resize';
135
136 // Start with a clean attachment ID each time
137 $attachment_id = false;
138
139 // Flag if we need to munge a fullsize URL
140 $fullsize_url = false;
141
142 // Identify image source
143 $src = $src_orig = $images['img_url'][ $index ];
144
145 /**
146 * Allow specific images to be skipped by Photon.
147 *
148 * @module photon
149 *
150 * @since 2.0.3
151 *
152 * @param bool false Should Photon ignore this image. Default to false.
153 * @param string $src Image URL.
154 * @param string $tag Image Tag (Image HTML output).
155 */
156 if ( apply_filters( 'jetpack_photon_skip_image', false, $src, $tag ) )
157 continue;
158
159 // Support Automattic's Lazy Load plugin
160 // Can't modify $tag yet as we need unadulterated version later
161 if ( preg_match( '#data-lazy-src=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
162 $placeholder_src = $placeholder_src_orig = $src;
163 $src = $src_orig = $lazy_load_src[1];
164 } elseif ( preg_match( '#data-lazy-original=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
165 $placeholder_src = $placeholder_src_orig = $src;
166 $src = $src_orig = $lazy_load_src[1];
167 }
168
169 // Check if image URL should be used with Photon
170 if ( self::validate_image_url( $src ) ) {
171 // Find the width and height attributes
172 $width = $height = false;
173
174 // First, check the image tag
175 if ( preg_match( '#width=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $width_string ) )
176 $width = $width_string[1];
177
178 if ( preg_match( '#height=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $height_string ) )
179 $height = $height_string[1];
180
181 // Can't pass both a relative width and height, so unset the height in favor of not breaking the horizontal layout.
182 if ( false !== strpos( $width, '%' ) && false !== strpos( $height, '%' ) )
183 $width = $height = false;
184
185 // Detect WP registered image size from HTML class
186 if ( preg_match( '#class=["|\']?[^"\']*size-([^"\'\s]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $size ) ) {
187 $size = array_pop( $size );
188
189 if ( false === $width && false === $height && 'full' != $size && array_key_exists( $size, $image_sizes ) ) {
190 $width = (int) $image_sizes[ $size ]['width'];
191 $height = (int) $image_sizes[ $size ]['height'];
192 $transform = $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
193 }
194 } else {
195 unset( $size );
196 }
197
198 // WP Attachment ID, if uploaded to this site
199 if (
200 preg_match( '#class=["|\']?[^"\']*wp-image-([\d]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $attachment_id ) &&
201 0 === strpos( $src, $upload_dir['baseurl'] ) &&
202 /**
203 * Filter whether an image using an attachment ID in its class has to be uploaded to the local site to go through Photon.
204 *
205 * @module photon
206 *
207 * @since 2.0.3
208 *
209 * @param bool false Was the image uploaded to the local site. Default to false.
210 * @param array $args {
211 * Array of image details.
212 *
213 * @type $src Image URL.
214 * @type tag Image tag (Image HTML output).
215 * @type $images Array of information about the image.
216 * @type $index Image index.
217 * }
218 */
219 apply_filters( 'jetpack_photon_image_is_local', false, compact( 'src', 'tag', 'images', 'index' ) )
220 ) {
221 $attachment_id = intval( array_pop( $attachment_id ) );
222
223 if ( $attachment_id ) {
224 $attachment = get_post( $attachment_id );
225
226 // Basic check on returned post object
227 if ( is_object( $attachment ) && ! is_wp_error( $attachment ) && 'attachment' == $attachment->post_type ) {
228 $src_per_wp = wp_get_attachment_image_src( $attachment_id, isset( $size ) ? $size : 'full' );
229
230 if ( self::validate_image_url( $src_per_wp[0] ) ) {
231 $src = $src_per_wp[0];
232 $fullsize_url = true;
233
234 // Prevent image distortion if a detected dimension exceeds the image's natural dimensions
235 if ( ( false !== $width && $width > $src_per_wp[1] ) || ( false !== $height && $height > $src_per_wp[2] ) ) {
236 $width = false === $width ? false : min( $width, $src_per_wp[1] );
237 $height = false === $height ? false : min( $height, $src_per_wp[2] );
238 }
239
240 // If no width and height are found, max out at source image's natural dimensions
241 // Otherwise, respect registered image sizes' cropping setting
242 if ( false === $width && false === $height ) {
243 $width = $src_per_wp[1];
244 $height = $src_per_wp[2];
245 $transform = 'fit';
246 } elseif ( isset( $size ) && array_key_exists( $size, $image_sizes ) && isset( $image_sizes[ $size ]['crop'] ) ) {
247 $transform = (bool) $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
248 }
249 }
250 } else {
251 unset( $attachment_id );
252 unset( $attachment );
253 }
254 }
255 }
256
257 // If image tag lacks width and height arguments, try to determine from strings WP appends to resized image filenames.
258 if ( false === $width && false === $height ) {
259 list( $width, $height ) = Jetpack_Photon::parse_dimensions_from_filename( $src );
260 }
261
262 // If width is available, constrain to $content_width
263 if ( false !== $width && false === strpos( $width, '%' ) && is_numeric( $content_width ) ) {
264 if ( $width > $content_width && false !== $height && false === strpos( $height, '%' ) ) {
265 $height = round( ( $content_width * $height ) / $width );
266 $width = $content_width;
267 } elseif ( $width > $content_width ) {
268 $width = $content_width;
269 }
270 }
271
272 // Set a width if none is found and $content_width is available
273 // If width is set in this manner and height is available, use `fit` instead of `resize` to prevent skewing
274 if ( false === $width && is_numeric( $content_width ) ) {
275 $width = (int) $content_width;
276
277 if ( false !== $height )
278 $transform = 'fit';
279 }
280
281 // Detect if image source is for a custom-cropped thumbnail and prevent further URL manipulation.
282 if ( ! $fullsize_url && preg_match_all( '#-e[a-z0-9]+(-\d+x\d+)?\.(' . implode('|', self::$extensions ) . '){1}$#i', basename( $src ), $filename ) )
283 $fullsize_url = true;
284
285 // Build URL, first maybe removing WP's resized string so we pass the original image to Photon
286 if ( ! $fullsize_url ) {
287 $src = self::strip_image_dimensions_maybe( $src );
288 }
289
290 // Build array of Photon args and expose to filter before passing to Photon URL function
291 $args = array();
292
293 if ( false !== $width && false !== $height && false === strpos( $width, '%' ) && false === strpos( $height, '%' ) )
294 $args[ $transform ] = $width . ',' . $height;
295 elseif ( false !== $width )
296 $args['w'] = $width;
297 elseif ( false !== $height )
298 $args['h'] = $height;
299
300 /**
301 * Filter the array of Photon arguments added to an image when it goes through Photon.
302 * By default, only includes width and height values.
303 * @see https://developer.wordpress.com/docs/photon/api/
304 *
305 * @module photon
306 *
307 * @since 2.0.0
308 *
309 * @param array $args Array of Photon Arguments.
310 * @param array $args {
311 * Array of image details.
312 *
313 * @type $tag Image tag (Image HTML output).
314 * @type $src Image URL.
315 * @type $src_orig Original Image URL.
316 * @type $width Image width.
317 * @type $height Image height.
318 * }
319 */
320 $args = apply_filters( 'jetpack_photon_post_image_args', $args, compact( 'tag', 'src', 'src_orig', 'width', 'height' ) );
321
322 $photon_url = jetpack_photon_url( $src, $args );
323
324 // Modify image tag if Photon function provides a URL
325 // 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.
326 if ( $src != $photon_url ) {
327 $new_tag = $tag;
328
329 // If present, replace the link href with a Photoned URL for the full-size image.
330 if ( ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) )
331 $new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . jetpack_photon_url( $images['link_url'][ $index ] ) . '\2', $new_tag, 1 );
332
333 // Supplant the original source value with our Photon URL
334 $photon_url = esc_url( $photon_url );
335 $new_tag = str_replace( $src_orig, $photon_url, $new_tag );
336
337 // If Lazy Load is in use, pass placeholder image through Photon
338 if ( isset( $placeholder_src ) && self::validate_image_url( $placeholder_src ) ) {
339 $placeholder_src = jetpack_photon_url( $placeholder_src );
340
341 if ( $placeholder_src != $placeholder_src_orig )
342 $new_tag = str_replace( $placeholder_src_orig, esc_url( $placeholder_src ), $new_tag );
343
344 unset( $placeholder_src );
345 }
346
347 // If we are not transforming the image with resize, fit, or letterbox (lb), then we should remove
348 // the width and height arguments from the image to prevent distortion. Even if $args['w'] and $args['h']
349 // are present, Photon does not crop to those dimensions. Instead, it appears to favor height.
350 //
351 // If we are transforming the image via one of those methods, let's update the width and height attributes.
352 if ( empty( $args['resize'] ) && empty( $args['fit'] ) && empty( $args['lb'] ) ) {
353 $new_tag = preg_replace( '#(?<=\s)(width|height)=["|\']?[\d%]+["|\']?\s?#i', '', $new_tag );
354 } else {
355 $resize_args = isset( $args['resize'] ) ? $args['resize'] : false;
356 if ( false == $resize_args ) {
357 $resize_args = ( ! $resize_args && isset( $args['fit'] ) )
358 ? $args['fit']
359 : false;
360 }
361 if ( false == $resize_args ) {
362 $resize_args = ( ! $resize_args && isset( $args['lb'] ) )
363 ? $args['lb']
364 : false;
365 }
366
367 $resize_args = array_map( 'trim', explode( ',', $resize_args ) );
368
369 // (?<=\s) - Ensure width or height attribute is preceded by a space
370 // (width=["|\']?) - Matches, and captures, width=, width=", or width='
371 // [\d%]+ - Matches 1 or more digits
372 // (["|\']?) - Matches, and captures, ", ', or empty string
373 // \s - Ensures there's a space after the attribute
374 $new_tag = preg_replace( '#(?<=\s)(width=["|\']?)[\d%]+(["|\']?)\s?#i', sprintf( '${1}%d${2} ', $resize_args[0] ), $new_tag );
375 $new_tag = preg_replace( '#(?<=\s)(height=["|\']?)[\d%]+(["|\']?)\s?#i', sprintf( '${1}%d${2} ', $resize_args[1] ), $new_tag );
376 }
377
378 // Tag an image for dimension checking
379 $new_tag = preg_replace( '#(\s?/)?>(\s*</a>)?$#i', ' data-recalc-dims="1"\1>\2', $new_tag );
380
381 // Replace original tag with modified version
382 $content = str_replace( $tag, $new_tag, $content );
383 }
384 } elseif ( preg_match( '#^http(s)?://i[\d]{1}.wp.com#', $src ) && ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
385 $new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . jetpack_photon_url( $images['link_url'][ $index ] ) . '\2', $tag, 1 );
386
387 $content = str_replace( $tag, $new_tag, $content );
388 }
389 }
390 }
391
392 return $content;
393 }
394
395 public static function filter_the_galleries( $galleries ) {
396 if ( empty( $galleries ) || ! is_array( $galleries ) ) {
397 return $galleries;
398 }
399
400 // Pass by reference, so we can modify them in place.
401 foreach ( $galleries as &$this_gallery ) {
402 if ( is_string( $this_gallery ) ) {
403 $this_gallery = self::filter_the_content( $this_gallery );
404 // LEAVING COMMENTED OUT as for the moment it doesn't seem
405 // necessary and I'm not sure how it would propagate through.
406 // } elseif ( is_array( $this_gallery )
407 // && ! empty( $this_gallery['src'] )
408 // && ! empty( $this_gallery['type'] )
409 // && in_array( $this_gallery['type'], array( 'rectangle', 'square', 'circle' ) ) ) {
410 // $this_gallery['src'] = array_map( 'jetpack_photon_url', $this_gallery['src'] );
411 }
412 }
413 unset( $this_gallery ); // break the reference.
414
415 return $galleries;
416 }
417
418
419 /**
420 * Runs the image widget through photon.
421 *
422 * @param array $instance Image widget instance data.
423 * @return array
424 */
425 public static function filter_the_image_widget( $instance ) {
426 if ( Jetpack::is_module_active( 'photon' ) && ! $instance['attachment_id'] && $instance['url'] ) {
427 jetpack_photon_url( $instance['url'], array(
428 'w' => $instance['width'],
429 'h' => $instance['height'],
430 ) );
431 }
432
433 return $instance;
434 }
435
436 /**
437 ** CORE IMAGE RETRIEVAL
438 **/
439
440 /**
441 * Filter post thumbnail image retrieval, passing images through Photon
442 *
443 * @param string|bool $image
444 * @param int $attachment_id
445 * @param string|array $size
446 * @uses is_admin, apply_filters, wp_get_attachment_url, self::validate_image_url, this::image_sizes, jetpack_photon_url
447 * @filter image_downsize
448 * @return string|bool
449 */
450 public function filter_image_downsize( $image, $attachment_id, $size ) {
451 // Don't foul up the admin side of things, unless a plugin wants to.
452 if ( is_admin() &&
453 /**
454 * Provide plugins a way of running Photon for images in the WordPress Dashboard (wp-admin).
455 *
456 * 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.
457 *
458 * @module photon
459 *
460 * @since 4.8.0
461 *
462 * @param bool false Stop Photon from being run on the Dashboard. Default to false.
463 * @param array $args {
464 * Array of image details.
465 *
466 * @type $image Image URL.
467 * @type $attachment_id Attachment ID of the image.
468 * @type $size Image size. Can be a string (name of the image size, e.g. full) or an array of width and height.
469 * }
470 */
471 false === apply_filters( 'jetpack_photon_admin_allow_image_downsize', false, compact( 'image', 'attachment_id', 'size' ) )
472 ) {
473 return $image;
474 }
475
476 /**
477 * Provide plugins a way of preventing Photon from being applied to images retrieved from WordPress Core.
478 *
479 * @module photon
480 *
481 * @since 2.0.0
482 *
483 * @param bool false Stop Photon from being applied to the image. Default to false.
484 * @param array $args {
485 * Array of image details.
486 *
487 * @type $image Image URL.
488 * @type $attachment_id Attachment ID of the image.
489 * @type $size Image size. Can be a string (name of the image size, e.g. full) or an array of width and height.
490 * }
491 */
492 if ( apply_filters( 'jetpack_photon_override_image_downsize', false, compact( 'image', 'attachment_id', 'size' ) ) ) {
493 return $image;
494 }
495
496 // Get the image URL and proceed with Photon-ification if successful
497 $image_url = wp_get_attachment_url( $attachment_id );
498
499 // Set this to true later when we know we have size meta.
500 $has_size_meta = false;
501
502 if ( $image_url ) {
503 // Check if image URL should be used with Photon
504 if ( ! self::validate_image_url( $image_url ) )
505 return $image;
506
507 $intermediate = true; // For the fourth array item returned by the image_downsize filter.
508
509 // If an image is requested with a size known to WordPress, use that size's settings with Photon.
510 // WP states that `add_image_size()` should use a string for the name, but doesn't enforce that.
511 // Due to differences in how Core and Photon check for the registered image size, we check both types.
512 if ( ( is_string( $size ) || is_int( $size ) ) && array_key_exists( $size, self::image_sizes() ) ) {
513 $image_args = self::image_sizes();
514 $image_args = $image_args[ $size ];
515
516 $photon_args = array();
517
518 $image_meta = image_get_intermediate_size( $attachment_id, $size );
519
520 // 'full' is a special case: We need consistent data regardless of the requested size.
521 if ( 'full' == $size ) {
522 $image_meta = wp_get_attachment_metadata( $attachment_id );
523 $intermediate = false;
524 } elseif ( ! $image_meta ) {
525 // If we still don't have any image meta at this point, it's probably from a custom thumbnail size
526 // for an image that was uploaded before the custom image was added to the theme. Try to determine the size manually.
527 $image_meta = wp_get_attachment_metadata( $attachment_id );
528
529 if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
530 $image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $image_args['width'], $image_args['height'], $image_args['crop'] );
531 if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
532 $image_meta['width'] = $image_resized[6];
533 $image_meta['height'] = $image_resized[7];
534 }
535 }
536 }
537
538 if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
539 $image_args['width'] = $image_meta['width'];
540 $image_args['height'] = $image_meta['height'];
541
542 list( $image_args['width'], $image_args['height'] ) = image_constrain_size_for_editor( $image_args['width'], $image_args['height'], $size, 'display' );
543 $has_size_meta = true;
544 }
545
546 // Expose determined arguments to a filter before passing to Photon
547 $transform = $image_args['crop'] ? 'resize' : 'fit';
548
549 // Check specified image dimensions and account for possible zero values; photon fails to resize if a dimension is zero.
550 if ( 0 == $image_args['width'] || 0 == $image_args['height'] ) {
551 if ( 0 == $image_args['width'] && 0 < $image_args['height'] ) {
552 $photon_args['h'] = $image_args['height'];
553 } elseif ( 0 == $image_args['height'] && 0 < $image_args['width'] ) {
554 $photon_args['w'] = $image_args['width'];
555 }
556 } else {
557 if ( ( 'resize' === $transform ) && $image_meta = wp_get_attachment_metadata( $attachment_id ) ) {
558 if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
559 // Lets make sure that we don't upscale images since wp never upscales them as well
560 $smaller_width = ( ( $image_meta['width'] < $image_args['width'] ) ? $image_meta['width'] : $image_args['width'] );
561 $smaller_height = ( ( $image_meta['height'] < $image_args['height'] ) ? $image_meta['height'] : $image_args['height'] );
562
563 $photon_args[ $transform ] = $smaller_width . ',' . $smaller_height;
564 }
565 } else {
566 $photon_args[ $transform ] = $image_args['width'] . ',' . $image_args['height'];
567 }
568
569 }
570
571
572 /**
573 * Filter the Photon Arguments added to an image when going through Photon, when that image size is a string.
574 * Image size will be a string (e.g. "full", "medium") when it is known to WordPress.
575 *
576 * @module photon
577 *
578 * @since 2.0.0
579 *
580 * @param array $photon_args Array of Photon arguments.
581 * @param array $args {
582 * Array of image details.
583 *
584 * @type $image_args Array of Image arguments (width, height, crop).
585 * @type $image_url Image URL.
586 * @type $attachment_id Attachment ID of the image.
587 * @type $size Image size. Can be a string (name of the image size, e.g. full) or an integer.
588 * @type $transform Value can be resize or fit.
589 * @see https://developer.wordpress.com/docs/photon/api
590 * }
591 */
592 $photon_args = apply_filters( 'jetpack_photon_image_downsize_string', $photon_args, compact( 'image_args', 'image_url', 'attachment_id', 'size', 'transform' ) );
593
594 // Generate Photon URL
595 $image = array(
596 jetpack_photon_url( $image_url, $photon_args ),
597 $has_size_meta ? $image_args['width'] : false,
598 $has_size_meta ? $image_args['height'] : false,
599 $intermediate
600 );
601 } elseif ( is_array( $size ) ) {
602 // Pull width and height values from the provided array, if possible
603 $width = isset( $size[0] ) ? (int) $size[0] : false;
604 $height = isset( $size[1] ) ? (int) $size[1] : false;
605
606 // Don't bother if necessary parameters aren't passed.
607 if ( ! $width || ! $height ) {
608 return $image;
609 }
610
611 $image_meta = wp_get_attachment_metadata( $attachment_id );
612 if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
613 $image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $width, $height );
614
615 if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
616 $width = $image_resized[6];
617 $height = $image_resized[7];
618 } else {
619 $width = $image_meta['width'];
620 $height = $image_meta['height'];
621 }
622
623 $has_size_meta = true;
624 }
625
626 list( $width, $height ) = image_constrain_size_for_editor( $width, $height, $size );
627
628 // Expose arguments to a filter before passing to Photon
629 $photon_args = array(
630 'fit' => $width . ',' . $height
631 );
632
633 /**
634 * Filter the Photon Arguments added to an image when going through Photon,
635 * when the image size is an array of height and width values.
636 *
637 * @module photon
638 *
639 * @since 2.0.0
640 *
641 * @param array $photon_args Array of Photon arguments.
642 * @param array $args {
643 * Array of image details.
644 *
645 * @type $width Image width.
646 * @type height Image height.
647 * @type $image_url Image URL.
648 * @type $attachment_id Attachment ID of the image.
649 * }
650 */
651 $photon_args = apply_filters( 'jetpack_photon_image_downsize_array', $photon_args, compact( 'width', 'height', 'image_url', 'attachment_id' ) );
652
653 // Generate Photon URL
654 $image = array(
655 jetpack_photon_url( $image_url, $photon_args ),
656 $has_size_meta ? $width : false,
657 $has_size_meta ? $height : false,
658 $intermediate
659 );
660 }
661 }
662
663 return $image;
664 }
665
666 /**
667 * Filters an array of image `srcset` values, replacing each URL with its Photon equivalent.
668 *
669 * @since 3.8.0
670 * @since 4.0.4 Added automatically additional sizes beyond declared image sizes.
671 * @param array $sources An array of image urls and widths.
672 * @uses self::validate_image_url, jetpack_photon_url, Jetpack_Photon::parse_from_filename
673 * @uses Jetpack_Photon::strip_image_dimensions_maybe, Jetpack::get_content_width
674 * @return array An array of Photon image urls and widths.
675 */
676 public function filter_srcset_array( $sources = array(), $size_array = array(), $image_src = array(), $image_meta = array(), $attachment_id = 0 ) {
677 if ( ! is_array( $sources ) ) {
678 return $sources;
679 }
680 $upload_dir = wp_get_upload_dir();
681
682 foreach ( $sources as $i => $source ) {
683 if ( ! self::validate_image_url( $source['url'] ) ) {
684 continue;
685 }
686
687 /** This filter is already documented in class.photon.php */
688 if ( apply_filters( 'jetpack_photon_skip_image', false, $source['url'], $source ) ) {
689 continue;
690 }
691
692 $url = $source['url'];
693 list( $width, $height ) = Jetpack_Photon::parse_dimensions_from_filename( $url );
694
695 // It's quicker to get the full size with the data we have already, if available
696 if ( ! empty( $attachment_id ) ) {
697 $url = wp_get_attachment_url( $attachment_id );
698 } else {
699 $url = Jetpack_Photon::strip_image_dimensions_maybe( $url );
700 }
701
702 $args = array();
703 if ( 'w' === $source['descriptor'] ) {
704 if ( $height && ( $source['value'] == $width ) ) {
705 $args['resize'] = $width . ',' . $height;
706 } else {
707 $args['w'] = $source['value'];
708 }
709
710 }
711
712 $sources[ $i ]['url'] = jetpack_photon_url( $url, $args );
713 }
714
715 /**
716 * At this point, $sources is the original srcset with Photonized URLs.
717 * Now, we're going to construct additional sizes based on multiples of the content_width.
718 * This will reduce the gap between the largest defined size and the original image.
719 */
720
721 /**
722 * Filter the multiplier Photon uses to create new srcset items.
723 * Return false to short-circuit and bypass auto-generation.
724 *
725 * @module photon
726 *
727 * @since 4.0.4
728 *
729 * @param array|bool $multipliers Array of multipliers to use or false to bypass.
730 */
731 $multipliers = apply_filters( 'jetpack_photon_srcset_multipliers', array( 2, 3 ) );
732 $url = trailingslashit( $upload_dir['baseurl'] ) . $image_meta['file'];
733
734 if (
735 /** Short-circuit via jetpack_photon_srcset_multipliers filter. */
736 is_array( $multipliers )
737 /** This filter is already documented in class.photon.php */
738 && ! apply_filters( 'jetpack_photon_skip_image', false, $url, null )
739 /** Verify basic meta is intact. */
740 && isset( $image_meta['width'] ) && isset( $image_meta['height'] ) && isset( $image_meta['file'] )
741 /** Verify we have the requested width/height. */
742 && isset( $size_array[0] ) && isset( $size_array[1] )
743 ) {
744
745 $fullwidth = $image_meta['width'];
746 $fullheight = $image_meta['height'];
747 $reqwidth = $size_array[0];
748 $reqheight = $size_array[1];
749
750 $constrained_size = wp_constrain_dimensions( $fullwidth, $fullheight, $reqwidth );
751 $expected_size = array( $reqwidth, $reqheight );
752
753 if ( abs( $constrained_size[0] - $expected_size[0] ) <= 1 && abs( $constrained_size[1] - $expected_size[1] ) <= 1 ) {
754 $crop = 'soft';
755 $base = Jetpack::get_content_width() ? Jetpack::get_content_width() : 1000; // Provide a default width if none set by the theme.
756 } else {
757 $crop = 'hard';
758 $base = $reqwidth;
759 }
760
761
762 $currentwidths = array_keys( $sources );
763 $newsources = null;
764
765 foreach ( $multipliers as $multiplier ) {
766
767 $newwidth = $base * $multiplier;
768 foreach ( $currentwidths as $currentwidth ){
769 // If a new width would be within 100 pixes of an existing one or larger than the full size image, skip.
770 if ( abs( $currentwidth - $newwidth ) < 50 || ( $newwidth > $fullwidth ) ) {
771 continue 2; // Back to the foreach ( $multipliers as $multiplier )
772 }
773 } // foreach ( $currentwidths as $currentwidth ){
774
775 if ( 'soft' == $crop ) {
776 $args = array(
777 'w' => $newwidth,
778 );
779 } else { // hard crop, e.g. add_image_size( 'example', 200, 200, true );
780 $args = array(
781 'zoom' => $multiplier,
782 'resize' => $reqwidth . ',' . $reqheight,
783 );
784 }
785
786 $newsources[ $newwidth ] = array(
787 'url' => jetpack_photon_url( $url, $args ),
788 'descriptor' => 'w',
789 'value' => $newwidth,
790 );
791 } // foreach ( $multipliers as $multiplier )
792 if ( is_array( $newsources ) ) {
793 if ( function_exists( 'array_replace' ) ) { // PHP 5.3+, preferred
794 $sources = array_replace( $sources, $newsources ); // phpcs:ignore PHPCompatibility.PHP.NewFunctions.array_replaceFound
795 } else { // For PHP 5.2 using WP shim function
796 $sources = array_replace_recursive( $sources, $newsources );
797 }
798 }
799 } // if ( isset( $image_meta['width'] ) && isset( $image_meta['file'] ) )
800
801 return $sources;
802 }
803
804 /**
805 * Filters an array of image `sizes` values, using $content_width instead of image's full size.
806 *
807 * @since 4.0.4
808 * @since 4.1.0 Returns early for images not within the_content.
809 * @param array $sizes An array of media query breakpoints.
810 * @param array $size Width and height of the image
811 * @uses Jetpack::get_content_width
812 * @return array An array of media query breakpoints.
813 */
814 public function filter_sizes( $sizes, $size ) {
815 if ( ! doing_filter( 'the_content' ) ){
816 return $sizes;
817 }
818 $content_width = Jetpack::get_content_width();
819 if ( ! $content_width ) {
820 $content_width = 1000;
821 }
822
823 if ( ( is_array( $size ) && $size[0] < $content_width ) ) {
824 return $sizes;
825 }
826
827 return sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $content_width );
828 }
829
830 /**
831 ** GENERAL FUNCTIONS
832 **/
833
834 /**
835 * Ensure image URL is valid for Photon.
836 * 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.
837 *
838 * @param string $url
839 * @uses wp_parse_args
840 * @return bool
841 */
842 protected static function validate_image_url( $url ) {
843 $parsed_url = @parse_url( $url );
844
845 if ( ! $parsed_url )
846 return false;
847
848 // Parse URL and ensure needed keys exist, since the array returned by `parse_url` only includes the URL components it finds.
849 $url_info = wp_parse_args( $parsed_url, array(
850 'scheme' => null,
851 'host' => null,
852 'port' => null,
853 'path' => null
854 ) );
855
856 // Bail if scheme isn't http or port is set that isn't port 80
857 if (
858 ( 'http' != $url_info['scheme'] || ! in_array( $url_info['port'], array( 80, null ) ) ) &&
859 /**
860 * Allow Photon to fetch images that are served via HTTPS.
861 *
862 * @module photon
863 *
864 * @since 2.4.0
865 * @since 3.9.0 Default to false.
866 *
867 * @param bool $reject_https Should Photon ignore images using the HTTPS scheme. Default to false.
868 */
869 apply_filters( 'jetpack_photon_reject_https', false )
870 ) {
871 return false;
872 }
873
874 // Bail if no host is found
875 if ( is_null( $url_info['host'] ) )
876 return false;
877
878 // Bail if the image alredy went through Photon
879 if ( preg_match( '#^i[\d]{1}.wp.com$#i', $url_info['host'] ) )
880 return false;
881
882 // Bail if no path is found
883 if ( is_null( $url_info['path'] ) )
884 return false;
885
886 // Ensure image extension is acceptable
887 if ( ! in_array( strtolower( pathinfo( $url_info['path'], PATHINFO_EXTENSION ) ), self::$extensions ) )
888 return false;
889
890 // If we got this far, we should have an acceptable image URL
891 // But let folks filter to decline if they prefer.
892 /**
893 * Overwrite the results of the validation steps an image goes through before to be considered valid to be used by Photon.
894 *
895 * @module photon
896 *
897 * @since 3.0.0
898 *
899 * @param bool true Is the image URL valid and can it be used by Photon. Default to true.
900 * @param string $url Image URL.
901 * @param array $parsed_url Array of information about the image.
902 */
903 return apply_filters( 'photon_validate_image_url', true, $url, $parsed_url );
904 }
905
906 /**
907 * Checks if the file exists before it passes the file to photon
908 *
909 * @param string $src The image URL
910 * @return string
911 **/
912 protected static function strip_image_dimensions_maybe( $src ){
913 $stripped_src = $src;
914
915 // Build URL, first removing WP's resized string so we pass the original image to Photon
916 if ( preg_match( '#(-\d+x\d+)\.(' . implode('|', self::$extensions ) . '){1}$#i', $src, $src_parts ) ) {
917 $stripped_src = str_replace( $src_parts[1], '', $src );
918 $upload_dir = wp_get_upload_dir();
919
920 // Extracts the file path to the image minus the base url
921 $file_path = substr( $stripped_src, strlen ( $upload_dir['baseurl'] ) );
922
923 if( file_exists( $upload_dir["basedir"] . $file_path ) )
924 $src = $stripped_src;
925 }
926
927 return $src;
928 }
929
930 /**
931 * Provide an array of available image sizes and corresponding dimensions.
932 * Similar to get_intermediate_image_sizes() except that it includes image sizes' dimensions, not just their names.
933 *
934 * @global $wp_additional_image_sizes
935 * @uses get_option
936 * @return array
937 */
938 protected static function image_sizes() {
939 if ( null == self::$image_sizes ) {
940 global $_wp_additional_image_sizes;
941
942 // Populate an array matching the data structure of $_wp_additional_image_sizes so we have a consistent structure for image sizes
943 $images = array(
944 'thumb' => array(
945 'width' => intval( get_option( 'thumbnail_size_w' ) ),
946 'height' => intval( get_option( 'thumbnail_size_h' ) ),
947 'crop' => (bool) get_option( 'thumbnail_crop' )
948 ),
949 'medium' => array(
950 'width' => intval( get_option( 'medium_size_w' ) ),
951 'height' => intval( get_option( 'medium_size_h' ) ),
952 'crop' => false
953 ),
954 'large' => array(
955 'width' => intval( get_option( 'large_size_w' ) ),
956 'height' => intval( get_option( 'large_size_h' ) ),
957 'crop' => false
958 ),
959 'full' => array(
960 'width' => null,
961 'height' => null,
962 'crop' => false
963 )
964 );
965
966 // Compatibility mapping as found in wp-includes/media.php
967 $images['thumbnail'] = $images['thumb'];
968
969 // Update class variable, merging in $_wp_additional_image_sizes if any are set
970 if ( is_array( $_wp_additional_image_sizes ) && ! empty( $_wp_additional_image_sizes ) )
971 self::$image_sizes = array_merge( $images, $_wp_additional_image_sizes );
972 else
973 self::$image_sizes = $images;
974 }
975
976 return is_array( self::$image_sizes ) ? self::$image_sizes : array();
977 }
978
979 /**
980 * Pass og:image URLs through Photon
981 *
982 * @param array $tags
983 * @param array $parameters
984 * @uses jetpack_photon_url
985 * @return array
986 */
987 function filter_open_graph_tags( $tags, $parameters ) {
988 if ( empty( $tags['og:image'] ) ) {
989 return $tags;
990 }
991
992 $photon_args = array(
993 'fit' => sprintf( '%d,%d', 2 * $parameters['image_width'], 2 * $parameters['image_height'] ),
994 );
995
996 if ( is_array( $tags['og:image'] ) ) {
997 $images = array();
998 foreach ( $tags['og:image'] as $image ) {
999 $images[] = jetpack_photon_url( $image, $photon_args );
1000 }
1001 $tags['og:image'] = $images;
1002 } else {
1003 $tags['og:image'] = jetpack_photon_url( $tags['og:image'], $photon_args );
1004 }
1005
1006 return $tags;
1007 }
1008
1009 /**
1010 * Enqueue Photon helper script
1011 *
1012 * @uses wp_enqueue_script, plugins_url
1013 * @action wp_enqueue_script
1014 * @return null
1015 */
1016 public function action_wp_enqueue_scripts() {
1017 if ( Jetpack_AMP_Support::is_amp_request() ) {
1018 return;
1019 }
1020 wp_enqueue_script(
1021 'jetpack-photon',
1022 Jetpack::get_file_url_for_environment(
1023 '_inc/build/photon/photon.min.js',
1024 'modules/photon/photon.js'
1025 ),
1026 array( 'jquery' ),
1027 20130122,
1028 true
1029 );
1030 }
1031 }
1032