PluginProbe
Autoptimize / 2.4.3
Autoptimize v2.4.3
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.3, at classes/autoptimizeScripts.php

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