As the sun rises and the forest mist clears, and the clouds return and the caves darken, these changes of light and shadow are the morning and evening in the mountains. Wildflowers bloom with their subtle fragrance, fine trees flourish with their dense shade, the wind and frost are pure and clean, and the water recedes to reveal the rocks—these are the four seasons in the mountains. Going out in the morning and returning in the evening, the scenery of the four seasons is different, and the joy is endless.至于负者歌于途,行者休于树,前者呼,后者应,伛偻提携,往来而不绝者,滁人游也。临溪而渔,溪深而鱼肥,酿泉为酒,泉香而酒洌,山肴野蔌,杂然而前陈者,太守宴也。宴酣之乐,非丝非竹,射者中,弈者胜,觥筹交错,起坐而喧哗者,众宾欢也。苍颜白发,颓然乎其间者,太守醉也。
<?php
// phpcs:ignoreFile
/**
* The optimize class.
*
* @since 1.2.2
*/
namespace LiteSpeed;
defined('WPINC') || exit();
class Optimize extends Base {
const LOG_TAG = '🎢';
const LIB_FILE_CSS_ASYNC = 'assets/js/css_async.min.js';
const LIB_FILE_WEBFONTLOADER = 'assets/js/webfontloader.min.js';
const LIB_FILE_JS_DELAY = 'assets/js/js_delay.min.js';
const ITEM_TIMESTAMP_PURGE_CSS = 'timestamp_purge_css';
const DUMMY_CSS_REGEX = "#<link [ \w='\"/]*id=['\"]litespeed-cache-dummy-css['\"] href=['\"].+assets/css/litespeed-dummy\.css[?\w.=-]*['\"][ \w='\"/]*>#isU";
private $content;
private $content_ori;
private $cfg_css_min;
private $cfg_css_comb;
private $cfg_js_min;
private $cfg_js_comb;
private $cfg_css_async;
private $cfg_js_delay_inc = array();
private $cfg_js_defer;
private $cfg_js_defer_exc = false;
private $cfg_ggfonts_async;
private $_conf_css_font_display;
private $cfg_ggfonts_rm;
private $dns_prefetch;
private $dns_preconnect;
private $_ggfonts_urls = array();
private $_ccss;
private $_ucss = false;
private $__optimizer;
private $html_foot = ''; // The html info append to <body>
private $html_head = ''; // The html info append to <head>
private $html_head_early = ''; // The html info prepend to top of head
private static $_var_i = 0;
private $_var_preserve_js = array();
private $_request_url;
/**
* Constructor
*
* @since 4.0
*/
public function __construct() {
self::debug('init');
$this->__optimizer = $this->cls('Optimizer');
}
/**
* Init optimizer
*
* @since 3.0
* @access protected
*/
public function init() {
$this->cfg_css_async = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_CSS_ASYNC);
if ($this->cfg_css_async) {
if (!$this->cls('Cloud')->activated()) {
self::debug('❌ CCSS set to OFF due to QC not activated');
$this->cfg_css_async = false;
}
if ((defined('LITESPEED_GUEST_OPTM') || ($this->conf(self::O_OPTM_UCSS) && $this->conf(self::O_OPTM_CSS_COMB))) && $this->conf(self::O_OPTM_UCSS_INLINE)) {
self::debug('⚠️ CCSS set to OFF due to UCSS Inline');
$this->cfg_css_async = false;
}
}
$this->cfg_js_defer = $this->conf(self::O_OPTM_JS_DEFER);
if (defined('LITESPEED_GUEST_OPTM')) {
$this->cfg_js_defer = 2;
}
if ($this->cfg_js_defer == 2) {
add_filter(
'litespeed_optm_cssjs',
function ( $con, $file_type ) {
if ($file_type == 'js') {
$con = str_replace('DOMContentLoaded', 'DOMContentLiteSpeedLoaded', $con);
// $con = str_replace( 'addEventListener("load"', 'addEventListener("litespeedLoad"', $con );
}
return $con;
},
20,
2
);
}
// To remove emoji from WP
if ($this->conf(self::O_OPTM_EMOJI_RM)) {
$this->_emoji_rm();
}
if ($this->conf(self::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);
}
// GM JS exclude @since 4.1
if (defined('LITESPEED_GUEST_OPTM')) {
$this->cfg_js_defer_exc = apply_filters('litespeed_optm_gm_js_exc', $this->conf(self::O_OPTM_GM_JS_EXC));
} else {
/**
* Exclude js from deferred setting
*
* @since 1.5
*/
if ($this->cfg_js_defer) {
add_filter('litespeed_optm_js_defer_exc', array( $this->cls('Data'), 'load_js_defer_exc' ));
$this->cfg_js_defer_exc = apply_filters('litespeed_optm_js_defer_exc', $this->conf(self::O_OPTM_JS_DEFER_EXC));
$this->cfg_js_delay_inc = apply_filters('litespeed_optm_js_delay_inc', $this->conf(self::O_OPTM_JS_DELAY_INC));
}
}
// Add vary filter for Role Excludes @since 1.6
add_filter('litespeed_vary', array( $this, 'vary_add_role_exclude' ));
// DNS optm (Prefetch/Preconnect) @since 7.3
$this->_dns_optm_init();
add_filter('litespeed_buffer_finalize', array( $this, 'finalize' ), 20);
// Inject a dummy CSS file to control final optimized data location in <head>
wp_enqueue_style(Core::PLUGIN_NAME . '-dummy', LSWCP_PLUGIN_URL . 'assets/css/litespeed-dummy.css');
}
/**
* Exclude role from optimization filter
*
* @since 1.6
* @access public
*/
public function vary_add_role_exclude( $vary ) {
if ($this->cls('Conf')->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');
}
/**
* Delete file-based cache folder
*
* @since 2.1
* @access public
*/
public function rm_cache_folder( $subsite_id = false ) {
if ($subsite_id) {
file_exists(LITESPEED_STATIC_DIR . '/css/' . $subsite_id) && File::rrmdir(LITESPEED_STATIC_DIR . '/css/' . $subsite_id);
file_exists(LITESPEED_STATIC_DIR . '/js/' . $subsite_id) && File::rrmdir(LITESPEED_STATIC_DIR . '/js/' . $subsite_id);
return;
}
file_exists(LITESPEED_STATIC_DIR . '/css') && File::rrmdir(LITESPEED_STATIC_DIR . '/css');
file_exists(LITESPEED_STATIC_DIR . '/js') && File::rrmdir(LITESPEED_STATIC_DIR . '/js');
}
/**
* 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 (!Utility::is_internal_file($src)) {
return $src;
}
if (strpos($src, '.js?') !== false || strpos($src, '.css?') !== false) {
$src = preg_replace('/\?.*/', '', $src);
}
return $src;
}
/**
* 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 function finalize( $content ) {
$content = $this->_finalize($content);
// Fallback to replace dummy css placeholder
if (false !== preg_match(self::DUMMY_CSS_REGEX, $content)) {
self::debug('Fallback to drop dummy CSS');
$content = preg_replace( self::DUMMY_CSS_REGEX, '', $content );
}
return $content;
}
private function _finalize( $content ) {
if (defined('LITESPEED_NO_PAGEOPTM')) {
self::debug2('bypass: NO_PAGEOPTM const');
return $content;
}
if (!defined('LITESPEED_IS_HTML')) {
self::debug('bypass: Not frontend HTML type');
return $content;
}
if (!defined('LITESPEED_GUEST_OPTM')) {
if (!Control::is_cacheable()) {
self::debug('bypass: Not cacheable');
return $content;
}
// Check if hit URI excludes
add_filter('litespeed_optm_uri_exc', array( $this->cls('Data'), 'load_optm_uri_exc' ));
$excludes = apply_filters('litespeed_optm_uri_exc', $this->conf(self::O_OPTM_EXC));
$result = Utility::str_hit_array($_SERVER['REQUEST_URI'], $excludes);
if ($result) {
self::debug('bypass: hit URI Excludes setting: ' . $result);
return $content;
}
}
self::debug('start');
$this->content_ori = $this->content = $content;
$this->_optimize();
return $this->content;
}
/**
* Optimize css src
*
* @since 1.2.2
* @access private
*/
private function _optimize() {
global $wp;
// get current request url
$permalink_structure = get_option( 'permalink_structure' );
if ( ! empty( $permalink_structure ) ) {
$this->_request_url = trailingslashit( home_url( $wp->request ) );
} else {
$qs_add = $wp->query_string ? '?' . (string) $wp->query_string : '' ;
$this->_request_url = home_url( $wp->request ) . $qs_add;
}
$this->cfg_css_min = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_CSS_MIN);
$this->cfg_css_comb = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_CSS_COMB);
$this->cfg_js_min = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_JS_MIN);
$this->cfg_js_comb = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_JS_COMB);
$this->cfg_ggfonts_rm = defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_GGFONTS_RM);
$this->cfg_ggfonts_async = !defined('LITESPEED_GUEST_OPTM') && $this->conf(self::O_OPTM_GGFONTS_ASYNC); // forced rm already
$this->_conf_css_font_display = !defined('LITESPEED_GUEST_OPTM') && $this->conf(self::O_OPTM_CSS_FONT_DISPLAY);
if (!$this->cls('Router')->can_optm()) {
self::debug('bypass: admin/feed/preview');
return;
}
if ($this->cfg_css_async) {
$this->_ccss = $this->cls('CSS')->prepare_ccss();
if (!$this->_ccss) {
self::debug('❌ CCSS set to OFF due to CCSS not generated yet');
$this->cfg_css_async = false;
} elseif (strpos($this->_ccss, '<style id="litespeed-ccss" data-error') === 0) {
self::debug('❌ CCSS set to OFF due to CCSS failed to generate');
$this->cfg_css_async = false;
}
}
do_action('litespeed_optm');
// Parse css from content
$src_list = false;
if ($this->cfg_css_min || $this->cfg_css_comb || $this->cfg_ggfonts_rm || $this->cfg_css_async || $this->cfg_ggfonts_async || $this->_conf_css_font_display) {
add_filter('litespeed_optimize_css_excludes', array( $this->cls('Data'), 'load_css_exc' ));
list($src_list, $html_list) = $this->_parse_css();
}
// css optimizer
if ($this->cfg_css_min || $this->cfg_css_comb) {
if ($src_list) {
// IF combine
if ($this->cfg_css_comb) {
// Check if has inline UCSS enabled or not
if ((defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_UCSS)) && $this->conf(self::O_OPTM_UCSS_INLINE)) {
$filename = $this->cls('UCSS')->load($this->_request_url, true);
if ($filename) {
$filepath_prefix = $this->_build_filepath_prefix('ucss');
$this->_ucss = File::read(LITESPEED_STATIC_DIR . $filepath_prefix . $filename);
// Drop all css
$this->content = str_replace($html_list, '', $this->content);
}
}
if (!$this->_ucss) {
$url = $this->_build_hash_url($src_list);
if ($url) {
// Handle css async load
if ($this->cfg_css_async) {
$this->html_head .=
'<link rel="preload" data-asynced="1" data-optimized="2" as="style" onload="this.onload=null;this.rel=\'stylesheet\'" href="' .
Str::trim_quotes($url) .
'" />'; // todo: How to use " in attr wrapper "
} else {
$this->html_head .= '<link data-optimized="2" rel="stylesheet" href="' . Str::trim_quotes($url) . '" />'; // use 2 as combined
}
// Move all css to top
$this->content = str_replace($html_list, '', $this->content);
}
}
}
// Only minify
elseif ($this->cfg_css_min) {
// will handle async css load inside
$this->_src_queue_handler($src_list, $html_list);
}
// Only HTTP2 push
else {
foreach ($src_list as $src_info) {
if (!empty($src_info['inl'])) {
continue;
}
}
}
}
}
// 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, $src_list);
// Replace async css
$this->content = str_replace($html_list, $html_list_async, $this->content);
}
// Parse js from buffer as needed
$src_list = false;
if ($this->cfg_js_min || $this->cfg_js_comb || $this->cfg_js_defer || $this->cfg_js_delay_inc) {
add_filter('litespeed_optimize_js_excludes', array( $this->cls('Data'), 'load_js_exc' ));
list($src_list, $html_list) = $this->_parse_js();
}
// js optimizer
if ($src_list) {
// IF combine
if ($this->cfg_js_comb) {
$url = $this->_build_hash_url($src_list, 'js');
if ($url) {
$this->html_foot .= $this->_build_js_tag($url);
// Will move all JS to bottom combined one
$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_list, $html_list, 'js');
}
// Only HTTP2 push and Defer
else {
foreach ($src_list as $k => $src_info) {
// Inline JS
if (!empty($src_info['inl'])) {
if ($this->cfg_js_defer) {
$attrs = !empty($src_info['attrs']) ? $src_info['attrs'] : '';
$deferred = $this->_js_inline_defer($src_info['src'], $attrs);
if ($deferred) {
$this->content = str_replace($html_list[$k], $deferred, $this->content);
}
}
}
// JS files
elseif ($this->cfg_js_defer) {
$deferred = $this->_js_defer($html_list[$k], $src_info['src']);
if ($deferred) {
$this->content = str_replace($html_list[$k], $deferred, $this->content);
}
} elseif ($this->cfg_js_delay_inc) {
$deferred = $this->_js_delay($html_list[$k], $src_info['src']);
if ($deferred) {
$this->content = str_replace($html_list[$k], $deferred, $this->content);
}
}
}
}
}
// Append JS inline var for preserved ESI
// Shouldn't give any optm (defer/delay) @since 4.4
if ($this->_var_preserve_js) {
$this->html_head .= '<script>var ' . implode(',', $this->_var_preserve_js) . ';</script>';
self::debug2('Inline JS defer vars', $this->_var_preserve_js);
}
// Append async compatibility lib to head
if ($this->cfg_css_async) {
// Inline css async lib
if ($this->conf(self::O_OPTM_CSS_ASYNC_INLINE)) {
$this->html_head .= $this->_build_js_inline(File::read(LSCWP_DIR . self::LIB_FILE_CSS_ASYNC), true);
} else {
$css_async_lib_url = LSWCP_PLUGIN_URL . self::LIB_FILE_CSS_ASYNC;
$this->html_head .= $this->_build_js_tag($css_async_lib_url); // Don't exclude it from defer for now
}
}
/**
* 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();
// Inject JS Delay lib
$this->_maybe_js_delay();
/**
* HTML Lazyload
*/
if ($this->conf(self::O_OPTM_HTML_LAZY)) {
$this->html_head = $this->cls('CSS')->prepare_html_lazy() . $this->html_head;
}
// Maybe prepend inline UCSS
if ($this->_ucss) {
$this->html_head = '<style id="litespeed-ucss">' . $this->_ucss . '</style>' . $this->html_head;
}
// Check if there is any critical css rules setting
if ($this->cfg_css_async && $this->_ccss) {
$this->html_head = $this->_ccss . $this->html_head;
}
// Replace html head part
$this->html_head_early = apply_filters('litespeed_optm_html_head_early', $this->html_head_early);
if ($this->html_head_early) {
// Put header content to be after charset
if (false !== strpos($this->content, '<meta charset')) {
self::debug('Put early optm data to be after <meta charset>');
$this->content = preg_replace('#<meta charset([^>]*)>#isU', '<meta charset$1>' . $this->html_head_early, $this->content, 1);
} else {
self::debug('Put early optm data to be right after <head>');
$this->content = preg_replace('#<head([^>]*)>#isU', '<head$1>' . $this->html_head_early, $this->content, 1);
}
}
$this->html_head = apply_filters('litespeed_optm_html_head', $this->html_head);
if ($this->html_head) {
if (apply_filters('litespeed_optm_html_after_head', false)) {
$this->content = str_replace('</head>', $this->html_head . '</head>', $this->content);
} else {
// Put header content to dummy css position
if (false !== preg_match(self::DUMMY_CSS_REGEX, $this->content)) {
self::debug('Put optm data to dummy css location');
$this->content = preg_replace( self::DUMMY_CSS_REGEX, $this->html_head, $this->content );
}
// Fallback: try to be after charset
elseif (strpos($this->content, '<meta charset') !== false) {
self::debug('Put optm data to be after <meta charset>');
$this->content = preg_replace('#<meta charset([^>]*)>#isU', '<meta charset$1>' . $this->html_head, $this->content, 1);
} else {
self::debug('Put optm data to be after <head>');
$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 ($this->conf(self::O_OPTM_NOSCRIPT_RM)) {
// $this->content = preg_replace( '#<noscript>.*</noscript>#isU', '', $this->content );
}
// Inline font-face optimize
$this->content = $this->__optimizer->optm_font_face( $this->content );
// HTML minify
if (defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_HTML_MIN)) {
$this->content = $this->__optimizer->html_min($this->content);
}
}
/**
* Build a full JS tag
*
* @since 4.0
*/
private function _build_js_tag( $src ) {
if ($this->cfg_js_defer === 2 || Utility::str_hit_array($src, $this->cfg_js_delay_inc)) {
return '<script data-optimized="1" type="litespeed/javascript" data-src="' . Str::trim_quotes($src) . '"></script>';
}
if ($this->cfg_js_defer) {
return '<script data-optimized="1" src="' . Str::trim_quotes($src) . '" defer></script>';
}
return '<script data-optimized="1" src="' . Str::trim_quotes($src) . '"></script>';
}
/**
* Build a full inline JS snippet
*
* @since 4.0
*/
private function _build_js_inline( $script, $minified = false ) {
if ($this->cfg_js_defer) {
$deferred = $this->_js_inline_defer($script, false, $minified);
if ($deferred) {
return $deferred;
}
}
return '<script>' . $script . '</script>';
}
/**
* Load JS delay lib
*
* @since 4.0
*/
private function _maybe_js_delay() {
if ($this->cfg_js_defer !== 2 && !$this->cfg_js_delay_inc) {
return;
}
if (!defined('LITESPEED_JS_DELAY_LIB_LOADED')) {
define('LITESPEED_JS_DELAY_LIB_LOADED', true);
$this->html_foot .= '<script>' . File::read(LSCWP_DIR . self::LIB_FILE_JS_DELAY) . '</script>';
}
}
/**
* Google font async
*
* @since 2.7.3
* @access private
*/
private function _async_ggfonts() {
if (!$this->cfg_ggfonts_async || !$this->_ggfonts_urls) {
return;
}
self::debug2('google fonts async found: ', $this->_ggfonts_urls);
$this->html_head_early .= '<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' />
*/
$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'])) {
self::debug('ERR ggfonts failed to find family: ' . $v);
continue;
}
$subset = empty($qs['subset']) ? '' : ':' . $qs['subset'];
foreach (array_filter(explode('|', $qs['family'])) as $v2) {
$families[] = Str::trim_quotes($v2 . $subset);
}
}
$script .= '"' . implode('","', $families) . ($this->_conf_css_font_display ? '&display=swap' : '') . '"';
$script .= ']}};';
// if webfontloader lib was loaded before WebFontConfig variable, call WebFont.load
$script .= 'if ( typeof WebFont === "object" && typeof WebFont.load === "function" ) { WebFont.load( WebFontConfig ); }';
$html = $this->_build_js_inline($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
$html .= $this->_build_js_tag($webfont_lib_url);
// 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;
}
self::debug2('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=swap', $this->html_head);
$this->html_foot = str_replace($v, $v . '&display=swap', $this->html_foot);
$this->content = str_replace($v, $v . '&display=swap', $this->content);
}
}
/**
* Prefetch DNS
*
* @since 1.7.1 DNS prefetch
* @since 5.6.1 DNS preconnect
* @access private
*/
private function _dns_optm_init() {
// Widely enable link DNS prefetch
if (defined('LITESPEED_GUEST_OPTM') || $this->conf(self::O_OPTM_DNS_PREFETCH_CTRL)) {
@header('X-DNS-Prefetch-Control: on');
}
$this->dns_prefetch = $this->conf(self::O_OPTM_DNS_PREFETCH);
$this->dns_preconnect = $this->conf(self::O_OPTM_DNS_PRECONNECT);
if (!$this->dns_prefetch && !$this->dns_preconnect) {
return;
}
if (function_exists('wp_resource_hints')) {
add_filter('wp_resource_hints', array( $this, 'dns_optm_filter' ), 10, 2);
} else {
add_action('litespeed_optm', array( $this, 'dns_optm_output' ));
}
}
/**
* DNS optm hook for WP
*
* @since 1.7.1
* @access public
*/
public function dns_optm_filter( $urls, $relation_type ) {
if ('dns-prefetch' === $relation_type) {
foreach ($this->dns_prefetch as $v) {
if ($v) {
$urls[] = $v;
}
}
}
if ('preconnect' === $relation_type) {
foreach ($this->dns_preconnect as $v) {
if ($v) {
$urls[] = $v;
}
}
}
return $urls;
}
/**
* DNS optm output directly
*
* @since 1.7.1 DNS prefetch
* @since 5.6.1 DNS preconnect
* @access public
*/
public function dns_optm_output() {
foreach ($this->dns_prefetch as $v) {
if ($v) {
$this->html_head_early .= '<link rel="dns-prefetch" href="' . Str::trim_quotes($v) . '" />';
}
}
foreach ($this->dns_preconnect as $v) {
if ($v) {
$this->html_head_early .= '<link rel="preconnect" href="' . Str::trim_quotes($v) . '" crossorigin />';
}
}
}
/**
* Run minify with src queue list
*
* @since 1.2.2
* @access private
*/
private function _src_queue_handler( $src_list, $html_list, $file_type = 'css' ) {
$html_list_ori = $html_list;
$can_webp = $this->cls('Media')->webp_support();
$tag = $file_type == 'css' ? 'link' : 'script';
foreach ($src_list as $key => $src_info) {
// Minify inline CSS/JS
if (!empty($src_info['inl'])) {
if ($file_type == 'css') {
$code = Optimizer::minify_css($src_info['src']);
$can_webp && ($code = $this->cls('Media')->replace_background_webp($code));
$snippet = str_replace($src_info['src'], $code, $html_list[$key]);
} else {
// Inline defer JS
if ($this->cfg_js_defer) {
$attrs = !empty($src_info['attrs']) ? $src_info['attrs'] : '';
$snippet = $this->_js_inline_defer($src_info['src'], $attrs) ?: $html_list[$key];
} else {
$code = Optimizer::minify_js($src_info['src']);
$snippet = str_replace($src_info['src'], $code, $html_list[$key]);
}
}
}
// CSS/JS files
else {
$url = $this->_build_single_hash_url($src_info['src'], $file_type);
if ($url) {
$snippet = str_replace($src_info['src'], $url, $html_list[$key]);
}
// Handle css async load
if ($file_type == 'css' && $this->cfg_css_async) {
$snippet = $this->_async_css($snippet);
}
// Handle js defer
if ($file_type === 'js' && $this->cfg_js_defer) {
$snippet = $this->_js_defer($snippet, $src_info['src']) ?: $snippet;
}
}
$snippet = str_replace("<$tag ", '<' . $tag . ' data-optimized="1" ', $snippet);
$html_list[$key] = $snippet;
}
$this->content = str_replace($html_list_ori, $html_list, $this->content);
}
/**
* Build a single URL mapped filename (This will not save in DB)
*
* @since 4.0
*/
private function _build_single_hash_url( $src, $file_type = 'css' ) {
$content = $this->__optimizer->load_file($src, $file_type);
$is_min = $this->__optimizer->is_min($src);
$content = $this->__optimizer->optm_snippet($content, $file_type, !$is_min, $src);
$filepath_prefix = $this->_build_filepath_prefix($file_type);
// Save to file
$filename = $filepath_prefix . md5($this->remove_query_strings($src)) . '.' . $file_type;
$static_file = LITESPEED_STATIC_DIR . $filename;
File::save($static_file, $content, true);
// QS is required as $src may contains version info
$qs_hash = substr(md5($src), -5);
return LITESPEED_STATIC_URL . "$filename?ver=$qs_hash";
}
/**
* Generate full URL path with hash for a list of src
*
* @since 1.2.2
* @access private
*/
private function _build_hash_url( $src_list, $file_type = 'css' ) {
// $url_sensitive = $this->conf( self::O_OPTM_CSS_UNIQUE ) && $file_type == 'css'; // If need to keep unique CSS per URI
// Replace preserved ESI (before generating hash)
if ($file_type == 'js') {
foreach ($src_list as $k => $v) {
if (empty($v['inl'])) {
continue;
}
$src_list[$k]['src'] = $this->_preserve_esi($v['src']);
}
}
$minify = $file_type === 'css' ? $this->cfg_css_min : $this->cfg_js_min;
$filename_info = $this->__optimizer->serve($this->_request_url, $file_type, $minify, $src_list);
if (!$filename_info) {
return false; // Failed to generate
}
list($filename, $type) = $filename_info;
// Add cache tag in case later file deleted to avoid lscache served stale non-existed files @since 4.4.1
Tag::add(Tag::TYPE_MIN . '.' . $filename);
$qs_hash = substr(md5(self::get_option(self::ITEM_TIMESTAMP_PURGE_CSS)), -5);
// As filename is already related to filecon md5, no need QS anymore
$filepath_prefix = $this->_build_filepath_prefix($type);
return LITESPEED_STATIC_URL . $filepath_prefix . $filename . '?ver=' . $qs_hash;
}
/**
* Parse js src
*
* @since 1.2.2
* @access private
*/
private function _parse_js() {
$excludes = apply_filters('litespeed_optimize_js_excludes', $this->conf(self::O_OPTM_JS_EXC));
$combine_ext_inl = $this->conf(self::O_OPTM_JS_COMB_EXT_INL);
if (!apply_filters('litespeed_optm_js_comb_ext_inl', true)) {
self::debug2('js_comb_ext_inl bypassed via litespeed_optm_js_comb_ext_inl filter');
$combine_ext_inl = false;
}
$src_list = array();
$html_list = array();
// V7 added: (?:\r\n?|\n?) to fix replacement leaving empty new line
$content = preg_replace('#<!--.*-->(?:\r\n?|\n?)#sU', '', $this->content);
preg_match_all('#<script([^>]*)>(.*)</script>(?:\r\n?|\n?)#isU', $content, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$attrs = empty($match[1]) ? array() : Utility::parse_attr($match[1]);
if (isset($attrs['data-optimized'])) {
continue;
}
if (!empty($attrs['data-no-optimize'])) {
continue;
}
if (!empty($attrs['data-cfasync']) && $attrs['data-cfasync'] === 'false') {
continue;
}
if (!empty($attrs['type']) && $attrs['type'] != 'text/javascript') {
continue;
}
// to avoid multiple replacement
if (in_array($match[0], $html_list)) {
continue;
}
$this_src_arr = array();
// JS files
if (!empty($attrs['src'])) {
// Exclude check
$js_excluded = Utility::str_hit_array($attrs['src'], $excludes);
$is_internal = Utility::is_internal_file($attrs['src']);
$is_file = substr($attrs['src'], 0, 5) != 'data:';
$ext_excluded = !$combine_ext_inl && !$is_internal;
if ($js_excluded || $ext_excluded || !$is_file) {
// Maybe defer
if ($this->cfg_js_defer) {
$deferred = $this->_js_defer($match[0], $attrs['src']);
if ($deferred) {
$this->content = str_replace($match[0], $deferred, $this->content);
}
}
self::debug2('_parse_js bypassed due to ' . ($js_excluded ? 'js files excluded [hit] ' . $js_excluded : 'external js'));
continue;
}
if (strpos($attrs['src'], '/localres/') !== false) {
continue;
}
if (strpos($attrs['src'], 'instant_click') !== false) {
continue;
}
$this_src_arr['src'] = $attrs['src'];
}
// Inline JS
elseif (!empty($match[2])) {
// self::debug( '🌹🌹🌹 ' . $match[2] . '🌹' );
// Exclude check
$js_excluded = Utility::str_hit_array($match[2], $excludes);
if ($js_excluded || !$combine_ext_inl) {
// Maybe defer
if ($this->cfg_js_defer) {
$deferred = $this->_js_inline_defer($match[2], $match[1]);
if ($deferred) {
$this->content = str_replace($match[0], $deferred, $this->content);
}
}
self::debug2('_parse_js bypassed due to ' . ($js_excluded ? 'js excluded [hit] ' . $js_excluded : 'inline js'));
continue;
}
$this_src_arr['inl'] = true;
$this_src_arr['src'] = $match[2];
if ($match[1]) {
$this_src_arr['attrs'] = $match[1];
}
} else {
// Compatibility to those who changed src to data-src already
self::debug2('No JS src or inline JS content');
continue;
}
$src_list[] = $this_src_arr;
$html_list[] = $match[0];
}
return array( $src_list, $html_list );
}
/**
* Inline JS defer
*
* @since 3.0
* @access private
*/
private function _js_inline_defer( $con, $attrs = false, $minified = false ) {
if (strpos($attrs, 'data-no-defer') !== false) {
self::debug2('bypass: attr api data-no-defer');
return false;
}
$hit = Utility::str_hit_array($con, $this->cfg_js_defer_exc);
if ($hit) {
self::debug2('inline js defer excluded [setting] ' . $hit);
return false;
}
$con = trim($con);
// Minify JS first
if (!$minified) {
// && $this->cfg_js_defer !== 2
$con = Optimizer::minify_js($con);
}
if (!$con) {
return false;
}
// Check if the content contains ESI nonce or not
$con = $this->_preserve_esi($con);
if ($this->cfg_js_defer === 2) {
// Drop type attribute from $attrs
$attrs = Utility::remove_attr( $attrs, 'type' );
// Replace DOMContentLoaded
$con = str_replace('DOMContentLoaded', 'DOMContentLiteSpeedLoaded', $con);
return '<script' . $attrs . ' type="litespeed/javascript">' . $con . '</script>';
// return '<script' . $attrs . ' type="litespeed/javascript" src="data:text/javascript;base64,' . base64_encode( $con ) . '"></script>';
// return '<script' . $attrs . ' type="litespeed/javascript">' . $con . '</script>';
}
return '<script' . $attrs . ' src="data:text/javascript;base64,' . base64_encode($con) . '" defer></script>';
}
/**
* Replace ESI to JS inline var (mainly used to avoid nonce timeout)
*
* @since 3.5.1
*/
private function _preserve_esi( $con ) {
$esi_placeholder_list = $this->cls('ESI')->contain_preserve_esi($con);
if (!$esi_placeholder_list) {
return $con;
}
foreach ($esi_placeholder_list as $esi_placeholder) {
$js_var = '__litespeed_var_' . self::$_var_i++ . '__';
$con = str_replace($esi_placeholder, $js_var, $con);
$this->_var_preserve_js[] = $js_var . '=' . $esi_placeholder;
}
return $con;
}
/**
* 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 _parse_css() {
$excludes = apply_filters('litespeed_optimize_css_excludes', $this->conf(self::O_OPTM_CSS_EXC));
$ucss_file_exc_inline = apply_filters('litespeed_optimize_ucss_file_exc_inline', $this->conf(self::O_OPTM_UCSS_FILE_EXC_INLINE));
// Append dummy css to exclude list
$excludes[] = 'litespeed-dummy.css';
$combine_ext_inl = $this->conf(self::O_OPTM_CSS_COMB_EXT_INL);
if (!apply_filters('litespeed_optm_css_comb_ext_inl', true)) {
self::debug2('css_comb_ext_inl bypassed via litespeed_optm_css_comb_ext_inl filter');
$combine_ext_inl = false;
}
$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' );
// V7 added: (?:\r\n?|\n?) to fix replacement leaving empty new line
$content = preg_replace(
array( '#<!--.*-->(?:\r\n?|\n?)#sU', '#<script([^>]*)>.*</script>(?:\r\n?|\n?)#isU', '#<noscript([^>]*)>.*</noscript>(?:\r\n?|\n?)#isU' ),
'',
$this->content
);
preg_match_all('#<link ([^>]+)/?>|<style([^>]*)>([^<]+)</style>(?:\r\n?|\n?)#isU', $content, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
// to avoid multiple replacement
if (in_array($match[0], $html_list)) {
continue;
}
if ($exclude = Utility::str_hit_array($match[0], $excludes)) {
self::debug2('_parse_css bypassed exclude ' . $exclude);
continue;
}
$this_src_arr = array();
if (strpos($match[0], '<link') === 0) {
$attrs = Utility::parse_attr($match[1]);
if (empty($attrs['rel']) || $attrs['rel'] !== 'stylesheet') {
continue;
}
if (empty($attrs['href'])) {
continue;
}
// Check if need to remove this css
if (Utility::str_hit_array($attrs['href'], $css_to_be_removed)) {
self::debug('rm css snippet ' . $attrs['href']);
// Delete this css snippet from orig html
$this->content = str_replace($match[0], '', $this->content);
continue;
}
// Check if need to inline this css file
if ($this->conf(self::O_OPTM_UCSS) && Utility::str_hit_array($attrs['href'], $ucss_file_exc_inline)) {
self::debug('ucss_file_exc_inline hit ' . $attrs['href']);
// Replace this css to inline from orig html
$inline_script = '<style>' . $this->__optimizer->load_file($attrs['href']) . '</style>';
$this->content = str_replace($match[0], $inline_script, $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 font 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) {
self::debug('rm css snippet [Google fonts] ' . $attrs['href']);
$this->content = str_replace($match[0], '', $this->content);
continue;
}
}
if (isset($attrs['data-optimized'])) {
// $this_src_arr[ 'exc' ] = true;
continue;
} elseif (!empty($attrs['data-no-optimize'])) {
// $this_src_arr[ 'exc' ] = true;
continue;
}
$is_internal = Utility::is_internal_file($attrs['href']);
$ext_excluded = !$combine_ext_inl && !$is_internal;
if ($ext_excluded) {
self::debug2('Bypassed due to external link');
// Maybe defer
if ($this->cfg_css_async) {
$snippet = $this->_async_css($match[0]);
if ($snippet != $match[0]) {
$this->content = str_replace($match[0], $snippet, $this->content);
}
}
continue;
}
if (!empty($attrs['media']) && $attrs['media'] !== 'all') {
$this_src_arr['media'] = $attrs['media'];
}
$this_src_arr['src'] = $attrs['href'];
} else {
// Inline style
if (!$combine_ext_inl) {
self::debug2('Bypassed due to inline');
continue;
}
$attrs = Utility::parse_attr($match[2]);
if (!empty($attrs['data-no-optimize'])) {
continue;
}
if (!empty($attrs['media']) && $attrs['media'] !== 'all') {
$this_src_arr['media'] = $attrs['media'];
}
$this_src_arr['inl'] = true;
$this_src_arr['src'] = $match[3];
}
$src_list[] = $this_src_arr;
$html_list[] = $match[0];
}
return array( $src_list, $html_list );
}
/**
* Replace css to async loaded css
*
* @since 1.3
* @access private
*/
private function _async_css_list( $html_list, $src_list ) {
foreach ($html_list as $k => $ori) {
if (!empty($src_list[$k]['inl'])) {
continue;
}
$html_list[$k] = $this->_async_css($ori);
}
return $html_list;
}
/**
* Async CSS snippet
*
* @since 3.5
*/
private function _async_css( $ori ) {
if (strpos($ori, 'data-asynced') !== false) {
self::debug2('bypass: attr data-asynced exist');
return $ori;
}
if (strpos($ori, 'data-no-async') !== false) {
self::debug2('bypass: attr api data-no-async');
return $ori;
}
// 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 (!defined('LITESPEED_GUEST_OPTM') && !$this->conf(self::O_OPTM_NOSCRIPT_RM)) {
$v .= '<noscript>' . preg_replace('/ id=\'[\w-]+\' /U', ' ', $ori) . '</noscript>';
}
return $v;
}
/**
* Defer JS snippet
*
* @since 3.5
*/
private function _js_defer( $ori, $src ) {
$ori = Utility::remove_attr( $ori, 'async' );
if (strpos($ori, 'defer') !== false) {
return false;
}
if (strpos($ori, 'data-deferred') !== false) {
self::debug2('bypass: attr data-deferred exist');
return false;
}
if (strpos($ori, 'data-no-defer') !== false) {
self::debug2('bypass: attr api data-no-defer');
return false;
}
/**
* Exclude JS from setting
*
* @since 1.5
*/
if (Utility::str_hit_array($src, $this->cfg_js_defer_exc)) {
self::debug('js defer exclude ' . $src);
return false;
}
if ($this->cfg_js_defer === 2 || Utility::str_hit_array($src, $this->cfg_js_delay_inc)) {
$ori = Utility::remove_attr( $ori, 'type' );
return str_replace(' src=', ' type="litespeed/javascript" data-src=', $ori);
}
return str_replace('></script>', ' defer data-deferred="1"></script>', $ori);
}
/**
* Delay JS for included setting
*
* @since 5.6
*/
private function _js_delay( $ori, $src ) {
$ori = Utility::remove_attr( $ori, 'async' );
if (strpos($ori, 'defer') !== false) {
return false;
}
if (strpos($ori, 'data-deferred') !== false) {
self::debug2('bypass: attr data-deferred exist');
return false;
}
if (strpos($ori, 'data-no-defer') !== false) {
self::debug2('bypass: attr api data-no-defer');
return false;
}
if (!Utility::str_hit_array($src, $this->cfg_js_delay_inc)) {
return;
}
$ori = Utility::remove_attr( $ori, 'type' );
return str_replace(' src=', ' type="litespeed/javascript" data-src=', $ori);
}
}