PluginProbe
Autoptimize / 2.5.1
Autoptimize v2.5.1
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.5.1, at classes/autoptimizeScripts.php

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