PluginProbe
Autoptimize / 2.4.0
Autoptimize v2.4.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.4.0, at classes/autoptimizeScripts.php

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