Completed
Push — add/pwa ( 5527a4...302b61 )
by
unknown
16:35 queued 08:42
created

should_async_script()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 14
nc 8
nop 1
dl 0
loc 22
rs 8.6737
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Optimizes page assets for unreliable networks and fast rendering, particularly with empty caches
5
 * - inline scripts and styles
6
 * - async external JS
7
 * - remove references to external fonts
8
 */
9
10
class Jetpack_Perf_Optimize_Assets {
11
	private static $__instance = null;
12
	private $remove_remote_fonts = false;
13
	private $inline_scripts_and_styles = false;
14
	private $async_scripts = false;
15
	private $defer_scripts = false;
16
	private $defer_inline_scripts = false;
17
18
	/**
19
	 * Singleton implementation
20
	 *
21
	 * @return object
22
	 */
23
	public static function instance() {
24
		if ( ! is_a( self::$__instance, 'Jetpack_Perf_Optimize_Assets' ) ) {
25
			self::$__instance = new Jetpack_Perf_Optimize_Assets();
26
		}
27
28
		return self::$__instance;
29
	}
30
31
	public function disable_for_request() {
32
		$this->remove_remote_fonts = false;
33
		$this->inline_scripts_and_styles = false;
34
		$this->async_scripts = false;
35
		$this->defer_scripts = false;
36
		$this->defer_inline_scripts = false;
37
	}
38
39
	/**
40
	 * TODO: detect if this is worth doing for wp-admin?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
41
	 */
42
43
	/**
44
	 * Registers actions
45
	 */
46
	private function __construct() {
47
		$this->is_first_load             = ! isset( $_COOKIE['jetpack_perf_loaded'] );
0 ignored issues
show
Bug introduced by
The property is_first_load does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
48
		$this->remove_remote_fonts       = get_option( 'perf_remove_remote_fonts', true );
49
		$this->inline_always             = get_option( 'perf_inline_on_every_request', false );
0 ignored issues
show
Bug introduced by
The property inline_always does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
50
		$this->inline_scripts_and_styles = get_option( 'perf_inline_scripts_and_styles', true ) && ( $this->is_first_load || $this->inline_always );
51
		$this->async_scripts             = get_option( 'perf_async_scripts', true );
52
		$this->defer_scripts             = get_option( 'perf_defer_scripts', true );
53
		$this->defer_inline_scripts      = get_option( 'perf_defer_inline_scripts', true );
54
55
		if ( $this->remove_remote_fonts ) {
56
			add_filter( 'jetpack_perf_remove_script', array( $this, 'remove_external_font_scripts' ), 10, 3 );
57
			add_filter( 'jetpack_perf_remove_style', array( $this, 'remove_external_font_styles' ), 10, 3 );
58
		}
59
60
		add_filter( 'script_loader_src', array( $this, 'filter_inline_scripts' ), - 100, 2 );
61
		add_filter( 'script_loader_tag', array( $this, 'print_inline_scripts' ), - 100, 3 );
62
		add_filter( 'style_loader_src', array( $this, 'filter_inline_styles' ), - 100, 2 );
63
		add_filter( 'style_loader_tag', array( $this, 'print_inline_styles' ), - 100, 4 );
64
65
		if ( $this->defer_inline_scripts ) {
66
			add_filter( 'wp_head', array( $this, 'content_start' ), - 2000 );
67
			add_filter( 'wp_footer', array( $this, 'content_end' ), 2000 );
68
		}
69
70
		add_action( 'init', array( $this, 'set_first_load_cookie' ) );
71
72
		// remove emoji detection - TODO a setting for this
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
73
		add_action( 'init', array( $this, 'disable_emojis' ) );
74
75
	}
76
77
	function content_start() {
78
		ob_start( array( $this, 'do_defer_inline_scripts' ) );
79
	}
80
81
	function content_end() {
82
		ob_end_flush();
83
	}
84
85
	function do_defer_inline_scripts( $content ) {
86
		preg_match_all( '#<script.*?>(.*?)<\/script>#is', $content, $matches, PREG_OFFSET_CAPTURE );
87
		$original_length = mb_strlen( $content );
88
		$offset          = 0;
89
		$counter         = 0;
90
		$rewrite         = "";
91
		foreach ( $matches[1] as $value ) {
92
			if ( ! empty( $value[0] ) && strpos( $value[0], "<![CDATA" ) === false ) {
93
				$length    = mb_strlen( $matches[0][ $counter ][0] );
94
				$script    = base64_encode( $value[0] );
95
				$beginning = $matches[0][ $counter ][1];
96
				$rewrite   .= mb_substr( $content, $offset, $beginning );
97
				$rewrite   .= "<script defer src='data:text/javascript;base64,$script'></script>";
98
				$offset    += $beginning + $length;
99
				unset( $length, $script, $beginning );
100
			}
101
			$counter ++;
102
		}
103
		$rewrite .= mb_substr( $content, $offset, $original_length );
104
105
		return $rewrite . "\n<!-- This is The End: All Inline Scripts Deferred -->";
106
	}
107
108
	/** Disabling Emojis **/
109
	// improves page load performance
110
111
	function disable_emojis() {
112
		remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
113
		remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
114
		remove_action( 'embed_head', 'print_emoji_detection_script', 7 );
115
116
		remove_action( 'wp_print_styles', 'print_emoji_styles' );
117
		remove_action( 'admin_print_styles', 'print_emoji_styles' );
118
119
		remove_filter( 'the_content_feed', 'wp_staticize_emoji' );
120
		remove_filter( 'comment_text_rss', 'wp_staticize_emoji' );
121
		remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
122
123
		add_filter( 'tiny_mce_plugins', array( $this, 'disable_emojis_tinymce' ) );
124
		add_filter( 'wp_resource_hints', array( $this, 'disable_emojis_remove_dns_prefetch' ), 10, 2 );
125
	}
126
127
	/**
128
	 * Filter function used to remove the tinymce emoji plugin.
129
	 *
130
	 * @param array $plugins
131
	 * @return array Difference betwen the two arrays
132
	 */
133
	function disable_emojis_tinymce( $plugins ) {
134
		if ( is_array( $plugins ) ) {
135
			return array_diff( $plugins, array( 'wpemoji' ) );
136
		} else {
137
			return array();
138
		}
139
	}
140
141
	/**
142
	 * Remove emoji CDN hostname from DNS prefetching hints.
143
	 *
144
	 * @param array $urls URLs to print for resource hints.
145
	 * @param string $relation_type The relation type the URLs are printed for.
146
	 * @return array Difference betwen the two arrays.
147
	 */
148
	function disable_emojis_remove_dns_prefetch( $urls, $relation_type ) {
149
		if ( 'dns-prefetch' == $relation_type ) {
150
			/** This filter is documented in wp-includes/formatting.php */
151
			$emoji_svg_url = apply_filters( 'emoji_svg_url', 'https://s.w.org/images/core/emoji/2/svg/' );
152
153
			$urls = array_diff( $urls, array( $emoji_svg_url ) );
154
		}
155
156
		return $urls;
157
	}
158
159
	// by default we only inline scripts+styles on first page load for a given user
160
	function set_first_load_cookie() {
161 View Code Duplication
		if ( ! isset( $_COOKIE['jetpack_perf_loaded'] ) ) {
162
			setcookie( 'jetpack_perf_loaded', '1', time() + YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
163
		}
164
	}
165
166
	/** FILTERS **/
167
	public function remove_external_font_scripts( $should_remove, $handle, $asset_url ) {
168
		$font_script_url = 'http://use.typekit.com/';
169
		return strncmp( $asset_url, $font_script_url, strlen( $font_script_url ) ) === 0;
170
	}
171
172
	public function remove_external_font_styles( $should_remove, $handle, $asset_url ) {
173
		$font_url = 'https://fonts.googleapis.com';
174
		return strncmp( $asset_url, $font_url, strlen( $font_url ) ) === 0;
175
	}
176
177
	/** SCRIPTS **/
178 View Code Duplication
	public function filter_inline_scripts( $src, $handle ) {
179
		global $wp_scripts;
180
181
		if ( is_admin() || ! isset( $wp_scripts->registered[$handle] ) ) {
182
			return $src;
183
		}
184
185
		$script = $wp_scripts->registered[$handle];
186
187
		// reset src to empty - can't return empty string though because then it skips rendering the tag
188
		if ( $this->should_inline_script( $script ) ) {
189
			return '#';
190
		}
191
192
		return $src;
193
	}
194
195
	public function print_inline_scripts( $tag, $handle, $src ) {
196
		global $wp_scripts;
197
198
		if ( is_admin() || ! isset( $wp_scripts->registered[$handle] ) ) {
199
			return $tag;
200
		}
201
202
		$script = $wp_scripts->registered[$handle];
203
204
		if ( $this->should_remove_script( $script ) ) {
205
			return '';
206
		}
207
208
		if ( $this->should_inline_script( $script ) ) {
209
			$tag = '<script type="text/javascript" src="data:text/javascript;base64,' . base64_encode( file_get_contents( $script->extra['jetpack-inline-file'] ) ) . '"></script>';
210
		}
211
212
		if ( $this->should_async_script( $script ) ) {
213
			$tag = preg_replace( '/<script /', '<script async ', $tag );
214
		} elseif ( $this->should_defer_script( $script ) ) {
215
			$tag = preg_replace( '/<script /', '<script defer ', $tag );
216
		}
217
218
		return $tag;
219
	}
220
221
	private function should_async_script( $script ) {
222
		global $wp_scripts;
223
		$should_async_script = empty( $script->deps );
224
225
		$skip_self = true;
226
		// only make scripts async if nothing depends on them and they don't depend on anything else
227
		foreach ( $wp_scripts->to_do as $other_script_handle ) {
228
			if ( $skip_self ) {
229
				$skip_self = false;
230
				continue;
231
			}
232
			$other_script = $wp_scripts->registered[ $other_script_handle ];
233
			if ( array_intersect( array( $script->handle ), $other_script->deps ) ) {
234
				$should_async_script = false;
235
				break;
236
			}
237
		}
238
239
		$script->extra['jetpack-defer'] = ! $should_async_script;
240
241
		return $this->async_scripts && apply_filters( 'jetpack_perf_async_script', $should_async_script, $script->handle, $script->src );
242
	}
243
244
	private function should_defer_script( $script ) {
245
		$should_defer_script = isset( $script->extra['jetpack-defer'] ) && $script->extra['jetpack-defer'];
246
		return $this->defer_scripts && apply_filters( 'jetpack_perf_defer_script', $should_defer_script, $script->handle, $script->src );
247
	}
248
249
	private function should_remove_script( $script ) {
250
		return $this->should_remove_asset( 'jetpack_perf_remove_script', $script );
251
	}
252
253
	private function should_inline_script( $script ) {
254
		return ( $this->inline_scripts_and_styles || $this->inline_always ) && $this->should_inline_asset( 'jetpack_perf_inline_script', $script );
255
	}
256
257
	/** STYLES **/
258 View Code Duplication
	public function filter_inline_styles( $src, $handle ) {
259
		global $wp_scripts;
260
261
		if ( is_admin() || ! isset( $wp_scripts->registered[$handle] ) ) {
262
			return $src;
263
		}
264
265
		$style = $wp_scripts->registered[$handle];
266
267
		if ( $this->should_inline_style( $style ) ) {
268
			return '#';
269
		}
270
271
		return $src;
272
	}
273
274
	public function print_inline_styles( $tag, $handle, $href, $media ) {
275
		global $wp_styles;
276
277
		if ( is_admin() || ! isset( $wp_styles->registered[$handle] ) ) {
278
			return $tag;
279
		}
280
281
		$style = $wp_styles->registered[$handle];
282
283
		if ( $this->should_inline_style( $style ) ) {
284
			return "<style type='text/css' media='$media'>" . file_get_contents( $style->extra['jetpack-inline-file'] ) . '</style>';
285
		}
286
287
		if ( $this->should_remove_style( $style ) ) {
288
			return '';
289
		}
290
291
		return $tag;
292
	}
293
294
	private function should_inline_style( $style ) {
295
		return ( $this->inline_scripts_and_styles || $this->inline_always ) && $this->should_inline_asset( 'jetpack_perf_inline_style', $style );
296
	}
297
298
	private function should_remove_style( $style ) {
299
		return $this->should_remove_asset( 'jetpack_perf_remove_style', $style );
300
	}
301
302
	/** shared code **/
303
304
	private function should_inline_asset( $filter, $dependency ) {
305
		// inline anything local, with a src starting with /, or starting with site_url
306
		$site_url = site_url();
307
308
		$is_local_url = ( strncmp( $dependency->src, '/', 1 ) === 0 && strncmp( $dependency->src, '//', 2 ) !== 0 )
309
			|| strpos( $dependency->src, $site_url ) === 0;
310
311
		if ( $is_local_url && ! isset( $dependency->extra['jetpack-inline'] ) ) {
312
			$dependency->extra['jetpack-inline'] = true;
313
314
			$path = untrailingslashit( ABSPATH ) . str_replace( $site_url, '', $dependency->src );
315
316
			if ( ! file_exists( $path ) ) {
317
				$is_windows = strtoupper( substr( php_uname( 's' ), 0, 3 ) ) === 'WIN';
318
				$path       = $is_windows
319
					? str_replace( '/', '\\', str_replace( $site_url, '', $dependency->src ) )
320
					: str_replace( $site_url, '', $dependency->src );
321
322
				$glue = $is_windows ? '\\' : '/';
323
324
				$prefix = explode( $glue, untrailingslashit( WP_CONTENT_DIR ) );
325
				$prefix = array_slice( $prefix, 0, array_search( $path[1], $prefix ) - 1 );
326
327
				$path = implode( $glue, $prefix ) . $path;
328
			}
329
330
			$dependency->extra['jetpack-inline-file'] = $path;
331
		}
332
333
		$should_inline = isset( $dependency->extra['jetpack-inline'] ) && $dependency->extra['jetpack-inline'];
334
335
		$will_inline = apply_filters( $filter, $should_inline, $dependency->handle, $dependency->src ) && file_exists( $dependency->extra['jetpack-inline-file'] );
336
337
		return $will_inline;
338
	}
339
340
	private function should_remove_asset( $filter, $dependency ) {
341
		return apply_filters( $filter, false, $dependency->handle, $dependency->src );
342
	}
343
344
	/**
345
	 * if inline assets are enabled, renders inline
346
	 * TODO: enable this just for certain paths/patterns/filetypes
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
347
	 * This is actually currently unused
348
	 */
349
	 public function register_inline_script( $handle, $file, $plugin_file, $deps = false, $ver = false, $in_footer = false ) {
350
		$registered = wp_register_script( $handle, plugins_url( $file, $plugin_file ), $deps, $ver, $in_footer );
351
352 View Code Duplication
		if ( $registered ) {
353
			$file_full_path = dirname( $plugin_file ) . '/' . $file;
354
			wp_script_add_data( $handle, 'jetpack-inline', true );
355
			wp_script_add_data( $handle, 'jetpack-inline-file', $file_full_path );
356
		}
357
358
		return $registered;
359
	}
360
361
	/**
362
	 * if inline assets are enabled, renders inline
363
	 * TODO: enable this just for certain paths/patterns/filetypes
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
364
	 * This is actually currently unused
365
	 */
366
	public function register_inline_style( $handle, $file, $plugin_file, $deps = array(), $ver = false, $media = 'all' ) {
367
		$registered = wp_register_style( $handle, plugins_url( $file, $plugin_file ), $deps, $ver, $media );
368
369 View Code Duplication
		if ( $registered ) {
370
			$file_full_path = dirname( $plugin_file ) . '/' . $file;
371
			wp_style_add_data( $handle, 'jetpack-inline', true );
372
			wp_style_add_data( $handle, 'jetpack-inline-file', $file_full_path );
373
		}
374
	}
375
}
376