Completed
Push — update/readme-contributors ( d0ad08...a4dd26 )
by
unknown
23:17 queued 14:53
created

Jetpack_Lazy_Images::process_image()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 3
nop 1
dl 0
loc 20
rs 9.6
c 0
b 0
f 0
1
<?php
2
3
use Automattic\Jetpack\Assets;
4
5
class Jetpack_Lazy_Images {
6
	private static $__instance = null;
7
8
	/**
9
	 * Singleton implementation
10
	 *
11
	 * @return object
12
	 */
13
	public static function instance() {
14
		if ( is_null( self::$__instance ) ) {
15
			self::$__instance = new Jetpack_Lazy_Images();
16
		}
17
18
		return self::$__instance;
19
	}
20
21
	/**
22
	 * Registers actions
23
	 */
24
	private function __construct() {
25
		if ( is_admin() ) {
26
			return;
27
		}
28
29
		/**
30
		 * Whether the lazy-images module should load.
31
		 *
32
		 * This filter is not prefixed with jetpack_ to provide a smoother migration
33
		 * process from the WordPress Lazy Load plugin.
34
		 *
35
		 * @module lazy-images
36
		 *
37
		 * @since 5.6.0
38
		 *
39
		 * @param bool true Whether lazy image loading should occur.
40
		 */
41
		if ( ! apply_filters( 'lazyload_is_enabled', true ) ) {
42
			return;
43
		}
44
45
		if ( Jetpack_AMP_Support::is_amp_request() ) {
46
			return;
47
		}
48
49
		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
50
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
51
52
		// Do not lazy load avatar in admin bar
53
		add_action( 'admin_bar_menu', array( $this, 'remove_filters' ), 0 );
54
55
		add_filter( 'wp_kses_allowed_html', array( $this, 'allow_lazy_attributes' ) );
56
		add_action( 'wp_head', array( $this, 'add_nojs_fallback' ) );
57
	}
58
59 View Code Duplication
	public function setup_filters() {
60
		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
61
		add_filter( 'post_thumbnail_html', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
62
		add_filter( 'get_avatar', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
63
		add_filter( 'widget_text', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
64
		add_filter( 'get_image_tag', array( $this, 'add_image_placeholders' ), PHP_INT_MAX);
65
		add_filter( 'wp_get_attachment_image_attributes', array( __CLASS__, 'process_image_attributes' ), PHP_INT_MAX );
66
	}
67
68 View Code Duplication
	public function remove_filters() {
69
		remove_filter( 'the_content', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
70
		remove_filter( 'post_thumbnail_html', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
71
		remove_filter( 'get_avatar', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
72
		remove_filter( 'widget_text', array( $this, 'add_image_placeholders' ), PHP_INT_MAX );
73
		remove_filter( 'get_image_tag', array( $this, 'add_image_placeholders' ), PHP_INT_MAX);
74
		remove_filter( 'wp_get_attachment_image_attributes', array( __CLASS__, 'process_image_attributes' ), PHP_INT_MAX );
75
	}
76
77
	/**
78
	 * Ensure that our lazy image attributes are not filtered out of image tags.
79
	 *
80
	 * @param array $allowed_tags The allowed tags and their attributes.
81
	 * @return array
82
	 */
83
	public function allow_lazy_attributes( $allowed_tags ) {
84
		if ( ! isset( $allowed_tags['img'] ) ) {
85
			return $allowed_tags;
86
		}
87
88
		// But, if images are allowed, ensure that our attributes are allowed!
89
		$img_attributes = array_merge( $allowed_tags['img'], array(
90
			'data-lazy-src' => 1,
91
			'data-lazy-srcset' => 1,
92
			'data-lazy-sizes' => 1,
93
		) );
94
95
		$allowed_tags['img'] = $img_attributes;
96
97
		return $allowed_tags;
98
	}
99
100
	public function add_image_placeholders( $content ) {
101
		// Don't lazyload for feeds, previews
102
		if ( is_feed() || is_preview() ) {
103
			return $content;
104
		}
105
106
		// Don't lazy-load if the content has already been run through previously
107
		if ( false !== strpos( $content, 'data-lazy-src' ) ) {
108
			return $content;
109
		}
110
111
		// This is a pretty simple regex, but it works
112
		$content = preg_replace_callback( '#<(img)([^>]+?)(>(.*?)</\\1>|[\/]?>)#si', array( __CLASS__, 'process_image' ), $content );
113
114
		return $content;
115
	}
116
117
	/**
118
	 * Returns true when a given string of classes contains a class signifying lazy images
119
	 * should not process the image.
120
	 *
121
	 * @since 5.9.0
122
	 *
123
	 * @param string $classes A string of space-separated classes.
124
	 * @return bool
125
	 */
126
	public static function should_skip_image_with_blocked_class( $classes ) {
127
		$blocked_classes = array(
128
			'skip-lazy',
129
			'gazette-featured-content-thumbnail',
130
		);
131
132
		/**
133
		 * Allow plugins and themes to tell lazy images to skip an image with a given class.
134
		 *
135
		 * @module lazy-images
136
		 *
137
		 * @since 5.9.0
138
		 * @deprecated 8.7.0 Use jetpack_lazy_images_blocked_classes
139
		 *
140
		 * @param array An array of strings where each string is a class.
141
		 */
142
		$blocked_classes = apply_filters_deprecated( 'jetpack_lazy_images_blacklisted_classes', array( $blocked_classes ), 'Jetpack 8.7.0', 'jetpack_lazy_images_blocked_classes' );
143
144
		/**
145
		 * Allow plugins and themes to tell lazy images to skip an image with a given class.
146
		 *
147
		 * @module lazy-images
148
		 *
149
		 * @since 8.7.0
150
		 *
151
		 * @param array An array of strings where each string is a class.
152
		 */
153
		$blocked_classes = apply_filters( 'jetpack_lazy_images_blocked_classes', $blocked_classes );
154
155
		if ( ! is_array( $blocked_classes ) || empty( $blocked_classes ) ) {
156
			return false;
157
		}
158
159
		foreach ( $blocked_classes as $class ) {
160
			if ( false !== strpos( $classes, $class ) ) {
161
				return true;
162
			}
163
		}
164
165
		return false;
166
	}
167
168
	/**
169
	 * Processes images in content by acting as the preg_replace_callback
170
	 *
171
	 * @since 5.6.0
172
	 *
173
	 * @param array $matches
174
	 *
175
	 * @return string The image with updated lazy attributes
176
	 */
177
	static function process_image( $matches ) {
178
		$old_attributes_str       = $matches[2];
179
		$old_attributes_kses_hair = wp_kses_hair( $old_attributes_str, wp_allowed_protocols() );
180
181
		if ( empty( $old_attributes_kses_hair['src'] ) ) {
182
			return $matches[0];
183
		}
184
185
		$old_attributes = self::flatten_kses_hair_data( $old_attributes_kses_hair );
186
187
		// If we didn't add lazy attributes, just return the original image source.
188
		if ( ! empty( $old_attributes['class'] ) && false !== strpos( $old_attributes['class'], 'jetpack-lazy-image' ) ) {
189
			return $matches[0];
190
		}
191
192
		$new_attributes     = self::process_image_attributes( $old_attributes );
193
		$new_attributes_str = self::build_attributes_string( $new_attributes );
194
195
		return sprintf( '<img %1$s><noscript>%2$s</noscript>', $new_attributes_str, $matches[0] );
196
	}
197
198
	/**
199
	 * Given an array of image attributes, updates the `src`, `srcset`, and `sizes` attributes so
200
	 * that they load lazily.
201
	 *
202
	 * @since 5.7.0
203
	 *
204
	 * @param array $attributes
205
	 *
206
	 * @return array The updated image attributes array with lazy load attributes
207
	 */
208
	static function process_image_attributes( $attributes ) {
209
		if ( empty( $attributes['src'] ) ) {
210
			return $attributes;
211
		}
212
213
		if ( ! empty( $attributes['class'] ) && self::should_skip_image_with_blocked_class( $attributes['class'] ) ) {
214
			return $attributes;
215
		}
216
217
		if ( isset( $attributes['data-skip-lazy'] ) ) {
218
			return $attributes;
219
		}
220
221
		/**
222
		 * Allow plugins and themes to conditionally skip processing an image via its attributes.
223
		 *
224
		 * @module-lazy-images
225
		 *
226
		 * @deprecated 6.5.0 Use jetpack_lazy_images_skip_image_with_attributes instead.
227
		 *
228
		 * @since 5.9.0
229
		 *
230
		 * @param bool  Default to not skip processing the current image.
231
		 * @param array An array of attributes via wp_kses_hair() for the current image.
232
		 */
233
		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...
234
			return $attributes;
235
		}
236
237
		/**
238
		 * Allow plugins and themes to conditionally skip processing an image via its attributes.
239
		 *
240
		 * @module-lazy-images
241
		 *
242
		 * @since 6.5.0 Filter name was updated from jetpack_lazy_images_skip_image_with_atttributes to correct typo.
243
		 * @since 5.9.0
244
		 *
245
		 * @param bool  Default to not skip processing the current image.
246
		 * @param array An array of attributes via wp_kses_hair() for the current image.
247
		 */
248
		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...
249
			return $attributes;
250
		}
251
252
		$old_attributes = $attributes;
253
254
		// Stash srcset and sizes in data attributes.
255
		foreach ( array( 'srcset', 'sizes' ) as $attribute ) {
256
			if ( isset( $old_attributes[ $attribute ] ) ) {
257
				$attributes[ "data-lazy-$attribute" ] = $old_attributes[ $attribute ];
258
				unset( $attributes[ $attribute ] );
259
			}
260
		}
261
262
		// We set this, adding the query arg so that it doesn't exactly equal the src attribute, so that photon JavaScript
263
		// will hold off on processing this image.
264
		$attributes['data-lazy-src'] = esc_url_raw( add_query_arg( 'is-pending-load', true, $attributes['src'] ) );
265
266
		$attributes['srcset'] = self::get_placeholder_image();
267
		$attributes['class']  = sprintf(
268
			'%s jetpack-lazy-image',
269
			empty( $old_attributes['class'] )
270
				? ''
271
				: $old_attributes['class']
272
		);
273
274
		/**
275
		 * Allow plugins and themes to override the attributes on the image before the content is updated.
276
		 *
277
		 * One potential use of this filter is for themes that set `height:auto` on the `img` tag.
278
		 * With this filter, the theme could get the width and height attributes from the
279
		 * $attributes array and then add a style tag that sets those values as well, which could
280
		 * minimize reflow as images load.
281
		 *
282
		 * @module lazy-images
283
		 *
284
		 * @since 5.6.0
285
		 *
286
		 * @param array An array containing the attributes for the image, where the key is the attribute name
287
		 *              and the value is the attribute value.
288
		 */
289
		return apply_filters( 'jetpack_lazy_images_new_attributes', $attributes );
290
	}
291
292
	/**
293
	 * Adds JavaScript to check if the current browser supports JavaScript as well as some styles to hide lazy
294
	 * images when the browser does not support JavaScript.
295
	 *
296
	 * @return void
297
	 */
298
	public function add_nojs_fallback() {
299
		?>
300
			<style type="text/css">
301
				/* If html does not have either class, do not show lazy loaded images. */
302
				html:not( .jetpack-lazy-images-js-enabled ):not( .js ) .jetpack-lazy-image {
303
					display: none;
304
				}
305
			</style>
306
			<script>
307
				document.documentElement.classList.add(
308
					'jetpack-lazy-images-js-enabled'
309
				);
310
			</script>
311
		<?php
312
	}
313
314
	/**
315
	 * Retrieves the placeholder image after running it through the lazyload_images_placeholder_image filter.
316
	 *
317
	 * @return string The placeholder image source.
318
	 */
319
	private static function get_placeholder_image() {
320
		/**
321
		 * Allows plugins and themes to modify the placeholder image.
322
		 *
323
		 * This filter is not prefixed with jetpack_ to provide a smoother migration
324
		 * process from the WordPress Lazy Load plugin.
325
		 *
326
		 * @module lazy-images
327
		 *
328
		 * @since 5.6.0
329
		 * @since 6.5.0 Default image is now a base64 encoded transparent gif.
330
		 *
331
		 * @param string The URL to the placeholder image
332
		 */
333
		return apply_filters(
334
			'lazyload_images_placeholder_image',
335
			'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
336
		);
337
	}
338
339
	private static function flatten_kses_hair_data( $attributes ) {
340
		$flattened_attributes = array();
341
		foreach ( $attributes as $name => $attribute ) {
342
			$flattened_attributes[ $name ] = $attribute['value'];
343
		}
344
		return $flattened_attributes;
345
	}
346
347
	private static function build_attributes_string( $attributes ) {
348
		$string = array();
349
		foreach ( $attributes as $name => $value ) {
350
			if ( '' === $value ) {
351
				$string[] = sprintf( '%s', $name );
352
			} else {
353
				$string[] = sprintf( '%s="%s"', $name, esc_attr( $value ) );
354
			}
355
		}
356
		return implode( ' ', $string );
357
	}
358
359
	public function enqueue_assets() {
360
		wp_enqueue_script(
361
			'jetpack-lazy-images',
362
			Assets::get_file_url_for_environment(
363
				'_inc/build/lazy-images/js/lazy-images.min.js',
364
				'modules/lazy-images/js/lazy-images.js'
365
			),
366
			array(),
367
			JETPACK__VERSION,
368
			true
369
		);
370
	}
371
}
372