Completed
Push — update/setup-wizard-free-creat... ( a13f63...9ba60b )
by Jeremy
50:18 queued 42:19
created

Jetpack_Lazy_Images::is_amp_request()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 0
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
1
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
3
/**
4
 * The Lazy Images package.
5
 *
6
 * @since 8.8
7
 *
8
 * This package has been lifted from the Jetpack modules folder and adapted to be
9
 * an standalone package reusable outside Jetpack.
10
 *
11
 * @package automattic/jetpack-lazy-images
12
 */
13
14
/**
15
 * This package relies heavily upon the Lazy Load plugin which was worked on by
16
 * Mohammad Jangda (batmoo), the WordPress.com VIP team, the TechCrunch 2011
17
 * redesign team, and Jake Goldman of 10up LLC.
18
 *
19
 * The JavaScript has been updated to rely on InterSection observer instead of
20
 * jQuery Sonar. Many thanks to Dean Hume (deanhume) and his example:
21
 * https://github.com/deanhume/lazy-observer-load
22
 */
23
24
namespace Automattic\Jetpack;
25
26
use Automattic\Jetpack\Constants as Jetpack_Constants;
27
28
/**
29
 * Class Automattic\Jetpack\Jetpack_Lazy_Images
30
 *
31
 * @since 8.8
32
 */
33
class Jetpack_Lazy_Images {
34
35
	/**
36
	 * The assets version.
37
	 *
38
	 * @since 8.8
39
	 *
40
	 * @var string Assets version.
41
	 */
42
	const ASSETS_VERSION = '1.0.0';
43
44
	/**
45
	 * Class instance.
46
	 *
47
	 * @since 5.6.0
48
	 *
49
	 * @var null
50
	 */
51
	private static $instance = null;
52
53
	/**
54
	 * Singleton implementation.
55
	 *
56
	 * @since 5.6.0
57
	 *
58
	 * @return object The class instance.
59
	 */
60
	public static function instance() {
61
		if ( is_null( self::$instance ) ) {
62
			self::$instance = new Jetpack_Lazy_Images();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Automattic\Jetpack\Jetpack_Lazy_Images() of type object<Automattic\Jetpack\Jetpack_Lazy_Images> is incompatible with the declared type null of property $instance.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
63
		}
64
65
		return self::$instance;
66
	}
67
68
	/**
69
	 * Check if the request is a AMP request.
70
	 *
71
	 * @since 8.8
72
	 *
73
	 * @return bool
74
	 */
75
	public static function is_amp_request() {
76
		$is_amp_request = ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() );
77
78
		/**
79
		 * Returns true if the current request should return valid AMP content.
80
		 *
81
		 * @since 8.8
82
		 *
83
		 * @param boolean $is_amp_request Is this request supposed to return valid AMP content?
84
		 */
85
		return apply_filters( 'jetpack_lazy_images_is_amp_request', $is_amp_request );
86
	}
87
88
	/**
89
	 * Given a minified path, and a non-minified path, will return
90
	 * a minified or non-minified file URL based on whether SCRIPT_DEBUG is set and truthy.
91
	 *
92
	 * Both `$min_base` and `$non_min_base` can be either full URLs, or are expected to be relative to the
93
	 * root package directory.
94
	 *
95
	 * @since 8.8
96
	 *
97
	 * @param string $min_path Minified path.
98
	 * @param string $non_min_path Non-minified path.
99
	 * @return string The URL to the file.
100
	 */
