PluginProbe
Autoptimize / 2.6.0
Autoptimize v2.6.0
2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 All 107 releases
autoptimize / classes / autoptimizeScripts.php

autoptimizeScripts.php in Autoptimize 2.6.0, at classes/autoptimizeScripts.php

579 lines 22.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if ( ! defined( 'ABSPATH' ) ) {
4 exit;
5 }
6
7 class autoptimizeScripts extends autoptimizeBase
8 {
9 private $scripts = array();
10 private $move = array(
11 'first' => array(),
12 'last' => array()
13 );
14
15 private $dontmove = array(
16 'document.write','html5.js','show_ads.js','google_ad','histats.com/js','statcounter.com/counter/counter.js',
17 'ws.amazon.com/widgets','media.fastclick.net','/ads/','comment-form-quicktags/quicktags.php','edToolbar',
18 'intensedebate.com','scripts.chitika.net/','_gaq.push','jotform.com/','admin-bar.min.js','GoogleAnalyticsObject',
19 'plupload.full.min.js','syntaxhighlighter','adsbygoogle','gist.github.com','_stq','nonce','post_id','data-noptimize'
20 ,'logHuman'
21 );
22 private $domove = array(
23 'gaJsHost','load_cmc','jd.gallery.transitions.js','swfobject.embedSWF(','tiny_mce.js','tinyMCEPreInit.go'
24 );
25 private $domovelast = array(
26 'addthis.com','/afsonline/show_afs_search.js','disqus.js','networkedblogs.com/getnetworkwidget','infolinks.com/js/',
27 'jd.gallery.js.php','jd.gallery.transitions.js','swfobject.embedSWF(','linkwithin.com/widget.js','tiny_mce.js','tinyMCEPreInit.go'
28 );
29 public $cdn_url = '';
30
31 private $aggregate = true;
32 private $trycatch = false;
33 private $alreadyminified = false;
34 private $forcehead = true;
35 private $include_inline = false;
36 private $jscode = '';
37 private $url = '';
38 private $restofcontent = '';
39 private $md5hash = '';
40 private $whitelist = '';
41 private $jsremovables = array();
42 private $inject_min_late = '';
43 private $minify_excluded = true;
44
45 // Reads the page and collects script tags.
46 public function read( $options )
47 {
48 $noptimizeJS = apply_filters( 'autoptimize_filter_js_noptimize', false, $this->content );
49 if ( $noptimizeJS ) {
50 return false;
51 }
52
53 // only optimize known good JS?
54 $whitelistJS = apply_filters( 'autoptimize_filter_js_whitelist', '', $this->content );
55 if ( ! empty( $whitelistJS ) ) {
56 $this->whitelist = array_filter( array_map( 'trim', explode( ',', $whitelistJS ) ) );
57 }
58
59 // is there JS we should simply remove?
60 $removableJS = apply_filters( 'autoptimize_filter_js_removables', '', $this->content );
61 if ( !empty( $removableJS ) ) {
62 $this->jsremovables = array_filter( array_map( 'trim', explode( ',', $removableJS ) ) );
63 }
64
65 // only header?
66 if ( apply_filters( 'autoptimize_filter_js_justhead', $options['justhead'] ) ) {
67 $content = explode( '</head>', $this->content, 2 );
68 $this->content = $content[0] . '</head>';
69 $this->restofcontent = $content[1];
70 }
71
72 // Determine whether we're doing JS-files aggregation or not.
73 if ( ! $options['aggregate'] ) {
74 $this->aggregate = false;
75 }
76 // Returning true for "dontaggregate" turns off aggregation.
77 if ( $this->aggregate && apply_filters( 'autoptimize_filter_js_dontaggregate', false ) ) {
78 $this->aggregate = false;
79 }
80
81 // include inline?
82 if ( apply_filters( 'autoptimize_js_include_inline', $options['include_inline'] ) ) {
83 $this->include_inline = true;
84 }
85
86 // filter to "late inject minified JS", default to true for now (it is faster).
87 $this->inject_min_late = apply_filters( 'autoptimize_filter_js_inject_min_late', true );
88
89 // filters to override hardcoded do(nt)move(last) array contents (array in, array out!).
90 $this->dontmove = apply_filters( 'autoptimize_filter_js_dontmove', $this->dontmove );
91 $this->domovelast = apply_filters( 'autoptimize_filter_js_movelast', $this->domovelast );
92 $this->domove = apply_filters( 'autoptimize_filter_js_domove', $this->domove );
93
94 // Determine whether excluded files should be minified if not yet so.
95 if ( ! $options['minify_excluded'] && $options['aggregate'] ) {
96 $this->minify_excluded = false;
97 }
98 $this->minify_excluded = apply_filters( 'autoptimize_filter_js_minify_excluded', $this->minify_excluded );
99
100 // get extra exclusions settings or filter.
101 $excludeJS = $options['js_exclude'];
102 $excludeJS = apply_filters( 'autoptimize_filter_js_exclude', $excludeJS, $this->content );
103
104 if ( '' !== $excludeJS ) {
105 if ( is_array( $excludeJS ) ) {
106 if ( ( $removeKeys = array_keys( $excludeJS, 'remove' ) ) !== false ) {
107 foreach ( $removeKeys as $removeKey ) {
108 unset( $excludeJS[$removeKey] );
109 $this->jsremovables[] = $removeKey;
110 }
111 }
112 $exclJSArr = array_keys( $excludeJS );
113 } else {
114 $exclJSArr = array_filter( array_map( 'trim', explode( ',', $excludeJS ) ) );
115 }
116 $this->dontmove = array_merge( $exclJSArr, $this->dontmove );
117 }
118
119 // Should we add try-catch?
120 if ( $options['trycatch'] ) {
121 $this->trycatch = true;
122 }
123
124 // force js in head?
125 if ( $options['forcehead'] ) {
126 $this->forcehead = true;
127 } else {
128 $this->forcehead = false;
129 }
130
131 $this->forcehead = apply_filters( 'autoptimize_filter_js_forcehead', $this->forcehead );
132
133 // get cdn url.
134 $this->cdn_url = $options['cdn_url'];
135
136 // noptimize me.
137 $this->content = $this->hide_noptimize($this->content);
138
139 // Save IE hacks.
140 $this->content = $this->hide_iehacks($this->content);
141
142 // comments.
143 $this->content = $this->hide_comments($this->content);
144
145 // Get script files.
146 if ( preg_match_all( '#<script.*</script>#Usmi', $this->content, $matches ) ) {
147 foreach( $matches[0] as $tag ) {
148 // only consider script aggregation for types whitelisted in should_aggregate-function.
149 $should_aggregate = $this->should_aggregate($tag);
150 if ( ! $should_aggregate ) {
151 $tag = '';
152 continue;
153 }
154
155 if ( preg_match( '#<script[^>]*src=("|\')([^>]*)("|\')#Usmi', $tag, $source ) ) {
156 // non-inline script.
157 if ( $this->isremovable($tag, $this->jsremovables) ) {
158 $this->content = str_replace( $tag, '', $this->content );
159 continue;
160 }
161
162 $origTag = null;
163 $url = current( explode( '?', $source[2], 2 ) );
164 $path = $this->getpath($url);
165 if ( false !== $path && preg_match( '#\.js$#', $path ) && $this->ismergeable($tag) ) {
166 // ok to optimize, add to array.
167 $this->scripts[] = $path;
168 } else {
169 $origTag = $tag;
170 $newTag = $tag;
171
172 // non-mergeable script (excluded or dynamic or external).
173 if ( is_array( $excludeJS ) ) {
174 // should we add flags?
175 foreach ( $excludeJS as $exclTag => $exclFlags) {
176 if ( false !== strpos( $origTag, $exclTag ) && in_array( $exclFlags, array( 'async', 'defer' ) ) ) {
177 $newTag = str_replace( '<script ', '<script ' . $exclFlags . ' ', $newTag );
178 }
179 }
180 }
181
182 // Should we minify the non-aggregated script?
183 // -> if aggregate is on and exclude minify is on
184 // -> if aggregate is off and the file is not in dontmove.
185 if ( $path && $this->minify_excluded ) {
186 $consider_minified_array = apply_filters( 'autoptimize_filter_js_consider_minified', false );
187 if ( ( false === $this->aggregate && str_replace( $this->dontmove, '', $path ) === $path ) || ( true === $this->aggregate && ( false === $consider_minified_array || str_replace( $consider_minified_array, '', $path ) === $path ) ) ) {
188 $minified_url = $this->minify_single( $path );
189 // replace orig URL with minified URL from cache if so.
190 if ( ! empty( $minified_url ) ) {
191 $newTag = str_replace( $url, $minified_url, $newTag );
192 }
193 }
194 }
195
196 if ( $this->ismovable($newTag) ) {
197 // can be moved, flags and all.
198 if ( $this->movetolast($newTag) ) {
199 $this->move['last'][] = $newTag;
200 } else {
201 $this->move['first'][] = $newTag;
202 }
203 } else {
204 // cannot be moved, so if flag was added re-inject altered tag immediately.
205 if ( $origTag !== $newTag ) {
206 $this->content = str_replace( $origTag, $newTag, $this->content );
207 $origTag = '';
208 }
209 // and forget about the $tag (not to be touched any more).
210 $tag = '';
211 }
212 }
213 } else {
214 // Inline script.
215 if ( $this->isremovable($tag, $this->jsremovables) ) {
216 $this->content = str_replace( $tag, '', $this->content );
217 continue;
218 }
219
220 // unhide comments, as javascript may be wrapped in comment-tags for old times' sake.
221 $tag = $this->restore_comments($tag);
222 if ( $this->ismergeable($tag) && $this->include_inline ) {
223 preg_match( '#<script.*>(.*)</script>#Usmi', $tag , $code );
224 $code = preg_replace('#.*<!\[CDATA\[(?:\s*\*/)?(.*)(?://|/\*)\s*?\]\]>.*#sm', '$1', $code[1] );
225 $code = preg_replace('/(?:^\\s*<!--\\s*|\\s*(?:\\/\\/)?\\s*-->\\s*$)/', '', $code );
226 $this->scripts[] = 'INLINE;' . $code;
227 } else {
228 // Can we move this?
229 $autoptimize_js_moveable = apply_filters( 'autoptimize_js_moveable', '', $tag );
230 if ( $this->ismovable($tag) || '' !== $autoptimize_js_moveable ) {
231 if ( $this->movetolast($tag) || 'last' === $autoptimize_js_moveable ) {
232 $this->move['last'][] = $tag;
233 } else {
234 $this->move['first'][] = $tag;
235 }
236 } else {
237 // We shouldn't touch this.
238 $tag = '';
239 }
240 }
241 // Re-hide comments to be able to do the removal based on tag from $this->content.
242 $tag = $this->hide_comments($tag);
243 }
244
245 //Remove the original script tag.
246 $this->content = str_replace( $tag, '', $this->content );
247 }
248
249 return true;
250 }
251
252 // No script files, great ;-)
253 return false;
254 }
255
256 /**
257 * Determines wheter a certain `<script>` $tag should be aggregated or not.
258 *
259 * We consider these as "aggregation-safe" currently:
260 * - script tags without a `type` attribute
261 * - script tags with these `type` attribute values: `text/javascript`, `text/ecmascript`, `application/javascript`,
262 * and `application/ecmascript`
263 *
264 * Everything else should return false.
265 *
266 * @link https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
267 *
268 * @param string $tag
269 * @return bool
270 */
271 public function should_aggregate($tag)
272 {
273 // We're only interested in the type attribute of the <script> tag itself, not any possible
274 // inline code that might just contain the 'type=' string...
275 $tag_parts = array();
276 preg_match( '#<(script[^>]*)>#i', $tag, $tag_parts);
277 $tag_without_contents = null;
278 if ( ! empty( $tag_parts[1] ) ) {
279 $tag_without_contents = $tag_parts[1];
280 }
281
282 $has_type = ( strpos( $tag_without_contents, 'type' ) !== false );
283
284 $type_valid = false;
285 if ( $has_type ) {
286 $type_valid = (bool) preg_match( '/type\s*=\s*[\'"]?(?:text|application)\/(?:javascript|ecmascript)[\'"]?/i', $tag_without_contents );
287 }
288
289 $should_aggregate = false;
290 if ( ! $has_type || $type_valid ) {
291 $should_aggregate = true;
292 }
293
294 return $should_aggregate;
295 }
296
297 //Joins and optimizes JS
298 public function minify()
299 {
300 foreach ( $this->scripts as $script ) {
301 // TODO/FIXME: some duplicate code here, can be reduced/simplified
302 if ( preg_match( '#^INLINE;#', $script ) ) {
303 // Inline script
304 $script = preg_replace( '#^INLINE;#', '', $script );
305 $script = rtrim( $script, ";\n\t\r" ) . ';';
306 // Add try-catch?
307 if ( $this->trycatch ) {
308 $script = 'try{' . $script . '}catch(e){}';
309 }
310 $tmpscript = apply_filters( 'autoptimize_js_individual_script', $script, '' );
311 if ( has_filter( 'autoptimize_js_individual_script' ) && ! empty( $tmpscript ) ) {
312 $script = $tmpscript;
313 $this->alreadyminified = true;
314 }
315 $this->jscode .= "\n" . $script;
316 } else {
317 // External script
318 if ( false !== $script && file_exists( $script ) && is_readable( $script ) ) {
319 $scriptsrc = file_get_contents( $script );
320 $scriptsrc = preg_replace( '/\x{EF}\x{BB}\x{BF}/', '', $scriptsrc );
321 $scriptsrc = rtrim( $scriptsrc, ";\n\t\r" ) . ';';
322 // Add try-catch?
323 if ( $this->trycatch ) {
324 $scriptsrc = 'try{' . $scriptsrc . '}catch(e){}';
325 }
326 $tmpscriptsrc = apply_filters( 'autoptimize_js_individual_script', $scriptsrc, $script );
327 if ( has_filter( 'autoptimize_js_individual_script' ) && ! empty( $tmpscriptsrc ) ) {
328 $scriptsrc = $tmpscriptsrc;
329 $this->alreadyminified = true;
330 } else if ( $this->can_inject_late($script) ) {
331 $scriptsrc = self::build_injectlater_marker($script, md5($scriptsrc));
332 }
333 $this->jscode .= "\n" . $scriptsrc;
334 }/*else{
335 //Couldn't read JS. Maybe getpath isn't working?
336 }*/
337 }
338 }
339
340 // Check for already-minified code
341 $this->md5hash = md5( $this->jscode );
342 $ccheck = new autoptimizeCache($this->md5hash, 'js');
343 if ( $ccheck->check() ) {
344 $this->jscode = $ccheck->retrieve();
345 return true;
346 }
347 unset( $ccheck );
348
349 // $this->jscode has all the uncompressed code now.
350 if ( true !== $this->alreadyminified ) {
351 if ( apply_filters( 'autoptimize_js_do_minify', true ) ) {
352 $tmp_jscode = trim( JSMin::minify( $this->jscode ) );
353 if ( ! empty( $tmp_jscode ) ) {
354 $this->jscode = $tmp_jscode;
355 unset( $tmp_jscode );
356 }
357 $this->jscode = $this->inject_minified( $this->jscode );
358 $this->jscode = apply_filters( 'autoptimize_js_after_minify', $this->jscode );
359 return true;
360 } else {
361 $this->jscode = $this->inject_minified( $this->jscode );
362 return false;
363 }
364 }
365
366 $this->jscode = apply_filters( 'autoptimize_js_after_minify', $this->jscode );
367 return true;
368 }
369
370 // Caches the JS in uncompressed, deflated and gzipped form.
371 public function cache()
372 {
373 $cache = new autoptimizeCache($this->md5hash, 'js');
374 if ( ! $cache->check() ) {
375 // Cache our code
376 $cache->cache($this->jscode, 'text/javascript');
377 }
378 $this->url = AUTOPTIMIZE_CACHE_URL . $cache->getname();
379 $this->url = $this->url_replace_cdn($this->url);
380 }
381
382 // Returns the content
383 public function getcontent()
384 {
385 // Restore the full content
386 if ( ! empty( $this->restofcontent ) ) {
387 $this->content .= $this->restofcontent;
388 $this->restofcontent = '';
389 }
390
391 // Add the scripts taking forcehead/ deferred (default) into account
392 if ( $this->forcehead ) {
393 $replaceTag = array( '</head>', 'before' );
394 $defer = '';
395 } else {
396 $replaceTag = array( '</body>', 'before' );
397 $defer = 'defer ';
398 }
399
400 $defer = apply_filters( 'autoptimize_filter_js_defer', $defer );
401 $type_js = '';
402 if ( apply_filters( 'autoptimize_filter_cssjs_addtype', false ) ) {
403 $type_js = 'type="text/javascript" ';
404 }
405
406 $bodyreplacementpayload = '<script ' . $type_js . $defer . 'src="' . $this->url . '"></script>';
407 $bodyreplacementpayload = apply_filters( 'autoptimize_filter_js_bodyreplacementpayload', $bodyreplacementpayload );
408
409 $bodyreplacement = implode( '', $this->move['first'] );
410 $bodyreplacement .= $bodyreplacementpayload;
411 $bodyreplacement .= implode( '', $this->move['last'] );
412
413 $replaceTag = apply_filters( 'autoptimize_filter_js_replacetag', $replaceTag );
414
415 if ( strlen( $this->jscode ) > 0 ) {
416 $this->inject_in_html( $bodyreplacement, $replaceTag );
417 }
418
419 // Restore comments.
420 $this->content = $this->restore_comments( $this->content );
421
422 // Restore IE hacks.
423 $this->content = $this->restore_iehacks( $this->content );
424
425 // Restore noptimize.
426 $this->content = $this->restore_noptimize( $this->content );
427
428 // Return the modified HTML.
429 return $this->content;
430 }
431
432 // Checks against the white- and blacklists
433 private function ismergeable($tag)
434 {
435 if ( ! $this->aggregate ) {
436 return false;
437 }
438
439 if ( ! empty( $this->whitelist ) ) {
440 foreach ( $this->whitelist as $match ) {
441 if (false !== strpos( $tag, $match ) ) {
442 return true;
443 }
444 }
445 // no match with whitelist
446 return false;
447 } else {
448 foreach($this->domove as $match) {
449 if ( false !== strpos( $tag, $match ) ) {
450 // Matched something
451 return false;
452 }
453 }
454
455 if ( $this->movetolast($tag) ) {
456 return false;
457 }
458
459 foreach( $this->dontmove as $match ) {
460 if ( false !== strpos( $tag, $match ) ) {
461 // Matched something
462 return false;
463 }
464 }
465
466 // If we're here it's safe to merge
467 return true;
468 }
469 }
470
471 // Checks agains the blacklist
472 private function ismovable($tag)
473 {
474 if ( true !== $this->include_inline || apply_filters( 'autoptimize_filter_js_unmovable', true ) ) {
475 return false;
476 }
477
478 foreach ( $this->domove as $match ) {
479 if ( false !== strpos( $tag, $match ) ) {
480 // Matched something
481 return true;
482 }
483 }
484
485 if ( $this->movetolast($tag) ) {
486 return true;
487 }
488
489 foreach ( $this->dontmove as $match ) {
490 if ( false !== strpos( $tag, $match ) ) {
491 // Matched something
492 return false;
493 }
494 }
495
496 // If we're here it's safe to move
497 return true;
498 }
499
500 private function movetolast($tag)
501 {
502 foreach ( $this->domovelast as $match ) {
503 if ( false !== strpos( $tag, $match ) ) {
504 // Matched, return true
505 return true;
506 }
507 }
508
509 // Should be in 'first'
510 return false;
511 }
512
513 /**
514 * Determines wheter a <script> $tag can be excluded from minification (as already minified) based on:
515 * - inject_min_late being active
516 * - filename ending in `min.js`
517 * - filename matching `js/jquery/jquery.js` (wordpress core jquery, is minified)
518 * - filename matching one passed in the consider minified filter
519 *
520 * @param string $jsPath
521 * @return bool
522 */
523 private function can_inject_late($jsPath) {
524 $consider_minified_array = apply_filters( 'autoptimize_filter_js_consider_minified', false );
525 if ( true !== $this->inject_min_late ) {
526 // late-inject turned off
527 return false;
528 } else if ( ( false === strpos( $jsPath, 'min.js' ) ) && ( false === strpos( $jsPath, 'wp-includes/js/jquery/jquery.js' ) ) && ( str_replace( $consider_minified_array, '', $jsPath ) === $jsPath ) ) {
529 // file not minified based on filename & filter
530 return false;
531 } else {
532 // phew, all is safe, we can late-inject
533 return true;
534 }
535 }
536
537 /**
538 * Returns whether we're doing aggregation or not.
539 *
540 * @return bool
541 */
542 public function aggregating()
543 {
544 return $this->aggregate;
545 }
546
547 /**
548 * Minifies a single local js file and returns its (cached) url.
549 *
550 * @param string $filepath Filepath.
551 * @param bool $cache_miss Optional. Force a cache miss. Default false.
552 *
553 * @return bool|string Url pointing to the minified js file or false.
554 */
555 public function minify_single( $filepath, $cache_miss = false )
556 {
557 $contents = $this->prepare_minify_single( $filepath );
558
559 if ( empty( $contents ) ) {
560 return false;
561 }
562
563 // Check cache.
564 $hash = 'single_' . md5( $contents );
565 $cache = new autoptimizeCache( $hash, 'js' );
566
567 // If not in cache already, minify...
568 if ( ! $cache->check() || $cache_miss ) {
569 $contents = trim( JSMin::minify( $contents ) );
570 // Store in cache.
571 $cache->cache( $contents, 'text/javascript' );
572 }
573
574 $url = $this->build_minify_single_url( $cache );
575
576 return $url;
577 }
578 }
579