Completed
Push — add/pwa ( e807cd...c0d2c6 )
by
unknown
15:07 queued 06:56
created

Jetpack_Perf_Optimize_Assets::set_asset_groups()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 0
loc 8
rs 9.4285
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
17
	/**
18
	 * Singleton implementation
19
	 *
20
	 * @return object
21
	 */
22
	public static function instance() {
23
		if ( ! is_a( self::$__instance, 'Jetpack_Perf_Optimize_Assets' ) ) {
24
			self::$__instance = new Jetpack_Perf_Optimize_Assets();
25
		}
26
27
		return self::$__instance;
28
	}
29
30
	public function disable_for_request() {
31
		$this->remove_remote_fonts = false;
32
		$this->inline_scripts_and_styles = false;
33
		$this->async_scripts = false;
34
		$this->defer_scripts = false;
35
	}
36
37
	/**
38
	 * 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...
39
	 */
40
41
	/**
42
	 * Registers actions
43
	 */
44
	private function __construct() {
45
		$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...
46
		$this->remove_remote_fonts       = get_option( 'perf_remove_remote_fonts', true );
47
		$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...
48
		$this->inline_scripts_and_styles = get_option( 'perf_inline_scripts_and_styles', true ) && ( $this->is_first_load || $this->inline_always );
49
		$this->async_scripts             = get_option( 'perf_async_scripts', true );
50
		$this->defer_scripts             = get_option( 'perf_defer_scripts', true );
51
		$this->move_scripts_to_footer    = true;
0 ignored issues
show
Bug introduced by
The property move_scripts_to_footer 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...
52
53
		if ( $this->remove_remote_fonts ) {
54
			add_filter( 'jetpack_perf_remove_script', array( $this, 'remove_external_font_scripts' ), 10, 3 );
55
			add_filter( 'jetpack_perf_remove_style', array( $this, 'remove_external_font_styles' ), 10, 3 );
56
		}
57
58
		if ( $this->move_scripts_to_footer ) {
59
			add_filter( 'jetpack_perf_asset_group', array( $this, 'set_asset_groups' ), 10, 2 );
60
		}
61
62
		add_action( 'wp_enqueue_scripts', array( $this, 'send_scripts_to_footer' ), PHP_INT_MAX );
63
		add_filter( 'script_loader_src', array( $this, 'filter_inline_scripts' ), -100, 2 );
64
		add_filter( 'script_loader_tag', array( $this, 'print_inline_scripts' ), -100, 3 );
65
		add_filter( 'style_loader_src', array( $this, 'filter_inline_styles' ), -100, 2 );
66
		add_filter( 'style_loader_tag', array( $this, 'print_inline_styles' ), -100, 4 );
67
68
		add_action( 'init', array( $this, 'set_first_load_cookie' ) );
69
70
		/**
71
		 * Feature, theme and plugin-specific hacks
72
		 */
73
		// 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...
74
		add_action( 'init', array( $this, 'disable_emojis' ) );
75
76
		// inline/defer/async stuff for Jetpack
77
		add_action( 'init', array( $this, 'optimize_jetpack' ) );
78
	}
79
80
	/** Disabling Emojis **/
81
	// improves page load performance
82
83
	function disable_emojis() {
84
		remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
85
		remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
86
		remove_action( 'embed_head', 'print_emoji_detection_script', 7 );
87
88
		remove_action( 'wp_print_styles', 'print_emoji_styles' );
89
		remove_action( 'admin_print_styles', 'print_emoji_styles' );
90
91
		remove_filter( 'the_content_feed', 'wp_staticize_emoji' );
92
		remove_filter( 'comment_text_rss', 'wp_staticize_emoji' );
93
		remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
94
95
		add_filter( 'tiny_mce_plugins', array( $this, 'disable_emojis_tinymce' ) );
96
		add_filter( 'wp_resource_hints', array( $this, 'disable_emojis_remove_dns_prefetch' ), 10, 2 );
97
	}
98
99
	function optimize_jetpack() {
100
101
	}