101 View Code Duplication
	public static function get_file_url_for_environment( $min_path, $non_min_path ) {
102
		$path = ( Jetpack_Constants::is_defined( 'SCRIPT_DEBUG' ) && Jetpack_Constants::get_constant( 'SCRIPT_DEBUG' ) )
103
				? $non_min_path
104
				: $min_path;
105
106
		/*
107
		 * If the path is actually a full URL, keep that.
108
		 * We look for a host value, since enqueues are sometimes without a scheme.
109
		 */
110
		$file_parts = wp_parse_url( $path );
111
		if ( ! empty( $file_parts['host'] ) ) {
112
			$url = $path;
113
		} else {
114
			$url = plugins_url( $path, __FILE__ );
115
		}
116
117
		/**
118
		 * Filters the URL for a file passed through the get_file_url_for_environment function.
119
		 *
120
		 * @since 8.8
121
		 *
122
		 * @package automattic/jetpack-lazy-images
123
		 *
124
		 * @param string $url The URL to the file.
125
		 * @param string $min_path The minified path.
126
		 * @param string $non_min_path The non-minified path.
127
		 */
128
		return apply_filters( 'jetpack_lazy_load_get_file_for_environment', $url, $min_path, $non_min_path );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $min_path.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
129
	}
130
131
	/**
132
	 * Registers actions.
133
	 *
134
	 * @since 8.8
135
	 *
136
	 * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
137
	 */
138
	private function __construct() {
139
		if ( is_admin() ) {
140
			return;
141
		}
142
143
		/**
144
		 * Whether the lazy-images module should load.
145
		 *
146
		 * This filter is not prefixed with jetpack_ to provide a smoother migration
147
		 * process from the WordPress Lazy Load plugin.
148
		 *
149
		 * @package automattic/jetpack-lazy-images
150
		 *
151
		 * @since 5.6.0
152
		 *
153
		 * @param bool true Whether lazy image loading should occur.
154
		 */
155
		if ( ! apply_filters( 'lazyload_is_enabled', true ) ) {
156
			return;
157
		}
158
159
		if ( self::is_amp_request() ) {
160
			return;
161
		}
162
163
		add_action( 'wp_head', array( $this, 'setup_filters' ), 9999 ); // We don't really want to modify anything in <head> since it's mostly all metadata.
164
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
165
166
		// Do not lazy load avatar in admin bar.
167
		add_action( 'admin_bar_menu', array( $this, 'remove_filters' ), 0 );
168
169
		add_filter( 'wp_kses_allowed_html', array( $this, 'allow_lazy_attributes' ) );
170
		add_action( 'wp_head', array( $this, 'add_nojs_fallback' ) );
171
	}
172
173
	/**
174
	 * Setup filters.
175
	 *
176
	 * @since 5.6.0
177
	 *
178
	 * @return void
179
	 */
