Completed
Push — add/changelog-910 ( c277c8...7fd9c0 )
by Jeremy
19:06 queued 09:10
created

functions.photon.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
use Automattic\Jetpack\Status;
4
5
/**
6
 * Generic functions using the Photon service.
7
 *
8
 * Some are used outside of the Photon module being active, so intentionally not within the module.
9
 *
10
 * @package jetpack
11
 */
12
13
/**
14
 * Generates a Photon URL.
15
 *
16
 * @see https://developer.wordpress.com/docs/photon/
17
 *
18
 * @param string       $image_url URL to the publicly accessible image you want to manipulate.
19
 * @param array|string $args An array of arguments, i.e. array( 'w' => '300', 'resize' => array( 123, 456 ) ), or in string form (w=123&h=456).
20
 * @param string|null  $scheme URL protocol.
21
 * @return string The raw final URL. You should run this through esc_url() before displaying it.
22
 */
23
function jetpack_photon_url( $image_url, $args = array(), $scheme = null ) {
24
	$image_url = trim( $image_url );
25
26
	if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
27
		/**
28
		 * Disables Photon URL processing for local development
29
		 *
30
		 * @module photon
31
		 *
32
		 * @since 4.1.0
33
		 *
34
		 * @param bool false Result of Automattic\Jetpack\Status->is_offline_mode().
35
		 */
36
		if ( true === apply_filters( 'jetpack_photon_development_mode', ( new Status() )->is_offline_mode() ) ) {
37
			return $image_url;
38
		}
39
	}
40
41
	/**
42
	 * Allow specific image URls to avoid going through Photon.
43
	 *
44
	 * @module photon
45
	 *
46
	 * @since 3.2.0
47
	 *
48
	 * @param bool false Should the image be returned as is, without going through Photon. Default to false.
49
	 * @param string $image_url Image URL.
50
	 * @param array|string $args Array of Photon arguments.
51
	 * @param string|null $scheme Image scheme. Default to null.
52
	 */
53
	if ( false !== apply_filters( 'jetpack_photon_skip_for_url', false, $image_url, $args, $scheme ) ) {
54
		return $image_url;
55
	}
56
57
	/**
58
	 * Filter the original image URL before it goes through Photon.
59
	 *
60
	 * @module photon
61
	 *
62
	 * @since 1.9.0
63
	 *
64
	 * @param string $image_url Image URL.
65
	 * @param array|string $args Array of Photon arguments.
66
	 * @param string|null $scheme Image scheme. Default to null.
67
	 */
68
	$image_url = apply_filters( 'jetpack_photon_pre_image_url', $image_url, $args, $scheme );
69
	/**
70
	 * Filter the original Photon image parameters before Photon is applied to an image.
71
	 *
72
	 * @module photon
73
	 *
74
	 * @since 1.9.0
75
	 *
76
	 * @param array|string $args Array of Photon arguments.
77
	 * @param string $image_url Image URL.
78
	 * @param string|null $scheme Image scheme. Default to null.
79
	 */
80
	$args = apply_filters( 'jetpack_photon_pre_args', $args, $image_url, $scheme );
81
82
	if ( empty( $image_url ) ) {
83
		return $image_url;
84
	}
85
86
	$image_url_parts = wp_parse_url( $image_url );
87
88
	// Unable to parse.
89
	if ( ! is_array( $image_url_parts ) || empty( $image_url_parts['host'] ) || empty( $image_url_parts['path'] ) ) {
90
		return $image_url;
91
	}
92
93
	if ( is_array( $args ) ) {
94
		// Convert values that are arrays into strings.
95
		foreach ( $args as $arg => $value ) {
96
			if ( is_array( $value ) ) {
97
				$args[ $arg ] = implode( ',', $value );
98
			}
99
		}
100
101
		// Encode values.
102
		// See https://core.trac.wordpress.org/ticket/17923 .
103
		$args = rawurlencode_deep( $args );
104
	}
