autoptimizeScripts::movetolast()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 4
nop 1
dl 0
loc 16
rs 9.7333
c 0
b 0
f 0
1
<?php
2
/**
3
 * Class for JS optimization.
4
 */
5
6
if ( ! defined( 'ABSPATH' ) ) {
7
    exit;
8
}
9
10
class autoptimizeScripts extends autoptimizeBase
11
{
12
13
    /**
14
     * Stores founds scripts.
15
     *
16
     * @var array
17
     */
18
    private $scripts = array();
19
20
    /**
21
     * Stores to be moved JS.
22
     *
23
     * @var array
24
     */
25
    private $move = array(
26
        'first' => array(),
27
        'last'  => array(),
28
    );
29
30
    /**
31
     * List of not to be moved JS.
32
     *
33
     * @var array
34
     */
35
    private $dontmove = array(
36
        'document.write',
37
        'html5.js',
38
        'show_ads.js',
39
        'google_ad',
40
        'histats.com/js',
41
        'statcounter.com/counter/counter.js',
42
        'ws.amazon.com/widgets',
43
        'media.fastclick.net',
44
        '/ads/',
45
        'comment-form-quicktags/quicktags.php',
46
        'edToolbar',
47
        'intensedebate.com',
48
        'scripts.chitika.net/',
49
        '_gaq.push',
50
        'jotform.com/',
51
        'admin-bar.min.js',
52
        'GoogleAnalyticsObject',
53
        'plupload.full.min.js',
54
        'syntaxhighlighter',
55
        'adsbygoogle',
56
        'gist.github.com',
57
        '_stq',
58
        'nonce',
59
        'post_id',
60
        'data-noptimize',
61
        'logHuman',
62
        'amp-mobile-version-switcher',
63
        'data-rocketlazyloadscript',
64
        'rocket-browser-checker-js-after',
65
    );
66
67
    /**
68
     * List of to be moved JS.
69
     *
70
     * @var array
71
     */
72
    private $domove = array(
73
        'gaJsHost',
74
        'load_cmc',
75
        'jd.gallery.transitions.js',
76
        'swfobject.embedSWF(',
77
        'tiny_mce.js',
78
        'tinyMCEPreInit.go',
79
    );
80
81
    /**
82
     * List of JS that can be moved last (not used any more).
83
     *
84
     * @var array
85
     */
86
    private $domovelast = array(
87
        'addthis.com',
88
        '/afsonline/show_afs_search.js',
89
        'disqus.js',
90
        'networkedblogs.com/getnetworkwidget',
91
        'infolinks.com/js/',
92
        'jd.gallery.js.php',
93
        'jd.gallery.transitions.js',
94
        'swfobject.embedSWF(',
95
        'linkwithin.com/widget.js',
96
        'tiny_mce.js',
97
        'tinyMCEPreInit.go',
98
    );
99
100
    /**
101
     * Setting CDN base URL.
102
     *
103
     * @var string
104
     */
105
    public $cdn_url = '';
106
107
    /**
108
     * Setting; aggregate or not.
109
     *
110
     * @var bool
111
     */
112
    private $aggregate = true;
113
114
    /**
115
     * Setting; if not aggregated, should we defer?
116
     *
117
     * @var bool
118
     */
119
    private $defer_not_aggregate = false;
120
121
    /**
122
     * Setting; try/catch wrapping or not.
123
     *
124
     * @var bool
125
     */
126
    private $trycatch = false;
127
128
    /**
129
     * State; is JS already minified.
130
     *
131
     * @var bool
132
     */
133
    private $alreadyminified = false;
134
135
    /**
136
     * Setting; force JS in head or not.
137
     *
138
     * @var bool
139
     */
140
    private $forcehead = true;
141
142
    /**
143
     * Setting; aggregate inline JS or not.
144
     *
145
     * @var bool
146
     */
147
    private $include_inline = false;
148
149
    /**
150
     * State; holds JS code.
151
     *
152
     * @var string
153
     */
154
    private $jscode = '';
155
156
    /**
157
     * State; holds URL of JS-file.
158
     *
159
     * @var string
160
     */
161
    private $url = '';
162
163
    /**
164
     * State; stores rest of HTML if (old) option "only in head" is on.
165
     *
166
     * @var string
167
     */
168
    private $restofcontent = '';
169
170
    /**
171
     * State; holds md5-hash.
172
     *
173
     * @var string
174
     */
175
    private $md5hash = '';
176
177
    /**
178
     * Setting (filter); allowlist of to be aggregated JS.
179
     *
180
     * @var string
181
     */
182
    private $allowlist = '';
183
184
    /**
185
     * Setting (filter); holds JS that should be removed.
186
     *
187
     * @var array
188
     */
189
    private $jsremovables = array();
190
191
    /**
192
     * Setting (filter); can we inject already minified files after the
193
     * unminified aggregate JS has been minified.
194
     *
195
     * @var bool
196
     */
197
    private $inject_min_late = true;
198
199
    /**
200
     * Setting; should excluded JS be minified (if not already).
201
     *
202
     * @var bool
203
     */
204
    private $minify_excluded = true;
205
206
    /**
207
     * Reads the page and collects script tags.
208
     *
209
     * @param array $options all options.
210
     */
211
    public function read( $options )
212
    {
213
        $noptimize_js = apply_filters( 'autoptimize_filter_js_noptimize', false, $this->content );
214
        if ( $noptimize_js ) {
215
            return false;
216
        }
217
218
        // only optimize known good JS?
219
        $allowlist_js = apply_filters( 'autoptimize_filter_js_allowlist', '', $this->content );
220
        $allowlist_js = apply_filters( 'autoptimize_filter_js_whitelist', $allowlist_js, $this->content ); // fixme: to be removed in next version.
221 View Code Duplication
        if ( ! empty( $allowlist_js ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
222
            $this->allowlist = array_filter( array_map( 'trim', explode( ',', $allowlist_js ) ) );
0 ignored issues
show
Documentation Bug introduced by
It seems like array_filter(array_map('...e(',', $allowlist_js))) of type array is incompatible with the declared type string of property $allowlist.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
223
        }
224
225
        // is there JS we should simply remove?
226
        $removable_js = apply_filters( 'autoptimize_filter_js_removables', '', $this->content );
227 View Code Duplication
        if ( ! empty( $removable_js ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
228
            $this->jsremovables = array_filter( array_map( 'trim', explode( ',', $removable_js ) ) );
229
        }
230
231
        // only header?
232 View Code Duplication
        if ( apply_filters( 'autoptimize_filter_js_justhead', $options['justhead'] ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
233
            $content             = explode( '</head>', $this->content, 2 );
234
            $this->content       = $content[0] . '</head>';
235
            $this->restofcontent = $content[1];
236
        }
237
238
        // Determine whether we're doing JS-files aggregation or not.
239
        if ( ! $options['aggregate'] ) {
240
            $this->aggregate = false;
241
        }
242
        // Returning true for "dontaggregate" turns off aggregation.
243
        if ( $this->aggregate && apply_filters( 'autoptimize_filter_js_dontaggregate', false ) ) {
244
            $this->aggregate = false;
245
        }
246
        
247
        // Defer when not aggregating.
248
        if ( false === $this->aggregate && apply_filters( 'autoptimize_js_filter_defer_not_aggregate', $options['defer_not_aggregate'] ) ) {
249
            $this->defer_not_aggregate = true;
250
        }
251
252
        // include inline?
253
        if ( apply_filters( 'autoptimize_js_include_inline', $options['include_inline'] ) ) {
254
            $this->include_inline = true;
255
        }
256
257
        // filter to "late inject minified JS", default to true for now (it is faster).
258
        $this->inject_min_late = apply_filters( 'autoptimize_filter_js_inject_min_late', true );
259
260
        // filters to override hardcoded do(nt)move(last) array contents (array in, array out!).
261
        $this->dontmove   = apply_filters( 'autoptimize_filter_js_dontmove', $this->dontmove );
262
        $this->domovelast = apply_filters( 'autoptimize_filter_js_movelast', $this->domovelast );
263
        $this->domove     = apply_filters( 'autoptimize_filter_js_domove', $this->domove );
264
265
        // Determine whether excluded files should be minified if not yet so.
266
        if ( ! $options['minify_excluded'] && $options['aggregate'] ) {
267
            $this->minify_excluded = false;
268
        }
269
        $this->minify_excluded = apply_filters( 'autoptimize_filter_js_minify_excluded', $this->minify_excluded, '' );
270
271
        // get extra exclusions settings or filter.
272
        $exclude_js = $options['js_exclude'];
273
        $exclude_js = apply_filters( 'autoptimize_filter_js_exclude', $exclude_js, $this->content );
274
275
        if ( '' !== $exclude_js ) {
276
            if ( is_array( $exclude_js ) ) {
277
                $remove_keys = array_keys( $exclude_js, 'remove' );
278
                if ( false !== $remove_keys ) {
279
                    foreach ( $remove_keys as $remove_key ) {
280
                        unset( $exclude_js[ $remove_key ] );
281
                        $this->jsremovables[] = $remove_key;
282
                    }
283
                }
284
                $excl_js_arr = array_keys( $exclude_js );
285
            } else {
286
                $excl_js_arr = array_filter( array_map( 'trim', explode( ',', $exclude_js ) ) );
287
            }
288
            $this->dontmove = array_merge( $excl_js_arr, $this->dontmove );
289
        }
290
291
        // Should we add try-catch?
292
        if ( $options['trycatch'] ) {
293
            $this->trycatch = true;
294
        }
295
296
        // force js in head?
297
        if ( $options['forcehead'] ) {
298
            $this->forcehead = true;
299
        } else {
300
            $this->forcehead = false;
301
        }
302
303
        $this->forcehead = apply_filters( 'autoptimize_filter_js_forcehead', $this->forcehead );
304
305
        // get cdn url.
306
        $this->cdn_url = $options['cdn_url'];
307
308
        // noptimize me.
309
        $this->content = $this->hide_noptimize( $this->content );
310
311
        // Save IE hacks.
312
        $this->content = $this->hide_iehacks( $this->content );
313
314
        // comments.
315
        $this->content = $this->hide_comments( $this->content );
316
317
        // Get script files.
318
        if ( preg_match_all( '#<script.*</script>#Usmi', $this->content, $matches ) ) {
319
            foreach ( $matches[0] as $tag ) {
320
                // only consider script aggregation for types allowlisted in should_aggregate-function.
321
                $should_aggregate = $this->should_aggregate( $tag );
322
                if ( ! $should_aggregate ) {
323
                    $tag = '';
324
                    continue;
325
                }
326
327
                if ( preg_match( '#<script[^>]*src=("|\')([^>]*)("|\')#Usmi', $tag, $source ) ) {
328
                    // non-inline script.
329 View Code Duplication
                    if ( $this->isremovable( $tag, $this->jsremovables ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
330
                        $this->content = str_replace( $tag, '', $this->content );
331
                        continue;
332
                    }
333
334
                    $orig_tag = null;
0 ignored issues
show
Unused Code introduced by
$orig_tag is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
335
                    $url      = current( explode( '?', $source[2], 2 ) );
336
                    $path     = $this->getpath( $url );
337
                    if ( false !== $path && preg_match( '#\.js$#', $path ) && $this->ismergeable( $tag ) ) {
338
                        // ok to optimize, add to array.
339
                        $this->scripts[] = $path;
340
                    } else {
341
                        $orig_tag = $tag;
342
                        $new_tag  = $tag;
343
344
                        // non-mergeable script (excluded or dynamic or external).
345
                        if ( is_array( $exclude_js ) ) {
346
                            // should we add flags?
347
                            foreach ( $exclude_js as $excl_tag => $excl_flags ) {
348
                                if ( false !== strpos( $orig_tag, $excl_tag ) && in_array( $excl_flags, array( 'async', 'defer' ) ) ) {
349
                                    $new_tag = str_replace( '<script ', '<script ' . $excl_flags . ' ', $new_tag );
350
                                }
351
                            }
352
                        }
353
                        
354
                        // not aggregating but deferring?
355
                        if ( $this->defer_not_aggregate && false === $this->aggregate && str_replace( $this->dontmove, '', $path ) === $path && strpos( $new_tag, ' defer' ) === false ) {
356
                            $new_tag = str_replace( '<script ', '<script defer ', $new_tag );
357
                            // and remove async as async+defer=async while we explicitly want defer.
358
                            if ( strpos( $new_tag, ' async' ) !== false && apply_filters( 'autoptimize_filter_js_defer_remove_async', true ) ) {
359
                                $new_tag = str_replace( array( ' async', ' async="async"', " async='async'" ), '', $new_tag );
360
                            }
361
                        }
362
363
                        // Should we minify the non-aggregated script?
364
                        // -> if aggregate is on and exclude minify is on
365
                        // -> if aggregate is off and the file is not in dontmove.
366 View Code Duplication
                        if ( $path && $this->minify_excluded ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
Bug Best Practice introduced by
The expression $path of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
367
                            $consider_minified_array = apply_filters( 'autoptimize_filter_js_consider_minified', false );
368
                            if ( ( false === $this->aggregate && str_replace( $this->dontmove, '', $path ) === $path ) || ( true === $this->aggregate && ( false === $consider_minified_array || str_replace( $consider_minified_array, '', $path ) === $path ) ) ) {
369
                                $minified_url = $this->minify_single( $path );
370
                                if ( ! empty( $minified_url ) ) {
371
                                    // Replace original URL with minified URL from cache.
372
                                    $new_tag = str_replace( $url, $minified_url, $new_tag );
373
                                } elseif ( apply_filters( 'autoptimize_filter_ccsjs_remove_empty_minified_url', false ) ) {
374
                                    // Remove the original script tag, because cache content is empty but only if filter
375
                                    // is trued because $minified_url is also false if original JS is minified already.
376
                                    $new_tag = '';
377
                                }
378
                            }
379
                        }
380
381
                        if ( $this->ismovable( $new_tag ) ) {
382
                            // can be moved, flags and all.
383
                            if ( $this->movetolast( $new_tag ) ) {
384
                                $this->move['last'][] = $new_tag;
385
                            } else {
386
                                $this->move['first'][] = $new_tag;
387
                            }
388 View Code Duplication
                        } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
389
                            // cannot be moved, so if flag was added re-inject altered tag immediately.
390
                            if ( ( '' !== $new_tag && $orig_tag !== $new_tag ) || ( '' === $new_tag && apply_filters( 'autoptimize_filter_js_remove_empty_files', false ) ) ) {
391
                                $this->content = str_replace( $orig_tag, $new_tag, $this->content );
392
                                $orig_tag      = '';
0 ignored issues
show
Unused Code introduced by
$orig_tag is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
393
                            }
394
                            // and forget about the $tag (not to be touched any more).
395
                            $tag = '';
396
                        }
397
                    }
398
                } else {
399
                    // Inline script.
400 View Code Duplication
                    if ( $this->isremovable( $tag, $this->jsremovables ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
401
                        $this->content = str_replace( $tag, '', $this->content );
402
                        continue;
403
                    }
404
405
                    // unhide comments, as javascript may be wrapped in comment-tags for old times' sake.
406
                    $tag = $this->restore_comments( $tag );
407
                    if ( $this->ismergeable( $tag ) && $this->include_inline ) {
408
                        preg_match( '#<script.*>(.*)</script>#Usmi', $tag, $code );
409
                        $code            = preg_replace( '#.*<!\[CDATA\[(?:\s*\*/)?(.*)(?://|/\*)\s*?\]\]>.*#sm', '$1', $code[1] );
410
                        $code            = preg_replace( '/(?:^\\s*<!--\\s*|\\s*(?:\\/\\/)?\\s*-->\\s*$)/', '', $code );
411
                        $this->scripts[] = 'INLINE;' . $code;
412
                    } else {
413
                        // Can we move this?
414
                        $autoptimize_js_moveable = apply_filters( 'autoptimize_js_moveable', '', $tag );
415
                        if ( $this->ismovable( $tag ) || '' !== $autoptimize_js_moveable ) {
416
                            if ( $this->movetolast( $tag ) || 'last' === $autoptimize_js_moveable ) {
417
                                $this->move['last'][] = $tag;
418
                            } else {
419
                                $this->move['first'][] = $tag;
420
                            }
421
                        } else {
422
                            // We shouldn't touch this.
423
                            $tag = '';
424
                        }
425
                    }
426
                    // Re-hide comments to be able to do the removal based on tag from $this->content.
427
                    $tag = $this->hide_comments( $tag );
428
                }
429
430
                // Remove the original script tag.
431
                $this->content = str_replace( $tag, '', $this->content );
432
            }
433
434
            return true;
435
        }
436
437
        // No script files, great ;-) .
438
        return false;
439
    }
440
441
    /**
442
     * Determines wheter a certain `<script>` $tag should be aggregated or not.
443
     *
444
     * We consider these as "aggregation-safe" currently:
445
     * - script tags without a `type` attribute
446
     * - script tags with these `type` attribute values: `text/javascript`, `text/ecmascript`, `application/javascript`,
447
     * and `application/ecmascript`
448
     *
449
     * Everything else should return false.
450
     *
451
     * @link https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
452
     *
453
     * @param string $tag Script node & child(ren).
454
     * @return bool
455
     */
456
    public function should_aggregate( $tag )
457
    {
458
        if ( empty( $tag ) ) {
459
            return false;
460
        }
461
462
        // We're only interested in the type attribute of the <script> tag itself, not any possible
463
        // inline code that might just contain the 'type=' string...
464
        $tag_parts = array();
465
        preg_match( '#<(script[^>]*)>#i', $tag, $tag_parts );
466
        $tag_without_contents = null;
467
        if ( ! empty( $tag_parts[1] ) ) {
468
            $tag_without_contents = $tag_parts[1];
469
        }
470
471
        $has_type = ( strpos( $tag_without_contents, 'type' ) !== false );
472
473
        $type_valid = false;
474
        if ( $has_type ) {
475
            $type_valid = (bool) preg_match( '/type\s*=\s*[\'"]?(?:text|application)\/(?:javascript|ecmascript)[\'"]?/i', $tag_without_contents );
476
        }
477
478
        $should_aggregate = false;
479
        if ( ! $has_type || $type_valid ) {
480
            $should_aggregate = true;
481
        }
482
483
        return $should_aggregate;
484
    }
485
486
    /**
487
     * Joins and optimizes JS.
488
     */
489
    public function minify()
490
    {
491
        foreach ( $this->scripts as $script ) {
492
            if ( empty( $script ) ) {
493
                continue;
494
            }
495
496
            // TODO/FIXME: some duplicate code here, can be reduced/simplified.
497
            if ( preg_match( '#^INLINE;#', $script ) ) {
498
                // Inline script.
499
                $script = preg_replace( '#^INLINE;#', '', $script );
500
                $script = rtrim( $script, ";\n\t\r" ) . ';';
501
                // Add try-catch?
502
                if ( $this->trycatch ) {
503
                    $script = 'try{' . $script . '}catch(e){}';
504
                }
505
                $tmpscript = apply_filters( 'autoptimize_js_individual_script', $script, '' );
506
                if ( has_filter( 'autoptimize_js_individual_script' ) && ! empty( $tmpscript ) ) {
507
                    $script                = $tmpscript;
508
                    $this->alreadyminified = true;
509
                }
510
                $this->jscode .= "\n" . $script;
511
            } else {
512
                // External script.
513
                if ( false !== $script && file_exists( $script ) && is_readable( $script ) ) {
514
                    $scriptsrc = file_get_contents( $script );
515
                    $scriptsrc = preg_replace( '/\x{EF}\x{BB}\x{BF}/', '', $scriptsrc );
516
                    $scriptsrc = rtrim( $scriptsrc, ";\n\t\r" ) . ';';
517
                    // Add try-catch?
518
                    if ( $this->trycatch ) {
519
                        $scriptsrc = 'try{' . $scriptsrc . '}catch(e){}';
520
                    }
521
                    $tmpscriptsrc = apply_filters( 'autoptimize_js_individual_script', $scriptsrc, $script );
522 View Code Duplication
                    if ( has_filter( 'autoptimize_js_individual_script' ) && ! empty( $tmpscriptsrc ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
523
                        $scriptsrc             = $tmpscriptsrc;
524
                        $this->alreadyminified = true;
525
                    } elseif ( $this->can_inject_late( $script ) ) {
526
                        $scriptsrc = self::build_injectlater_marker( $script, md5( $scriptsrc ) );
527
                    }
528
                    $this->jscode .= "\n" . $scriptsrc;
529
                }
530
            }
531
        }
532
533
        // Check for already-minified code.
534
        $this->md5hash = md5( $this->jscode );
535
        $ccheck        = new autoptimizeCache( $this->md5hash, 'js' );
536
        if ( $ccheck->check() ) {
537
            $this->jscode = $ccheck->retrieve();
0 ignored issues
show
Documentation Bug introduced by
It seems like $ccheck->retrieve() can also be of type false. However, the property $jscode is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
538
            return true;
539
        }
540
        unset( $ccheck );
541
542
        // $this->jscode has all the uncompressed code now.
543
        if ( true !== $this->alreadyminified ) {
544
            if ( apply_filters( 'autoptimize_js_do_minify', true ) ) {
545
                $tmp_jscode = trim( JSMin::minify( $this->jscode ) );
546
                if ( ! empty( $tmp_jscode ) ) {
547
                    $this->jscode = $tmp_jscode;
548
                    unset( $tmp_jscode );
549
                }
550
                $this->jscode = $this->inject_minified( $this->jscode );
551
                $this->jscode = apply_filters( 'autoptimize_js_after_minify', $this->jscode );
552
                return true;
553
            } else {
554
                $this->jscode = $this->inject_minified( $this->jscode );
555
                return false;
556
            }
557
        }
558
559
        $this->jscode = apply_filters( 'autoptimize_js_after_minify', $this->jscode );
560
        return true;
561
    }
562
563
    /**
564
     * Caches the JS in uncompressed, deflated and gzipped form.
565
     */
566
    public function cache()
567
    {
568
        $cache = new autoptimizeCache( $this->md5hash, 'js' );
569
        if ( ! $cache->check() ) {
570
            // Cache our code.
571
            $cache->cache( $this->jscode, 'text/javascript' );
572
        }
573
        $this->url = AUTOPTIMIZE_CACHE_URL . $cache->getname();
574
        $this->url = $this->url_replace_cdn( $this->url );
575
    }
576
577
    /**
578
     * Returns the content.
579
     */
580
    public function getcontent()
581
    {
582
        // Restore the full content.
583
        if ( ! empty( $this->restofcontent ) ) {
584
            $this->content      .= $this->restofcontent;
585
            $this->restofcontent = '';
586
        }
587
588
        // Add the scripts taking forcehead/ deferred (default) into account.
589
        if ( $this->forcehead ) {
590
            $replace_tag = array( '</head>', 'before' );
591
            $defer       = '';
592
        } else {
593
            $replace_tag = array( '</body>', 'before' );
594
            $defer       = 'defer ';
595
        }
596
597
        $defer   = apply_filters( 'autoptimize_filter_js_defer', $defer );
598
        $type_js = '';
599
        if ( apply_filters( 'autoptimize_filter_cssjs_addtype', false ) ) {
600
            $type_js = 'type="text/javascript" ';
601
        }
602
603
        $bodyreplacementpayload = '<script ' . $type_js . $defer . 'src="' . $this->url . '"></script>';
604
        $bodyreplacementpayload = apply_filters( 'autoptimize_filter_js_bodyreplacementpayload', $bodyreplacementpayload );
605
606
        $bodyreplacement  = implode( '', $this->move['first'] );
607
        $bodyreplacement .= $bodyreplacementpayload;
608
        $bodyreplacement .= implode( '', $this->move['last'] );
609
610
        $replace_tag = apply_filters( 'autoptimize_filter_js_replacetag', $replace_tag );
611
612
        if ( strlen( $this->jscode ) > 0 ) {
613
            $this->inject_in_html( $bodyreplacement, $replace_tag );
614
        }
615
616
        // Restore comments.
617
        $this->content = $this->restore_comments( $this->content );
618
619
        // Restore IE hacks.
620
        $this->content = $this->restore_iehacks( $this->content );
621
622
        // Restore noptimize.
623
        $this->content = $this->restore_noptimize( $this->content );
624
625
        // Return the modified HTML.
626
        return $this->content;
627
    }
628
629
    /**
630
     * Checks against the allow- and blocklists.
631
     *
632
     * @param string $tag JS tag.
633
     */
634
    private function ismergeable( $tag )
635
    {
636
        if ( empty( $tag ) || ! $this->aggregate ) {
637
            return false;
638
        }
639
640
        if ( ! empty( $this->allowlist ) ) {
641
            foreach ( $this->allowlist as $match ) {
0 ignored issues
show
Bug introduced by
The expression $this->allowlist of type string is not traversable.
Loading history...
642
                if ( false !== strpos( $tag, $match ) ) {
643
                    return true;
644
                }
645
            }
646
            // No match with allowlist.
647
            return false;
648
        } else {
649
            foreach ( $this->domove as $match ) {
650
                if ( false !== strpos( $tag, $match ) ) {
651
                    // Matched something.
652
                    return false;
653
                }
654
            }
655
656
            if ( $this->movetolast( $tag ) ) {
657
                return false;
658
            }
659
660
            foreach ( $this->dontmove as $match ) {
661
                if ( false !== strpos( $tag, $match ) ) {
662
                    // Matched something.
663
                    return false;
664
                }
665
            }
666
667
            // If we're here it's safe to merge.
668
            return true;
669
        }
670
    }
671
672
    /**
673
     * Checks agains the blocklist.
674
     *
675
     * @param string $tag tag to check for blocklist (exclusions).
676
     */
677
    private function ismovable( $tag )
678
    {
679
        if ( empty( $tag ) || true !== $this->include_inline || apply_filters( 'autoptimize_filter_js_unmovable', true ) ) {
680
            return false;
681
        }
682
683
        foreach ( $this->domove as $match ) {
684
            if ( false !== strpos( $tag, $match ) ) {
685
                // Matched something.
686
                return true;
687
            }
688
        }
689
690
        if ( $this->movetolast( $tag ) ) {
691
            return true;
692
        }
693
694
        foreach ( $this->dontmove as $match ) {
695
            if ( false !== strpos( $tag, $match ) ) {
696
                // Matched something.
697
                return false;
698
            }
699
        }
700
701
        // If we're here it's safe to move.
702
        return true;
703
    }
704
705
    private function movetolast( $tag )
706
    {
707
        if ( empty( $tag ) ) {
708
            return false;
709
        }
710
711
        foreach ( $this->domovelast as $match ) {
712
            if ( false !== strpos( $tag, $match ) ) {
713
                // Matched, return true.
714
                return true;
715
            }
716
        }
717
718
        // Should be in 'first'.
719
        return false;
720
    }
721
722
    /**
723
     * Determines wheter a <script> $tag can be excluded from minification (as already minified) based on:
724
     * - inject_min_late being active
725
     * - filename ending in `min.js`
726
     * - filename matching `js/jquery/jquery.js` (WordPress core jquery, is minified)
727
     * - filename matching one passed in the consider minified filter
728
     *
729
     * @param string $js_path Path to JS file.
730
     * @return bool
731
     */
732
    private function can_inject_late( $js_path ) {
733
        $consider_minified_array = apply_filters( 'autoptimize_filter_js_consider_minified', false );
734
        if ( true !== $this->inject_min_late ) {
735
            // late-inject turned off.
736
            return false;
737
        } elseif ( ( false === strpos( $js_path, 'min.js' ) ) && ( false === strpos( $js_path, 'wp-includes/js/jquery/jquery.js' ) ) && ( str_replace( $consider_minified_array, '', $js_path ) === $js_path ) ) {
738
            // file not minified based on filename & filter.
739
            return false;
740
        } else {
741
            // phew, all is safe, we can late-inject.
742
            return true;
743
        }
744
    }
745
746
    /**
747
     * Returns whether we're doing aggregation or not.
748
     *
749
     * @return bool
750
     */
751
    public function aggregating()
752
    {
753
        return $this->aggregate;
754
    }
755
756
    /**
757
     * Minifies a single local js file and returns its (cached) url.
758
     *
759
     * @param string $filepath Filepath.
760
     * @param bool   $cache_miss Optional. Force a cache miss. Default false.
761
     *
762
     * @return bool|string Url pointing to the minified js file or false.
763
     */
764
    public function minify_single( $filepath, $cache_miss = false )
765
    {
766
        $contents = $this->prepare_minify_single( $filepath );
767
768
        if ( empty( $contents ) ) {
769
            return false;
770
        }
771
772
        // Check cache.
773
        $hash  = 'single_' . md5( $contents );
774
        $cache = new autoptimizeCache( $hash, 'js' );
775
776
        // If not in cache already, minify...
777
        if ( ! $cache->check() || $cache_miss ) {
778
            $contents = trim( JSMin::minify( $contents ) );
779
780
            // Check if minified cache content is empty.
781
            if ( empty( $contents ) ) {
782
                return false;
783
            }
784
785
            // Filter contents of excluded minified CSS.
786
            $contents = apply_filters( 'autoptimize_filter_js_single_after_minify', $contents );
787
788
            // Store in cache.
789
            $cache->cache( $contents, 'text/javascript' );
790
        }
791
792
        $url = $this->build_minify_single_url( $cache );
793
794
        return $url;
795
    }
796
}
797