Completed
Push — add/pwa ( 45ed8c...dcada3 )
by
unknown
13:01 queued 04:54
created

Jetpack_Perf_Optimize_Assets::optimize_jetpack()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
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
 * - move CSS links below scripts in head (scripts after CSS blocks render until script finishes downloading)
9
 */
10
11
class Jetpack_Perf_Optimize_Assets {
12
	private static $__instance = null;
13
	private $remove_remote_fonts = false;
14
	private $inline_scripts_and_styles = false;
15
	private $async_scripts = false;
16
	private $defer_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
	}
37
38
	/**
39
	 * 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...
40
	 */
41
42
	/**
43
	 * Registers actions
44
	 */
45
	private function __construct() {
46
		$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...
47
		$this->remove_remote_fonts       = get_option( 'perf_remove_remote_fonts', true );
48
		$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...
49
		$this->inline_scripts_and_styles = get_option( 'perf_inline_scripts_and_styles', true ) && ( $this->is_first_load || $this->inline_always );
50
		$this->async_scripts             = get_option( 'perf_async_scripts', true );
51
		$this->defer_scripts             = get_option( 'perf_defer_scripts', true );
52
		$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...
53
		$this->move_scripts_above_css_in_header = true;
0 ignored issues
show
Bug introduced by
The property move_scripts_above_css_in_header 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...
54
		$this->remove_core_emojis        = true;
0 ignored issues
show
Bug introduced by
The property remove_core_emojis 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...
55
		$this->prevent_jetpack_implode_css = true;
0 ignored issues
show
Bug introduced by
The property prevent_jetpack_implode_css 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...
56
57
		if ( $this->remove_remote_fonts ) {
58
			add_filter( 'jetpack_perf_remove_script', array( $this, 'remove_external_font_scripts' ), 10, 3 );
59
			add_filter( 'jetpack_perf_remove_style', array( $this, 'remove_external_font_styles' ), 10, 3 );
60
		}
61
62
		if ( $this->move_scripts_to_footer ) {
63
			add_filter( 'jetpack_perf_asset_group', array( $this, 'set_asset_groups' ), 10, 2 );
64
		}
65
66
		if ( $this->move_scripts_above_css_in_header ) {
67
			add_action( 'init', array( $this, 'move_styles_to_bottom_of_header' ), PHP_INT_MAX );
68
		}
69
70
		if ( $this->prevent_jetpack_implode_css ) {
71
			add_filter( 'jetpack_implode_frontend_css', '__return_false' );
72
		}
73
74
		add_action( 'wp_enqueue_scripts', array( $this, 'send_scripts_to_footer' ), PHP_INT_MAX );
75
		add_filter( 'script_loader_src', array( $this, 'filter_inline_scripts' ), -100, 2 );
76
		add_filter( 'script_loader_tag', array( $this, 'print_inline_scripts' ), -100, 3 );
77
		add_filter( 'style_loader_src', array( $this, 'filter_inline_styles' ), -100, 2 );
78
		add_filter( 'style_loader_tag', array( $this, 'print_inline_styles' ), -100, 4 );
79
80
		add_action( 'init', array( $this, 'set_first_load_cookie' ) );
81
82
		/**
83
		 * Feature, theme and plugin-specific hacks
84
		 */
85
86
		// 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...
87
		if ( $this->remove_core_emojis ) {
88
			add_action( 'init', array( $this, 'disable_emojis' ) );
89
		}
90
91
		// inline/defer/async stuff for Jetpack
92
		add_action( 'init', array( $this, 'optimize_jetpack' ) );
93
	}
94
95
	/** Disabling Emojis **/
96
	// improves page load performance
97
98
	function disable_emojis() {
99
		remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
100
		remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
101
		remove_action( 'embed_head', 'print_emoji_detection_script', 7 );
102
103
		remove_action( 'wp_print_styles', 'print_emoji_styles' );
104
		remove_action( 'admin_print_styles', 'print_emoji_styles' );
105
106
		remove_filter( 'the_content_feed', 'wp_staticize_emoji' );
107
		remove_filter( 'comment_text_rss', 'wp_staticize_emoji' );
108
		remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
109
110
		add_filter( 'tiny_mce_plugins', array( $this, 'disable_emojis_tinymce' ) );
111
		add_filter( 'wp_resource_hints', array( $this, 'disable_emojis_remove_dns_prefetch' ), 10, 2 );
112
	}
113
114
	function optimize_jetpack() {
115
116
	}
117
118
	/**
119
	 * Filter function used to remove the tinymce emoji plugin.
120
	 *
121
	 * @param array $plugins
122
	 * @return array Difference betwen the two arrays
123
	 */
124
	function disable_emojis_tinymce( $plugins ) {
125
		if ( is_array( $plugins ) ) {
126
			return array_diff( $plugins, array( 'wpemoji' ) );
127
		} else {
128
			return array();
129
		}
130
	}
