PluginProbe
Autoptimize / 2.5.0
Autoptimize v2.5.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.5.0, at classes/autoptimizeScripts.php

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