105
106
	// Don't photon-ize WPCOM hosted images -- we can serve them up from wpcom directly.
107
	$is_wpcom_image = false;
108
	if ( wp_endswith( strtolower( $image_url_parts['host'] ), '.files.wordpress.com' ) ) {
109
		$is_wpcom_image = true;
110
		if ( isset( $args['ssl'] ) ) {
111
			// Do not send the ssl argument to prevent caching issues.
112
			unset( $args['ssl'] );
113
		}
114
	}
115
116
	/** This filter is documented below. */
117
	$custom_photon_url = apply_filters( 'jetpack_photon_domain', '', $image_url );
118
	$custom_photon_url = esc_url( $custom_photon_url );
119
120
	// You can't run a Photon URL through Photon again because query strings are stripped.
121
	// So if the image is already a Photon URL, append the new arguments to the existing URL.
122
	// Alternately, if it's a *.files.wordpress.com url, then keep the domain as is.
123
	if (
124
		in_array( $image_url_parts['host'], array( 'i0.wp.com', 'i1.wp.com', 'i2.wp.com' ), true )
125
		|| wp_parse_url( $custom_photon_url, PHP_URL_HOST ) === $image_url_parts['host']
126
		|| $is_wpcom_image
127
	) {
128
		/*
129
		 * VideoPress Poster images should only keep one param, ssl.
130
		 */
131
		if (
132
			is_array( $args )
133
			&& 'videos.files.wordpress.com' === strtolower( $image_url_parts['host'] )
134
		) {
135
			$args = array_intersect_key( array( 'ssl' => 1 ), $args );
136
		}
137
138
		$photon_url = add_query_arg( $args, $image_url );
139
		return jetpack_photon_url_scheme( $photon_url, $scheme );
140
	}
141
142
	/**
143
	 * Allow Photon to use query strings as well.
144
	 * By default, Photon doesn't support query strings so we ignore them and look only at the path.
145
	 * This setting is Photon Server dependent.
146
	 *
147
	 * @module photon
148
	 *
149
	 * @since 1.9.0
150
	 *
151
	 * @param bool false Should images using query strings go through Photon. Default is false.
152
	 * @param string $image_url_parts['host'] Image URL's host.
153
	 */
154
	if ( ! apply_filters( 'jetpack_photon_any_extension_for_domain', false, $image_url_parts['host'] ) ) {
155
		// Photon doesn't support query strings so we ignore them and look only at the path.
156
		// However some source images are served via PHP so check the no-query-string extension.
157
		// For future proofing, this is an excluded list of common issues rather than an allow list.
158
		$extension = pathinfo( $image_url_parts['path'], PATHINFO_EXTENSION );
159
		if ( empty( $extension ) || in_array( $extension, array( 'php', 'ashx' ), true ) ) {
160
			return $image_url;
161
		}
162
	}
163
164
	$image_host_path = $image_url_parts['host'] . $image_url_parts['path'];
165
166
	/*
167
	 * Figure out which CDN subdomain to use.
168
	 *
169
	 * The goal is to have the same subdomain for any particular image to prevent multiple runs resulting in multiple
170
	 * images needing to be downloaded by the browser.
171
	 *
172
	 * We are providing our own generated value by taking the modulus of the crc32 value of the URL.
173
	 *
174
	 * Valid values are 0, 1, and 2.
175
	 */
176
	$subdomain = abs( crc32( $image_host_path ) % 3 );
177
178
	/**
179
	 * Filters the domain used by the Photon module.
180
	 *
181
	 * @module photon
182
	 *
183
	 * @since 3.4.2
184
	 *
185
	 * @param string https://i{$subdomain}.wp.com Domain used by Photon. $subdomain is a random number between 0 and 2.
186
	 * @param string $image_url URL of the image to be photonized.
187
	 */
188
	$photon_domain = apply_filters( 'jetpack_photon_domain', "https://i{$subdomain}.wp.com", $image_url );
