Completed
Push — add/pwa ( 80f574...bade54 )
by
unknown
16:03 queued 07:49
created

Jetpack_Perf_Optimize_Assets   D

Complexity

Total Complexity 83

Size/Duplication

Total Lines 405
Duplicated Lines 22.72 %

Coupling/Cohesion

Components 2
Dependencies 0

Importance

Changes 0
Metric Value
dl 92
loc 405
rs 4.8717
c 0
b 0
f 0
wmc 83
lcom 2
cbo 0

27 Methods

Rating   Name   Duplication   Size   Complexity  
A instance() 0 7 2
A disable_for_request() 0 6 1
C __construct() 0 48 8
A disable_emojis() 0 15 1
A optimize_jetpack() 0 3 1
A disable_emojis_tinymce() 0 7 2
A disable_emojis_remove_dns_prefetch() 0 10 2
A set_first_load_cookie() 3 5 2
A send_scripts_to_footer() 11 17 3
A move_styles_to_bottom_of_header() 0 4 1
A set_asset_groups() 0 8 2
A remove_external_font_scripts() 0 4 1
A remove_external_font_styles() 0 4 1
A filter_inline_scripts() 16 16 4
D print_inline_scripts() 0 33 9
B should_async_script() 19 19 6
B should_defer_script() 18 18 6
A should_remove_script() 0 3 1
A should_inline_script() 0 3 3
A filter_inline_styles() 15 15 4
B print_inline_styles() 0 20 5
A should_inline_style() 0 3 3
A should_remove_style() 0 3 1
D should_inline_asset() 0 29 9
A should_remove_asset() 0 3 1
A register_inline_script() 5 11 2
A register_inline_style() 5 9 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Jetpack_Perf_Optimize_Assets often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Jetpack_Perf_Optimize_Assets, and based on these observations, apply Extract Interface, too.

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