PluginProbe
Page Optimize / trunk
Page Optimize vtrunk
trunk 0.4.2 0.4.3 0.4.4 0.4.6 0.4.7 0.5.0 0.5.1 0.5.2 0.5.3 0.5.4 0.5.5 0.5.6 0.5.7 0.5.8 0.6.0 0.6.1 0.6.2 0.6.3
page-optimize / concat-js.php

concat-js.php in Page Optimize trunk, at concat-js.php

368 lines 11.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 require_once __DIR__ . '/dependency-path-mapping.php';
4 require_once __DIR__ . '/utils.php';
5
6 if ( ! defined( 'ALLOW_GZIP_COMPRESSION' ) ) {
7 define( 'ALLOW_GZIP_COMPRESSION', true );
8 }
9
10 class Page_Optimize_JS_Concat extends WP_Scripts {
11 private $dependency_path_mapping;
12 private $old_scripts;
13
14 public $allow_gzip_compression;
15
16 function __construct( $scripts ) {
17 if ( empty( $scripts ) || ! ( $scripts instanceof WP_Scripts ) ) {
18 $this->old_scripts = new WP_Scripts();
19 } else {
20 $this->old_scripts = $scripts;
21 }
22
23 // Unset all the object properties except our private copy of the scripts object.
24 // We have to unset everything so that the overload methods talk to $this->old_scripts->whatever
25 // instead of $this->whatever.
26 foreach ( array_keys( get_object_vars( $this ) ) as $key ) {
27 if ( 'old_scripts' === $key ) {
28 continue;
29 }
30 unset( $this->$key );
31 }
32
33 $this->dependency_path_mapping = new Page_Optimize_Dependency_Path_Mapping(
34 apply_filters( 'page_optimize_site_url', $this->base_url )
35 );
36 }
37
38 protected function has_inline_content( $handle ) {
39 $before_output = $this->get_data( $handle, 'before' );
40 if ( ! empty( $before_output ) ) {
41 return true;
42 }
43
44 $after_output = $this->get_data( $handle, 'after' );
45 if ( ! empty( $after_output ) ) {
46 return true;
47 }
48
49 // JavaScript translations
50 $has_translations = ! empty( $this->registered[ $handle ]->textdomain );
51 if ( $has_translations ) {
52 return true;
53 }
54
55 return false;
56 }
57
58 /**
59 * Check whether script_loader_tag filters mutate this handle's script tag.
60 *
61 * If the tag is mutated, we keep the handle standalone so core can render it.
62 */
63 protected function script_loader_tag_changes_output( $handle, $js_url ) {
64 if ( ! has_filter( 'script_loader_tag' ) ) {
65 return false;
66 }
67
68 if ( function_exists( 'wp_get_script_tag' ) ) {
69 $probe_tag = wp_get_script_tag(
70 array(
71 'id' => "{$handle}-js",
72 'src' => $js_url,
73 )
74 );
75 } else {
76 $probe_tag = "<script type='text/javascript' src='" . esc_url( $js_url ) . "'></script>\n";
77 }
78
79 $filtered_tag = apply_filters( 'script_loader_tag', $probe_tag, $handle, $js_url );
80
81 return $filtered_tag !== $probe_tag;
82 }
83
84 function do_items( $handles = false, $group = false ) {
85 $handles = false === $handles ? $this->queue : (array) $handles;
86 $javascripts = array();
87 $siteurl = apply_filters( 'page_optimize_site_url', $this->base_url );
88 $this->all_deps( $handles );
89 $level = 0;
90
91 $using_strict = false;
92 foreach ( $this->to_do as $key => $handle ) {
93 $script_is_strict = false;
94 if ( in_array( $handle, $this->done ) || ! isset( $this->registered[ $handle ] ) ) {
95 continue;
96 }
97
98 if ( 0 === $group && $this->groups[ $handle ] > 0 ) {
99 $this->in_footer[] = $handle;
100 unset( $this->to_do[ $key ] );
101 continue;
102 }
103
104 if ( ! $this->registered[ $handle ]->src ) { // Defines a group.
105 if ( $this->has_inline_content( $handle ) ) {
106 $level ++;
107 $javascripts[ $level ]['type'] = 'do_item';
108 $javascripts[ $level ]['handle'] = $handle;
109 $level ++;
110 unset( $this->to_do[ $key ] );
111 } else {
112 // if there are localized items, echo them
113 $this->print_extra_script( $handle );
114 $this->done[] = $handle;
115 }
116 continue;
117 }
118
119 if ( false === $group && in_array( $handle, $this->in_footer, true ) ) {
120 $this->in_footer = array_diff( $this->in_footer, (array) $handle );
121 }
122
123 $obj = $this->registered[ $handle ];
124 $js_url = $obj->src;
125 $js_url_parsed = parse_url( $js_url );
126 $script_type = strtolower( trim( (string) $this->get_data( $handle, 'type' ) ) );
127 $script_strategy = strtolower( trim( (string) $this->get_data( $handle, 'strategy' ) ) );
128
129 // Don't concat by default
130 $do_concat = false;
131
132 // Only try to concat static js files
133 if ( false !== strpos( $js_url_parsed['path'], '.js' ) ) {
134 $do_concat = page_optimize_should_concat_js();
135 } else {
136 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
137 echo sprintf( "\n<!-- No Concat JS %s => Maybe Not Static File %s -->\n", esc_html( $handle ), esc_html( $obj->src ) );
138 }
139 }
140
141 // Don't try to concat externally hosted scripts
142 $is_internal_uri = $this->dependency_path_mapping->is_internal_uri( $js_url );
143 if ( $do_concat && ! $is_internal_uri ) {
144 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
145 echo sprintf( "\n<!-- No Concat JS %s => External URL: %s -->\n", esc_html( $handle ), esc_url( $js_url ) );
146 }
147 $do_concat = false;
148 }
149
150 if ( $do_concat ) {
151 // Resolve paths and concat scripts that exist in the filesystem
152 $js_realpath = $this->dependency_path_mapping->dependency_src_to_fs_path( $js_url );
153 if ( false === $js_realpath ) {
154 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
155 echo sprintf( "\n<!-- No Concat JS %s => Invalid Path %s -->\n", esc_html( $handle ), esc_html( $js_realpath ) );
156 }
157 $do_concat = false;
158 }
159 }
160
161 if ( $do_concat && $this->has_inline_content( $handle ) ) {
162 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
163 echo sprintf( "\n<!-- No Concat JS %s => Has Inline Content -->\n", esc_html( $handle ) );
164 }
165 $do_concat = false;
166 }
167
168 // Module scripts are not safe to concatenate with classic scripts.
169 if ( $do_concat && 'module' === $script_type ) {
170 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
171 echo sprintf( "\n<!-- No Concat JS %s => Module Script -->\n", esc_html( $handle ) );
172 }
173 $do_concat = false;
174 }
175
176 // Keep requested delayed scripts standalone to avoid strategy eligibility pitfalls.
177 if ( $do_concat && in_array( $script_strategy, array( 'defer', 'async' ), true ) ) {
178 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
179 echo sprintf( "\n<!-- No Concat JS %s => Requested Strategy %s -->\n", esc_html( $handle ), esc_html( $script_strategy ) );
180 }
181 $do_concat = false;
182 }
183
184 // If a script tag is mutated by script_loader_tag, preserve it via core do_item().
185 if ( $do_concat && $this->script_loader_tag_changes_output( $handle, $js_url ) ) {
186 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
187 echo sprintf( "\n<!-- No Concat JS %s => script_loader_tag modified -->\n", esc_html( $handle ) );
188 }
189 $do_concat = false;
190 }
191
192 // Skip core scripts that use Strict Mode
193 if ( $do_concat && ( 'react' === $handle || 'react-dom' === $handle ) ) {
194 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
195 echo sprintf( "\n<!-- No Concat JS %s => Has Strict Mode (Core) -->\n", esc_html( $handle ) );
196 }
197 $do_concat = false;
198 $script_is_strict = true;
199 } else if ( $do_concat && preg_match_all( '/^[\',"]use strict[\',"];/Uims', file_get_contents( $js_realpath ), $matches ) ) {
200 // Skip third-party scripts that use Strict Mode
201 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
202 echo sprintf( "\n<!-- No Concat JS %s => Has Strict Mode (Third-Party) -->\n", esc_html( $handle ) );
203 }
204 $do_concat = false;
205 $script_is_strict = true;
206 } else {
207 $script_is_strict = false;
208 }
209
210 // Skip concating scripts from exclusion list
211 $exclude_list = page_optimize_js_exclude_list();
212 foreach ( $exclude_list as $exclude ) {
213 if ( $do_concat && $handle === $exclude ) {
214 $do_concat = false;
215 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
216 echo sprintf( "\n<!-- No Concat JS %s => Excluded option -->\n", esc_html( $handle ) );
217 }
218 }
219 }
220
221 // Allow plugins to disable concatenation of certain scripts.
222 if ( $do_concat && ! apply_filters( 'js_do_concat', $do_concat, $handle ) ) {
223 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
224 echo sprintf( "\n<!-- No Concat JS %s => Filtered `false` -->\n", esc_html( $handle ) );
225 }
226 }
227 $do_concat = apply_filters( 'js_do_concat', $do_concat, $handle );
228
229 if ( true === $do_concat ) {
230 if ( ! isset( $javascripts[ $level ] ) ) {
231 $javascripts[ $level ]['type'] = 'concat';
232 }
233
234 $javascripts[ $level ]['paths'][] = $js_url_parsed['path'];
235 $javascripts[ $level ]['handles'][] = $handle;
236
237 } else {
238 $level ++;
239 $javascripts[ $level ]['type'] = 'do_item';
240 $javascripts[ $level ]['handle'] = $handle;
241 $level ++;
242 }
243 unset( $this->to_do[ $key ] );
244
245 if ( $using_strict !== $script_is_strict ) {
246 if ( $script_is_strict ) {
247 $using_strict = true;
248 $strict_count = 0;
249 } else {
250 $using_strict = false;
251 }
252 }
253
254 if ( $script_is_strict ) {
255 $strict_count ++;
256 }
257 }
258
259 if ( empty( $javascripts ) ) {
260 return $this->done;
261 }
262
263 foreach ( $javascripts as $js_array ) {
264 if ( 'do_item' == $js_array['type'] ) {
265 if ( $this->do_item( $js_array['handle'], $group ) ) {
266 $this->done[] = $js_array['handle'];
267 }
268 } else if ( 'concat' == $js_array['type'] ) {
269 array_map( array( $this, 'print_extra_script' ), $js_array['handles'] );
270
271 if ( isset( $js_array['paths'] ) && count( $js_array['paths'] ) > 1 ) {
272 $fs_paths = array();
273 foreach ( $js_array['paths'] as $js_url ) {
274 $fs_paths[] = $this->dependency_path_mapping->uri_path_to_fs_path( $js_url );
275 }
276
277 $mtime = max( array_map( 'filemtime', $fs_paths ) );
278 if ( page_optimize_use_concat_base_dir() ) {
279 $path_str = implode( ',', array_map( 'page_optimize_remove_concat_base_prefix', $fs_paths ) );
280 } else {
281 $path_str = implode( ',', $js_array['paths'] );
282 }
283 $path_str = "$path_str?m=$mtime";
284
285 if ( $this->allow_gzip_compression ) {
286 $path_64 = base64_encode( gzcompress( $path_str ) );
287 if ( strlen( $path_str ) > ( strlen( $path_64 ) + 1 ) ) {
288 $path_str = '-' . $path_64;
289 }
290 }
291
292 $href = $siteurl . "/_static/??" . $path_str;
293 } elseif ( isset( $js_array['paths'] ) && is_array( $js_array['paths'] ) ) {
294 $href = Page_Optimize_Utils::cache_bust_mtime( $js_array['paths'][0], $siteurl );
295 }
296
297 $this->done = array_merge( $this->done, $js_array['handles'] );
298
299 // Print before/after scripts from wp_inline_scripts() and concatenated script tag
300 if ( isset( $js_array['extras']['before'] ) ) {
301 foreach ( $js_array['extras']['before'] as $inline_before ) {
302 echo $inline_before;
303 }
304 }
305
306 if ( isset( $href ) ) {
307 $handles = implode( ',', $js_array['handles'] );
308
309 $load_mode = page_optimize_load_mode_js();
310
311 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
312 $tag = "<script data-handles='" . esc_attr( $handles ) . "' $load_mode type='text/javascript' src='$href'></script>\n";
313 } else {
314 $tag = "<script type='text/javascript' $load_mode src='$href'></script>\n";
315 }
316
317 if ( is_array( $js_array['handles'] ) && count( $js_array['handles'] ) === 1 ) {
318 // Because we have a single script, let's apply the `script_loader_tag` filter as core does in `do_item()`.
319 // That way, we interfere less with plugin and theme script filtering. For example, without this filter,
320 // there is a case where we block the TwentyTwenty theme from adding async/defer attributes.
321 // https://github.com/Automattic/page-optimize/pull/44
322 $tag = apply_filters( 'script_loader_tag', $tag, $js_array['handles'][0], $href );
323 }
324
325 echo $tag;
326 }
327
328 if ( isset( $js_array['extras']['after'] ) ) {
329 foreach ( $js_array['extras']['after'] as $inline_after ) {
330 echo $inline_after;
331 }
332 }
333 }
334 }
335
336 do_action( 'js_concat_did_items', $javascripts );
337
338 return $this->done;
339 }
340
341 function __isset( $key ) {
342 return isset( $this->old_scripts->$key );
343 }
344
345 function __unset( $key ) {
346 unset( $this->old_scripts->$key );
347 }
348
349 function &__get( $key ) {
350 return $this->old_scripts->$key;
351 }
352
353 function __set( $key, $value ) {
354 $this->old_scripts->$key = $value;
355 }
356 }
357
358 function page_optimize_js_concat_init() {
359 global $wp_scripts;
360
361 $wp_scripts = new Page_Optimize_JS_Concat( $wp_scripts );
362 $wp_scripts->allow_gzip_compression = ALLOW_GZIP_COMPRESSION;
363 }
364
365 if ( ! is_admin() && ( page_optimize_should_concat_js() || page_optimize_load_mode_js() ) ) {
366 add_action( 'init', 'page_optimize_js_concat_init' );
367 }
368