189
	$photon_domain = trailingslashit( esc_url( $photon_domain ) );
190
	$photon_url    = $photon_domain . $image_host_path;
191
192
	/**
193
	 * Add query strings to Photon URL.
194
	 * By default, Photon doesn't support query strings so we ignore them.
195
	 * This setting is Photon Server dependent.
196
	 *
197
	 * @module photon
198
	 *
199
	 * @since 1.9.0
200
	 *
201
	 * @param bool false Should query strings be added to the image URL. Default is false.
202
	 * @param string $image_url_parts['host'] Image URL's host.
203
	 */
204
	if ( isset( $image_url_parts['query'] ) && apply_filters( 'jetpack_photon_add_query_string_to_domain', false, $image_url_parts['host'] ) ) {
205
		$photon_url .= '?q=' . rawurlencode( $image_url_parts['query'] );
206
	}
207
208
	if ( $args ) {
209
		if ( is_array( $args ) ) {
210
			$photon_url = add_query_arg( $args, $photon_url );
211
		} else {
212
			// You can pass a query string for complicated requests but where you still want CDN subdomain help, etc.
213
			$photon_url .= '?' . $args;
214
		}
215
	}
216
217
	if ( isset( $image_url_parts['scheme'] ) && 'https' === $image_url_parts['scheme'] ) {
218
		$photon_url = add_query_arg( array( 'ssl' => 1 ), $photon_url );
219
	}
220
221
	return jetpack_photon_url_scheme( $photon_url, $scheme );
222
}
223
224
/**
225
 * Add an easy way to photon-ize a URL that is safe to call even if Jetpack isn't active.
226
 *
227
 * See: https://jetpack.com/2013/07/11/photon-and-themes/
228
 */
229
add_filter( 'jetpack_photon_url', 'jetpack_photon_url', 10, 3 );
230
231
/**
232
 * WordPress.com
233
 *
234
 * If a cropped WP.com-hosted image is the source image, have Photon replicate the crop.
235
 */
236
add_filter( 'jetpack_photon_pre_args', 'jetpack_photon_parse_wpcom_query_args', 10, 2 );
237
238
/**
239
 * Parses WP.com-hosted image args to replicate the crop.
240
 *
241
 * @param mixed  $args Args set during Photon's processing.
242
 * @param string $image_url URL of the image.
243
 * @return array|string Args for Photon to use for the URL.
244
 */
245
function jetpack_photon_parse_wpcom_query_args( $args, $image_url ) {
246
	$parsed_url = wp_parse_url( $image_url );
247
248
	if ( ! $parsed_url ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parsed_url of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
249
		return $args;
250
	}
251
252
	$image_url_parts = wp_parse_args(
253
		$parsed_url,
254
		array(
255
			'host'  => '',
256
			'query' => '',
257
		)
258
	);
259
260
	if ( '.files.wordpress.com' !== substr( $image_url_parts['host'], -20 ) ) {
261
		return $args;
262
	}
263
264
	if ( empty( $image_url_parts['query'] ) ) {
265
		return $args;
266
	}
267
268
	$wpcom_args = wp_parse_args( $image_url_parts['query'] );
269
270
	if ( empty( $wpcom_args['w'] ) || empty( $wpcom_args['h'] ) ) {
271
		return $args;
272
	}
273
274
	// Keep the crop by using "resize".
275
	if ( ! empty( $wpcom_args['crop'] ) ) {
276 View Code Duplication
		if ( is_array( $args ) ) {
277
			$args = array_merge( array( 'resize' => array( $wpcom_args['w'], $wpcom_args['h'] ) ), $args );
278
		} else {
279
			$args = 'resize=' . rawurlencode( absint( $wpcom_args['w'] ) . ',' . absint( $wpcom_args['h'] ) ) . '&' . $args;
280
		}
281 View Code Duplication
	} else {
282
		if ( is_array( $args ) ) {
283
			$args = array_merge( array( 'fit' => array( $wpcom_args['w'], $wpcom_args['h'] ) ), $args );
284
		} else {
285
			$args = 'fit=' . rawurlencode( absint( $wpcom_args['w'] ) . ',' . absint( $wpcom_args['h'] ) ) . '&' . $args;
286
		}
287
	}
288
289
	return $args;
290
}
291
292
/**
293
 * Sets the scheme for a URL
294
 *
295
 * @param string $url URL to set scheme.
296
 * @param string $scheme Scheme to use. Accepts http, https, network_path.
297
 *
298
 * @return string URL.
299
 */
