Completed
Push — master ( 9a01d9...c990d6 )
by frank
08:32
created

classes/autoptimizeScripts.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
3
4
class autoptimizeScripts extends autoptimizeBase {
5
    private $scripts = array();
6
    private $dontmove = array('document.write','html5.js','show_ads.js','google_ad','histats.com/js','statcounter.com/counter/counter.js','ws.amazon.com/widgets','media.fastclick.net','/ads/','comment-form-quicktags/quicktags.php','edToolbar','intensedebate.com','scripts.chitika.net/','_gaq.push','jotform.com/','admin-bar.min.js','GoogleAnalyticsObject','plupload.full.min.js','syntaxhighlighter','adsbygoogle','gist.github.com','_stq','nonce','post_id','data-noptimize','wordfence_logHuman');
7
    private $domove = array('gaJsHost','load_cmc','jd.gallery.transitions.js','swfobject.embedSWF(','tiny_mce.js','tinyMCEPreInit.go');
8
    private $domovelast = array('addthis.com','/afsonline/show_afs_search.js','disqus.js','networkedblogs.com/getnetworkwidget','infolinks.com/js/','jd.gallery.js.php','jd.gallery.transitions.js','swfobject.embedSWF(','linkwithin.com/widget.js','tiny_mce.js','tinyMCEPreInit.go');
9
    private $trycatch = false;
10
    private $alreadyminified = false;
11
    private $forcehead = true;
12
    private $include_inline = false;
13
    private $jscode = '';
14
    private $url = '';
15
    private $move = array('first' => array(), 'last' => array());
16
    private $restofcontent = '';
17
    private $md5hash = '';
18
    private $whitelist = '';
19
    private $jsremovables = array();
20
    private $inject_min_late = '';
21
    
22
    //Reads the page and collects script tags
23
    public function read($options) {
24
        $noptimizeJS = apply_filters( 'autoptimize_filter_js_noptimize', false, $this->content );
25
        if ($noptimizeJS) return false;
26
27
        // only optimize known good JS?
28
        $whitelistJS = apply_filters( 'autoptimize_filter_js_whitelist', '', $this->content );
29
        if (!empty($whitelistJS)) {
30
            $this->whitelist = array_filter(array_map('trim',explode(",",$whitelistJS)));
31
        }
32
33
        // is there JS we should simply remove
34
        $removableJS = apply_filters( 'autoptimize_filter_js_removables', '', $this->content );
35 View Code Duplication
        if (!empty($removableJS)) {
36
            $this->jsremovables = array_filter(array_map('trim',explode(",",$removableJS)));
37
        }
38
39
        // only header?
40 View Code Duplication
        if( apply_filters('autoptimize_filter_js_justhead', $options['justhead']) == true ) {
41
            $content = explode('</head>',$this->content,2);
42
            $this->content = $content[0].'</head>';
43
            $this->restofcontent = $content[1];
44
        }
45
        
46
        // include inline?
47
        if( apply_filters('autoptimize_js_include_inline', $options['include_inline']) == true ) {
48
            $this->include_inline = true;
49
        }
50
51
        // filter to "late inject minified JS", default to true for now (it is faster)
52
        $this->inject_min_late = apply_filters('autoptimize_filter_js_inject_min_late',true);
53
54
        // filters to override hardcoded do(nt)move(last) array contents (array in, array out!)
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
55
        $this->dontmove = apply_filters( 'autoptimize_filter_js_dontmove', $this->dontmove );        
56
        $this->domovelast = apply_filters( 'autoptimize_filter_js_movelast', $this->domovelast );
57
        $this->domove = apply_filters( 'autoptimize_filter_js_domove', $this->domove );
58
59
        // get extra exclusions settings or filter
60
        $excludeJS = $options['js_exclude'];
61
        $excludeJS = apply_filters( 'autoptimize_filter_js_exclude', $excludeJS, $this->content );
62
        if ($excludeJS!=="") {
63
            if (is_array($excludeJS)) {
64
                if(($removeKeys = array_keys($excludeJS,"remove")) !== false) {
65
                    foreach ($removeKeys as $removeKey) {
66
                        unset($excludeJS[$removeKey]);
67
                        $this->jsremovables[]=$removeKey;
68
                    }
69
                }
70
                $exclJSArr = array_keys($excludeJS);
71
            } else {
72
                $exclJSArr = array_filter(array_map('trim',explode(",",$excludeJS)));
73
            }
74
            $this->dontmove = array_merge($exclJSArr,$this->dontmove);
75
        }
76
77
        //Should we add try-catch?
78
        if($options['trycatch'] == true)
79
            $this->trycatch = true;
80
81
        // force js in head?    
82
        if($options['forcehead'] == true) {
83
            $this->forcehead = true;
84
        } else {
85
            $this->forcehead = false;
86
        }
87
        $this->forcehead = apply_filters( 'autoptimize_filter_js_forcehead', $this->forcehead );
88
89
        // get cdn url
90
        $this->cdn_url = $options['cdn_url'];
91
            
92
        // noptimize me
93
        $this->content = $this->hide_noptimize($this->content);
94
95
        // Save IE hacks
96
        $this->content = $this->hide_iehacks($this->content);
97
98
        // comments
99
        $this->content = $this->hide_comments($this->content);
100
101
        // Get script files
102
        if (preg_match_all('#<script.*</script>#Usmi',$this->content,$matches)) {
103
            foreach($matches[0] as $tag) {
104
                // only consider script aggregation for types whitelisted in should_aggregate-function
105
                if( !$this->should_aggregate($tag) ) {
106
                    $tag='';
107
                    continue;
108
                }
109
110
                if (preg_match('#<script[^>]*src=("|\')([^>]*)("|\')#Usmi',$tag,$source)) {
111
                    // non-inline script
112 View Code Duplication
                    if ($this->isremovable($tag,$this->jsremovables)) {
113
                        $this->content = str_replace($tag,'',$this->content);
114
                        continue;
115
                    }
116
                    $explUrl = explode('?',$source[2],2);
117
                    $url = $explUrl[0];
118
                    $path = $this->getpath($url);
119
                    if($path !== false && preg_match('#\.js$#',$path) && $this->ismergeable($tag)) {
120
                        // ok to optimize, add to array
121
                        $this->scripts[] = $path;
122
                    } else {
123
                        $origTag = $tag;
124
                        $newTag = $tag;
125
                        
126
                        // non-mergeable script (excluded or dynamic or external)
127
                        if (is_array($excludeJS)) {
128
                            // should we add flags?
129
                            foreach ($excludeJS as $exclTag => $exclFlags) {
130
                                if ( strpos($origTag,$exclTag)!==false && in_array($exclFlags,array("async","defer")) ) {
131
                                   $newTag = str_replace('<script ','<script '.$exclFlags.' ',$newTag);
132
                                }
133
                            }
134
                        }
135
                        
136
   						// should we minify the non-aggregated script?
137
						if ($path && apply_filters('autoptimize_filter_js_minify_excluded',false)) {
138
							$_CachedMinifiedUrl = $this->minify_single($path);
139
140
							// replace orig URL with minified URL from cache if so
141
							if (!empty($_CachedMinifiedUrl)) {
142
								$newTag = str_replace($url, $_CachedMinifiedUrl, $newTag);
143
							}
144
							
145
							// remove querystring from URL in newTag
146 View Code Duplication
							if ( !empty($explUrl[1]) ) {
147
								$newTag = str_replace("?".$explUrl[1],"",$newTag);
148
							}
149
						}
150
151
						// should we move the non-aggregated script?
152
                        if( $this->ismovable($newTag) ) {
153
                            // can be moved, flags and all
154
                            if( $this->movetolast($newTag) )    {
155
                                $this->move['last'][] = $newTag;
156
                            } else {
157
                                $this->move['first'][] = $newTag;
158
                            }
159
                        } else {
160
                            // cannot be moved, so if flag was added re-inject altered tag immediately
161
                            if ( $origTag !== $newTag ) {
162
                                $this->content = str_replace($origTag,$newTag,$this->content);
163
                                $origTag = '';
164
                            }
165
                            // and forget about the $tag (not to be touched any more)
166
                            $tag = '';
167
                        }
168
                    }
169
                } else {
170
                    // Inline script
171 View Code Duplication
                    if ($this->isremovable($tag,$this->jsremovables)) {
172
                        $this->content = str_replace($tag,'',$this->content);
173
                        continue;
174
                    }
175
                    
176
                    // unhide comments, as javascript may be wrapped in comment-tags for old times' sake
177
                    $tag = $this->restore_comments($tag);
178
                    if($this->ismergeable($tag) && ( $this->include_inline )) {
179
                        preg_match('#<script.*>(.*)</script>#Usmi',$tag,$code);
180
                        $code = preg_replace('#.*<!\[CDATA\[(?:\s*\*/)?(.*)(?://|/\*)\s*?\]\]>.*#sm','$1',$code[1]);
181
                        $code = preg_replace('/(?:^\\s*<!--\\s*|\\s*(?:\\/\\/)?\\s*-->\\s*$)/','',$code);
182
                        $this->scripts[] = 'INLINE;'.$code;
183
                    } else {
184
                        // Can we move this?
185
                        $autoptimize_js_moveable = apply_filters( 'autoptimize_js_moveable', '', $tag );
186
                        if( $this->ismovable($tag) || $autoptimize_js_moveable !== '' ) {
187
                            if( $this->movetolast($tag) || $autoptimize_js_moveable === 'last' ) {
188
                                $this->move['last'][] = $tag;
189
                            } else {
190
                                $this->move['first'][] = $tag;
191
                            }
192
                        } else {
193
                            //We shouldn't touch this
194
                            $tag = '';
195
                        }
196
                    }
197
                    // re-hide comments to be able to do the removal based on tag from $this->content
198
                    $tag = $this->hide_comments($tag);
199
                }
200
                
201
                //Remove the original script tag
202
                $this->content = str_replace($tag,'',$this->content);
203
            }
204
            
205
            return true;
206
        }
207
    
208
        // No script files, great ;-)
209
        return false;
210
    }
211
    
212
    //Joins and optimizes JS
213
    public function minify() {
214
        foreach($this->scripts as $script) {
215
            if(preg_match('#^INLINE;#',$script)) {
216
                //Inline script
217
                $script = preg_replace('#^INLINE;#','',$script);
218
                $script = rtrim( $script, ";\n\t\r" ) . ';';
219
                //Add try-catch?
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
220
                if($this->trycatch) {
221
                    $script = 'try{'.$script.'}catch(e){}';
222
                }
223
                $tmpscript = apply_filters( 'autoptimize_js_individual_script', $script, '' );
224
                if ( has_filter('autoptimize_js_individual_script') && !empty($tmpscript) ) {
225
                    $script=$tmpscript;
226
                    $this->alreadyminified=true;
227
                }
228
                $this->jscode .= "\n" . $script;
229
            } else {
230
                //External script
231
                if($script !== false && file_exists($script) && is_readable($script)) {
232
                    $scriptsrc = file_get_contents($script);
233
                    $scripthash = md5($scriptsrc);
234
                    $scriptsrc = preg_replace('/\x{EF}\x{BB}\x{BF}/','',$scriptsrc);
235
                    $scriptsrc = rtrim($scriptsrc,";\n\t\r").';';
236
237
                    //Add try-catch?
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
238
                    if($this->trycatch) {
239
                        $scriptsrc = 'try{'.$scriptsrc.'}catch(e){}';
240
                    }
241
                    $tmpscriptsrc = apply_filters( 'autoptimize_js_individual_script', $scriptsrc, $script );
242 View Code Duplication
                    if ( has_filter('autoptimize_js_individual_script') && !empty($tmpscriptsrc) ) {
243
                        $scriptsrc=$tmpscriptsrc;
244
                        $this->alreadyminified=true;
245
                    } else if ($this->can_inject_late($script)) {
246
                        $scriptsrc="/*!%%INJECTLATER".AUTOPTIMIZE_HASH."%%".base64_encode($script)."|".$scripthash."%%INJECTLATER%%*/";
247
                    }
248
                    $this->jscode .= "\n".$scriptsrc;
249
                }/*else{
0 ignored issues
show
Unused Code Comprehensibility introduced by
46% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
250
                    //Couldn't read JS. Maybe getpath isn't working?
251
                }*/
252
            }
253
        }
254
255
        //Check for already-minified code
256
        $this->md5hash = md5($this->jscode);
257
        $ccheck = new autoptimizeCache($this->md5hash,'js');
258
        if($ccheck->check()) {
259
            $this->jscode = $ccheck->retrieve();
260
            return true;
261
        }
262
        unset($ccheck);
263
        
264
        //$this->jscode has all the uncompressed code now.
265
        if ($this->alreadyminified!==true) {
266
          if (class_exists('JSMin') && apply_filters( 'autoptimize_js_do_minify' , true)) {
267
            if (@is_callable(array("JSMin","minify"))) {
268
                $tmp_jscode = trim(JSMin::minify($this->jscode));
269
                if (!empty($tmp_jscode)) {
270
                    $this->jscode = $tmp_jscode;
271
                    unset($tmp_jscode);
272
                }
273
                $this->jscode = $this->inject_minified($this->jscode);
274
                $this->jscode = apply_filters( 'autoptimize_js_after_minify', $this->jscode );
275
                return true;
276
            } else {
277
                $this->jscode = $this->inject_minified($this->jscode);
278
                return false;
279
            }
280
          } else {
281
              $this->jscode = $this->inject_minified($this->jscode);
282
              return false;
283
          }
284
        }
285
        $this->jscode = apply_filters( 'autoptimize_js_after_minify', $this->jscode );
286
        return true;
287
    }
288
    
289
    //Caches the JS in uncompressed, deflated and gzipped form.
290
    public function cache()    {
291
        $cache = new autoptimizeCache($this->md5hash,'js');
292
        if(!$cache->check()) {
293
            //Cache our code
294
            $cache->cache($this->jscode,'text/javascript');
295
        }
296
        $this->url = AUTOPTIMIZE_CACHE_URL.$cache->getname();
297
        $this->url = $this->url_replace_cdn($this->url);
298
    }
299
    
300
    // Returns the content
301
    public function getcontent() {
302
        // Restore the full content
303
        if(!empty($this->restofcontent)) {
304
            $this->content .= $this->restofcontent;
305
            $this->restofcontent = '';
306
        }
307
        
308
        // Add the scripts taking forcehead/ deferred (default) into account
309
        if($this->forcehead == true) {
310
            $replaceTag=array("</head>","before");
311
            $defer="";
312
        } else {
313
            $replaceTag=array("</body>","before");
314
            $defer="defer ";
315
        }
316
        
317
        $defer = apply_filters( 'autoptimize_filter_js_defer', $defer );
318
        $bodyreplacementpayload = '<script type="text/javascript" '.$defer.'src="'.$this->url.'"></script>';
319
        $bodyreplacementpayload = apply_filters('autoptimize_filter_js_bodyreplacementpayload',$bodyreplacementpayload);
320
321
        $bodyreplacement = implode('',$this->move['first']);
322
        $bodyreplacement .= $bodyreplacementpayload;
323
        $bodyreplacement .= implode('',$this->move['last']);
324
325
        $replaceTag = apply_filters( 'autoptimize_filter_js_replacetag', $replaceTag );
326
327
        if (strlen($this->jscode)>0) {
328
            $this->inject_in_html($bodyreplacement,$replaceTag);
329
        }
330
        
331
        // restore comments
332
        $this->content = $this->restore_comments($this->content);
333
334
        // Restore IE hacks
335
        $this->content = $this->restore_iehacks($this->content);
336
        
337
        // Restore noptimize
338
        $this->content = $this->restore_noptimize($this->content);
339
340
        // Return the modified HTML
341
        return $this->content;
342
    }
343
    
344
    // Checks against the white- and blacklists
345
    private function ismergeable($tag) {
346
		if (apply_filters('autoptimize_filter_js_dontaggregate',false)) {
347
			return false;
348
        } else if (!empty($this->whitelist)) {
349
            foreach ($this->whitelist as $match) {
350
                if(strpos($tag,$match)!==false) {
351
                    return true;
352
                }
353
            }
354
            // no match with whitelist
355
            return false;
356
        } else {
357
            foreach($this->domove as $match) {
358
                if(strpos($tag,$match)!==false)    {
359
                    // Matched something
360
                    return false;
361
                }
362
            }
363
            
364
            if ($this->movetolast($tag)) {
365
                return false;
366
            }
367
            
368
            foreach($this->dontmove as $match) {
369
                if(strpos($tag,$match)!==false)    {
370
                    //Matched something
371
                    return false;
372
                }
373
            }
374
            
375
            // If we're here it's safe to merge
376
            return true;
377
        }
378
    }
379
    
380
    // Checks againstt the blacklist
381
    private function ismovable($tag) {
382
        if ($this->include_inline !== true || apply_filters('autoptimize_filter_js_unmovable',true)) {
383
            return false;
384
        }
385
        
386
        foreach($this->domove as $match) {
387
            if(strpos($tag,$match)!==false)    {
388
                // Matched something
389
                return true;
390
            }
391
        }
392
        
393
        if ($this->movetolast($tag)) {
394
            return true;
395
        }
396
        
397
        foreach($this->dontmove as $match) {
398
            if(strpos($tag,$match)!==false) {
399
                // Matched something
400
                return false;
401
            }
402
        }
403
        
404
        // If we're here it's safe to move
405
        return true;
406
    }
407
    
408
    private function movetolast($tag) {
409
        foreach($this->domovelast as $match) {
410
            if(strpos($tag,$match)!==false)    {
411
                // Matched, return true
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
412
                return true;
413
            }
414
        }
415
        
416
        // Should be in 'first'
417
        return false;
418
    }
419
    
420
    /**
421
     * Determines wheter a <script> $tag should be aggregated or not.
422
     *
423
     * We consider these as "aggregation-safe" currently:
424
     * - script tags without a `type` attribute
425
     * - script tags with an explicit `type` of `text/javascript`, 'text/ecmascript', 
426
     *   'application/javascript' or 'application/ecmascript'
427
     *
428
     * Everything else should return false.
429
     *
430
     * @param string $tag
431
     * @return bool
432
     * 
433
     * original function by https://github.com/zytzagoo/ on his AO fork, thanks Tomas!
434
     */
435
    public function should_aggregate($tag) {
436
        preg_match('#<(script[^>]*)>#i',$tag,$scripttag);
437
        if ( strpos($scripttag[1], 'type')===false ) {
438
            return true;
439
        } else if ( preg_match('/type\s*=\s*["\']?(?:text|application)\/(?:javascript|ecmascript)["\']?/i', $scripttag[1]) ) {
440
            return true;
441
        } else {
442
            return false;
443
        }
444
    }
445
446
    /**
447
     * Determines wheter a <script> $tag can be excluded from minification (as already minified) based on:
448
     * - inject_min_late being active
449
     * - filename ending in `min.js`
450
     * - filename matching `js/jquery/jquery.js` (wordpress core jquery, is minified)
451
     * - filename matching one passed in the consider minified filter
452
     * 
453
     * @param string $jsPath
454
     * @return bool
455
	 */
456
	private function can_inject_late($jsPath) {
457
		$consider_minified_array = apply_filters('autoptimize_filter_js_consider_minified',false);
458
        if ( $this->inject_min_late !== true ) {
459
            // late-inject turned off
460
            return false;
461
        } else if ( (strpos($jsPath,"min.js") === false) && ( strpos($jsPath,"wp-includes/js/jquery/jquery.js") === false ) && ( str_replace($consider_minified_array, '', $jsPath) === $jsPath ) ) {
462
			// file not minified based on filename & filter
463
			return false;
464
        } else {
465
            // phew, all is safe, we can late-inject
466
            return true;
467
        }
468
    }
469
}
470