PluginProbe ʕ •ᴥ•ʔ
LiteSpeed Cache / 1.9.1.1
LiteSpeed Cache v1.9.1.1
trunk 1.0.15 1.9.1.1 2.9.9.2 3.6.4 4.6 5.7.0.1 6.5.4 7.0.0.1 7.0.1 7.1 7.2 7.3 7.3.0.1 7.4 7.5 7.5.0.1 7.6 7.6.1 7.6.2 7.7 7.8 7.8.0.1 7.8.1
litespeed-cache / inc / optimize.class.php
litespeed-cache / inc Last commit date
activation.class.php 8 years ago api.class.php 8 years ago cdn.class.php 8 years ago config.class.php 8 years ago control.class.php 8 years ago crawler-sitemap.class.php 8 years ago crawler.class.php 8 years ago data.class.php 8 years ago esi.class.php 8 years ago gui.class.php 8 years ago import.class.php 8 years ago litespeed-cache.class.php 8 years ago litespeed.autoload.php 8 years ago log.class.php 8 years ago media.class.php 8 years ago object.class.php 8 years ago object.lib.php 8 years ago optimize.class.php 8 years ago optimizer.class.php 8 years ago purge.class.php 8 years ago router.class.php 8 years ago tag.class.php 8 years ago task.class.php 8 years ago utility.class.php 8 years ago vary.class.php 8 years ago
optimize.class.php
1163 lines
1 <?php
2
3 /**
4 * The optimize class.
5 *
6 * @since 1.2.2
7 * @since 1.5 Moved into /inc
8 * @package LiteSpeed_Cache
9 * @subpackage LiteSpeed_Cache/inc
10 * @author LiteSpeed Technologies <info@litespeedtech.com>
11 */
12
13 class LiteSpeed_Cache_Optimize
14 {
15 private static $_instance ;
16
17 const DIR_MIN = '/min' ;
18 const CSS_ASYNC_LIB = '/min/css_async.js' ;
19
20 private $content ;
21 private $http2_headers = array() ;
22
23 private $cfg_http2_css ;
24 private $cfg_http2_js ;
25 private $cfg_css_minify ;
26 private $cfg_css_combine ;
27 private $cfg_js_minify ;
28 private $cfg_js_combine ;
29 private $cfg_html_minify ;
30 private $cfg_css_async ;
31 private $cfg_js_defer ;
32 private $cfg_js_defer_exc = false ;
33 private $cfg_qs_rm ;
34 private $cfg_exc_jquery ;
35 private $cfg_ggfonts_async ;
36 private $cfg_optm_max_size ;
37
38 private $dns_prefetch ;
39
40 private $html_foot = '' ; // The html info append to <body>
41 private $html_head = '' ; // The html info prepend to <body>
42 private $css_to_be_removed = array() ;
43
44 private $minify_cache ;
45 private $minify_minify ;
46 private $minify_env ;
47 private $minify_sourceFactory ;
48 private $minify_controller ;
49 private $minify_options ;
50
51 /**
52 * Init optimizer
53 *
54 * @since 1.2.2
55 * @access private
56 */
57 private function __construct()
58 {
59 $this->cfg_http2_css = LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_CSS_HTTP2 ) ;
60 $this->cfg_http2_js = LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_JS_HTTP2 ) ;
61 $this->cfg_css_minify = LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_CSS_MINIFY ) ;
62 $this->cfg_css_combine = LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_CSS_COMBINE ) ;
63 $this->cfg_js_minify = LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_JS_MINIFY ) ;
64 $this->cfg_js_combine = LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_JS_COMBINE ) ;
65 $this->cfg_html_minify = LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_HTML_MINIFY ) ;
66 $this->cfg_css_async = LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_OPTM_CSS_ASYNC ) ;
67 $this->cfg_js_defer = LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_OPTM_JS_DEFER ) ;
68 $this->cfg_qs_rm = LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_OPTM_QS_RM ) ;
69 $this->cfg_exc_jquery = LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_OPTM_EXC_JQUERY ) ;
70 $this->cfg_ggfonts_async = LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_OPTM_GGFONTS_ASYNC ) ;
71 $this->cfg_optm_max_size = LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_OPTM_MAX_SIZE ) * 1000000 ;
72
73 $this->_static_request_check() ;
74
75 if ( ! $this->_can_optm() ) {
76 return ;
77 }
78
79 // To remove emoji from WP
80 if ( LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_OPTM_EMOJI_RM ) ) {
81 add_action( 'init', array( $this, 'emoji_rm' ) ) ;
82 }
83
84 if ( $this->cfg_qs_rm ) {
85 add_filter( 'style_loader_src', array( $this, 'remove_query_strings' ), 999 ) ;
86 add_filter( 'script_loader_src', array( $this, 'remove_query_strings' ), 999 ) ;
87 }
88
89 // Check if there is any critical css rules setting
90 if ( $this->cfg_css_async ) {
91 add_action( 'wp_head', array( $this, 'prepend_critical_css' ), 1 ) ;
92 }
93
94 /**
95 * Exclude js from deferred setting
96 * @since 1.5
97 */
98 if ( $this->cfg_js_defer ) {
99 $this->cfg_js_defer_exc = apply_filters( 'litespeed_optm_js_defer_exc', get_option( LiteSpeed_Cache_Config::ITEM_OPTM_JS_DEFER_EXC ) ) ;
100 if ( $this->cfg_js_defer_exc ) {
101 $this->cfg_js_defer_exc = explode( "\n", $this->cfg_js_defer_exc ) ;
102 }
103 }
104
105 /**
106 * Add vary filter for Role Excludes
107 * @since 1.6
108 */
109 add_filter( 'litespeed_vary', array( $this, 'vary_add_role_exclude' ) ) ;
110
111 /**
112 * Prefetch DNS
113 * @since 1.7.1
114 */
115 $this->_dns_prefetch_init() ;
116
117 }
118
119 /**
120 * Exclude role from optimization filter
121 *
122 * @since 1.6
123 * @access public
124 */
125 public function vary_add_role_exclude( $varys )
126 {
127 if ( ! LiteSpeed_Cache_Config::get_instance()->in_exclude_optimization_roles() ) {
128 return $varys ;
129 }
130 $varys[ 'role_exclude_optm' ] = 1 ;
131 return $varys ;
132 }
133
134 /**
135 * Remove emoji from WP
136 *
137 * @since 1.4
138 * @access public
139 */
140 public function emoji_rm()
141 {
142 remove_action( 'wp_head' , 'print_emoji_detection_script', 7 ) ;
143 remove_action( 'admin_print_scripts' , 'print_emoji_detection_script' ) ;
144 remove_filter( 'the_content_feed' , 'wp_staticize_emoji' ) ;
145 remove_filter( 'comment_text_rss' , 'wp_staticize_emoji' ) ;
146 /**
147 * Added for better result
148 * @since 1.6.2.1
149 */
150 remove_action( 'wp_print_styles', 'print_emoji_styles' ) ;
151 remove_action( 'admin_print_styles', 'print_emoji_styles' ) ;
152 remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' ) ;
153 }
154
155 /**
156 * Output critical css
157 *
158 * @since 1.3
159 * @access public
160 * @return string The static file content
161 */
162 public function prepend_critical_css()
163 {
164
165 $rules = get_option( LiteSpeed_Cache_Config::ITEM_OPTM_CSS ) ;
166 if ( ! $rules ) {
167 return ;
168 }
169
170 echo '<style id="litespeed-optm-css-rules">' . $rules . '</style>' ;
171 }
172
173 /**
174 * Check if the request is for static file
175 *
176 * @since 1.2.2
177 * @access private
178 * @return string The static file content
179 */
180 private function _static_request_check()
181 {
182 // This request is for js/css_async.js
183 if ( ( $this->cfg_css_async || $this->cfg_ggfonts_async ) && strpos( $_SERVER[ 'REQUEST_URI' ], self::CSS_ASYNC_LIB ) !== false ) {
184 LiteSpeed_Cache_Log::debug( 'Optimizer start serving static file' ) ;
185
186 LiteSpeed_Cache_Control::set_cacheable() ;
187 LiteSpeed_Cache_Control::set_public_forced( 'OPTM: css async js' ) ;
188 LiteSpeed_Cache_Control::set_no_vary() ;
189 LiteSpeed_Cache_Control::set_custom_ttl( 8640000 ) ;
190 LiteSpeed_Cache_Tag::add( LiteSpeed_Cache_Tag::TYPE_MIN . '_CSS_ASYNC' ) ;
191
192 $file = LSCWP_DIR . 'js/css_async.min.js' ;
193
194 header( 'Content-Length: ' . filesize( $file ) ) ;
195 header( 'Content-Type: application/x-javascript; charset=utf-8' ) ;
196
197 echo file_get_contents( $file ) ;
198 exit ;
199 }
200
201 // If not turn on min files
202 if ( ! $this->cfg_css_minify && ! $this->cfg_css_combine && ! $this->cfg_js_minify && ! $this->cfg_js_combine ) {
203 return ;
204 }
205
206 if ( empty( $_SERVER[ 'REQUEST_URI' ] ) || strpos( $_SERVER[ 'REQUEST_URI' ], self::DIR_MIN . '/' ) === false ) {
207 return ;
208 }
209
210 // try to match `http://home_url/min/xx.css
211 if ( ! preg_match( '#' . self::DIR_MIN . '/(\w+\.(css|js))#U', $_SERVER[ 'REQUEST_URI' ], $match ) ) {
212 return ;
213 }
214
215 LiteSpeed_Cache_Log::debug( 'Optimizer start minifying file' ) ;
216
217 // Proceed css/js file generation
218 define( 'LITESPEED_MIN_FILE', true ) ;
219
220 $file_type = substr( $match[ 1 ], strrpos( $match[ 1 ], '.' ) + 1 ) ;
221 $concat_only = ! ( $file_type === 'css' ? $this->cfg_css_minify : $this->cfg_js_minify ) ;
222
223 $content = LiteSpeed_Cache_Optimizer::get_instance()->serve( $match[ 1 ], $concat_only ) ;
224
225 if ( ! $content ) {
226 LiteSpeed_Cache_Control::set_nocache( 'Empty content from optimizer' ) ;
227 exit ;
228 }
229
230 LiteSpeed_Cache_Control::set_cacheable() ;
231 LiteSpeed_Cache_Control::set_public_forced( 'OPTM: min file ' . $match[ 1 ] ) ;
232 LiteSpeed_Cache_Control::set_no_vary() ;
233 LiteSpeed_Cache_Control::set_custom_ttl( LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_OPTIMIZE_TTL ) ) ;
234 LiteSpeed_Cache_Tag::add( LiteSpeed_Cache_Tag::TYPE_MIN ) ;
235
236 echo $content ;
237 exit ;
238 }
239
240 /**
241 * Remove QS
242 *
243 * @since 1.3
244 * @access public
245 */
246 public function remove_query_strings( $src )
247 {
248 if ( strpos( $src, 'ver=' ) !== false ) {
249 $src = preg_replace( '#[&\?]+(ver=([\w\-\.]+))#i', '', $src ) ;
250 }
251 return $src ;
252 }
253
254 /**
255 * Check if can run optimize
256 *
257 * @since 1.3
258 * @access private
259 */
260 private function _can_optm()
261 {
262 if ( is_admin() ) {
263 return false ;
264 }
265
266 if ( is_feed() ) {
267 return false ;
268 }
269
270 if ( is_preview() ) {
271 return false ;
272 }
273
274 if ( LiteSpeed_Cache_Router::is_ajax() ) {
275 return false ;
276 }
277
278 return true ;
279 }
280
281 /**
282 * Run optimize process
283 * NOTE: As this is after cache finalized, can NOT set any cache control anymore
284 *
285 * @since 1.2.2
286 * @access public
287 * @return string The content that is after optimization
288 */
289 public static function finalize( $content )
290 {
291 if ( defined( 'LITESPEED_MIN_FILE' ) ) {// Must have this to avoid css/js from optimization again ( But can be removed as mini file doesn't have LITESPEED_IS_HTML, keep for efficiency)
292 return $content ;
293 }
294
295 if ( ! defined( 'LITESPEED_IS_HTML' ) ) {
296 LiteSpeed_Cache_Log::debug( 'Optimizer bypass: Not frontend HTML type' ) ;
297 return $content ;
298 }
299
300 // Check if hit URI excludes
301 $excludes = LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_OPTM_EXCLUDES ) ;
302 if ( ! empty( $excludes ) ) {
303 $result = LiteSpeed_Cache_Utility::str_hit_array( esc_url( $_SERVER[ 'REQUEST_URI' ] ), explode( "\n", $excludes ) ) ;
304 if ( $result ) {
305 LiteSpeed_Cache_Log::debug( 'Optimizer bypass: hit URI Excludes setting: ' . $result ) ;
306 return $content ;
307 }
308 }
309
310 // Check if is exclude optm roles ( Need to set Vary too )
311 if ( $result = LiteSpeed_Cache_Config::get_instance()->in_exclude_optimization_roles() ) {
312 LiteSpeed_Cache_Log::debug( 'Optimizer bypass: hit Role Excludes setting: ' . $result ) ;
313 return $content ;
314 }
315
316
317 LiteSpeed_Cache_Log::debug( 'Optimizer start' ) ;
318
319 $instance = self::get_instance() ;
320 $instance->content = $content ;
321
322 $instance->_optimize() ;
323 return $instance->content ;
324 }
325
326 /**
327 * Optimize css src
328 *
329 * @since 1.2.2
330 * @access private
331 */
332 private function _optimize()
333 {
334 if ( ! $this->_can_optm() ) {
335 LiteSpeed_Cache_Log::debug( 'Optimizer bypass: admin/feed/preview' ) ;
336 return ;
337 }
338
339 do_action( 'litespeed_optm' ) ;
340
341 // Parse css from content
342 $ggfonts_rm = LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_OPTM_GGFONTS_RM ) ;
343 if ( $this->cfg_css_minify || $this->cfg_css_combine || $this->cfg_http2_css || $ggfonts_rm || $this->cfg_css_async || $this->cfg_ggfonts_async ) {
344 // To remove google fonts
345 if ( $ggfonts_rm ) {
346 $this->css_to_be_removed[] = 'fonts.googleapis.com' ;
347 }
348 list( $src_list, $html_list ) = $this->_handle_css() ;
349 }
350
351 // css optimizer
352 if ( $this->cfg_css_minify || $this->cfg_css_combine || $this->cfg_http2_css ) {
353
354 if ( $src_list ) {
355 // Analyze local file
356 list( $ignored_html, $src_queue_list, $file_size_list ) = $this->_analyse_links( $src_list, $html_list ) ;
357
358 // IF combine
359 if ( $this->cfg_css_combine ) {
360 $enqueue_first = LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_CSS_COMBINED_PRIORITY ) ;
361
362 $urls = $this->_limit_size_build_hash_url( $src_queue_list, $file_size_list ) ;
363
364 $snippet = '' ;
365 foreach ( $urls as $url ) {
366 $snippet .= "<link data-optimized='2' rel='stylesheet' href='$url' />" ;// use 2 as combined
367 }
368
369 // Handle css async load
370 if ( $this->cfg_css_async ) {
371 // Only ignored html snippet needs async
372 $ignored_html_async = $this->_async_css_list( $ignored_html ) ;
373
374 $snippet = '' ;
375 foreach ( $urls as $url ) {
376 $snippet .= "<link rel='preload' data-asynced='1' data-optimized='2' as='style' onload='this.rel=\"stylesheet\"' href='$url' />" ;
377 }
378
379 // enqueue combined file first
380 if ( $enqueue_first ) {
381 $this->html_head .= $snippet . implode( '', $ignored_html_async ) ;
382 }
383 else {
384 $this->html_head .= implode( '', $ignored_html_async ) . $snippet ;
385 }
386
387 }
388 else {
389 // enqueue combined file first
390 if ( $enqueue_first ) {
391 $this->html_head .= $snippet . implode( '', $ignored_html ) ;
392 }
393 else {
394 $this->html_head .= implode( '', $ignored_html ) . $snippet ;
395 }
396 }
397
398 // Move all css to top
399 $this->content = str_replace( $html_list, '', $this->content ) ;// todo: need to keep position for certain files
400
401 // Add to HTTP2
402 foreach ( $urls as $url ) {
403 $this->append_http2( $url ) ;
404 }
405
406 }
407 // Only minify
408 elseif ( $this->cfg_css_minify ) {
409 // will handle async css load inside
410 $this->_src_queue_handler( $src_queue_list, $html_list ) ;
411 }
412 // Only HTTP2 push
413 else {
414 foreach ( $src_queue_list as $val ) {
415 $this->append_http2( $val ) ;
416 }
417 }
418 }
419 }
420
421 // Handle css lazy load if not handled async loaded yet
422 if ( $this->cfg_css_async && ! $this->cfg_css_minify && ! $this->cfg_css_combine ) {
423 // async html
424 $html_list_async = $this->_async_css_list( $html_list ) ;
425
426 // Replace async css
427 $this->content = str_replace( $html_list, $html_list_async, $this->content ) ;
428
429 }
430
431 // Handle google fonts async
432 if ( ! $this->cfg_css_async && $this->cfg_ggfonts_async ) {
433 foreach ( $html_list as $k => $v ) {
434 if ( strpos( $src_list[ $k ], 'fonts.googleapis.com' ) === false ) {
435 unset( $html_list[ $k ] ) ;
436 continue ;
437 }
438
439 LiteSpeed_Cache_Log::debug( 'Optm: google fonts async loading: ' . $src_list[ $k ] ) ;
440 }
441 // async html
442 $html_list_async = $this->_async_css_list( $html_list ) ;
443
444 // Replace async css
445 $this->content = str_replace( $html_list, $html_list_async, $this->content ) ;
446 }
447
448 // Parse js from buffer as needed
449 if ( $this->cfg_js_minify || $this->cfg_js_combine || $this->cfg_http2_js || $this->cfg_js_defer ) {
450 list( $src_list, $html_list, $head_src_list ) = $this->_parse_js() ;
451 }
452
453 // js optimizer
454 if ( $this->cfg_js_minify || $this->cfg_js_combine || $this->cfg_http2_js ) {
455
456 if ( $src_list ) {
457 list( $ignored_html, $src_queue_list, $file_size_list ) = $this->_analyse_links( $src_list, $html_list, 'js' ) ;
458
459 // IF combine
460 if ( $this->cfg_js_combine ) {
461 $enqueue_first = LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_JS_COMBINED_PRIORITY ) ;
462
463 // separate head/foot js/raw html
464 $head_js = array() ;
465 $head_ignored_html = array() ;
466 $foot_js = array() ;
467 $foot_ignored_html = array() ;
468 foreach ( $src_queue_list as $k => $src ) {
469 if ( in_array( $src, $head_src_list ) ) {
470 $head_js[ $k ] = $src ;
471 }
472 else {
473 $foot_js[ $k ] = $src ;
474 }
475 }
476 foreach ( $ignored_html as $src => $html ) {
477 if ( in_array( $src, $head_src_list ) ) {
478 $head_ignored_html[ $src ] = $html ;
479 }
480 else {
481 $foot_ignored_html[] = $html ;
482 }
483 }
484
485 $snippet = '' ;
486 if ( $head_js ) {
487 $urls = $this->_limit_size_build_hash_url( $head_js, $file_size_list, 'js' ) ;
488 foreach ( $urls as $url ) {
489 $snippet .= "<script data-optimized='1' src='$url' " . ( $this->cfg_js_defer ? 'defer' : '' ) . "></script>" ;
490
491 // Add to HTTP2
492 $this->append_http2( $url, 'js' ) ;
493 }
494 }
495 if ( $this->cfg_js_defer ) {
496 $head_ignored_html = $this->_js_defer( $head_ignored_html ) ;
497 }
498
499 /**
500 * Enqueue combined file first
501 * @since 1.6
502 */
503 if ( $enqueue_first ) {
504 // Make jQuery to be the first one
505 // Suppose jQuery is in header
506 foreach ( $head_ignored_html as $src => $html ) {
507 if ( $this->_is_jquery( $src ) ) {
508 // jQuery should be always the first one
509 $this->html_head .= $html ;
510 unset( $head_ignored_html[ $src ] ) ;
511 break ;
512 }
513 }
514 $this->html_head .= $snippet . implode( '', $head_ignored_html ) ;
515 }
516 else {
517 $this->html_head .= implode( '', $head_ignored_html ) . $snippet ;
518 }
519
520 $snippet = '' ;
521 if ( $foot_js ) {
522 $urls = $this->_limit_size_build_hash_url( $foot_js, $file_size_list, 'js' ) ;
523 foreach ( $urls as $url ) {
524 $snippet .= "<script data-optimized='1' src='$url' " . ( $this->cfg_js_defer ? 'defer' : '' ) . "></script>" ;
525
526 // Add to HTTP2
527 $this->append_http2( $url, 'js' ) ;
528 }
529 }
530 if ( $this->cfg_js_defer ) {
531 $foot_ignored_html = $this->_js_defer( $foot_ignored_html ) ;
532 }
533
534 // enqueue combined file first
535 if ( $enqueue_first ) {
536 $this->html_foot .= $snippet . implode( '', $foot_ignored_html ) ;
537 }
538 else {
539 $this->html_foot .= implode( '', $foot_ignored_html ) . $snippet ;
540 }
541
542 // Will move all js to top/bottom
543 $this->content = str_replace( $html_list, '', $this->content ) ;
544
545 }
546 // Only minify
547 elseif ( $this->cfg_js_minify ) {
548 // Will handle js defer inside
549 $this->_src_queue_handler( $src_queue_list, $html_list, 'js' ) ;
550 }
551 // Only HTTP2 push
552 else {
553 foreach ( $src_queue_list as $val ) {
554 $this->append_http2( $val, 'js' ) ;
555 }
556 }
557 }
558 }
559
560 // Handle js defer if not handled defer yet
561 if ( $this->cfg_js_defer && ! $this->cfg_js_minify && ! $this->cfg_js_combine ) {
562 // defer html
563 $html_list2 = $this->_js_defer( $html_list ) ;
564
565 // Replace async js
566 $this->content = str_replace( $html_list, $html_list2, $this->content ) ;
567 }
568
569
570 // Append async compatibility lib to head
571 if ( $this->cfg_css_async || $this->cfg_ggfonts_async ) {
572 $css_async_lib_url = LiteSpeed_Cache_Utility::get_permalink_url( self::CSS_ASYNC_LIB ) ;
573 $this->html_head .= "<script src='" . $css_async_lib_url . "' " . ( $this->cfg_js_defer ? 'defer' : '' ) . "></script>" ;// Don't exclude it from defer for now
574 $this->append_http2( $css_async_lib_url, 'js' ) ; // async lib will be http/2 pushed always
575 }
576
577 if ( $this->cfg_ggfonts_async ) {
578 $this->html_head .= '<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin />' ;
579 }
580
581 // Replace html head part
582 $this->html_head = apply_filters( 'litespeed_optm_html_head', $this->html_head ) ;
583 if ( $this->html_head ) {
584 // Put header content to be after charset
585 if ( strpos( $this->content, '<meta charset' ) !== false ) {
586 $this->content = preg_replace( '#<meta charset([^>]*)>#isU', '<meta charset$1>' . $this->html_head , $this->content, 1 ) ;
587 }
588 else {
589 $this->content = preg_replace( '#<head([^>]*)>#isU', '<head$1>' . $this->html_head , $this->content, 1 ) ;
590 }
591 }
592
593 // Replace html foot part
594 $this->html_foot = apply_filters( 'litespeed_optm_html_foot', $this->html_foot ) ;
595 if ( $this->html_foot ) {
596 $this->content = str_replace( '</body>', $this->html_foot . '</body>' , $this->content ) ;
597 }
598
599 // HTML minify
600 if ( $this->cfg_html_minify ) {
601 $ori = $this->content ;
602
603 set_error_handler( 'litespeed_exception_handler' ) ;
604 try {
605 $this->content = LiteSpeed_Cache_Optimizer::get_instance()->html_min( $this->content ) ;
606 $this->content .= "\n" . '<!-- Page optimized by LiteSpeed Cache on '.date('Y-m-d H:i:s').' -->' ;
607
608 } catch ( ErrorException $e ) {
609 LiteSpeed_Cache_Log::debug( 'Error when optimizing HTML: ' . $e->getMessage() ) ;
610 error_log( 'LiteSpeed Optimizer optimizing HTML Error: ' . $e->getMessage() ) ;
611 // If failed to minify HTML, restore original content
612 $this->content = $ori ;
613 }
614 restore_error_handler() ;
615
616 }
617
618 if ( $this->http2_headers ) {
619 @header( 'Link: ' . implode( ',', $this->http2_headers ), false ) ;
620 }
621
622 }
623
624 /**
625 * Prefetch DNS
626 *
627 * @since 1.7.1
628 * @access private
629 */
630 private function _dns_prefetch_init()
631 {
632 $this->dns_prefetch = get_option( LiteSpeed_Cache_Config::ITEM_DNS_PREFETCH ) ;
633 if ( ! $this->dns_prefetch ) {
634 return ;
635 }
636
637 if ( function_exists( 'wp_resource_hints' ) ) {
638 add_filter( 'wp_resource_hints', array( $this, 'dns_prefetch_filter' ), 10, 2 ) ;
639 }
640 else {
641 add_action( 'litespeed_optm', array( $this, 'dns_prefetch_output' ) ) ;
642 }
643 }
644
645 /**
646 * Prefetch DNS hook for WP
647 *
648 * @since 1.7.1
649 * @access public
650 */
651 public function dns_prefetch_filter( $urls, $relation_type )
652 {
653 if ( $relation_type !== 'dns-prefetch' ) {
654 return $urls ;
655 }
656
657 foreach ( explode( "\n", $this->dns_prefetch ) as $v ) {
658 if ( $v ) {
659 $urls[] = $v ;
660 }
661 }
662
663 return $urls ;
664 }
665
666 /**
667 * Prefetch DNS
668 *
669 * @since 1.7.1
670 * @access public
671 */
672 public function dns_prefetch_output()
673 {
674 foreach ( explode( "\n", $this->dns_prefetch ) as $v ) {
675 if ( $v ) {
676 $this->html_head .= "<link rel='dns-prefetch' href='$v' />" ;
677 }
678 }
679 }
680
681 /**
682 * Limit combined filesize when build hash url
683 *
684 * @since 1.3
685 * @access private
686 */
687 private function _limit_size_build_hash_url( $src_queue_list, $file_size_list, $file_type = 'css' )
688 {
689 $total = 0 ;
690 $i = 0 ;
691 $src_arr = array() ;
692 foreach ( $src_queue_list as $k => $v ) {
693
694 empty( $src_arr[ $i ] ) && $src_arr[ $i ] = array() ;
695
696 $src_arr[ $i ][] = $v ;
697
698 $total += $file_size_list[ $k ] ;
699
700 if ( $total > $this->cfg_optm_max_size ) { // If larger than 1M, separate them
701 $total = 0;
702 $i ++ ;
703 }
704 }
705 if ( count( $src_arr ) > 1 ) {
706 LiteSpeed_Cache_Log::debug( 'Optimizer: separate ' . $file_type . ' to ' . count( $src_arr ) ) ;
707 }
708
709 // group build
710 $hashed_arr = array() ;
711 foreach ( $src_arr as $v ) {
712 $hashed_arr[] = $this->_build_hash_url( $v, $file_type ) ;
713 }
714
715 return $hashed_arr ;
716 }
717
718 /**
719 * Run minify with src queue list
720 *
721 * @since 1.2.2
722 * @access private
723 */
724 private function _src_queue_handler( $src_queue_list, $html_list, $file_type = 'css' )
725 {
726 $html_list_ori = $html_list ;
727
728 $tag = $file_type === 'css' ? 'link' : 'script' ;
729 foreach ( $src_queue_list as $key => $src ) {
730 $url = $this->_build_hash_url( $src, $file_type ) ;
731 $snippet = str_replace( $src, $url, $html_list[ $key ] ) ;
732 $snippet = str_replace( "<$tag ", "<$tag data-optimized='1' ", $snippet ) ;
733
734 $html_list[ $key ] = $snippet ;
735
736 // Add to HTTP2
737 $this->append_http2( $url, $file_type ) ;
738 }
739
740 // Handle css async load
741 if ( $file_type === 'css' && $this->cfg_css_async ) {
742 $html_list = $this->_async_css_list( $html_list ) ;
743 }
744
745 // Handle js defer
746 if ( $file_type === 'js' && $this->cfg_js_defer ) {
747 $html_list = $this->_js_defer( $html_list ) ;
748 }
749
750 $this->content = str_replace( $html_list_ori, $html_list, $this->content ) ;
751 }
752
753 /**
754 * Check that links are internal or external
755 *
756 * @since 1.2.2
757 * @access private
758 * @return array Array(Ignored raw html, src needed to be handled list, filesize for param 2nd )
759 */
760 private function _analyse_links( $src_list, $html_list, $file_type = 'css' )
761 {
762 // if ( $file_type == 'css' ) {
763 // $excludes = apply_filters( 'litespeed_cache_optimize_css_excludes', LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_CSS_EXCLUDES ) ) ;
764 // }
765 // else {
766 // $excludes = apply_filters( 'litespeed_cache_optimize_js_excludes', LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_JS_EXCLUDES ) ) ;
767 // }
768 // if ( $excludes ) {
769 // $excludes = explode( "\n", $excludes ) ;
770 // }
771
772 $ignored_html = array() ;
773 $src_queue_list = array() ;
774 $file_size_list = array() ;
775
776 // Analyse links
777 foreach ( $src_list as $key => $src ) {
778 LiteSpeed_Cache_Log::debug2( 'Optm: ' . $src ) ;
779
780 /**
781 * Excluded links won't be done any optm
782 * @since 1.7
783 */
784 // if ( $excludes && $exclude = LiteSpeed_Cache_Utility::str_hit_array( $src, $excludes ) ) {
785 // $ignored_html[] = $html_list[ $key ] ;
786 // LiteSpeed_Cache_Log::debug2( 'Optm: Abort excludes: ' . $exclude ) ;
787 // continue ;
788 // }
789
790 // Check if has no-optimize attr
791 if ( strpos( $html_list[ $key ], 'data-no-optimize' ) !== false ) {
792 $ignored_html[] = $html_list[ $key ] ;
793 LiteSpeed_Cache_Log::debug2( 'Optm: Abort excludes: attr data-no-optimize' ) ;
794 continue ;
795 }
796
797 // Check if is external URL
798 $url_parsed = parse_url( $src ) ;
799 if ( ! $file_info = LiteSpeed_Cache_Utility::is_internal_file( $src ) ) {
800 $ignored_html[ $src ] = $html_list[ $key ] ;
801 LiteSpeed_Cache_Log::debug2( 'Optm: Abort external/non-exist' ) ;
802 continue ;
803 }
804
805 /**
806 * Check if exclude jQuery or not
807 * Exclude from minify/combine
808 * @since 1.5
809 */
810 if ( $this->cfg_exc_jquery && $this->_is_jquery( $src ) ) {
811 $ignored_html[ $src ] = $html_list[ $key ] ;
812 LiteSpeed_Cache_Log::debug2( 'Optm: Abort jQuery by setting' ) ;
813
814 // Add to HTTP2 as its ignored but still internal src
815 $this->append_http2( $src, 'js' ) ;
816
817 continue ;
818 }
819
820 $src_queue_list[ $key ] = $src ;
821 $file_size_list[ $key ] = $file_info[ 1 ] ;
822 }
823
824 return array( $ignored_html, $src_queue_list, $file_size_list ) ;
825 }
826
827 /**
828 * Generate full URL path with hash for a list of src
829 *
830 * @since 1.2.2
831 * @access private
832 * @return string The final URL
833 */
834 private function _build_hash_url( $src, $file_type = 'css' )
835 {
836 if ( ! $src ) {
837 return false ;
838 }
839
840 if ( ! is_array( $src ) ) {
841 $src = array( $src ) ;
842 }
843 $src = array_values( $src ) ;
844
845 $hash = md5( serialize( $src ) ) ;
846
847 $short = substr( $hash, -5 ) ;
848
849 $filename = $short ;
850
851 // Need to check conflicts
852 // If short hash exists
853 if ( $urls = LiteSpeed_Cache_Data::optm_hash2src( $short . '.' . $file_type ) ) {
854 // If conflicts
855 if ( $urls !== $src ) {
856 LiteSpeed_Cache_Data::optm_save_src( $hash . '.' . $file_type, $src ) ;
857 $filename = $hash ;
858 }
859 }
860 else {
861 // Short hash is safe now
862 LiteSpeed_Cache_Data::optm_save_src( $short . '.' . $file_type, $src ) ;
863 }
864
865 $file_to_save = self::DIR_MIN . '/' . $filename . '.' . $file_type ;
866
867 return LiteSpeed_Cache_Utility::get_permalink_url( $file_to_save ) ;
868 }
869
870 /**
871 * Parse js src
872 *
873 * @since 1.2.2
874 * @access private
875 * @return array All the src & related raw html list
876 */
877 private function _parse_js()
878 {
879 $excludes = apply_filters( 'litespeed_cache_optimize_js_excludes', LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_JS_EXCLUDES ) ) ;
880 if ( $excludes ) {
881 $excludes = explode( "\n", $excludes ) ;
882 }
883
884 $src_list = array() ;
885 $html_list = array() ;
886 $head_src_list = array() ;
887
888 $content = preg_replace( '#<!--.*-->#sU', '', $this->content ) ;
889 preg_match_all( '#<script \s*([^>]+)>\s*</script>|</head>#isU', $content, $matches, PREG_SET_ORDER ) ;
890 $is_head = true ;
891 foreach ( $matches as $match ) {
892 if ( $match[ 0 ] === '</head>' ) {
893 $is_head = false ;
894 continue ;
895 }
896 $attrs = LiteSpeed_Cache_Utility::parse_attr( $match[ 1 ] ) ;
897
898 if ( isset( $attrs[ 'data-optimized' ] ) ) {
899 continue ;
900 }
901 if ( ! empty( $attrs[ 'data-no-optimize' ] ) ) {
902 continue ;
903 }
904 if ( empty( $attrs[ 'src' ] ) ) {
905 continue ;
906 }
907
908 // to avoid multiple replacement
909 if ( in_array( $match[ 0 ], $html_list ) ) {
910 continue ;
911 }
912 // todo @v2.0: allow defer even exclude from optm
913 if ( $excludes && $exclude = LiteSpeed_Cache_Utility::str_hit_array( $attrs[ 'src' ], $excludes ) ) {
914 LiteSpeed_Cache_Log::debug2( 'Optm: _parse_js bypassed exclude ' . $exclude ) ;
915 continue ;
916 }
917
918 $src_list[] = $attrs[ 'src' ] ;
919 $html_list[] = $match[ 0 ] ;
920
921 if ( $is_head ) {
922 $head_src_list[] = $attrs[ 'src' ] ;
923 }
924 }
925
926 return array( $src_list, $html_list, $head_src_list ) ;
927 }
928
929 /**
930 * Parse css src and remove to-be-removed css
931 *
932 * @since 1.2.2
933 * @access private
934 * @return array All the src & related raw html list
935 */
936 private function _handle_css()
937 {
938 $excludes = apply_filters( 'litespeed_cache_optimize_css_excludes', LiteSpeed_Cache::config( LiteSpeed_Cache_Config::OPID_CSS_EXCLUDES ) ) ;
939 if ( $excludes ) {
940 $excludes = explode( "\n", $excludes ) ;
941 }
942
943 $this->css_to_be_removed = apply_filters( 'litespeed_optm_css_to_be_removed', $this->css_to_be_removed ) ;
944
945 $src_list = array() ;
946 $html_list = array() ;
947
948 // $dom = new PHPHtmlParser\Dom ;
949 // $dom->load( $content ) ;return $val;
950 // $items = $dom->find( 'link' ) ;
951
952 $content = preg_replace( '#<!--.*-->#sU', '', $this->content ) ;
953 preg_match_all( '#<link \s*([^>]+)/?>#isU', $content, $matches, PREG_SET_ORDER ) ;
954 foreach ( $matches as $match ) {
955 $attrs = LiteSpeed_Cache_Utility::parse_attr( $match[ 1 ] ) ;
956
957 if ( empty( $attrs[ 'rel' ] ) || $attrs[ 'rel' ] !== 'stylesheet' ) {
958 continue ;
959 }
960 if ( isset( $attrs[ 'data-optimized' ] ) ) {
961 continue ;
962 }
963 if ( ! empty( $attrs[ 'data-no-optimize' ] ) ) {
964 continue ;
965 }
966 if ( ! empty( $attrs[ 'media' ] ) && strpos( $attrs[ 'media' ], 'print' ) !== false ) {
967 continue ;
968 }
969 if ( empty( $attrs[ 'href' ] ) ) {
970 continue ;
971 }
972
973 if ( $excludes && $exclude = LiteSpeed_Cache_Utility::str_hit_array( $attrs[ 'href' ], $excludes ) ) {
974 LiteSpeed_Cache_Log::debug2( 'Optm: _handle_css bypassed exclude ' . $exclude ) ;
975 continue ;
976 }
977
978 // Check if need to remove this css
979 if ( $this->css_to_be_removed && LiteSpeed_Cache_Utility::str_hit_array( $attrs[ 'href' ], $this->css_to_be_removed ) ) {
980 LiteSpeed_Cache_Log::debug( 'Optm: rm css snippet ' . $attrs[ 'href' ] ) ;
981 // Delete this css snippet from orig html
982 $this->content = str_replace( $match[ 0 ], '', $this->content ) ;
983 continue ;
984 }
985
986 // to avoid multiple replacement
987 if ( in_array( $match[ 0 ], $html_list ) ) {
988 continue ;
989 }
990
991 $src_list[] = $attrs[ 'href' ] ;
992 $html_list[] = $match[ 0 ] ;
993 }
994
995 return array( $src_list, $html_list ) ;
996 }
997
998 /**
999 * Replace css to async loaded css
1000 *
1001 * @since 1.3
1002 * @access private
1003 * @param array $html_list Orignal css array
1004 * @return array (array)css_async_list
1005 */
1006 private function _async_css_list( $html_list )
1007 {
1008 foreach ( $html_list as $k => $ori ) {
1009 if ( strpos( $ori, 'data-asynced' ) !== false ) {
1010 LiteSpeed_Cache_Log::debug2( 'Optm bypass: attr data-asynced exist' ) ;
1011 continue ;
1012 }
1013
1014 if ( strpos( $ori, 'data-no-async' ) !== false ) {
1015 LiteSpeed_Cache_Log::debug2( 'Optm bypass: attr api data-no-async' ) ;
1016 continue ;
1017 }
1018
1019 // async replacement
1020 $v = str_replace( 'stylesheet', 'preload', $ori ) ;
1021 $v = str_replace( '<link', "<link data-asynced='1' as='style' onload='this.rel=\"stylesheet\"' ", $v ) ;
1022 // Append to noscript content
1023 $v .= '<noscript>' . $ori . '</noscript>' ;
1024 $html_list[ $k ] = $v ;
1025 }
1026 return $html_list ;
1027 }
1028
1029 /**
1030 * Add defer to js
1031 *
1032 * @since 1.3
1033 * @access private
1034 */
1035 private function _js_defer( $html_list )
1036 {
1037 foreach ( $html_list as $k => $v ) {
1038 if ( strpos( $v, 'async' ) !== false ) {
1039 continue ;
1040 }
1041 if ( strpos( $v, 'defer' ) !== false ) {
1042 continue ;
1043 }
1044 if ( strpos( $v, 'data-deferred' ) !== false ) {
1045 LiteSpeed_Cache_Log::debug2( 'Optm bypass: attr data-deferred exist' ) ;
1046 continue ;
1047 }
1048 if ( strpos( $v, 'data-no-defer' ) !== false ) {
1049 LiteSpeed_Cache_Log::debug2( 'Optm bypass: attr api data-no-defer' ) ;
1050 continue ;
1051 }
1052
1053 /**
1054 * Parse src for excluding js from setting
1055 * @since 1.5
1056 */
1057 if ( $this->cfg_js_defer_exc || $this->cfg_exc_jquery ) {
1058 // parse js src
1059 preg_match( '#<script \s*([^>]+)>#isU', $v, $matches ) ;
1060 if ( empty( $matches[ 1 ] ) ) {
1061 LiteSpeed_Cache_Log::debug( 'Optm: js defer parse html failed: ' . $v ) ;
1062 continue ;
1063 }
1064
1065 $attrs = LiteSpeed_Cache_Utility::parse_attr( $matches[ 1 ] ) ;
1066
1067 if ( empty( $attrs[ 'src' ] ) ) {
1068 LiteSpeed_Cache_Log::debug( 'Optm: js defer parse src failed: ' . $matches[ 1 ] ) ;
1069 continue ;
1070 }
1071
1072 $src = $attrs[ 'src' ] ;
1073 }
1074
1075 /**
1076 * Exclude js from setting
1077 * @since 1.5
1078 */
1079 if ( $this->cfg_js_defer_exc && LiteSpeed_Cache_Utility::str_hit_array( $src, $this->cfg_js_defer_exc ) ) {
1080 LiteSpeed_Cache_Log::debug( 'Optm: js defer exclude ' . $src ) ;
1081 continue ;
1082 }
1083
1084 /**
1085 * Check if exclude jQuery
1086 * @since 1.5
1087 */
1088 if ( $this->cfg_exc_jquery && $this->_is_jquery( $src ) ) {
1089 LiteSpeed_Cache_Log::debug2( 'Optm: js defer Abort jQuery by setting' ) ;
1090 continue ;
1091 }
1092
1093 $html_list[ $k ] = str_replace( '></script>', ' defer data-deferred="1"></script>', $v ) ;
1094 }
1095
1096 return $html_list ;
1097 }
1098
1099 /**
1100 * Check if is jq lib
1101 *
1102 * @since 1.5
1103 * @access private
1104 */
1105 private function _is_jquery( $src )
1106 {
1107 return stripos( $src, 'jquery.js' ) !== false || stripos( $src, 'jquery.min.js' ) !== false ;
1108 }
1109
1110 /**
1111 * Append to HTTP2 header
1112 *
1113 * @since 1.2.2
1114 * @access private
1115 */
1116 private function append_http2( $url, $file_type = 'css' )
1117 {
1118 if ( ! ( $file_type === 'css' ? $this->cfg_http2_css : $this->cfg_http2_js ) ) {
1119 return ;
1120 }
1121
1122 /**
1123 * For CDN enabled ones, bypass http/2 push
1124 * @since 1.6.2.1
1125 */
1126 if ( LiteSpeed_Cache_CDN::inc_type( $file_type ) ) {
1127 return ;
1128 }
1129
1130 /**
1131 * Keep QS for constance by set 2nd param to true
1132 * @since 1.6.2.1
1133 */
1134 $uri = LiteSpeed_Cache_Utility::url2uri( $url, true ) ;
1135
1136 if ( ! $uri ) {
1137 return ;
1138 }
1139
1140 $this->http2_headers[] = '<' . $uri . '>; rel=preload; as=' . ( $file_type === 'css' ? 'style' : 'script' ) ;
1141 }
1142
1143 /**
1144 * Get the current instance object.
1145 *
1146 * @since 1.2.2
1147 * @access public
1148 * @return Current class instance.
1149 */
1150 public static function get_instance()
1151 {
1152 if ( ! isset(self::$_instance) ) {
1153 self::$_instance = new self() ;
1154 }
1155
1156 return self::$_instance ;
1157 }
1158
1159 }
1160
1161
1162
1163