Completed
Push — add/pwa ( 271bee...a1e925 )
by
unknown
08:34
created

should_remove_style()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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