131
132
	/**
133
	 * Remove emoji CDN hostname from DNS prefetching hints.
134
	 *
135
	 * @param array $urls URLs to print for resource hints.
136
	 * @param string $relation_type The relation type the URLs are printed for.
137
	 * @return array Difference betwen the two arrays.
138
	 */
139
	function disable_emojis_remove_dns_prefetch( $urls, $relation_type ) {
140
		if ( 'dns-prefetch' == $relation_type ) {
141
			/** This filter is documented in wp-includes/formatting.php */
142
			$emoji_svg_url = apply_filters( 'emoji_svg_url', 'https://s.w.org/images/core/emoji/2/svg/' );
143
144
			$urls = array_diff( $urls, array( $emoji_svg_url ) );
145
		}
146
147
		return $urls;
148
	}
149
150
	// by default we only inline scripts+styles on first page load for a given user
151
	function set_first_load_cookie() {
152 View Code Duplication
		if ( ! isset( $_COOKIE['jetpack_perf_loaded'] ) ) {
153
			setcookie( 'jetpack_perf_loaded', '1', time() + YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
154
		}
155
	}
156
157
	// this code essentially sets the default asset location to the footer rather than the head
158
	function send_scripts_to_footer() {
159
		global $wp_scripts;
160
161
		// fetch all deps for head
162
		$wp_scripts->all_deps( $wp_scripts->queue, true, 1 );
163 View Code Duplication
		foreach( $wp_scripts->to_do as $handle ) {
164
			$registration = $wp_scripts->registered[$handle];
165
			if ( $registration->args !== NULL ) {
166
				// skip, this asset has an explicit location
167
				continue;
168
			}
169
170
			$asset_group = apply_filters( 'jetpack_perf_asset_group', 1, $handle );
171
			$registration->args = $asset_group;
172
			$wp_scripts->groups[$handle] = $asset_group;
173
		}
174
	}
175
176
	// scripts that run after CSS <link>s in the header block waiting for the CSS to load
177
	// so we move styles as late as possible in the wp_head action to maximise the chance
178
	// of non-blocking rendering
179
	function move_styles_to_bottom_of_header() {
180
		remove_action( 'wp_head', 'wp_print_styles', 8 );
181
		add_action( 'wp_head', 'wp_print_styles', 999 );
182
	}
183
184
	function set_asset_groups( $group, $handle ) {
185
		// force jquery into header, everything else can go in footer unless filtered elsewhere
186
		if ( in_array( $handle, array( 'jquery-core', 'jquery-migrate', 'jquery' ) ) ) {
187
			return 0;
188
		}
189
190
		return $group;
191
	}
192
193
	/** FILTERS **/
194
	public function remove_external_font_scripts( $should_remove, $handle, $asset_url ) {
195
		$font_script_url = 'http://use.typekit.com/';
196
		return strncmp( $asset_url, $font_script_url, strlen( $font_script_url ) ) === 0;
197
	}
198
199
	public function remove_external_font_styles( $should_remove, $handle, $asset_url ) {
200
		$font_url = 'https://fonts.googleapis.com';
201
		return strncmp( $asset_url, $font_url, strlen( $font_url ) ) === 0;
202
	}
203
204
	/** SCRIPTS **/
205 View Code Duplication
	public function filter_inline_scripts( $src, $handle ) {
206
		global $wp_scripts;
207
208
		if ( is_admin() || ! isset( $wp_scripts->registered[$handle] ) ) {
209
			return $src;
210
		}
211
212
		$script = $wp_scripts->registered[$handle];
213
214
		// reset src to empty - can't return empty string though because then it skips rendering the tag
215
		if ( $this->should_inline_script( $script ) ) {
216
			return '#';
217
		}
218
219
		return $src;
220
	}
221
222
	public function print_inline_scripts( $tag, $handle, $src ) {
223
		global $wp_scripts;
224
225
		if ( is_admin() || ! isset( $wp_scripts->registered[$handle] ) ) {
226
			return $tag;
227
		}
228
229
		$script = $wp_scripts->registered[$handle];
230
231
		if ( $this->should_remove_script( $script ) ) {
232
			return '';
233
		}
234
235
		if ( $this->should_inline_script( $script ) ) {
236
			$label = '<!-- ' . $script->src . '-->';
237
			// base64-encoding a script into the src URL only makes sense if we intend to async or defer it
238
			if ( $this->should_defer_script( $script ) ) {
239
				$tag = $label . '<script defer type="text/javascript" src="data:text/javascript;base64,' . base64_encode( file_get_contents( $script->extra['jetpack-inline-file'] ) ) . '"></script>';
240
			} elseif ( $this->should_async_script( $script ) ) {
241
				$tag = $label . '<script async type="text/javascript" src="data:text/javascript;base64,' . base64_encode( file_get_contents( $script->extra['jetpack-inline-file'] ) ) . '"></script>';
242
			} else {
243
				$tag = $label . '<script type="text/javascript">' . file_get_contents( $script->extra['jetpack-inline-file'] ) . '</script>';
244
			}
245
		} else {
246
			if ( $this->should_defer_script( $script ) ) {
247
				$tag = preg_replace( '/<script /', '<script defer ', $tag );
248
			} elseif ( $this->should_async_script( $script ) ) {
249
				$tag = preg_replace( '/<script /', '<script async ', $tag );
250
			}
251
		}
252
253
		return $tag;
254
	}
255
256 View Code Duplication
	private function should_async_script( $script ) {
257
		global $wp_scripts;
258
259
		// explicitly in the header (scripts aren't affected much by async)
260
		$should_async_script = $script->args === 0;
261
262
		// only make scripts async if nothing depends on them
263
		foreach ( $wp_scripts->to_do as $other_script_handle ) {
264
			$other_script = $wp_scripts->registered[ $other_script_handle ];
265
			if ( in_array( $script->handle, $other_script->deps ) ) {
266
				$should_async_script = false;
267
				break;
268
			}
269
		}
270
271
		// you can override this logic by setting jetpack-async
272
		$should_async_script = $should_async_script || ( isset( $script->extra['jetpack-async'] ) && $script->extra['jetpack-async'] );
273
		return $this->async_scripts && apply_filters( 'jetpack_perf_async_script', $should_async_script, $script->handle, $script->src );
274
	}
275
276 View Code Duplication
	private function should_defer_script( $script ) {
277
		global $wp_scripts;
278
279
		// if it's explicitly not in the footer, or we have Jetpack Defer set, and has no dependencies
280
		$should_defer_script = $script->args === 0;
281
282
		// only make scripts deferred if nothing depends on them
283
		foreach ( $wp_scripts->to_do as $other_script_handle ) {
284
			$other_script = $wp_scripts->registered[ $other_script_handle ];
285
			if ( in_array( $script->handle, $other_script->deps ) ) {
286
				$should_defer_script = false;
287
				break;
288
			}
289
		}
290
291
		$should_defer_script = $should_defer_script || ( isset( $script->extra['jetpack-defer'] ) && $script->extra['jetpack-defer'] );
292
		return $this->defer_scripts && apply_filters( 'jetpack_perf_defer_script', $should_defer_script, $script->handle, $script->src );
293
	}
294
295
	private function should_remove_script( $script ) {
296
		return $this->should_remove_asset( 'jetpack_perf_remove_script', $script );
297
	}
298
299
	private function should_inline_script( $script ) {
300
		return ( $this->inline_scripts_and_styles || $this->inline_always ) && $this->should_inline_asset( 'jetpack_perf_inline_script', $script );
301
	}
302
303
	/** STYLES **/
304 View Code Duplication
	public function filter_inline_styles( $src, $handle ) {
305
		global $wp_scripts;
306
307
		if ( is_admin() || ! isset( $wp_scripts->registered[$handle] ) ) {
308
			return $src;
309
		}
310
311
		$style = $wp_scripts->registered[$handle];
312
313
		if ( $this->should_inline_style( $style ) ) {
314
			return '#';
315
		}
316
317
		return $src;
318
	}
319
320
	public function print_inline_styles( $tag, $handle, $href, $media ) {
321
		global $wp_styles;
322
323
		if ( is_admin() || ! isset( $wp_styles->registered[$handle] ) ) {
324
			return $tag;
325
		}
326
327
		$style = $wp_styles->registered[$handle];
328
329
		if ( $this->should_inline_style( $style ) ) {
330
			$label = '<!-- ' . $style->src . '-->';
331
			$css = $this->fix_css_urls( file_get_contents( $style->extra['jetpack-inline-file'] ), $style->src ); 
332
			return "$label<style type='text/css' media='$media'>$css</style>";
333
		}
334
335
		if ( $this->should_remove_style( $style ) ) {
336
			return '';
337
		}
338
339
		return $tag;
340
	}
341
342
	public function fix_css_urls( $css, $css_url ) {
343
		$base = trailingslashit( dirname( $css_url ) );
344
		$base = str_replace( site_url(), '', $base );
345
		
346
		// reject absolute site_url 
347
		if ( 'http' === substr( $base, 0, 4 ) ) {
348
			return $css;
349
		}
350
		return preg_replace_callback( '/url[\s]*\([\s]*["\']?[\s]*(?!https?:\/\/)(?!data:)(?!#)([^\)"\']*)["\']?\)/i', function( $matches ) use ( $base ) {
351
			// TODO: embed data-encoded file, for files smaller than certain size?
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...
352
			return 'url('.$this->rel2abspath( $matches[1], $base ).')';
353
		}, $css );
354
	}
355
356
	// see: http://stackoverflow.com/questions/4444475/transfrom-relative-path-into-absolute-url-using-php
357
	private function rel2abspath( $rel, $path) {
358
		/* remove non-directory element from path */
359
		$path = preg_replace( '#/[^/]*$#', '', $path );
360
361
		/* destroy path if relative url points to root */
362
		if( $rel[0] == '/' )
363
			$path = '';
364
365
		/* dirty absolute URL */
366
		$abs = '';
367
368
		$abs .= $path . '/' . $rel;
369
370
		/* replace '//' or '/./' or '/foo/../' with '/' */
371
		$re = array('#(/\.?/)#', '#/(?!\.\.)[^/]+/\.\./#');
372
		for( $n=1; $n>0; $abs = preg_replace( $re, '/', $abs, -1, $n ) ) {}
0 ignored issues
show
Unused Code introduced by
This for loop is empty and can be removed.

This check looks for for loops that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

Consider removing the loop.

Loading history...
373
374
		/* absolute path is ready! */
375
		return $abs;
376
	}
377
378
	private function should_inline_style( $style ) {
379
		return ( $this->inline_scripts_and_styles || $this->inline_always ) && $this->should_inline_asset( 'jetpack_perf_inline_style', $style );
380
	}
381
382
	private function should_remove_style( $style ) {
383
		return $this->should_remove_asset( 'jetpack_perf_remove_style', $style );
384
	}
385
386
	/** shared code **/
387
388
	private function should_inline_asset( $filter, $dependency ) {
389
		// inline anything local, with a src starting with /, or starting with site_url
390
		$site_url = site_url();
391
392
		$is_local_url = ( strncmp( $dependency->src, '/', 1 ) === 0 && strncmp( $dependency->src, '//', 2 ) !== 0 )
393
			|| strpos( $dependency->src, $site_url ) === 0;
394
395
		if ( $is_local_url && ! isset( $dependency->extra['jetpack-inline'] ) ) {
396
			$dependency->extra['jetpack-inline'] = true;
397
398
			$path = untrailingslashit( ABSPATH ) . str_replace( $site_url, '', $dependency->src );
399
400
			if ( ! file_exists( $path ) ) {
401
				$path = str_replace('/', DIRECTORY_SEPARATOR, str_replace( $site_url, '', $dependency->src ));
402
403
				$prefix = explode( DIRECTORY_SEPARATOR, untrailingslashit( WP_CONTENT_DIR ) );
404
				$prefix = array_slice( $prefix, 0, array_search( $path[1], $prefix ) - 1 );
405
406
				$path = implode( DIRECTORY_SEPARATOR, $prefix ) . $path;
407
			}
408
409
			$dependency->extra['jetpack-inline-file'] = $path;
410
		}
411
412
		// only inline if we don't have a conditional
413
		$should_inline = ! isset( $dependency->extra['conditional'] ) && isset( $dependency->extra['jetpack-inline'] ) && $dependency->extra['jetpack-inline'];
414
415
		return apply_filters( $filter, $should_inline, $dependency->handle, $dependency->src ) && file_exists( $dependency->extra['jetpack-inline-file'] );
416
	}
417
418
	private function should_remove_asset( $filter, $dependency ) {
419
		return apply_filters( $filter, false, $dependency->handle, $dependency->src );
420
	}
421
422
	/**
423
	 * if inline assets are enabled, renders inline
424
	 * 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...
425
	 * This is actually currently unused
426
	 */
427
	 public function register_inline_script( $handle, $file, $plugin_file, $deps = false, $ver = false, $in_footer = false ) {
428
		$registered = wp_register_script( $handle, plugins_url( $file, $plugin_file ), $deps, $ver, $in_footer );
429
430 View Code Duplication
		if ( $registered ) {
431
			$file_full_path = dirname( $plugin_file ) . '/' . $file;
432
			wp_script_add_data( $handle, 'jetpack-inline', true );
433
			wp_script_add_data( $handle, 'jetpack-inline-file', $file_full_path );
434
		}
435
436
		return $registered;
437
	}
438
439
	/**
440
	 * if inline assets are enabled, renders inline
441
	 * 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...
442
	 * This is actually currently unused
443
	 */
444
	public function register_inline_style( $handle, $file, $plugin_file, $deps = array(), $ver = false, $media = 'all' ) {
445
		$registered = wp_register_style( $handle, plugins_url( $file, $plugin_file ), $deps, $ver, $media );
446
447 View Code Duplication
		if ( $registered ) {
448
			$file_full_path = dirname( $plugin_file ) . '/' . $file;
449
			wp_style_add_data( $handle, 'jetpack-inline', true );
450
			wp_style_add_data( $handle, 'jetpack-inline-file', $file_full_path );
451
		}
452
	}
453
}
454