PluginProbe
Autoptimize / 3.1.10
Autoptimize v3.1.10
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 3.1.10, at classes/autoptimizeScripts.php

862 lines 31.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class for JS optimization.
4 */
5
6 if ( ! defined( 'ABSPATH' ) ) {
7 exit;
8 }
9
10 class autoptimizeScripts extends autoptimizeBase
11 {
12
13 /**
14 * Stores founds scripts.
15 *
16 * @var array
17 */
18 private $scripts = array();
19
20 /**
21 * Stores to be moved JS.
22 *
23 * @var array
24 */
25 private $move = array(
26 'first' => array(),
27 'last' => array(),
28 );
29
30 /**
31 * List of not to be moved JS.
32 *
33 * @var array
34 */
35 private $dontmove = array(
36 'document.write',
37 'html5.js',
38 'show_ads.js',
39 'google_ad',
40 'histats.com/js',
41 'statcounter.com/counter/counter.js',
42 'ws.amazon.com/widgets',
43 'media.fastclick.net',
44 '/ads/',
45 'comment-form-quicktags/quicktags.php',
46 'edToolbar',
47 'intensedebate.com',
48 'scripts.chitika.net/',
49 '_gaq.push',
50 'jotform.com/',
51 'admin-bar.min.js',
52 'GoogleAnalyticsObject',
53 'plupload.full.min.js',
54 'syntaxhighlighter',
55 'adsbygoogle',
56 'gist.github.com',
57 '_stq',
58 'nonce',
59 'post_id',
60 'data-noptimize',
61 'data-cfasync',
62 'data-pagespeed-no-defer',
63 'logHuman',
64 'amp-mobile-version-switcher',
65 'data-rocketlazyloadscript',
66 'rocket-browser-checker-js-after',
67 );
68
69 /**
70 * List of to be moved JS.
71 *
72 * @var array
73 */
74 private $domove = array(
75 'gaJsHost',
76 'load_cmc',
77 'jd.gallery.transitions.js',
78 'swfobject.embedSWF(',
79 'tiny_mce.js',
80 'tinyMCEPreInit.go',
81 );
82
83 /**
84 * List of JS that can be moved last (not used any more).
85 *
86 * @var array
87 */
88 private $domovelast = array(
89 'addthis.com',
90 '/afsonline/show_afs_search.js',
91 'disqus.js',
92 'networkedblogs.com/getnetworkwidget',
93 'infolinks.com/js/',
94 'jd.gallery.js.php',
95 'jd.gallery.transitions.js',
96 'swfobject.embedSWF(',
97 'linkwithin.com/widget.js',
98 'tiny_mce.js',
99 'tinyMCEPreInit.go',
100 );
101
102 /**
103 * Setting CDN base URL.
104 *
105 * @var string
106 */
107 public $cdn_url = '';
108
109 /**
110 * Setting; aggregate or not.
111 *
112 * @var bool
113 */
114 private $aggregate = true;
115
116 /**
117 * Setting; if not aggregated, should we defer?
118 *
119 * @var bool
120 */
121 private $defer_not_aggregate = false;
122
123 /**
124 * Setting; defer inline JS?
125 *
126 * @var bool
127 */
128 private $defer_inline = false;
129
130 /**
131 * Setting; try/catch wrapping or not.
132 *
133 * @var bool
134 */
135 private $trycatch = false;
136
137 /**
138 * State; is JS already minified.
139 *
140 * @var bool
141 */
142 private $alreadyminified = false;
143
144 /**
145 * Setting; force JS in head or not.
146 *
147 * @var bool
148 */
149 private $forcehead = true;
150
151 /**
152 * Setting; aggregate inline JS or not.
153 *
154 * @var bool
155 */
156 private $include_inline = false;
157
158 /**
159 * State; holds JS code.
160 *
161 * @var string
162 */
163 private $jscode = '';
164
165 /**
166 * State; holds URL of JS-file.
167 *
168 * @var string
169 */
170 private $url = '';
171
172 /**
173 * State; stores rest of HTML if (old) option "only in head" is on.
174 *
175 * @var string
176 */
177 private $restofcontent = '';
178
179 /**
180 * State; holds md5-hash.
181 *
182 * @var string
183 */
184 private $md5hash = '';
185
186 /**
187 * Setting (filter); allowlist of to be aggregated JS.
188 *
189 * @var string
190 */
191 private $allowlist = '';
192
193 /**
194 * Setting (filter); holds JS that should be removed.
195 *
196 * @var array
197 */
198 private $jsremovables = array();
199
200 /**
201 * Setting (filter); can we inject already minified files after the
202 * unminified aggregate JS has been minified.
203 *
204 * @var bool
205 */
206 private $inject_min_late = true;
207
208 /**
209 * Setting; should excluded JS be minified (if not already).
210 *
211 * @var bool
212 */
213 private $minify_excluded = true;
214
215 /**
216 * Reads the page and collects script tags.
217 *
218 * @param array $options all options.
219 */
220 public function read( $options )
221 {
222 $noptimize_js = false;
223
224 // If page/ post check post_meta to see if optimize is off.
225 if ( false === autoptimizeConfig::get_post_meta_ao_settings( 'ao_post_js_optimize' ) ) {
226 $noptimize_js = true;
227 }
228
229 // And a filter to enforce JS noptimize.
230 $noptimize_js = apply_filters( 'autoptimize_filter_js_noptimize', $noptimize_js, $this->content );
231
232 // And finally bail if noptimize_js is true.
233 if ( $noptimize_js ) {
234 return false;
235 }
236
237 // only optimize known good JS?
238 $allowlist_js = apply_filters( 'autoptimize_filter_js_allowlist', '', $this->content );
239 if ( ! empty( $allowlist_js ) ) {
240 $this->allowlist = array_filter( array_map( 'trim', explode( ',', $allowlist_js ) ) );
241 }
242
243 // is there JS we should simply remove?
244 $removable_js = apply_filters( 'autoptimize_filter_js_removables', '', $this->content );
245 if ( ! empty( $removable_js ) ) {
246 $this->jsremovables = array_filter( array_map( 'trim', explode( ',', $removable_js ) ) );
247 }
248
249 // only header?
250 if ( apply_filters( 'autoptimize_filter_js_justhead', $options['justhead'] ) ) {
251 $content = explode( '</head>', $this->content, 2 );
252 $this->content = $content[0] . '</head>';
253 $this->restofcontent = $content[1];
254 }
255
256 // Determine whether we're doing JS-files aggregation or not.
257 if ( ! $options['aggregate'] ) {
258 $this->aggregate = false;
259 }
260 // Returning true for "dontaggregate" turns off aggregation.
261 if ( $this->aggregate && apply_filters( 'autoptimize_filter_js_dontaggregate', false ) ) {
262 $this->aggregate = false;
263 }
264 // and the filter that should have been there to begin with.
265 $this->aggregate = apply_filters( 'autoptimize_filter_js_aggregate', $this->aggregate );
266
267 // Defer when not aggregating.
268 if ( false === $this->aggregate && apply_filters( 'autoptimize_filter_js_defer_not_aggregate', $options['defer_not_aggregate'] ) ) {
269 $this->defer_not_aggregate = true;
270 }
271
272 // Defer inline JS?
273 if ( ( true === $this->defer_not_aggregate && apply_filters( 'autoptimize_js_filter_defer_inline', $options['defer_inline'] ) ) || apply_filters( 'autoptimize_js_filter_force_defer_inline', false ) ) {
274 $this->defer_inline = true;
275 }
276
277 // include inline?
278 if ( apply_filters( 'autoptimize_js_include_inline', $options['include_inline'] ) ) {
279 $this->include_inline = true;
280 }
281
282 // filter to "late inject minified JS", default to true for now (it is faster).
283 $this->inject_min_late = apply_filters( 'autoptimize_filter_js_inject_min_late', true );
284
285 // filters to override hardcoded do(nt)move(last) array contents (array in, array out!).
286 $this->dontmove = apply_filters( 'autoptimize_filter_js_dontmove', $this->dontmove );
287 $this->domovelast = apply_filters( 'autoptimize_filter_js_movelast', $this->domovelast );
288 $this->domove = apply_filters( 'autoptimize_filter_js_domove', $this->domove );
289
290 // Determine whether excluded files should be minified if not yet so.
291 if ( ! $options['minify_excluded'] && $options['aggregate'] ) {
292 $this->minify_excluded = false;
293 }
294 $this->minify_excluded = apply_filters( 'autoptimize_filter_js_minify_excluded', $this->minify_excluded, '' );
295
296 // get extra exclusions settings or filter.
297 $exclude_js = $options['js_exclude'];
298 $exclude_js = apply_filters( 'autoptimize_filter_js_exclude', $exclude_js, $this->content );
299
300 if ( '' !== $exclude_js ) {
301 if ( is_array( $exclude_js ) ) {
302 $remove_keys = array_keys( $exclude_js, 'remove' );
303 if ( false !== $remove_keys ) {
304 foreach ( $remove_keys as $remove_key ) {
305 unset( $exclude_js[ $remove_key ] );
306 $this->jsremovables[] = $remove_key;
307 }
308 }
309 $excl_js_arr = array_keys( $exclude_js );
310 } else {
311 $excl_js_arr = array_filter( array_map( 'trim', explode( ',', $exclude_js ) ) );
312 }
313 $this->dontmove = array_merge( $excl_js_arr, $this->dontmove );
314 }
315
316 // Should we add try-catch?
317 if ( $options['trycatch'] ) {
318 $this->trycatch = true;
319 }
320
321 // force js in head?
322 if ( $options['forcehead'] ) {
323 $this->forcehead = true;
324 } else {
325 $this->forcehead = false;
326 }
327
328 $this->forcehead = apply_filters( 'autoptimize_filter_js_forcehead', $this->forcehead );
329
330 // get cdn url.
331 $this->cdn_url = $options['cdn_url'];
332
333 // noptimize me.
334 $this->content = $this->hide_noptimize( $this->content );
335
336 // Save IE hacks.
337 $this->content = $this->hide_iehacks( $this->content );
338
339 // comments.
340 $this->content = $this->hide_comments( $this->content );
341
342 // Get script files.
343 if ( preg_match_all( '#<script.*</script>#Usmi', $this->content, $matches ) ) {
344 foreach ( $matches[0] as $tag ) {
345 // only consider script aggregation for types allowlisted in should_aggregate-function.
346 $should_aggregate = $this->should_aggregate( $tag );
347 if ( ! $should_aggregate ) {
348 $tag = '';
349 continue;
350 }
351
352 if ( preg_match( '#<script[^>]*src=("|\')([^>]*)("|\')#Usmi', $tag, $source ) ) {
353 // non-inline script.
354 if ( $this->isremovable( $tag, $this->jsremovables ) ) {
355 $this->content = str_replace( $tag, '', $this->content );
356 continue;
357 }
358
359 $orig_tag = null;
360 $url = current( explode( '?', $source[2], 2 ) );
361 $path = $this->getpath( $url );
362 if ( false !== $path && preg_match( '#\.js$#', $path ) && $this->ismergeable( $tag ) ) {
363 // ok to optimize, add to array.
364 $this->scripts[] = $path;
365 } else {
366 $orig_tag = $tag;
367 $new_tag = $tag;
368
369 // non-mergeable script (excluded or dynamic or external).
370 if ( is_array( $exclude_js ) ) {
371 // should we add flags?
372 foreach ( $exclude_js as $excl_tag => $excl_flags ) {
373 if ( false !== strpos( $orig_tag, $excl_tag ) && in_array( $excl_flags, array( 'async', 'defer' ) ) ) {
374 $new_tag = str_replace( '<script ', '<script ' . $excl_flags . ' ', $new_tag );
375 }
376 }
377 }
378
379 // not aggregating but deferring?
380 if ( $this->defer_not_aggregate && false === $this->aggregate && ( str_replace( $this->dontmove, '', $path ) === $path || ( apply_filters( 'autoptimize_filter_js_defer_external', true ) && str_replace( $this->dontmove, '', $orig_tag ) === $orig_tag ) ) && strpos( $new_tag, ' defer' ) === false ) {
381 if ( false !== strpos( $new_tag, ' async' ) && true === apply_filters( 'autoptimize_filter_js_defer_trumps_async', true ) ) {
382 // remove async flag to ensure JS is properly deferred, otherwise the asynced JS might fire
383 // before deferred inlined JS is executed, off course except filter is set to false which
384 // re-institutes previous behavior.
385 $new_tag = str_replace( array( " async='async'", ' async="async"', ' async=async', ' async' ), '', $new_tag );
386 }
387
388 if ( false === strpos( $new_tag, ' async' ) ) {
389 // either async wasn't there to begin with or it was removed.
390 // if async is there, the autoptimize_filter_js_defer_trumps_async
391 // filter was set to false and in that case defer should not be added.
392 $new_tag = str_replace( '<script ', '<script defer ', $new_tag );
393 }
394 }
395
396 // Should we minify the non-aggregated script?
397 // -> if aggregate is on and exclude minify is on
398 // -> if aggregate is off and the file is not in dontmove.
399 if ( $path && $this->minify_excluded ) {
400 $consider_minified_array = apply_filters( 'autoptimize_filter_js_consider_minified', false );
401 if ( ( false === $this->aggregate && str_replace( $this->dontmove, '', $path ) === $path ) || ( true === $this->aggregate && ( false === $consider_minified_array || str_replace( $consider_minified_array, '', $path ) === $path ) ) ) {
402 $minified_url = $this->minify_single( $path );
403 if ( ! empty( $minified_url ) ) {
404 // Replace original URL with minified URL from cache.
405 $new_tag = str_replace( $url, $minified_url, $new_tag );
406 } elseif ( apply_filters( 'autoptimize_filter_ccsjs_remove_empty_minified_url', false ) ) {
407 // Remove the original script tag, because cache content is empty but only if filter
408 // is trued because $minified_url is also false if original JS is minified already.
409 $new_tag = '';
410 }
411 }
412 }
413
414 // Check if we still need to CDN (esp. for already minified resources).
415 if ( ! empty( $this->cdn_url ) || has_filter( 'autoptimize_filter_base_replace_cdn' ) ) {
416 $new_tag = str_replace( $url, $this->url_replace_cdn( $url ), $new_tag );
417 }
418
419 if ( $this->ismovable( $new_tag ) ) {
420 // can be moved, flags and all.
421 if ( $this->movetolast( $new_tag ) ) {
422 $this->move['last'][] = $new_tag;
423 } else {
424 $this->move['first'][] = $new_tag;
425 }
426 } else {
427 // cannot be moved, so if flag was added re-inject altered tag immediately.
428 if ( ( '' !== $new_tag && $orig_tag !== $new_tag ) || ( '' === $new_tag && apply_filters( 'autoptimize_filter_js_remove_empty_files', false ) ) ) {
429 $this->content = str_replace( $orig_tag, $new_tag, $this->content );
430 $orig_tag = '';
431 }
432 // and forget about the $tag (not to be touched any more).
433 $tag = '';
434 }
435 }
436 } else {
437 // Inline script.
438 if ( $this->isremovable( $tag, $this->jsremovables ) ) {
439 $this->content = str_replace( $tag, '', $this->content );
440 continue;
441 }
442
443 // unhide comments, as javascript may be wrapped in comment-tags for old times' sake.
444 $tag = $this->restore_comments( $tag );
445 if ( $this->ismergeable( $tag ) && $this->include_inline ) {
446 preg_match( '#<script.*>(.*)</script>#Usmi', $tag, $code );
447 $code = preg_replace( '#.*<!\[CDATA\[(?:\s*\*/)?(.*)(?://|/\*)\s*?\]\]>.*#sm', '$1', $code[1] );
448 $code = preg_replace( '/(?:^\\s*<!--\\s*|\\s*(?:\\/\\/)?\\s*-->\\s*$)/', '', $code );
449 $this->scripts[] = 'INLINE;' . $code;
450 } else {
451 $_inline_deferable = apply_filters( 'autoptimize_filters_js_inline_deferable', array( 'nonce', 'post_id', 'syntaxhighlighter' ) );
452 $_inline_dontmove = array_values( array_diff( $this->dontmove, $_inline_deferable ) );
453 if ( false === $this->defer_inline ) {
454 // Can we move this?
455 $autoptimize_js_moveable = apply_filters( 'autoptimize_js_moveable', '', $tag );
456 if ( $this->ismovable( $tag ) || '' !== $autoptimize_js_moveable ) {
457 if ( $this->movetolast( $tag ) || 'last' === $autoptimize_js_moveable ) {
458 $this->move['last'][] = $tag;
459 } else {
460 $this->move['first'][] = $tag;
461 }
462 } else {
463 $tag = '';
464 }
465 } else if ( str_replace( $_inline_dontmove, '', $tag ) === $tag && strlen( $tag ) < apply_filters( 'autoptimize_filter_script_defer_inline_maxsize', 200000 ) ) {
466 // defer inline JS by base64 encoding it but only if string is not ridiculously huge (to avoid issues with below regex mainly).
467 preg_match( '#<script(?:[^>](?!id=))*\s*(?:id=(["\'])([^"\']+)\1)*+[^>]*+>(.*?)<\/script>#is', $tag, $match );
468 if ( $match[2] ) {
469 $_id = 'id="' . $match[2] . '" ';
470 } else {
471 $_id = '';
472 }
473
474 // if "minify inline" is on and if more then 9 spaces or 4 line breaks are found
475 // in the inline JS then it is likely not minified, so minify before base64-encoding.
476 $_script_contents = $match[3];
477 if ( 'on' === autoptimizeOptionWrapper::get_option( 'autoptimize_html_minify_inline', 'off' ) && substr_count( $_script_contents, ' ' ) > 9 && substr_count( $_script_contents, "\n" ) > 4 && true === apply_filters( 'autoptimize_filter_script_defer_inline_minify', true ) ) {
478 $_tmp_script_contents = trim( JSMin::minify( $_script_contents ) );
479 if ( ! empty( $_tmp_script_contents ) ) {
480 $_script_contents = $_tmp_script_contents;
481 }
482 }
483
484 // base64 and defer the lot already.
485 $new_tag = '<script defer ' . $_id . 'src="data:text/javascript;base64,' . base64_encode( $_script_contents ) . '"></script>';
486 $this->content = str_replace( $this->hide_comments( $tag ), $new_tag, $this->content );
487 $tag = '';
488 } else {
489 $tag = '';
490 }
491 }
492 // Re-hide comments to be able to do the removal based on tag from $this->content.
493 $tag = $this->hide_comments( $tag );
494 }
495
496 // Remove the original script tag.
497 $this->content = str_replace( $tag, '', $this->content );
498 }
499
500 return true;
501 }
502
503 // No script files, great ;-) .
504 return false;
505 }
506
507 /**
508 * Determines wheter a certain `<script>` $tag should be aggregated or not.
509 *
510 * We consider these as "aggregation-safe" currently:
511 * - script tags without a `type` attribute
512 * - script tags with these `type` attribute values: `text/javascript`, `text/ecmascript`, `application/javascript`,
513 * and `application/ecmascript`
514 *
515 * Everything else should return false.
516 *
517 * @link https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
518 *
519 * @param string $tag Script node & child(ren).
520 * @return bool
521 */
522 public static function should_aggregate( $tag )
523 {
524 if ( empty( $tag ) ) {
525 return false;
526 }
527
528 // We're only interested in the type attribute of the <script> tag itself, not any possible
529 // inline code that might just contain the 'type=' string...
530 $tag_parts = array();
531 preg_match( '#<(script[^>]*)>#i', $tag, $tag_parts );
532 $tag_without_contents = null;
533 if ( ! empty( $tag_parts[1] ) ) {
534 $tag_without_contents = $tag_parts[1];
535 }
536
537 $has_type = ( strpos( $tag_without_contents, 'type' ) !== false );
538
539 $type_valid = false;
540 if ( $has_type ) {
541 $type_valid = (bool) preg_match( '/type\s*=\s*[\'"]?(?:text|application)\/(?:javascript|ecmascript)[\'"]?/i', $tag_without_contents );
542 }
543
544 $should_aggregate = false;
545 if ( ! $has_type || $type_valid ) {
546 $should_aggregate = true;
547 }
548
549 return $should_aggregate;
550 }
551
552 /**
553 * Joins and optimizes JS.
554 */
555 public function minify()
556 {
557 foreach ( $this->scripts as $script ) {
558 if ( empty( $script ) ) {
559 continue;
560 }
561
562 // TODO/FIXME: some duplicate code here, can be reduced/simplified.
563 if ( preg_match( '#^INLINE;#', $script ) ) {
564 // Inline script.
565 $script = preg_replace( '#^INLINE;#', '', $script );
566 $script = rtrim( $script, ";\n\t\r" ) . ';';
567 // Add try-catch?
568 if ( $this->trycatch ) {
569 $script = 'try{' . $script . '}catch(e){}';
570 }
571 $tmpscript = apply_filters( 'autoptimize_js_individual_script', $script, '' );
572 if ( has_filter( 'autoptimize_js_individual_script' ) && ! empty( $tmpscript ) ) {
573 $script = $tmpscript;
574 $this->alreadyminified = true;
575 }
576 $this->jscode .= "\n" . $script;
577 } else {
578 // External script.
579 if ( false !== $script && file_exists( $script ) && is_readable( $script ) ) {
580 $scriptsrc = file_get_contents( $script );
581 $scriptsrc = preg_replace( '/\x{EF}\x{BB}\x{BF}/', '', $scriptsrc );
582 $scriptsrc = rtrim( $scriptsrc, ";\n\t\r" ) . ';';
583 // Add try-catch?
584 if ( $this->trycatch ) {
585 $scriptsrc = 'try{' . $scriptsrc . '}catch(e){}';
586 }
587 $tmpscriptsrc = apply_filters( 'autoptimize_js_individual_script', $scriptsrc, $script );
588 if ( has_filter( 'autoptimize_js_individual_script' ) && ! empty( $tmpscriptsrc ) ) {
589 $scriptsrc = $tmpscriptsrc;
590 $this->alreadyminified = true;
591 } elseif ( $this->can_inject_late( $script ) ) {
592 $scriptsrc = self::build_injectlater_marker( $script, md5( $scriptsrc ) );
593 }
594 $this->jscode .= "\n" . $scriptsrc;
595 }
596 }
597 }
598
599 // Check for already-minified code.
600 $this->md5hash = md5( $this->jscode );
601 $ccheck = new autoptimizeCache( $this->md5hash, 'js' );
602 if ( $ccheck->check() ) {
603 $this->jscode = $ccheck->retrieve();
604 return true;
605 }
606 unset( $ccheck );
607
608 // $this->jscode has all the uncompressed code now.
609 if ( true !== $this->alreadyminified ) {
610 if ( apply_filters( 'autoptimize_js_do_minify', true ) ) {
611 $tmp_jscode = trim( JSMin::minify( $this->jscode ) );
612 if ( ! empty( $tmp_jscode ) ) {
613 $this->jscode = $tmp_jscode;
614 unset( $tmp_jscode );
615 }
616 $this->jscode = $this->inject_minified( $this->jscode );
617 $this->jscode = apply_filters( 'autoptimize_js_after_minify', $this->jscode );
618 return true;
619 } else {
620 $this->jscode = $this->inject_minified( $this->jscode );
621 return false;
622 }
623 }
624
625 $this->jscode = apply_filters( 'autoptimize_js_after_minify', $this->jscode );
626 return true;
627 }
628
629 /**
630 * Caches the JS in uncompressed, deflated and gzipped form.
631 */
632 public function cache()
633 {
634 $cache = new autoptimizeCache( $this->md5hash, 'js' );
635 if ( ! $cache->check() ) {
636 // Cache our code.
637 $cache->cache( $this->jscode, 'text/javascript' );
638 }
639 $this->url = AUTOPTIMIZE_CACHE_URL . $cache->getname();
640 $this->url = $this->url_replace_cdn( $this->url );
641 }
642
643 /**
644 * Returns the content.
645 */
646 public function getcontent()
647 {
648 // Restore the full content.
649 if ( ! empty( $this->restofcontent ) ) {
650 $this->content .= $this->restofcontent;
651 $this->restofcontent = '';
652 }
653
654 // Add the scripts taking forcehead/ deferred (default) into account.
655 if ( $this->forcehead ) {
656 $replace_tag = array( '</head>', 'before' );
657 $defer = '';
658 } else {
659 $replace_tag = array( '</body>', 'before' );
660 $defer = 'defer ';
661 }
662
663 $defer = apply_filters( 'autoptimize_filter_js_defer', $defer );
664 $type_js = '';
665 if ( apply_filters( 'autoptimize_filter_cssjs_addtype', false ) ) {
666 $type_js = 'type="text/javascript" ';
667 }
668
669 $bodyreplacementpayload = '<script ' . $type_js . $defer . 'src="' . $this->url . '"></script>';
670 $bodyreplacementpayload = apply_filters( 'autoptimize_filter_js_bodyreplacementpayload', $bodyreplacementpayload );
671
672 $bodyreplacement = implode( '', $this->move['first'] );
673 $bodyreplacement .= $bodyreplacementpayload;
674 $bodyreplacement .= implode( '', $this->move['last'] );
675
676 $replace_tag = apply_filters( 'autoptimize_filter_js_replacetag', $replace_tag );
677
678 if ( strlen( $this->jscode ) > 0 ) {
679 $this->inject_in_html( $bodyreplacement, $replace_tag );
680 }
681
682 // Restore comments.
683 $this->content = $this->restore_comments( $this->content );
684
685 // Restore IE hacks.
686 $this->content = $this->restore_iehacks( $this->content );
687
688 // Restore noptimize.
689 $this->content = $this->restore_noptimize( $this->content );
690
691 // Return the modified HTML.
692 return $this->content;
693 }
694
695 /**
696 * Checks against the allow- and blocklists.
697 *
698 * @param string $tag JS tag.
699 */
700 private function ismergeable( $tag )
701 {
702 if ( empty( $tag ) || ! $this->aggregate ) {
703 return false;
704 }
705
706 if ( ! empty( $this->allowlist ) ) {
707 foreach ( $this->allowlist as $match ) {
708 if ( false !== strpos( $tag, $match ) ) {
709 return true;
710 }
711 }
712 // No match with allowlist.
713 return false;
714 } else {
715 foreach ( $this->domove as $match ) {
716 if ( false !== strpos( $tag, $match ) ) {
717 // Matched something.
718 return false;
719 }
720 }
721
722 if ( $this->movetolast( $tag ) ) {
723 return false;
724 }
725
726 foreach ( $this->dontmove as $match ) {
727 if ( false !== strpos( $tag, $match ) ) {
728 // Matched something.
729 return false;
730 }
731 }
732
733 // If we're here it's safe to merge.
734 return true;
735 }
736 }
737
738 /**
739 * Checks agains the blocklist.
740 *
741 * @param string $tag tag to check for blocklist (exclusions).
742 */
743 private function ismovable( $tag )
744 {
745 if ( empty( $tag ) || true !== $this->include_inline || apply_filters( 'autoptimize_filter_js_unmovable', true ) ) {
746 return false;
747 }
748
749 foreach ( $this->domove as $match ) {
750 if ( false !== strpos( $tag, $match ) ) {
751 // Matched something.
752 return true;
753 }
754 }
755
756 if ( $this->movetolast( $tag ) ) {
757 return true;
758 }
759
760 foreach ( $this->dontmove as $match ) {
761 if ( false !== strpos( $tag, $match ) ) {
762 // Matched something.
763 return false;
764 }
765 }
766
767 // If we're here it's safe to move.
768 return true;
769 }
770
771 private function movetolast( $tag )
772 {
773 if ( empty( $tag ) ) {
774 return false;
775 }
776
777 foreach ( $this->domovelast as $match ) {
778 if ( false !== strpos( $tag, $match ) ) {
779 // Matched, return true.
780 return true;
781 }
782 }
783
784 // Should be in 'first'.
785 return false;
786 }
787
788 /**
789 * Determines wheter a <script> $tag can be excluded from minification (as already minified) based on:
790 * - inject_min_late being active
791 * - filename ending in `min.js`
792 * - filename matching one passed in the consider minified filter
793 *
794 * @param string $js_path Path to JS file.
795 * @return bool
796 */
797 private function can_inject_late( $js_path ) {
798 $consider_minified_array = apply_filters( 'autoptimize_filter_js_consider_minified', false );
799 if ( true !== $this->inject_min_late ) {
800 // late-inject turned off.
801 return false;
802 } elseif ( ( false === strpos( $js_path, 'min.js' ) ) && ( str_replace( $consider_minified_array, '', $js_path ) === $js_path ) ) {
803 // file not minified based on filename & filter.
804 return false;
805 } else {
806 // phew, all is safe, we can late-inject.
807 return true;
808 }
809 }
810
811 /**
812 * Returns whether we're doing aggregation or not.
813 *
814 * @return bool
815 */
816 public function aggregating()
817 {
818 return $this->aggregate;
819 }
820
821 /**
822 * Minifies a single local js file and returns its (cached) url.
823 *
824 * @param string $filepath Filepath.
825 * @param bool $cache_miss Optional. Force a cache miss. Default false.
826 *
827 * @return bool|string Url pointing to the minified js file or false.
828 */
829 public function minify_single( $filepath, $cache_miss = false )
830 {
831 $contents = $this->prepare_minify_single( $filepath );
832
833 if ( empty( $contents ) ) {
834 return false;
835 }
836
837 // Check cache.
838 $hash = 'single_' . md5( $contents );
839 $cache = new autoptimizeCache( $hash, 'js' );
840
841 // If not in cache already, minify...
842 if ( ! $cache->check() || $cache_miss ) {
843 $contents = trim( JSMin::minify( $contents ) );
844
845 // Check if minified cache content is empty.
846 if ( empty( $contents ) ) {
847 return false;
848 }
849
850 // Filter contents of excluded minified CSS.
851 $contents = apply_filters( 'autoptimize_filter_js_single_after_minify', $contents );
852
853 // Store in cache.
854 $cache->cache( $contents, 'text/javascript' );
855 }
856
857 $url = $this->build_minify_single_url( $cache );
858
859 return $url;
860 }
861 }
862