102
103
	/**
104
	 * Filter function used to remove the tinymce emoji plugin.
105
	 *
106
	 * @param array $plugins
107
	 * @return array Difference betwen the two arrays
108
	 */
109
	function disable_emojis_tinymce( $plugins ) {
110
		if ( is_array( $plugins ) ) {
111
			return array_diff( $plugins, array( 'wpemoji' ) );
112
		} else {
113
			return array();
114
		}
115
	}
116
117
	/**
118
	 * Remove emoji CDN hostname from DNS prefetching hints.
119
	 *
120
	 * @param array $urls URLs to print for resource hints.
121
	 * @param string $relation_type The relation type the URLs are printed for.
122
	 * @return array Difference betwen the two arrays.
123
	 */
124
	function disable_emojis_remove_dns_prefetch( $urls, $relation_type ) {
125
		if ( 'dns-prefetch' == $relation_type ) {
126
			/** This filter is documented in wp-includes/formatting.php */
127
			$emoji_svg_url = apply_filters( 'emoji_svg_url', 'https://s.w.org/images/core/emoji/2/svg/' );
128
129
			$urls = array_diff( $urls, array( $emoji_svg_url ) );
130
		}
131
132
		return $urls;
133
	}
134
135
	// by default we only inline scripts+styles on first page load for a given user
