/home/arranoyd/rg2solis/wp-content/plugins/litespeed-cache/src/optimize.cls.php
<?php
/**
* The optimize class.
*
* @since 1.2.2
* @since 1.5 Moved into /inc
* @package LiteSpeed
* @subpackage LiteSpeed/inc
* @author LiteSpeed Technologies <info@litespeedtech.com>
*/
namespace LiteSpeed;
defined( 'WPINC' ) || exit;
class Optimize extends Base {
protected static $_instance;
const LIB_FILE_CSS_ASYNC = 'assets/js/css_async.min.js';
const LIB_FILE_WEBFONTLOADER = 'assets/js/webfontloader.min.js';
const ITEM_TIMESTAMP_PURGE_CSS = 'timestamp_purge_css';
private $content;
private $content_ori;
private $http2_headers = array();
private $cfg_http2_css;
private $cfg_http2_js;
private $cfg_css_min;
private $cfg_css_comb;
private $cfg_js_min;
private $cfg_js_comb;
private $cfg_css_async;
private $cfg_js_defer;
private $cfg_js_defer_exc = false;
private $cfg_exc_jquery;
private $cfg_ggfonts_async;
private $_conf_css_font_display;
private $cfg_optm_max_size;
private $cfg_ttl;
private $cfg_ggfonts_rm;
private $dns_prefetch;
private $_ggfonts_urls = array();
private $html_foot = ''; // The html info append to <body>
private $html_head = ''; // The html info prepend to <body>
private static $_var_i = 0;
/**
*
* @since 1.2.2
* @access protected
*/
protected function __construct() {
}
/**
* Init optimizer
*
* @since 3.0
* @access protected
*/
public function init() {
$this->cfg_css_async = Conf::val( Base::O_OPTM_CSS_ASYNC );
if ( $this->cfg_css_async && ! Conf::val( Base::O_API_KEY ) ) {
Debug2::debug( '[Optm] ❌ CCSS set to OFF due to lack of domain key' );
$this->cfg_css_async = false;
}
$this->cfg_js_defer = Conf::val( Base::O_OPTM_JS_DEFER );
if ( ! Router::can_optm() ) {
return;
}
// To remove emoji from WP
if ( Conf::val( Base::O_OPTM_EMOJI_RM ) ) {
$this->_emoji_rm();
}
if ( Conf::val( Base::O_OPTM_QS_RM ) ) {
add_filter( 'style_loader_src', array( $this, 'remove_query_strings' ), 999 );
add_filter( 'script_loader_src', array( $this, 'remove_query_strings' ), 999 );
}
/**
* Exclude js from deferred setting
* @since 1.5
*/
if ( $this->cfg_js_defer ) {
$this->cfg_js_defer_exc = apply_filters( 'litespeed_optm_js_defer_exc', Conf::val( Base::O_OPTM_JS_DEFER_EXC ) );
}
/**
* Add vary filter for Role Excludes
* @since 1.6
*/
add_filter( 'litespeed_vary', array( $this, 'vary_add_role_exclude' ) );
/**
* Prefetch DNS
* @since 1.7.1
*/
$this->_dns_prefetch_init();
}
/**
* Exclude role from optimization filter
*
* @since 1.6
* @access public
*/
public function vary_add_role_exclude( $vary ) {
if ( Conf::get_instance()->in_optm_exc_roles() ) {
$vary[ 'role_exclude_optm' ] = 1;
}
return $vary;
}
/**
* Remove emoji from WP
*
* @since 1.4
* @since 2.9.8 Changed to private
* @access private
*/
private function _emoji_rm() {
remove_action( 'wp_head' , 'print_emoji_detection_script', 7 );
remove_action( 'admin_print_scripts' , 'print_emoji_detection_script' );
remove_filter( 'the_content_feed' , 'wp_staticize_emoji' );
remove_filter( 'comment_text_rss' , 'wp_staticize_emoji' );
/**
* Added for better result
* @since 1.6.2.1
*/
remove_action( 'wp_print_styles', 'print_emoji_styles' );
remove_action( 'admin_print_styles', 'print_emoji_styles' );
remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
}
/**
* Check if the request is for static file
*
* @since 1.2.2
* @since 3.0 Renamed func. Changed access to public
* @access public
*/
public function serve_satic( $uri ) {
$this->cfg_css_min = Conf::val( Base::O_OPTM_CSS_MIN );
$this->cfg_css_comb = Conf::val( Base::O_OPTM_CSS_COMB );
$this->cfg_js_min = Conf::val( Base::O_OPTM_JS_MIN );
$this->cfg_js_comb = Conf::val( Base::O_OPTM_JS_COMB );
$this->cfg_ttl = Conf::val( Base::O_OPTM_TTL );
// If not turn on min files
if ( ! $this->cfg_css_min && ! $this->cfg_css_comb && ! $this->cfg_js_min && ! $this->cfg_js_comb ) {
return;
}
// try to match `xx.css`
if ( ! preg_match( '#^(\w+)\.(css|js)#U', $uri, $match ) ) {
return;
}
Debug2::debug( '[Optm] start minifying file' );
// Proceed css/js file generation
define( 'LITESPEED_MIN_FILE', true );
$file_type = $match[ 2 ];
$static_file = LITESPEED_STATIC_DIR . '/cssjs/' . $match[ 0 ];
// Even if hit PHP, still check if the file is valid to bypass minify process
if ( ! file_exists( $static_file ) || time() - filemtime( $static_file ) > $this->cfg_ttl ) {
$concat_only = ! ( $file_type === 'css' ? $this->cfg_css_min : $this->cfg_js_min );
$content = Optimizer::get_instance()->serve( $match[ 0 ], $concat_only );
if ( ! $content ) {
Debug2::debug( '[Optm] Static file generation bypassed due to empty' );
return;
}
// Generate static file
File::save( $static_file, $content, true );
Debug2::debug2( '[Optm] Saved cache to file [path] ' . $static_file );
}
else {
// Load file from file based cache if not expired
Debug2::debug2( '[Optm] Static file available' );
}
$url = LITESPEED_STATIC_URL . '/cssjs/' . $match[ 0 ];
Debug2::debug( '[Optm] Redirect to ' . $url );
wp_redirect( $url );
exit;
}
/**
* Delete file-based cache folder
*
* @since 2.1
* @access public
*/
public function rm_cache_folder() {
if ( file_exists( LITESPEED_STATIC_DIR . '/cssjs' ) ) {
File::rrmdir( LITESPEED_STATIC_DIR . '/cssjs' );
}
}
/**
* Remove QS
*
* @since 1.3
* @access public
*/
public function remove_query_strings( $src ) {
if ( strpos( $src, '_litespeed_rm_qs=0' ) || strpos( $src, '/recaptcha' ) ) {
return $src;
}
if ( strpos( $src, '.js?' ) !== false || strpos( $src, '.css?' ) !== false ) {
$src = preg_replace( '/\?.*/', '', $src );
}
return $src;
}
/**
* Check if need db table or not
*
* @since 3.0
* @access public
*/
public static function need_db() {
if ( Conf::val( Base::O_OPTM_CSS_MIN ) ) {
return true;
}
if ( Conf::val( Base::O_OPTM_CSS_COMB ) ) {
return true;
}
if ( Conf::val( Base::O_OPTM_JS_MIN ) ) {
return true;
}
if ( Conf::val( Base::O_OPTM_JS_COMB ) ) {
return true;
}
return false;
}
/**
* Run optimize process
* NOTE: As this is after cache finalized, can NOT set any cache control anymore
*
* @since 1.2.2
* @access public
* @return string The content that is after optimization
*/
public static function finalize( $content ) {
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)
return $content;
}
if ( ! defined( 'LITESPEED_IS_HTML' ) ) {
Debug2::debug( '[Optm] bypass: Not frontend HTML type' );
return $content;
}
// Check if hit URI excludes
$excludes = Conf::val( Base::O_OPTM_EXC );
if ( ! empty( $excludes ) ) {
$result = Utility::str_hit_array( $_SERVER[ 'REQUEST_URI' ], $excludes );
if ( $result ) {
Debug2::debug( '[Optm] bypass: hit URI Excludes setting: ' . $result );
return $content;
}
}
// Check if is exclude optm roles ( Need to set Vary too )
if ( $result = Conf::get_instance()->in_optm_exc_roles() ) {
Debug2::debug( '[Optm] bypass: hit Role Excludes setting: ' . $result );
return $content;
}
Debug2::debug( '[Optm] start' );
$instance = self::get_instance();
$instance->content_ori = $instance->content = $content;
$instance->_optimize();
return $instance->content;
}
/**
* Optimize css src
*
* @since 1.2.2
* @access private
*/
private function _optimize() {
$this->cfg_http2_css = Conf::val( Base::O_OPTM_CSS_HTTP2 );
$this->cfg_http2_js = Conf::val( Base::O_OPTM_JS_HTTP2 );
$this->cfg_css_min = Conf::val( Base::O_OPTM_CSS_MIN );
$this->cfg_css_comb = Conf::val( Base::O_OPTM_CSS_COMB );
$this->cfg_js_min = Conf::val( Base::O_OPTM_JS_MIN );
$this->cfg_js_comb = Conf::val( Base::O_OPTM_JS_COMB );
$this->cfg_exc_jquery = Conf::val( Base::O_OPTM_EXC_JQ );
$this->cfg_ggfonts_async = Conf::val( Base::O_OPTM_GGFONTS_ASYNC );
$this->_conf_css_font_display = Conf::val( Base::O_OPTM_CSS_FONT_DISPLAY );
if ( ! empty( Base::$CSS_FONT_DISPLAY_SET[ $this->_conf_css_font_display ] ) ) {
$this->_conf_css_font_display = Base::$CSS_FONT_DISPLAY_SET[ $this->_conf_css_font_display ];
}
$this->cfg_ttl = Conf::val( Base::O_OPTM_TTL );
$this->cfg_optm_max_size = Conf::val( Base::O_OPTM_MAX_SIZE ) * 1000000;
$this->cfg_ggfonts_rm = Conf::val( Base::O_OPTM_GGFONTS_RM );
if ( ! Router::can_optm() ) {
Debug2::debug( '[Optm] bypass: admin/feed/preview' );
return;
}
do_action( 'litespeed_optm' );
// Parse css from content
if ( $this->cfg_css_min || $this->cfg_css_comb || $this->cfg_http2_css || $this->cfg_ggfonts_rm || $this->cfg_css_async || $this->cfg_ggfonts_async || $this->_conf_css_font_display ) {
list( $src_list, $html_list ) = $this->_handle_css();
}
// css optimizer
if ( $this->cfg_css_min || $this->cfg_css_comb || $this->cfg_http2_css ) {
if ( $src_list ) {
// Analyze local file
list( $ignored_html, $src_queue_list, $file_size_list ) = $this->_analyse_links( $src_list, $html_list );
// IF combine
if ( $this->cfg_css_comb ) {
$enqueue_first = Conf::val( Base::O_OPTM_CSS_COMB_PRIO );
$urls = $this->_limit_size_build_hash_url( $src_queue_list, $file_size_list );
$snippet = '';
foreach ( $urls as $url ) {
$snippet .= '<link data-optimized="2" rel="stylesheet" href="' . $url . '" />';// use 2 as combined
}
// Handle css async load
if ( $this->cfg_css_async ) {
// Only ignored html snippet needs async
$ignored_html_async = $this->_async_css_list( $ignored_html );
$snippet = '';
foreach ( $urls as $url ) {
$snippet .= '<link rel="preload" data-asynced="1" data-optimized="2" as="style" onload="this.onload=null;this.rel=\'stylesheet\'" href="' . $url . '" />'; // todo: How to use " in attr wrapper "
}
// enqueue combined file first
if ( $enqueue_first ) {
$this->html_head .= $snippet . implode( '', $ignored_html_async );
}
else {
$this->html_head .= implode( '', $ignored_html_async ) . $snippet;
}
}
else {
// enqueue combined file first
if ( $enqueue_first ) {
$this->html_head .= $snippet . implode( '', $ignored_html );
}
else {
$this->html_head .= implode( '', $ignored_html ) . $snippet;
}
}
// Move all css to top
$this->content = str_replace( $html_list, '', $this->content );// todo: need to keep position for certain files
// Add to HTTP2
foreach ( $urls as $url ) {
$this->append_http2( $url );
}
}
// Only minify
elseif ( $this->cfg_css_min ) {
// will handle async css load inside
$this->_src_queue_handler( $src_queue_list, $html_list );
}
// Only HTTP2 push
else {
foreach ( $src_queue_list as $src ) {
if ( ! empty( $src[ 'src' ] ) ) {
$src = $src[ 'src' ];
}
$this->append_http2( $src );
}
}
}
}
// Handle css lazy load if not handled async loaded yet
if ( $this->cfg_css_async && ! $this->cfg_css_min && ! $this->cfg_css_comb ) {
// async html
$html_list_async = $this->_async_css_list( $html_list );
// Replace async css
$this->content = str_replace( $html_list, $html_list_async, $this->content );
}
// Parse js from buffer as needed
if ( $this->cfg_js_min || $this->cfg_js_comb || $this->cfg_http2_js || $this->cfg_js_defer ) {
list( $src_list, $html_list, $head_src_list ) = $this->_parse_js();
}
// js optimizer
if ( $this->cfg_js_min || $this->cfg_js_comb || $this->cfg_http2_js ) {
if ( $src_list ) {
list( $ignored_html, $src_queue_list, $file_size_list ) = $this->_analyse_links( $src_list, $html_list, 'js' );
// IF combine
if ( $this->cfg_js_comb ) {
$enqueue_first = Conf::val( Base::O_OPTM_JS_COMB_PRIO );
// separate head/foot js/raw html
$head_js = array();
$head_ignored_html = array();
$foot_js = array();
$foot_ignored_html = array();
foreach ( $src_queue_list as $k => $src ) {
if ( in_array( $src, $head_src_list ) ) {
$head_js[ $k ] = $src;
}
else {
$foot_js[ $k ] = $src;
}
}
foreach ( $ignored_html as $src => $html ) {
if ( in_array( $src, $head_src_list ) ) {
$head_ignored_html[ $src ] = $html;
}
else {
$foot_ignored_html[] = $html;
}
}
$snippet = '';
if ( $head_js ) {
$urls = $this->_limit_size_build_hash_url( $head_js, $file_size_list, 'js' );
foreach ( $urls as $url ) {
$snippet .= '<script data-optimized="1" src="' . $url . '" ' . ( $this->cfg_js_defer ? 'defer' : '' ) . '></script>';
// Add to HTTP2
$this->append_http2( $url, 'js' );
}
}
if ( $this->cfg_js_defer ) {
$head_ignored_html = $this->_js_defer( $head_ignored_html );
}
/**
* Enqueue combined file first
* @since 1.6
*/
if ( $enqueue_first ) {
// Make jQuery to be the first one
// Suppose jQuery is in header
foreach ( $head_ignored_html as $src => $html ) {
if ( $this->_is_jquery( $src ) ) {
// jQuery should be always the first one
$this->html_head .= $html;
unset( $head_ignored_html[ $src ] );
break;
}
}
$this->html_head .= $snippet . implode( '', $head_ignored_html );
}
else {
$this->html_head .= implode( '', $head_ignored_html ) . $snippet;
}
$snippet = '';
if ( $foot_js ) {
$urls = $this->_limit_size_build_hash_url( $foot_js, $file_size_list, 'js' );
foreach ( $urls as $url ) {
$snippet .= '<script data-optimized="1" src="' . $url . '" ' . ( $this->cfg_js_defer ? 'defer' : '' ) . '></script>';
// Add to HTTP2
$this->append_http2( $url, 'js' );
}
}
if ( $this->cfg_js_defer ) {
$foot_ignored_html = $this->_js_defer( $foot_ignored_html );
}
// enqueue combined file first
if ( $enqueue_first ) {
$this->html_foot .= $snippet . implode( '', $foot_ignored_html );
}
else {
$this->html_foot .= implode( '', $foot_ignored_html ) . $snippet;
}
// Will move all js to top/bottom
$this->content = str_replace( $html_list, '', $this->content );
}
// Only minify
elseif ( $this->cfg_js_min ) {
// Will handle js defer inside
$this->_src_queue_handler( $src_queue_list, $html_list, 'js' );
}
// Only HTTP2 push
else {
foreach ( $src_queue_list as $val ) {
$this->append_http2( $val, 'js' );
}
}
}
}
// Handle js defer if not handled defer yet
if ( $this->cfg_js_defer && ! $this->cfg_js_min && ! $this->cfg_js_comb ) {
// defer html
$html_list2 = $this->_js_defer( $html_list );
// Replace async js
$this->content = str_replace( $html_list, $html_list2, $this->content );
}
// Append async compatibility lib to head
if ( $this->cfg_css_async ) {
// Inline css async lib
if ( Conf::val( Base::O_OPTM_CSS_ASYNC_INLINE ) ) {
$this->html_head .= '<script id="litespeed-css-async-lib">' . File::read( LSCWP_DIR . self::LIB_FILE_CSS_ASYNC ) . '</script>';
}
else {
$css_async_lib_url = LSWCP_PLUGIN_URL . self::LIB_FILE_CSS_ASYNC;
$this->html_head .= '<script id="litespeed-css-async-lib" src="' . $css_async_lib_url . '" ' . ( $this->cfg_js_defer ? 'defer' : '' ) . '></script>';// Don't exclude it from defer for now
$this->append_http2( $css_async_lib_url, 'js' ); // async lib will be http/2 pushed always
}
}
/**
* Handle google fonts async
* This will result in a JS snippet in head, so need to put it in the end to avoid being replaced by JS parser
*/
$this->_async_ggfonts();
/**
* Font display optm
* @since 3.0
*/
$this->_font_optm();
/**
* Localize GG/FB JS/Fonts
* @since 3.3
*/
$this->content = Localization::get_instance()->finalize( $this->content );
/**
* Inline script manipulated until document is ready
* @since 3.0
*/
$this->_js_inline_defer();
// Check if there is any critical css rules setting
if ( $this->cfg_css_async ) {
$this->html_head = CSS::prepend_ccss( $this->html_head );
}
// Replace html head part
$this->html_head = apply_filters( 'litespeed_optm_html_head', $this->html_head );
if ( $this->html_head ) {
// Put header content to be after charset
if ( strpos( $this->content, '<meta charset' ) !== false ) {
$this->content = preg_replace( '#<meta charset([^>]*)>#isU', '<meta charset$1>' . $this->html_head , $this->content, 1 );
}
else {
$this->content = preg_replace( '#<head([^>]*)>#isU', '<head$1>' . $this->html_head , $this->content, 1 );
}
}
// Replace html foot part
$this->html_foot = apply_filters( 'litespeed_optm_html_foot', $this->html_foot );
if ( $this->html_foot ) {
$this->content = str_replace( '</body>', $this->html_foot . '</body>' , $this->content );
}
// Drop noscript if enabled
if ( Conf::val( Base::O_OPTM_NOSCRIPT_RM ) ) {
// $this->content = preg_replace( '#<noscript>.*</noscript>#isU', '', $this->content );
}
// HTML minify
if ( Conf::val( Base::O_OPTM_HTML_MIN ) ) {
$this->content = Optimizer::get_instance()->html_min( $this->content );
}
if ( $this->http2_headers ) {
@header( 'Link: ' . implode( ',', $this->http2_headers ), false );
}
}
/**
* Inline JS defer
*
* @since 3.0
* @access private
*/
private function _js_inline_defer() {
$optm_js_inline = Conf::val( Base::O_OPTM_JS_INLINE_DEFER );
if ( ! $optm_js_inline ) {
return;
}
$optm_js_inline_exc = Conf::val( Base::O_OPTM_JS_INLINE_DEFER_EXC );
Debug2::debug( '[Optm] Inline JS defer ' . $optm_js_inline );
preg_match_all( '#<script([^>]*)>(.*)</script>#isU', $this->content, $matches, PREG_SET_ORDER );
$script_ori = array();
$script_deferred = array();
$js_var_preserve = array();
foreach ( $matches as $match ) {
if ( ! empty( $match[ 1 ] ) ) {
$attrs = Utility::parse_attr( $match[ 1 ] );
if ( ! empty( $attrs[ 'src' ] ) ) {
continue;
}
if ( ! empty( $attrs[ 'data-no-optimize' ] ) ) {
continue;
}
if ( ! empty( $attrs[ 'type' ] ) && $attrs[ 'type' ] != 'text/javascript' ) {
continue;
}
}
$con = $match[ 2 ];
if ( $optm_js_inline_exc ) {
$hit = Utility::str_hit_array( $con, $optm_js_inline_exc );
if ( $hit ) {
Debug2::debug2( '[Optm] inline js defer excluded [setting] ' . $hit );
continue;
}
}
$con = trim( $con );
// Minify JS first
$con = Optimizer::minify_js( $con );
if ( ! $con ) {
continue;
}
if ( $optm_js_inline === 2 ) {
$script_ori[] = $match[ 0 ];
// Check if the content contains ESI nonce or not
if ( $esi_placeholder_list = ESI::get_instance()->contain_preserve_esi( $con ) ) {
foreach ( $esi_placeholder_list as $esi_placeholder ) {
$js_var = '__litespeed_var_' . ( self::$_var_i ++ ) . '__';
$con = str_replace( $esi_placeholder, $js_var, $con );
$js_var_preserve[] = $js_var . '=' . $esi_placeholder;
}
}
$script_deferred[] = '<script src="data:text/javascript;base64, ' . base64_encode( $con ) . '" defer ' . $match[ 1 ] . '></script>';
}
else {
// Prevent var scope issue
if ( strpos( $con, 'var ' ) !== false && strpos( $con, '{' ) === false ) {
continue;
}
if ( strpos( $con, 'var ' ) !== false && strpos( $con, '{' ) !== false && strpos( $con, '{' ) > strpos( $con, 'var ' ) ) {
continue;
}
if ( strpos( $con, 'document.addEventListener' ) !== false ) {
continue;
}
// $con = str_replace( 'var ', 'window.', $con );
$script_ori[] = $match[ 0 ];
$deferred = 'document.addEventListener("DOMContentLoaded",function(){' . $con . '});';
$script_deferred[] = '<script' . $match[ 1 ] . '>' . $deferred . '</script>';
}
}
if ( $js_var_preserve ) {
$this->html_head .= '<script>var ' . implode( ',', $js_var_preserve ) . ';</script>';
Debug2::debug2( '[Optm] Inline JS defer vars', $js_var_preserve );
}
$this->content = str_replace( $script_ori, $script_deferred, $this->content );
}
/**
* Google font async
*
* @since 2.7.3
* @access private
*/
private function _async_ggfonts() {
if ( ! $this->cfg_ggfonts_async || ! $this->_ggfonts_urls ) {
return;
}
Debug2::debug2( '[Optm] google fonts async found: ', $this->_ggfonts_urls );
$html = '<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin />';
/**
* Append fonts
*
* Could be multiple fonts
*
* <link rel='stylesheet' href='//fonts.googleapis.com/css?family=Open+Sans%3A400%2C600%2C700%2C800%2C300&ver=4.9.8' type='text/css' media='all' />
* <link rel='stylesheet' href='//fonts.googleapis.com/css?family=PT+Sans%3A400%2C700%7CPT+Sans+Narrow%3A400%7CMontserrat%3A600&subset=latin&ver=4.9.8' type='text/css' media='all' />
* -> family: PT Sans:400,700|PT Sans Narrow:400|Montserrat:600
* <link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,300,300italic,400italic,600,700,900&subset=latin%2Clatin-ext' />
*/
$html .='<script>WebFontConfig={google:{families:[';
$families = array();
foreach ( $this->_ggfonts_urls as $v ) {
$qs = wp_specialchars_decode( $v );
$qs = urldecode( $qs );
$qs = parse_url( $qs, PHP_URL_QUERY );
parse_str( $qs, $qs );
if ( empty( $qs[ 'family' ] ) ) {
Debug2::debug( '[Optm] ERR ggfonts failed to find family: ' . $v );
continue;
}
$subset = empty( $qs[ 'subset' ] ) ? '' : ':' . $qs[ 'subset' ];
foreach ( array_filter( explode( '|', $qs[ 'family' ] ) ) as $v2 ) {
$families[] = $v2 . $subset;
}
}
$html .= '"' . implode( '","', $families ) . ( $this->_conf_css_font_display ? '&display=' . $this->_conf_css_font_display : '' ) . '"';
$html .= ']}};';
// if webfontloader lib was loaded before WebFontConfig variable, call WebFont.load
$html .= 'if ( typeof WebFont === "object" && typeof WebFont.load === "function" ) { WebFont.load( WebFontConfig ); }';
$html .= '</script>';
// https://cdnjs.cloudflare.com/ajax/libs/webfont/1.6.28/webfontloader.js
$webfont_lib_url = LSWCP_PLUGIN_URL . self::LIB_FILE_WEBFONTLOADER;
// default async, if js defer set use defer
// TODO: make defer optional
$html .= '<script id="litespeed-webfont-lib" src="' . $webfont_lib_url . '" ' . ( $this->cfg_js_defer ? 'defer' : 'async' ) . '></script>';
$this->append_http2( $webfont_lib_url, 'js' ); // async lib will be http/2 pushed always
// Put this in the very beginning for preconnect
$this->html_head = $html . $this->html_head;
}
/**
* Font optm
*
* @since 3.0
* @access private
*/
private function _font_optm() {
if ( ! $this->_conf_css_font_display || ! $this->_ggfonts_urls ) {
return;
}
Debug2::debug2( '[Optm] google fonts optm ', $this->_ggfonts_urls );
foreach ( $this->_ggfonts_urls as $v ) {
if ( strpos( $v, 'display=' ) ) {
continue;
}
$this->html_head = str_replace( $v, $v . '&display=' . $this->_conf_css_font_display, $this->html_head );
$this->html_foot = str_replace( $v, $v . '&display=' . $this->_conf_css_font_display, $this->html_foot );
$this->content = str_replace( $v, $v . '&display=' . $this->_conf_css_font_display, $this->content );
}
}
/**
* Prefetch DNS
*
* @since 1.7.1
* @access private
*/
private function _dns_prefetch_init() {
// Widely enable link DNS prefetch
if ( Conf::val( Base::O_OPTM_DNS_PREFETCH_CTRL ) ) {
add_filter( 'litespeed_optm_html_head', array( $this, 'dns_prefetch_xmeta' ), 999 );
}
$this->dns_prefetch = Conf::val( Base::O_OPTM_DNS_PREFETCH );
if ( ! $this->dns_prefetch ) {
return;
}
if ( function_exists( 'wp_resource_hints' ) ) {
add_filter( 'wp_resource_hints', array( $this, 'dns_prefetch_filter' ), 10, 2 );
}
else {
add_action( 'litespeed_optm', array( $this, 'dns_prefetch_output' ) );
}
}
/**
* Append wide prefetch DNS meta
*
* @since 3.0
* @access public
*/
public function dns_prefetch_xmeta( $content ) {
$content .= '<meta http-equiv="x-dns-prefetch-control" content="on">';
return $content;
}
/**
* Prefetch DNS hook for WP
*
* @since 1.7.1
* @access public
*/
public function dns_prefetch_filter( $urls, $relation_type ) {
if ( $relation_type !== 'dns-prefetch' ) {
return $urls;
}
foreach ( $this->dns_prefetch as $v ) {
if ( $v ) {
$urls[] = $v;
}
}
return $urls;
}
/**
* Prefetch DNS
*
* @since 1.7.1
* @access public
*/
public function dns_prefetch_output() {
foreach ( $this->dns_prefetch as $v ) {
if ( $v ) {
$this->html_head .= '<link rel="dns-prefetch" href="' . $v . '" />';
}
}
}
/**
* Limit combined filesize when build hash url
*
* @since 1.3
* @access private
*/
private function _limit_size_build_hash_url( $src_queue_list, $file_size_list, $file_type = 'css' ) {
$total = 0;
$i = 0;
$src_arr = array();
$url_sensitive = Conf::val( Base::O_OPTM_CSS_UNIQUE ) && $file_type == 'css'; // If need to keep unique CSS per URI
foreach ( $src_queue_list as $k => $src ) {
empty( $src_arr[ $i ] ) && $src_arr[ $i ] = array();
$src_arr[ $i ][] = $src;
$total += $file_size_list[ $k ];
if ( $total > $this->cfg_optm_max_size && ! $url_sensitive ) { // If larger than 1M, separate them
$total = 0;
$i ++;
}
}
if ( count( $src_arr ) > 1 ) {
Debug2::debug( '[Optm] separate ' . $file_type . ' to ' . count( $src_arr ) );
}
// group build
$hashed_arr = array();
foreach ( $src_arr as $src_list ) {
$hashed_arr[] = $this->_build_hash_url( $src_list, $file_type, $url_sensitive );
}
return $hashed_arr;
}
/**
* Run minify with src queue list
*
* @since 1.2.2
* @access private
*/
private function _src_queue_handler( $src_queue_list, $html_list, $file_type = 'css' ) {
$html_list_ori = $html_list;
$tag = $file_type === 'css' ? 'link' : 'script';
foreach ( $src_queue_list as $key => $src ) {
if ( ! empty( $src[ 'src' ] ) ) {
$src = $src[ 'src' ];
}
$url = $this->_build_hash_url( $src, $file_type );
$snippet = str_replace( $src, $url, $html_list[ $key ] );
$snippet = str_replace( "<$tag ", '<' . $tag . ' data-optimized="1" ', $snippet );
$html_list[ $key ] = $snippet;
// Add to HTTP2
$this->append_http2( $url, $file_type );
}
// Handle css async load
if ( $file_type === 'css' && $this->cfg_css_async ) {
$html_list = $this->_async_css_list( $html_list );
}
// Handle js defer
if ( $file_type === 'js' && $this->cfg_js_defer ) {
$html_list = $this->_js_defer( $html_list );
}
$this->content = str_replace( $html_list_ori, $html_list, $this->content );
}
/**
* Check that links are internal or external
*
* @since 1.2.2
* @access private
* @return array Array(Ignored raw html, src needed to be handled list, filesize for param 2nd )
*/
private function _analyse_links( $src_list, $html_list, $file_type = 'css' ) {
// if ( $file_type == 'css' ) {
// $excludes = apply_filters( 'litespeed_optimize_css_excludes', Conf::val( Base::O_OPTM_CSS_EXC ) );
// }
// else {
// $excludes = apply_filters( 'litespeed_optimize_js_excludes', Conf::val( Base::O_OPTM_JS_EXC ) );
// }
// if ( $excludes ) {
// $excludes = explode( "\n", $excludes );
// }
$ignored_html = array();
$src_queue_list = array();
$file_size_list = array();
// Analyse links
foreach ( $src_list as $key => $src_info ) {
// CSS has different format when having media='' conditional attribute
if ( ! empty( $src_info[ 'src' ] ) ) {
$src = $src_info[ 'src' ];
}
else {
$src = $src_info;
}
Debug2::debug2( '[Optm] ' . $src );
/**
* Excluded links won't be done any optm
* @since 1.7
*/
// if ( $excludes && $exclude = Utility::str_hit_array( $src, $excludes ) ) {
// $ignored_html[] = $html_list[ $key ];
// Debug2::debug2( '[Optm]: Abort excludes: ' . $exclude );
// continue;
// }
// Check if has no-optimize attr
if ( strpos( $html_list[ $key ], 'data-ignore-optimize' ) !== false ) {
$ignored_html[] = $html_list[ $key ];
Debug2::debug2( '[Optm] Abort excludes: attr data-ignore-optimize' );
continue;
}
// Check if is external URL
$url_parsed = parse_url( $src );
if ( ! $file_info = Utility::is_internal_file( $src ) ) {
$ignored_html[ $src ] = $html_list[ $key ];
Debug2::debug2( '[Optm] Abort external/non-exist' );
continue;
}
/**
* Check if exclude jQuery or not
* Exclude from minify/combine
* @since 1.5
*/
if ( $this->cfg_exc_jquery && $this->_is_jquery( $src ) ) {
$ignored_html[ $src ] = $html_list[ $key ];
Debug2::debug2( '[Optm] Abort jQuery by setting' );
// Add to HTTP2 as its ignored but still internal src
$this->append_http2( $src, 'js' );
continue;
}
// Note: some CSS may have different format
$src_queue_list[ $key ] = $src_info;
$file_size_list[ $key ] = $file_info[ 1 ];
}
return array( $ignored_html, $src_queue_list, $file_size_list );
}
/**
* Generate full URL path with hash for a list of src
*
* @since 1.2.2
* @access private
* @return string The final URL
*/
private function _build_hash_url( $src, $file_type = 'css', $url_sensitive = false ) {
if ( ! $src ) {
return false;
}
global $wp;
$request_url = home_url( $wp->request );
if ( ! is_array( $src ) ) {
$src = array( $src );
}
// Query string hash
$qs_hash = substr( md5( json_encode( $src ) . self::get_option( self::ITEM_TIMESTAMP_PURGE_CSS ) ), -5 );
// Drop query strings
foreach ( $src as $k => $v ) {
if ( ! empty( $v[ 'src' ] ) ) {
$src[ $k ][ 'src' ] = $this->remove_query_strings( $v[ 'src' ] ); // CSS w/ cond `media=`
}
else {
$src[ $k ] = $this->remove_query_strings( $v );
}
}
// $src = array_values( $src );
$hash = md5( json_encode( $src ) );
$filename = substr( $hash, -5 ) . '.' . $file_type;
// Need to check conflicts
// If short hash exists
$existed = false;
if ( $optm_data = Data::get_instance()->optm_hash2src( $filename ) ) {
// If conflicts
if ( $optm_data[ 'src' ] === $src && ( ! $url_sensitive || $optm_data[ 'refer' ] === $request_url ) ) {
$existed = true;
}
else {
$filename = $hash . '.' . $file_type;
}
}
// Need to insert the record
if ( ! $existed ) {
Data::get_instance()->optm_save_src( $filename, $src, $request_url );
}
// Generate static files
$static_file = LITESPEED_STATIC_DIR . "/cssjs/$filename";
// Check if the file is valid to bypass minify process
if ( ! file_exists( $static_file ) || time() - filemtime( $static_file ) > $this->cfg_ttl ) {
$concat_only = ! ( $file_type === 'css' ? $this->cfg_css_min : $this->cfg_js_min );
$content = Optimizer::get_instance()->serve( $filename, $concat_only, $src, $request_url );
// Generate static file
File::save( $static_file, $content, true );
Debug2::debug2( '[Optm] Saved static file [path] ' . $static_file );
}
return LITESPEED_STATIC_URL . '/cssjs/' . $filename . '?' . $qs_hash;
}
/**
* Parse js src
*
* @since 1.2.2
* @access private
* @return array All the src & related raw html list
*/
private function _parse_js() {
$excludes = apply_filters( 'litespeed_optimize_js_excludes', Conf::val( Base::O_OPTM_JS_EXC ) );
$src_list = array();
$html_list = array();
$head_src_list = array();
$content = preg_replace( '#<!--.*-->#sU', '', $this->content );
preg_match_all( '#<script ([^>]+)>\s*</script>|</head>#isU', $content, $matches, PREG_SET_ORDER ); // v3.3 Changed `<script \s*(` to `<script (`
$is_head = true;
foreach ( $matches as $match ) {
if ( $match[ 0 ] === '</head>' ) {
$is_head = false;
continue;
}
$attrs = Utility::parse_attr( $match[ 1 ] );
if ( isset( $attrs[ 'data-optimized' ] ) ) {
continue;
}
if ( ! empty( $attrs[ 'data-no-optimize' ] ) ) {
continue;
}
if ( empty( $attrs[ 'src' ] ) ) {
continue;
}
if ( strpos( $attrs[ 'src' ], '/localres/' ) !== false ) {
continue;
}
$url_parsed = parse_url( $attrs[ 'src' ], PHP_URL_PATH );
if ( substr( $url_parsed, -3 ) !== '.js' ) {
Debug2::debug2( '[Optm] _parse_js bypassed due to not js file ' . $url_parsed );
continue;
}
// to avoid multiple replacement
if ( in_array( $match[ 0 ], $html_list ) ) {
continue;
}
// todo @v2.0: allow defer even exclude from optm
if ( $excludes && $exclude = Utility::str_hit_array( $attrs[ 'src' ], $excludes ) ) {
Debug2::debug2( '[Optm] _parse_js bypassed exclude ' . $exclude );
continue;
}
$src_list[] = $attrs[ 'src' ];
$html_list[] = $match[ 0 ];
if ( $is_head ) {
$head_src_list[] = $attrs[ 'src' ];
}
}
return array( $src_list, $html_list, $head_src_list );
}
/**
* Parse css src and remove to-be-removed css
*
* @since 1.2.2
* @access private
* @return array All the src & related raw html list
*/
private function _handle_css() {
$excludes = apply_filters( 'litespeed_optimize_css_excludes', Conf::val( Base::O_OPTM_CSS_EXC ) );
$css_to_be_removed = apply_filters( 'litespeed_optm_css_to_be_removed', array() );
$src_list = array();
$html_list = array();
// $dom = new \PHPHtmlParser\Dom;
// $dom->load( $content );return $val;
// $items = $dom->find( 'link' );
$content = preg_replace( '#<!--.*-->#sU', '', $this->content );
preg_match_all( '#<link ([^>]+)/?>#isU', $content, $matches, PREG_SET_ORDER ); // Changed in v3.3 `<link \s*` to `<link ` and see if css can parse w/o issue
foreach ( $matches as $match ) {
$attrs = Utility::parse_attr( $match[ 1 ] );
if ( empty( $attrs[ 'rel' ] ) || $attrs[ 'rel' ] !== 'stylesheet' ) {
continue;
}
if ( isset( $attrs[ 'data-optimized' ] ) ) {
continue;
}
if ( ! empty( $attrs[ 'data-no-optimize' ] ) ) {
continue;
}
if ( ! empty( $attrs[ 'media' ] ) && strpos( $attrs[ 'media' ], 'print' ) !== false ) {
// continue;
}
if ( empty( $attrs[ 'href' ] ) ) {
continue;
}
if ( $excludes && $exclude = Utility::str_hit_array( $attrs[ 'href' ], $excludes ) ) {
Debug2::debug2( '[Optm] _handle_css bypassed exclude ' . $exclude );
continue;
}
// Check if need to remove this css
if ( $css_to_be_removed && Utility::str_hit_array( $attrs[ 'href' ], $css_to_be_removed ) ) {
Debug2::debug( '[Optm] rm css snippet ' . $attrs[ 'href' ] );
// Delete this css snippet from orig html
$this->content = str_replace( $match[ 0 ], '', $this->content );
continue;
}
// Check Google fonts hit
if ( strpos( $attrs[ 'href' ], 'fonts.googleapis.com' ) !== false ) {
/**
* For async gg fonts, will add webfont into head, hence remove it from buffer and store the matches to use later
* @since 2.7.3
* @since 3.0 For fotn display optm, need to parse google fonts URL too
*/
if ( ! in_array( $attrs[ 'href' ], $this->_ggfonts_urls ) ) {
$this->_ggfonts_urls[] = $attrs[ 'href' ];
}
if ( $this->cfg_ggfonts_rm || $this->cfg_ggfonts_async ) {
Debug2::debug2( '[Optm] rm css snippet [Google fonts] ' . $attrs[ 'href' ] );
$this->content = str_replace( $match[ 0 ], '', $this->content );
continue;
}
}
// to avoid multiple replacement
if ( in_array( $match[ 0 ], $html_list ) ) {
continue;
}
if ( ! empty( $attrs[ 'media' ] ) && $attrs[ 'media' ] !== 'all' ) {
$src_list[] = array(
'src' => $attrs[ 'href' ],
'media' => $attrs[ 'media' ],
);
}
else {
$src_list[] = $attrs[ 'href' ];
}
$html_list[] = $match[ 0 ];
}
return array( $src_list, $html_list );
}
/**
* Replace css to async loaded css
*
* @since 1.3
* @access private
* @param array $html_list Orignal css array
* @return array (array)css_async_list
*/
private function _async_css_list( $html_list ) {
foreach ( $html_list as $k => $ori ) {
if ( strpos( $ori, 'data-asynced' ) !== false ) {
Debug2::debug2( '[Optm] bypass: attr data-asynced exist' );
continue;
}
if ( strpos( $ori, 'data-no-async' ) !== false ) {
Debug2::debug2( '[Optm] bypass: attr api data-no-async' );
continue;
}
// async replacement
$v = str_replace( 'stylesheet', 'preload', $ori );
$v = str_replace( '<link', '<link data-asynced="1" as="style" onload="this.onload=null;this.rel=\'stylesheet\'" ', $v );
// Append to noscript content
if ( ! Conf::val( Base::O_OPTM_NOSCRIPT_RM ) ) {
$v .= '<noscript>' . $ori . '</noscript>';
}
$html_list[ $k ] = $v;
}
return $html_list;
}
/**
* Add defer to js
*
* @since 1.3
* @access private
*/
private function _js_defer( $html_list ) {
foreach ( $html_list as $k => $v ) {
if ( strpos( $v, 'async' ) !== false ) {
continue;
}
if ( strpos( $v, 'defer' ) !== false ) {
continue;
}
if ( strpos( $v, 'data-deferred' ) !== false ) {
Debug2::debug2( '[Optm] bypass: attr data-deferred exist' );
continue;
}
if ( strpos( $v, 'data-no-defer' ) !== false ) {
Debug2::debug2( '[Optm] bypass: attr api data-no-defer' );
continue;
}
/**
* Parse src for excluding js from setting
* @since 1.5
*/
if ( $this->cfg_js_defer_exc || $this->cfg_exc_jquery ) {
// parse js src
preg_match( '#<script \s*([^>]+)>#isU', $v, $matches );
if ( empty( $matches[ 1 ] ) ) {
Debug2::debug( '[Optm] js defer parse html failed: ' . $v );
continue;
}
$attrs = Utility::parse_attr( $matches[ 1 ] );
if ( empty( $attrs[ 'src' ] ) ) {
Debug2::debug( '[Optm] js defer parse src failed: ' . $matches[ 1 ] );
continue;
}
$src = $attrs[ 'src' ];
}
/**
* Exclude js from setting
* @since 1.5
*/
if ( $this->cfg_js_defer_exc && Utility::str_hit_array( $src, $this->cfg_js_defer_exc ) ) {
Debug2::debug( '[Optm] js defer exclude ' . $src );
continue;
}
/**
* Check if exclude jQuery
* @since 1.5
*/
if ( $this->cfg_exc_jquery && $this->_is_jquery( $src ) ) {
Debug2::debug2( '[Optm] js defer Abort jQuery by setting' );
continue;
}
$html_list[ $k ] = str_replace( '></script>', ' defer data-deferred="1"></script>', $v );
}
return $html_list;
}
/**
* Check if is jq lib
*
* @since 1.5
* @access private
*/
private function _is_jquery( $src ) {
return stripos( $src, 'jquery.js' ) !== false || stripos( $src, 'jquery.min.js' ) !== false;
}
/**
* Append to HTTP2 header
*
* @since 1.2.2
* @access private
*/
private function append_http2( $url, $file_type = 'css' ) {
if ( ! ( $file_type === 'css' ? $this->cfg_http2_css : $this->cfg_http2_js ) ) {
return;
}
/**
* For CDN enabled ones, bypass http/2 push
* @since 1.6.2.1
*/
if ( CDN::inc_type( $file_type ) ) {
return;
}
/**
* Keep QS for constance by set 2nd param to true
* @since 1.6.2.1
*/
$uri = Utility::url2uri( $url, true );
if ( ! $uri ) {
return;
}
$this->http2_headers[] = '<' . $uri . '>; rel=preload; as=' . ( $file_type === 'css' ? 'style' : 'script' );
}
}