Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 166
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Concatenate_JS
0.00% covered (danger)
0.00%
0 / 166
0.00% covered (danger)
0.00%
0 / 8
8372
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 has_inline_content
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 do_items
0.00% covered (danger)
0.00%
0 / 132
0.00% covered (danger)
0.00%
0 / 1
5852
 get_script_type
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 __isset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __unset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __get
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __set
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Automattic\Jetpack_Boost\Lib\Minify;
4
5use WP_Scripts;
6
7// Disable complaints about enqueuing scripts, as this class alters the way enqueuing them works.
8// phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript
9
10/**
11 * Replacement for, and subclass of WP_Scripts - used to control the way that scripts are enqueued and output.
12 */
13class Concatenate_JS extends WP_Scripts {
14    private $dependency_path_mapping;
15    private $old_scripts;
16
17    public $allow_gzip_compression;
18
19    public function __construct( $scripts ) {
20        if ( empty( $scripts ) || ! ( $scripts instanceof WP_Scripts ) ) {
21            $this->old_scripts = new WP_Scripts();
22        } else {
23            $this->old_scripts = $scripts;
24        }
25
26        // Unset all the object properties except our private copy of the scripts object.
27        // We have to unset everything so that the overload methods talk to $this->old_scripts->whatever
28        // instead of $this->whatever.
29        foreach ( array_keys( get_object_vars( $this ) ) as $key ) {
30            if ( 'old_scripts' === $key ) {
31                continue;
32            }
33            unset( $this->$key );
34        }
35
36        $this->dependency_path_mapping = new Dependency_Path_Mapping(
37            /**
38             * Filter the URL of the site the plugin will be concatenating CSS or JS on
39             *
40             * @param bool $url URL of the page with CSS or JS to concatonate.
41             *
42             * @since   1.0.0
43             */
44            apply_filters( 'page_optimize_site_url', $this->base_url )
45        );
46    }
47
48    protected function has_inline_content( $handle ) {
49        $before_output = $this->get_data( $handle, 'before' );
50        if ( ! empty( $before_output ) ) {
51            return true;
52        }
53
54        $after_output = $this->get_data( $handle, 'after' );
55        if ( ! empty( $after_output ) ) {
56            return true;
57        }
58
59        // JavaScript translations
60        $has_translations = ! empty( $this->registered[ $handle ]->textdomain );
61        if ( $has_translations ) {
62            return true;
63        }
64
65        return false;
66    }
67
68    /**
69     * Override for WP_Scripts::do_item() - this is the method that actually outputs the scripts.
70     */
71    public function do_items( $handles = false, $group = false ) {
72        $handles     = false === $handles ? $this->queue : (array) $handles;
73        $javascripts = array();
74        /**
75         * Filter the URL of the site the plugin will be concatenating CSS or JS on
76         *
77         * @param bool $url URL of the page with CSS or JS to concatonate.
78         *
79         * @since   1.0.0
80         */
81        $siteurl = apply_filters( 'page_optimize_site_url', $this->base_url );
82        $this->all_deps( $handles );
83        $level = 0;
84
85        $using_strict = false;
86        $strict_count = 0;
87        foreach ( $this->to_do as $key => $handle ) {
88            $script_is_strict = false;
89            if ( in_array( $handle, $this->done, true ) || ! isset( $this->registered[ $handle ] ) ) {
90                continue;
91            }
92
93            if ( 0 === $group && $this->groups[ $handle ] > 0 ) {
94                $this->in_footer[] = $handle;
95                unset( $this->to_do[ $key ] );
96                continue;
97            }
98
99            if ( ! $this->registered[ $handle ]->src ) { // Defines a group.
100                if ( $this->has_inline_content( $handle ) ) {
101                    ++$level;
102                    $javascripts[ $level ]['type']   = 'do_item';
103                    $javascripts[ $level ]['handle'] = $handle;
104                    ++$level;
105                    unset( $this->to_do[ $key ] );
106                } else {
107                    // if there are localized items, echo them
108                    $this->print_extra_script( $handle );
109                    $this->done[] = $handle;
110                }
111                continue;
112            }
113
114            if ( false === $group && in_array( $handle, $this->in_footer, true ) ) {
115                $this->in_footer = array_diff( $this->in_footer, (array) $handle );
116            }
117
118            $obj           = $this->registered[ $handle ];
119            $js_url        = jetpack_boost_enqueued_to_absolute_url( $obj->src );
120            $js_url_parsed = wp_parse_url( $js_url );
121
122            // Don't concat by default
123            $do_concat = false;
124
125            // Only try to concat static js files
126            if ( str_contains( $js_url_parsed['path'], '.js' ) ) {
127                // Previously, the value of this variable was determined by a function.
128                // Now, since concatenation is always enabled when the module is active,
129                // the value will always be true for static files.
130                $do_concat = true;
131            } elseif ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
132                    printf( "\n<!-- No Concat JS %s => Maybe Not Static File %s -->\n", esc_html( $handle ), esc_html( $obj->src ) );
133            }
134
135            // Don't try to concat externally hosted scripts
136            $is_internal_uri = $this->dependency_path_mapping->is_internal_uri( $js_url );
137            if ( $do_concat && ! $is_internal_uri ) {
138                if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
139                    printf( "\n<!-- No Concat JS %s => External URL: %s -->\n", esc_html( $handle ), esc_url( $js_url ) );
140                }
141                $do_concat = false;
142            }
143
144            $js_realpath = false;
145            if ( $do_concat ) {
146                // Resolve paths and concat scripts that exist in the filesystem
147                $js_realpath = $this->dependency_path_mapping->dependency_src_to_fs_path( $js_url );
148                if ( false === $js_realpath ) {
149                    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
150                        printf( "\n<!-- No Concat JS %s => Invalid Path %s -->\n", esc_html( $handle ), esc_html( $js_realpath ) );
151                    }
152                    $do_concat = false;
153                }
154            }
155
156            if ( $do_concat && $this->has_inline_content( $handle ) ) {
157                if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
158                    printf( "\n<!-- No Concat JS %s => Has Inline Content -->\n", esc_html( $handle ) );
159                }
160                $do_concat = false;
161            }
162
163            // Skip core scripts that use Strict Mode
164            if ( $do_concat && ( 'react' === $handle || 'react-dom' === $handle ) ) {
165                if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
166                    printf( "\n<!-- No Concat JS %s => Has Strict Mode (Core) -->\n", esc_html( $handle ) );
167                }
168                $do_concat        = false;
169                $script_is_strict = true;
170            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
171            } elseif ( $do_concat && preg_match_all( '/^[\',"]use strict[\',"];/Uims', file_get_contents( $js_realpath ), $matches ) ) {
172                // Skip third-party scripts that use Strict Mode
173                if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
174                    printf( "\n<!-- No Concat JS %s => Has Strict Mode (Third-Party) -->\n", esc_html( $handle ) );
175                }
176                $do_concat        = false;
177                $script_is_strict = true;
178            } else {
179                $script_is_strict = false;
180            }
181
182            // Skip concating scripts from exclusion list
183            $exclude_list = jetpack_boost_page_optimize_js_exclude_list();
184            foreach ( $exclude_list as $exclude ) {
185                if ( $do_concat && $handle === $exclude ) {
186                    $do_concat = false;
187                    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
188                        printf( "\n<!-- No Concat JS %s => Excluded option -->\n", esc_html( $handle ) );
189                    }
190                }
191            }
192
193            /** This filter is documented in wp-includes/class-wp-scripts.php */
194            $js_url = esc_url_raw( apply_filters( 'script_loader_src', $js_url, $handle ) );
195            if ( ! $js_url ) {
196                $do_concat = false;
197                if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
198                    printf( "\n<!-- No Concat JS %s => No URL -->\n", esc_html( $handle ) );
199                }
200            } elseif ( 'module' === $this->get_script_type( $handle, $js_url ) ) {
201                $do_concat = false;
202                if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
203                    printf( "\n<!-- No Concat JS %s => Module Script -->\n", esc_html( $handle ) );
204                }
205            }
206
207            /**
208             * Filter that allows plugins to disable concatenation of certain scripts.
209             *
210             * @param bool $do_concat if true, then perform concatenation
211             * @param string $handle handle to JS file
212             *
213             * @since   1.0.0
214             */
215            if ( $do_concat && ! apply_filters( 'js_do_concat', $do_concat, $handle ) ) {
216                if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
217                    printf( "\n<!-- No Concat JS %s => Filtered `false` -->\n", esc_html( $handle ) );
218                }
219            }
220            /**
221             * Filter that allows plugins to disable concatenation of certain scripts.
222             *
223             * @param bool $do_concat if true, then perform concatenation
224             * @param string $handle handle to JS file
225             *
226             * @since   1.0.0
227             */
228            $do_concat = apply_filters( 'js_do_concat', $do_concat, $handle );
229
230            if ( true === $do_concat ) {
231
232                // If the number of files in the group is greater than the maximum, start a new group.
233                if ( isset( $javascripts[ $level ] ) && count( $javascripts[ $level ]['handles'] ) >= jetpack_boost_minify_concat_max_files() ) {
234                    ++$level;
235                }
236
237                if ( ! isset( $javascripts[ $level ] ) ) {
238                    $javascripts[ $level ]['type'] = 'concat';
239                }
240
241                $javascripts[ $level ]['paths'][]   = $js_url_parsed['path'];
242                $javascripts[ $level ]['handles'][] = $handle;
243
244            } else {
245                ++$level;
246                $javascripts[ $level ]['type']   = 'do_item';
247                $javascripts[ $level ]['handle'] = $handle;
248                ++$level;
249            }
250            unset( $this->to_do[ $key ] );
251
252            if ( $using_strict !== $script_is_strict ) {
253                if ( $script_is_strict ) {
254                    $using_strict = true;
255                    $strict_count = 0;
256                } else {
257                    $using_strict = false;
258                }
259            }
260
261            if ( $script_is_strict ) {
262                ++$strict_count;
263            }
264        }
265
266        if ( empty( $javascripts ) ) {
267            return $this->done;
268        }
269
270        foreach ( $javascripts as $js_array ) {
271            if ( 'do_item' === $js_array['type'] ) {
272                if ( $this->do_item( $js_array['handle'], $group ) ) {
273                    $this->done[] = $js_array['handle'];
274                }
275            } elseif ( 'concat' === $js_array['type'] ) {
276                array_map( array( $this, 'print_extra_script' ), $js_array['handles'] );
277
278                if ( isset( $js_array['paths'] ) && count( $js_array['paths'] ) > 1 ) {
279                    $file_name = jetpack_boost_page_optimize_generate_concat_path( $js_array['paths'], $this->dependency_path_mapping );
280
281                    if ( get_site_option( 'jetpack_boost_static_minification' ) ) {
282                        $href = jetpack_boost_get_minify_url( $file_name . '.min.js' );
283                    } else {
284                        $href = $siteurl . jetpack_boost_get_static_prefix() . '??' . $file_name;
285                    }
286                } elseif ( isset( $js_array['paths'] ) && is_array( $js_array['paths'] ) ) {
287                    $href = jetpack_boost_page_optimize_cache_bust_mtime( $js_array['paths'][0], $siteurl );
288                }
289
290                $this->done = array_merge( $this->done, $js_array['handles'] );
291
292                // Print before/after scripts from wp_inline_scripts() and concatenated script tag
293                if ( isset( $js_array['extras']['before'] ) ) {
294                    foreach ( $js_array['extras']['before'] as $inline_before ) {
295                        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
296                        echo $inline_before;
297                    }
298                }
299
300                if ( isset( $href ) ) {
301                    $handles = implode( ',', $js_array['handles'] );
302
303                    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
304                        $tag = "<script data-handles='" . esc_attr( $handles ) . "' type='text/javascript' src='" . esc_url( $href ) . "'></script>\n";
305                    } else {
306                        $tag = "<script type='text/javascript' src='" . esc_url( $href ) . "'></script>\n";
307                    }
308
309                    if ( is_array( $js_array['handles'] ) && count( $js_array['handles'] ) === 1 ) {
310                        /**
311                         * Filters the HTML script tag of an enqueued script
312                         * A copy of the core filter of the same name. https://developer.wordpress.org/reference/hooks/script_loader_tag/
313                         * Because we have a single script, let's apply the `script_loader_tag` filter as core does in `do_item()`.
314                         * That way, we interfere less with plugin and theme script filtering. For example, without this filter,
315                         * there is a case where we block the TwentyTwenty theme from adding async/defer attributes.
316                         * https://github.com/Automattic/page-optimize/pull/44
317                         *
318                         * @param string $tag Script tag for the enqueued script.
319                         * @param string $handle The script's registered handle.
320                         * @param string $href URL of the script.
321                         *
322                         * @since   1.0.0
323                         */
324                        $tag = apply_filters( 'script_loader_tag', $tag, $js_array['handles'][0], $href );
325                    }
326
327                    // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
328                    echo $tag;
329                }
330
331                if ( isset( $js_array['extras']['after'] ) ) {
332                    foreach ( $js_array['extras']['after'] as $inline_after ) {
333                        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
334                        echo $inline_after;
335                    }
336                }
337            }
338        }
339
340        do_action( 'js_concat_did_items', $javascripts );
341
342        return $this->done;
343    }
344
345    /**
346     * Returns the type of the script.
347     * module, text/javascript, etc. False if the script tag is invalid,
348     * or the type is not set.
349     *
350     * @since 4.1.2
351     *
352     * @param string $handle The script's registered handle.
353     * @param string $src The script's source URL.
354     *
355     * @return string|false The type of the script. False if the script tag is invalid,
356     * or the type is not set.
357     */
358    private function get_script_type( $handle, $src ) {
359        $script_tag_attr = array(
360            'src' => $src,
361            'id'  => "$handle-js",
362        );
363
364        // Get the script tag and allow plugins to filter it.
365        $script_tag = wp_get_script_tag( $script_tag_attr );
366
367        // This is a workaround to get the type of the script without outputting it.
368        $script_tag = apply_filters( 'script_loader_tag', $script_tag, $handle, $src );
369        $processor  = new \WP_HTML_Tag_Processor( $script_tag );
370
371        // If for some reason the script tag isn't valid, bail.
372        if ( ! $processor->next_tag() ) {
373            return false;
374        }
375
376        return $processor->get_attribute( 'type' );
377    }
378
379    public function __isset( $key ) {
380        return isset( $this->old_scripts->$key );
381    }
382
383    public function __unset( $key ) {
384        unset( $this->old_scripts->$key );
385    }
386
387    public function &__get( $key ) {
388        return $this->old_scripts->$key;
389    }
390
391    public function __set( $key, $value ) {
392        $this->old_scripts->$key = $value;
393    }
394}