Completed
Push — add/pwa ( a1e925...e807cd )
by
unknown
08:27
created

print_inline_scripts()   D

Complexity

Conditions 9
Paths 8

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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