136
	function set_first_load_cookie() {
137 View Code Duplication
		if ( ! isset( $_COOKIE['jetpack_perf_loaded'] ) ) {
138
			setcookie( 'jetpack_perf_loaded', '1', time() + YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
139
		}
140
	}
141
142
	// this code essentially sets the default asset location to the footer rather than the head
143
	function send_scripts_to_footer() {
144
		global $wp_scripts;
145
146
		// fetch all deps for head
147
		$wp_scripts->all_deps( $wp_scripts->queue, true, 1 );
148 View Code Duplication
		foreach( $wp_scripts->to_do as $handle ) {
149
			$registration = $wp_scripts->registered[$handle];
150
			if ( $registration->args !== NULL ) {
151
				// skip, this asset has an explicit location
152
				continue;
153
			}
154
155
			$asset_group = apply_filters( 'jetpack_perf_asset_group', 1, $handle );
156
			$registration->args = $asset_group;
157
			$wp_scripts->groups[$handle] = $asset_group;
158
		}
159
	}
160
161
	function set_asset_groups( $group, $handle ) {
162
		// force jquery into header, everything else can go in footer unless filtered elsewhere
163
		if ( in_array( $handle, array( 'jquery-core', 'jquery-migrate', 'jquery' ) ) ) {
164
			return 0;
165
		}
166
167
		return $group;
168
	}
169
170
	/** FILTERS **/
171
	public function remove_external_font_scripts( $should_remove, $handle, $asset_url ) {
172
		$font_script_url = 'http://use.typekit.com/';
173
		return strncmp( $asset_url, $font_script_url, strlen( $font_script_url ) ) === 0;
174
	}
175
176
	public function remove_external_font_styles( $should_remove, $handle, $asset_url ) {
177
		$font_url = 'https://fonts.googleapis.com';
178
		return strncmp( $asset_url, $font_url, strlen( $font_url ) ) === 0;
179
	}
180
181
	/** SCRIPTS **/
182 View Code Duplication
	public function filter_inline_scripts( $src, $handle ) {
183
		global $wp_scripts;
184
185
		if ( is_admin() || ! isset( $wp_scripts->registered[$handle] ) ) {
186
			return $src;
187
		}
188
189
		$script = $wp_scripts->registered[$handle];
190
191
		// reset src to empty - can't return empty string though because then it skips rendering the tag
192
		if ( $this->should_inline_script( $script ) ) {
193
			return '#';
194
		}
195
196
		return $src;
197
	}
198
199
	public function print_inline_scripts( $tag, $handle, $src ) {
200
		global $wp_scripts;
201
202
		if ( is_admin() || ! isset( $wp_scripts->registered[$handle] ) ) {
203
			return $tag;
204
		}
205
206
		$script = $wp_scripts->registered[$handle];
207
208
		if ( $this->should_remove_script( $script ) ) {
209
			return '';
210
		}
211
212
		if ( $this->should_inline_script( $script ) ) {
213
			$label = '<!-- ' . $script->src . '-->';
214
			// base64-encoding a script into the src URL only makes sense if we intend to async or defer it
215
			if ( $this->should_defer_script( $script ) ) {
216
				$tag = $label . '<script defer type="text/javascript" src="data:text/javascript;base64,' . base64_encode( file_get_contents( $script->extra['jetpack-inline-file'] ) ) . '"></script>';
217
			} elseif ( $this->should_async_script( $script ) ) {
218
				$tag = $label . '<script async type="text/javascript" src="data:text/javascript;base64,' . base64_encode( file_get_contents( $script->extra['jetpack-inline-file'] ) ) . '"></script>';
219
			} else {
220
				$tag = $label . '<script type="text/javascript">' . file_get_contents( $script->extra['jetpack-inline-file'] ) . '</script>';
221
			}
222
		} else {
223
			if ( $this->should_defer_script( $script ) ) {
224
				$tag = preg_replace( '/<script /', '<script defer ', $tag );
225
			} elseif ( $this->should_async_script( $script ) ) {
226
				$tag = preg_replace( '/<script /', '<script async ', $tag );
227
			}
228
		}
229
230
		return $tag;
231
	}
232
233
	private function should_async_script( $script ) {
234
		global $wp_scripts;
235
		$should_async_script = empty( $script->deps );
236
237
		$skip_self = true;
238
		// only make scripts async if nothing depends on them and they don't depend on anything else
239
		foreach ( $wp_scripts->to_do as $other_script_handle ) {
240
			if ( $skip_self ) {
241
				$skip_self = false;
242
				continue;
243
			}
244
			$other_script = $wp_scripts->registered[ $other_script_handle ];
245
			if ( array_intersect( array( $script->handle ), $other_script->deps ) ) {
246
				$should_async_script = false;
247
				break;
248
			}
249
		}
250
251
		$script->extra['jetpack-defer'] = ! $should_async_script;
252
253
		return $this->async_scripts && apply_filters( 'jetpack_perf_async_script', $should_async_script, $script->handle, $script->src );
254
	}
255
256
	private function should_defer_script( $script ) {
257
		$should_defer_script = isset( $script->extra['jetpack-defer'] ) && $script->extra['jetpack-defer'];
258
		return $this->defer_scripts && apply_filters( 'jetpack_perf_defer_script', $should_defer_script, $script->handle, $script->src );
259
	}
260
261
	private function should_remove_script( $script ) {
262
		return $this->should_remove_asset( 'jetpack_perf_remove_script', $script );
263
	}
264
265
	private function should_inline_script( $script ) {
266
		return ( $this->inline_scripts_and_styles || $this->inline_always ) && $this->should_inline_asset( 'jetpack_perf_inline_script', $script );
267
	}
268
269
	/** STYLES **/
270 View Code Duplication
	public function filter_inline_styles( $src, $handle ) {
271
		global $wp_scripts;
272
273
		if ( is_admin() || ! isset( $wp_scripts->registered[$handle] ) ) {
274
			return $src;
275
		}
276
277
		$style = $wp_scripts->registered[$handle];
278
279
		if ( $this->should_inline_style( $style ) ) {
280
			return '#';
281
		}
282
283
		return $src;
284
	}
285
286
	public function print_inline_styles( $tag, $handle, $href, $media ) {
287
		global $wp_styles;
288
289
		if ( is_admin() || ! isset( $wp_styles->registered[$handle] ) ) {
290
			return $tag;
291
		}
292
293
		$style = $wp_styles->registered[$handle];
294
295
		if ( $this->should_inline_style( $style ) ) {
296
			$label = '<!-- ' . $style->src . '-->';
297
			return "$label<style type='text/css' media='$media'>" . file_get_contents( $style->extra['jetpack-inline-file'] ) . '</style>';
298
		}
299
300
		if ( $this->should_remove_style( $style ) ) {
301
			return '';
302
		}
303
304
		return $tag;
305
	}
306
307
	private function should_inline_style( $style ) {
308
		return ( $this->inline_scripts_and_styles || $this->inline_always ) && $this->should_inline_asset( 'jetpack_perf_inline_style', $style );
309
	}
310
311
	private function should_remove_style( $style ) {
312
		return $this->should_remove_asset( 'jetpack_perf_remove_style', $style );
313
	}
314
315
	/** shared code **/
316
317
	private function should_inline_asset( $filter, $dependency ) {
318
		// inline anything local, with a src starting with /, or starting with site_url
319
		$site_url = site_url();
320
321
		$is_local_url = ( strncmp( $dependency->src, '/', 1 ) === 0 && strncmp( $dependency->src, '//', 2 ) !== 0 )
322
			|| strpos( $dependency->src, $site_url ) === 0;
323
324
		if ( $is_local_url && ! isset( $dependency->extra['jetpack-inline'] ) ) {
325
			$dependency->extra['jetpack-inline'] = true;
326
327
			$path = untrailingslashit( ABSPATH ) . str_replace( $site_url, '', $dependency->src );
328
329
			if ( ! file_exists( $path ) ) {
330
				$path = str_replace('/', DIRECTORY_SEPARATOR, str_replace( $site_url, '', $dependency->src ));
331
332
				$prefix = explode( DIRECTORY_SEPARATOR, untrailingslashit( WP_CONTENT_DIR ) );
333
				$prefix = array_slice( $prefix, 0, array_search( $path[1], $prefix ) - 1 );
334
335
				$path = implode( DIRECTORY_SEPARATOR, $prefix ) . $path;
336
			}
337
338
			$dependency->extra['jetpack-inline-file'] = $path;
339
		}
340
341
		// only inline if we don't have a conditional
342
		$should_inline = ! isset( $dependency->extra['conditional'] ) && isset( $dependency->extra['jetpack-inline'] ) && $dependency->extra['jetpack-inline'];
343
344
		return apply_filters( $filter, $should_inline, $dependency->handle, $dependency->src ) && file_exists( $dependency->extra['jetpack-inline-file'] );
345
	}
346
347
	private function should_remove_asset( $filter, $dependency ) {
348
		return apply_filters( $filter, false, $dependency->handle, $dependency->src );
349
	}
350
351
	/**
352
	 * if inline assets are enabled, renders inline
353
	 * 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...
354
	 * This is actually currently unused
355
	 */
356
	 public function register_inline_script( $handle, $file, $plugin_file, $deps = false, $ver = false, $in_footer = false ) {
357
		$registered = wp_register_script( $handle, plugins_url( $file, $plugin_file ), $deps, $ver, $in_footer );
358
359 View Code Duplication
		if ( $registered ) {
360
			$file_full_path = dirname( $plugin_file ) . '/' . $file;
361
			wp_script_add_data( $handle, 'jetpack-inline', true );
362
			wp_script_add_data( $handle, 'jetpack-inline-file', $file_full_path );
363
		}
364
365
		return $registered;
366
	}
367
368
	/**
369
	 * if inline assets are enabled, renders inline
370
	 * 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...
371
	 * This is actually currently unused
372
	 */
373
	public function register_inline_style( $handle, $file, $plugin_file, $deps = array(), $ver = false, $media = 'all' ) {
374
		$registered = wp_register_style( $handle, plugins_url( $file, $plugin_file ), $deps, $ver, $media );
375
376 View Code Duplication
		if ( $registered ) {
377
			$file_full_path = dirname( $plugin_file ) . '/' . $file;
378
			wp_style_add_data( $handle, 'jetpack-inline', true );
379
			wp_style_add_data( $handle, 'jetpack-inline-file', $file_full_path );
380
		}
381
	}
382
}
383