300
function jetpack_photon_url_scheme( $url, $scheme ) {
301
	if ( ! in_array( $scheme, array( 'http', 'https', 'network_path' ), true ) ) {
302
		if ( preg_match( '#^(https?:)?//#', $url ) ) {
303
			return $url;
304
		}
305
306
		$scheme = 'http';
307
	}
308
309
	if ( 'network_path' === $scheme ) {
310
		$scheme_slashes = '//';
311
	} else {
312
		$scheme_slashes = "$scheme://";
313
	}
314
315
	return preg_replace( '#^([a-z:]+)?//#i', $scheme_slashes, $url );
316
}
317
318
/**
319
 * A wrapper for PHP's parse_url, prepending assumed scheme for network path
320
 * URLs. PHP versions 5.4.6 and earlier do not correctly parse without scheme.
321
 *
322
 * WP ships with a wrapper for parse_url, wp_parse_url, that should be used instead.
323
 *
324
 * @see https://php.net/manual/en/function.parse-url.php#refsect1-function.parse-url-changelog
325
 * @deprecated 7.8.0 Use wp_parse_url instead.
326
 *
327
 * @param string  $url The URL to parse.
328
 * @param integer $component Retrieve specific URL component.
329
 * @return mixed Result of parse_url
330
 */
331
function jetpack_photon_parse_url( $url, $component = -1 ) {
332
	_deprecated_function( 'jetpack_photon_parse_url', 'jetpack-7.8.0', 'wp_parse_url' );
333
	return wp_parse_url( $url, $component );
334
}
335
336
add_filter( 'jetpack_photon_skip_for_url', 'jetpack_photon_banned_domains', 9, 2 );
337
338
/**
339
 * Check to skip Photon for a known domain that shouldn't be Photonized.
340
 *
341
 * @param bool   $skip If the image should be skipped by Photon.
342
 * @param string $image_url URL of the image.
343
 *
344
 * @return bool Should the image be skipped by Photon.
345
 */
346
function jetpack_photon_banned_domains( $skip, $image_url ) {
347
	$banned_host_patterns = array(
348
		'/^chart\.googleapis\.com$/',
349
		'/^chart\.apis\.google\.com$/',
350
		'/^graph\.facebook\.com$/',
351
		'/\.fbcdn\.net$/',
352
		'/\.paypalobjects\.com$/',
353
		'/\.dropbox\.com$/',
354
		'/\.cdninstagram\.com$/',
355
		'/^(commons|upload)\.wikimedia\.org$/',
356
	);
357
358
	$host = wp_parse_url( $image_url, PHP_URL_HOST );
359
360
	foreach ( $banned_host_patterns as $banned_host_pattern ) {
361
		if ( 1 === preg_match( $banned_host_pattern, $host ) ) {
362
			return true;
363
		}
364
	}
365
366
	return $skip;
367
}
368
369
370
/**
371
 * Jetpack Photon - Support Text Widgets.
372
 *
373
 * @access public
374
 * @param string $content Content from text widget.
375
 * @return string
376
 */
377
function jetpack_photon_support_text_widgets( $content ) {
378
	if ( class_exists( 'Jetpack_Photon' ) && Jetpack::is_module_active( 'photon' ) ) {
379
		return Jetpack_Photon::filter_the_content( $content );
380
	}
381
	return $content;
382
}
383
add_filter( 'widget_text', 'jetpack_photon_support_text_widgets' );
384