180 View Code Duplication
	public function setup_filters() {
181
		add_filter( 'the_content', array( $this, 'add_image_placeholders' ), PHP_INT_MAX ); // Run this later, so other content filters have run, including image_add_wh on WP.com.
182
		add_filter( 'post_thumbnail_html', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
183
		add_filter( 'get_avatar', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
184
		add_filter( 'widget_text', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
185
		add_filter( 'get_image_tag', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
186
		add_filter( 'wp_get_attachment_image_attributes', array( __CLASS__, 'process_image_attributes' ), PHP_INT_MAX );
187
	}
188
189
	/**
190
	 * Remove filters.
191
	 *
192
	 * @since 5.6.0
193
	 *
194
	 * @return void
195
	 */
196 View Code Duplication
	public function remove_filters() {
197
		remove_filter( 'the_content', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
198
		remove_filter( 'post_thumbnail_html', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
199
		remove_filter( 'get_avatar', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
200
		remove_filter( 'widget_text', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
201
		remove_filter( 'get_image_tag', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
202
		remove_filter( 'wp_get_attachment_image_attributes', array( __CLASS__, 'process_image_attributes' ), PHP_INT_MAX );
203
	}
204
205
	/**
206
	 * Ensure that our lazy image attributes are not filtered out of image tags.
207
	 *
208
	 * @since 5.6.0
209
	 *
210
	 * @param array $allowed_tags The allowed tags and their attributes.
211
	 * @return array
212
	 */
213
	public function allow_lazy_attributes( $allowed_tags ) {
214
		if ( ! isset( $allowed_tags['img'] ) ) {
215
			return $allowed_tags;
216
		}
217
218
		// But, if images are allowed, ensure that our attributes are allowed!
219
		$img_attributes = array_merge(
220
			$allowed_tags['img'],
221
			array(
222
				'data-lazy-src'    => 1,
223
				'data-lazy-srcset' => 1,
224
				'data-lazy-sizes'  => 1,
225
			)
226
		);
227
228
		$allowed_tags['img'] = $img_attributes;
229
230
		return $allowed_tags;
231
	}
232
233
	/**
234
	 * Add image placeholders.
235
	 *
236
	 * @since 5.6.0
237
	 *
238
	 * @param string $content Content.
239
	 * @return string
240
	 */
241
	public function add_image_placeholders( $content ) {
242
		// Don't lazy load for feeds, previews.
243
		if ( is_feed() || is_preview() ) {
244
			return $content;
245
		}
246
247
		// Don't lazy-load if the content has already been run through previously.
248
		if ( false !== strpos( $content, 'data-lazy-src' ) ) {
249
			return $content;
250
		}
251
252
		// This is a pretty simple regex, but it works.
253
		$content = preg_replace_callback( '#<(img)([^>]+?)(>(.*?)</\\1>|[\/]?>)#si', array( __CLASS__, 'process_image' ), $content );
254
255
		return $content;
256
	}
257
258
	/**
259
	 * Returns true when a given string of classes contains a class signifying lazy images.
260
	 * should not process the image.
261
	 *
262
	 * @since 5.9.0
263
	 *
264
	 * @param string $classes A string of space-separated classes.
265
	 * @return bool
266
	 */
267
	public static function should_skip_image_with_blocked_class( $classes ) {
268
		$blocked_classes = array(
269
			'skip-lazy',
270
			'gazette-featured-content-thumbnail',
271
		);
272
273
		/**
274
		 * Allow plugins and themes to tell lazy images to skip an image with a given class.
275
		 *
276
		 * @package automattic/jetpack-lazy-images
277
		 *
278
		 * @since 5.9.0
279
		 * @deprecated 8.7.0 Use jetpack_lazy_images_blocked_classes
280
		 *
281
		 * @param array An array of strings where each string is a class.
282
		 */
283
		$blocked_classes = apply_filters_deprecated( 'jetpack_lazy_images_blacklisted_classes', array( $blocked_classes ), 'Jetpack 8.7.0', 'jetpack_lazy_images_blocked_classes' );
284
285
		/**
286
		 * Allow plugins and themes to tell lazy images to skip an image with a given class.
287
		 *
288
		 * @package automattic/jetpack-lazy-images
289
		 *
290
		 * @since 8.7.0
291
		 *
292
		 * @param array An array of strings where each string is a class.
293
		 */
294
		$blocked_classes = apply_filters( 'jetpack_lazy_images_blocked_classes', $blocked_classes );
295
296
		if ( ! is_array( $blocked_classes ) || empty( $blocked_classes ) ) {
297
			return false;
298
		}
299
300
		foreach ( $blocked_classes as $class ) {
301
			if ( false !== strpos( $classes, $class ) ) {
302
				return true;
303
			}
304
		}
305
306
		return false;
307
	}
308
309
	/**
310
	 * Processes images in content by acting as the preg_replace_callback.
311
	 *
312
	 * @since 5.6.0
313
	 *
314
	 * @param array $matches Matches.
315
	 *
316
	 * @return string The image with updated lazy attributes.
317
	 */
318
	public static function process_image( $matches ) {
319
		$old_attributes_str       = $matches[2];
320
		$old_attributes_kses_hair = wp_kses_hair( $old_attributes_str, wp_allowed_protocols() );
321
322
		if ( empty( $old_attributes_kses_hair['src'] ) ) {
323
			return $matches[0];
324
		}
325
326
		$old_attributes = self::flatten_kses_hair_data( $old_attributes_kses_hair );
327
328
		// If we didn't add lazy attributes, just return the original image source.
329
		if ( ! empty( $old_attributes['class'] ) && false !== strpos( $old_attributes['class'], 'jetpack-lazy-image' ) ) {
330
			return $matches[0];
331
		}
332
333
		$new_attributes     = self::process_image_attributes( $old_attributes );
334
		$new_attributes_str = self::build_attributes_string( $new_attributes );
335
336
		return sprintf( '<img %1$s><noscript>%2$s</noscript>', $new_attributes_str, $matches[0] );
337
	}
338
339
	/**
340
	 * Given an array of image attributes, updates the `src`, `srcset`, and `sizes` attributes so
341
	 * that they load lazily.
342
	 *
343
	 * @since 5.7.0
344
	 *
345
	 * @param array $attributes Attributes.
346
	 *
347
	 * @return array The updated image attributes array with lazy load attributes.
348
	 */
349
	public static function process_image_attributes( $attributes ) {
350
		if ( empty( $attributes['src'] ) ) {
351
			return $attributes;
352
		}
353
354
		if ( ! empty( $attributes['class'] ) && self::should_skip_image_with_blocked_class( $attributes['class'] ) ) {
355
			return $attributes;
356
		}
357
358
		if ( isset( $attributes['data-skip-lazy'] ) ) {
359
			return $attributes;
360
		}
361
362
		/**
363
		 * Allow plugins and themes to conditionally skip processing an image via its attributes.
364
		 *
365
		 * @package automattic/jetpack-lazy-images
366
		 *
367
		 * @deprecated 6.5.0 Use jetpack_lazy_images_skip_image_with_attributes instead.
368
		 * @since 5.9.0
369
		 *
370
		 * @param bool  Default to not skip processing the current image.
371
		 * @param array An array of attributes via wp_kses_hair() for the current image.
372
		 */
373
		if ( apply_filters( 'jetpack_lazy_images_skip_image_with_atttributes', false, $attributes ) ) {
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $attributes.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
374
			return $attributes;
375
		}
376
377
		/**
378
		 * Allow plugins and themes to conditionally skip processing an image via its attributes.
379
		 *
380
		 * @package automattic/jetpack-lazy-images
381
		 *
382
		 * @since 6.5.0 Filter name was updated from jetpack_lazy_images_skip_image_with_atttributes to correct typo.
383
		 * @since 5.9.0
384
		 *
385
		 * @param bool  Default to not skip processing the current image.
386
		 * @param array An array of attributes via wp_kses_hair() for the current image.
387
		 */
388
		if ( apply_filters( 'jetpack_lazy_images_skip_image_with_attributes', false, $attributes ) ) {
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $attributes.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
389
			return $attributes;
390
		}
391
392
		$old_attributes = $attributes;
393
394
		// Stash srcset and sizes in data attributes.
395
		foreach ( array( 'srcset', 'sizes' ) as $attribute ) {
396
			if ( isset( $old_attributes[ $attribute ] ) ) {
397
				$attributes[ "data-lazy-$attribute" ] = $old_attributes[ $attribute ];
398
				unset( $attributes[ $attribute ] );
399
			}
400
		}
401
402
		// We set this, adding the query arg so that it doesn't exactly equal the src attribute, so that photon JavaScript
403
		// will hold off on processing this image.
404
		$attributes['data-lazy-src'] = esc_url_raw( add_query_arg( 'is-pending-load', true, $attributes['src'] ) );
405
406
		$attributes['srcset'] = self::get_placeholder_image();
407
		$attributes['class']  = sprintf(
408
			'%s jetpack-lazy-image',
409
			empty( $old_attributes['class'] )
410
				? ''
411
				: $old_attributes['class']
412
		);
413
414
		/**
415
		 * Allow plugins and themes to override the attributes on the image before the content is updated.
416
		 *
417
		 * One potential use of this filter is for themes that set `height:auto` on the `img` tag.
418
		 * With this filter, the theme could get the width and height attributes from the
419
		 * $attributes array and then add a style tag that sets those values as well, which could
420
		 * minimize reflow as images load.
421
		 *
422
		 * @package automattic/jetpack-lazy-images
423
		 *
424
		 * @since 5.6.0
425
		 *
426
		 * @param array An array containing the attributes for the image, where the key is the attribute name
427
		 *              and the value is the attribute value.
428
		 */
429
		return apply_filters( 'jetpack_lazy_images_new_attributes', $attributes );
430
	}
431
432
	/**
433
	 * Adds JavaScript to check if the current browser supports JavaScript as well as some styles to hide lazy
434
	 * images when the browser does not support JavaScript.
435
	 *
436
	 * @since 5.6.0
437
	 *
438
	 * @return void
439
	 */
440
	public function add_nojs_fallback() {
441
		?>
442
			<style type="text/css">
443
				/* If html does not have either class, do not show lazy loaded images. */
444
				html:not( .jetpack-lazy-images-js-enabled ):not( .js ) .jetpack-lazy-image {
445
					display: none;
446
				}
447
			</style>
448
			<script>
449
				document.documentElement.classList.add(
450
					'jetpack-lazy-images-js-enabled'
451
				);
452
			</script>
453
		<?php
454
	}
455
456
	/**
457
	 * Retrieves the placeholder image after running it through the lazyload_images_placeholder_image filter.
458
	 *
459
	 * @since 5.6.0
460
	 *
461
	 * @return string The placeholder image source.
462
	 */
463
	private static function get_placeholder_image() {
464
		/**
465
		 * Allows plugins and themes to modify the placeholder image.
466
		 *
467
		 * This filter is not prefixed with jetpack_ to provide a smoother migration
468
		 * process from the WordPress Lazy Load plugin.
469
		 *
470
		 * @module lazy-images
471
		 *
472
		 * @since 5.6.0
473
		 * @since 6.5.0 Default image is now a base64 encoded transparent gif.
474
		 *
475
		 * @param string The URL to the placeholder image.
476
		 */
477
		return apply_filters(
478
			'lazyload_images_placeholder_image',
479
			'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
480
		);
481
	}
482
483
	/**
484
	 * Flatter KSES hair data.
485
	 *
486
	 * @since 5.6.0
487
	 *
488
	 * @param array $attributes Attributes.
489
	 *
490
	 * @return array
491
	 */
492
	private static function flatten_kses_hair_data( $attributes ) {
493
		$flattened_attributes = array();
494
		foreach ( $attributes as $name => $attribute ) {
495
			$flattened_attributes[ $name ] = $attribute['value'];
496
		}
497
		return $flattened_attributes;
498
	}
499
500
	/**
501
	 * Build attributes string.
502
	 *
503
	 * @since 5.6.0
504
	 *
505
	 * @param array $attributes Attributes.
506
	 *
507
	 * @return string
508
	 */
509
	private static function build_attributes_string( $attributes ) {
510
		$string = array();
511
		foreach ( $attributes as $name => $value ) {
512
			if ( '' === $value ) {
513
				$string[] = sprintf( '%s', $name );
514
			} else {
515
				$string[] = sprintf( '%s="%s"', $name, esc_attr( $value ) );
516
			}
517
		}
518
		return implode( ' ', $string );
519
	}
520
521
	/**
522
	 * Enqueue assets.
523
	 *
524
	 * @since 5.6.0
525
	 *
526
	 * @return void
527
	 */
528
	public function enqueue_assets() {
529
		wp_enqueue_script(
530
			'jetpack-lazy-images',
531
			self::get_file_url_for_environment( 'js/lazy-images.min.js', 'js/lazy-images.js' ),
532
			array(),
533
			self::ASSETS_VERSION,
534
			true
535
		);
536
	}
537
}
538