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
/**
* Optimize CSS handler.
*
* @package LiteSpeed
* @since 2.3
*/
namespace LiteSpeed;
defined( 'WPINC' ) || exit();
/**
* Optimize CSS handler class.
*/
class CSS extends Base {
const LOG_TAG = '[CSS]';
const TYPE_GEN_CCSS = 'gen_ccss';
const TYPE_CLEAR_Q_CCSS = 'clear_q_ccss';
/**
* Summary cache.
*
* @var array
*/
protected $_summary;
/**
* Cached CCSS whitelist.
*
* @var array|null
*/
private $_ccss_whitelist;
/**
* Request queue.
*
* @var array
*/
private $_queue;
/**
* Init.
*
* @since 3.0
*/
public function __construct() {
$this->_summary = self::get_summary();
add_filter( 'litespeed_ccss_whitelist', [ $this->cls( 'Data' ), 'load_ccss_whitelist' ] );
}
/**
* HTML lazyload CSS.
*
* @since 4.0
* @return string
*/
public function prepare_html_lazy() {
return '<style>' . implode( ',', $this->conf( self::O_OPTM_HTML_LAZY ) ) . '{content-visibility:auto;contain-intrinsic-size:1px 1000px;}</style>';
}
/**
* Output critical CSS.
*
* @since 1.3
* @access public
* @return string|null
*/
public function prepare_ccss() {
// Get critical css for current page
// Note: need to consider mobile
$rules = $this->_ccss();
if ( ! $rules ) {
return null;
}
$error_tag = '';
if ( substr( $rules, 0, 2 ) === '/*' && substr( $rules, -2 ) === '*/' ) {
Core::comment( 'QUIC.cloud CCSS bypassed due to generation error ❌' );
$error_tag = ' data-error="failed to generate"';
}
// Append default critical css
$rules .= $this->conf( self::O_OPTM_CCSS_CON );
return '<style id="litespeed-ccss"' . $error_tag . '>' . $rules . '</style>';
}
/**
* Generate CCSS url tag.
*
* @since 4.0
* @param string $request_url Current request URL.
* @return string
*/
private function _gen_ccss_file_tag( $request_url ) {
if ( is_404() ) {
return '404';
}
if ( $this->conf( self::O_OPTM_CCSS_PER_URL ) ) {
return $request_url;
}
$sep_uri = $this->conf( self::O_OPTM_CCSS_SEP_URI );
$hit = false;
if ( $sep_uri ) {
$hit = Utility::str_hit_array( $request_url, $sep_uri );
}
if ( $sep_uri && $hit ) {
Debug2::debug( '[CCSS] Separate CCSS due to separate URI setting: ' . $hit );
return $request_url;
}
$pt = Utility::page_type();
$sep_pt = $this->conf( self::O_OPTM_CCSS_SEP_POSTTYPE );
if ( in_array( $pt, $sep_pt, true ) ) {
Debug2::debug( '[CCSS] Separate CCSS due to posttype setting: ' . $pt );
return $request_url;
}
// Per posttype
return $pt;
}
/**
* The critical css content of the current page.
*
* @since 2.3
* @return string|null
*/
private function _ccss() {
global $wp;
// get current request url
$permalink_structure = get_option( 'permalink_structure' );
if ( ! empty( $permalink_structure ) ) {
$request_url = trailingslashit( home_url( $wp->request ) );
} else {
$qs_add = $wp->query_string ? '?' . (string) $wp->query_string : '' ;
$request_url = home_url( $wp->request ) . $qs_add;
}
$filepath_prefix = $this->_build_filepath_prefix( 'ccss' );
$url_tag = $this->_gen_ccss_file_tag( $request_url );
$vary = $this->cls( 'Vary' )->finalize_full_varies();
$filename = $this->cls( 'Data' )->load_url_file( $url_tag, $vary, 'ccss' );
if ( $filename ) {
$static_file = LITESPEED_STATIC_DIR . $filepath_prefix . $filename . '.css';
if ( file_exists( $static_file ) ) {
Debug2::debug2( '[CSS] existing ccss ' . $static_file );
Core::comment( 'QUIC.cloud CCSS loaded ✅ ' . $filepath_prefix . $filename . '.css' );
return File::read( $static_file );
}
}
$uid = get_current_user_id();
$ua = isset( $_SERVER['HTTP_USER_AGENT'] )
? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) )
: '';
// Store it to prepare for cron
Core::comment( 'QUIC.cloud CCSS in queue' );
$this->_queue = $this->load_queue( 'ccss' );
if ( count( $this->_queue ) > 500 ) {
self::debug( 'CCSS Queue is full - 500' );
return null;
}
$queue_k = ( strlen( $vary ) > 32 ? md5( $vary ) : $vary ) . ' ' . $url_tag;
$this->_queue[ $queue_k ] = [
'url' => apply_filters( 'litespeed_ccss_url', $request_url ),
'user_agent' => substr( $ua, 0, 200 ),
'is_mobile' => $this->_separate_mobile(),
'is_webp' => $this->cls( 'Media' )->webp_support() ? 1 : 0,
'uid' => $uid,
'vary' => $vary,
'url_tag' => $url_tag,
]; // Current UA will be used to request
$this->save_queue( 'ccss', $this->_queue );
self::debug( 'Added queue_ccss [url_tag] ' . $url_tag . ' [UA] ' . $ua . ' [vary] ' . $vary . ' [uid] ' . $uid );
// Prepare cache tag for later purge
Tag::add( 'CCSS.' . md5( $queue_k ) );
return null;
}
/**
* Cron ccss generation.
*
* @since 2.3
* @access private
*
* @param bool $should_continue Continue processing multiple items.
* @return mixed
*/
public static function cron_ccss( $should_continue = false ) {
$_instance = self::cls();
return $_instance->_cron_handler( 'ccss', $should_continue );
}
/**
* Handle UCSS/CCSS cron.
*
* @since 4.2
*
* @param string $type Job type: 'ccss' or 'ucss'.
* @param bool $should_continue Continue processing multiple items.
* @return void
*/
private function _cron_handler( $type, $should_continue ) {
$this->_queue = $this->load_queue( $type );
if ( empty( $this->_queue ) ) {
return;
}
$type_tag = strtoupper( $type );
// For cron, need to check request interval too
if ( ! $should_continue ) {
if ( ! empty( $this->_summary[ 'curr_request_' . $type ] ) && time() - (int) $this->_summary[ 'curr_request_' . $type ] < 300 && ! $this->conf( self::O_DEBUG ) ) {
Debug2::debug( '[' . $type_tag . '] Last request not done' );
return;
}
}
$i = 0;
foreach ( $this->_queue as $k => $v ) {
if ( ! empty( $v['_status'] ) ) {
continue;
}
Debug2::debug( '[' . $type_tag . '] cron job [tag] ' . $k . ' [url] ' . $v['url'] . ( $v['is_mobile'] ? ' 📱 ' : '' ) . ' [UA] ' . $v['user_agent'] );
if ( 'ccss' === $type && empty( $v['url_tag'] ) ) {
unset( $this->_queue[ $k ] );
$this->save_queue( $type, $this->_queue );
Debug2::debug( '[CCSS] wrong queue_ccss format' );
continue;
}
if ( ! isset( $v['is_webp'] ) ) {
$v['is_webp'] = false;
}
++$i;
$res = $this->_send_req( $v['url'], $k, $v['uid'], $v['user_agent'], $v['vary'], $v['url_tag'], $type, $v['is_mobile'], $v['is_webp'] );
if ( ! $res ) {
// Status is wrong, drop this this->_queue
unset( $this->_queue[ $k ] );
$this->save_queue( $type, $this->_queue );
if ( ! $should_continue ) {
return;
}
if ( $i > 3 ) {
GUI::print_loading( count( $this->_queue ), $type_tag );
return Router::self_redirect( Router::ACTION_CSS, self::TYPE_GEN_CCSS );
}
continue;
}
// Exit queue if out of quota or service is hot
if ( 'out_of_quota' === $res || 'svc_hot' === $res ) {
return;
}
$this->_queue[ $k ]['_status'] = 'requested';
$this->save_queue( $type, $this->_queue );
// only request first one
if ( ! $should_continue ) {
return;
}
if ( $i > 3 ) {
GUI::print_loading( count( $this->_queue ), $type_tag );
return Router::self_redirect( Router::ACTION_CSS, self::TYPE_GEN_CCSS );
}
}
}
/**
* Send to QC API to generate CCSS/UCSS.
*
* @since 2.3
* @access private
*
* @param string $request_url Request URL.
* @param string $queue_k Queue key.
* @param int $uid WP User ID.
* @param string $user_agent User agent string.
* @param string $vary Vary string.
* @param string $url_tag URL tag.
* @param string $type Type: 'ccss' or 'ucss'.
* @param bool $is_mobile Is mobile.
* @param bool $is_webp Has webp support.
* @return bool|string True on success, 'out_of_quota' / 'svc_hot' on special cases, false on failure.
*/
private function _send_req( $request_url, $queue_k, $uid, $user_agent, $vary, $url_tag, $type, $is_mobile, $is_webp ) {
// Check if has credit to push or not
$err = false;
$allowance = $this->cls( 'Cloud' )->allowance( Cloud::SVC_CCSS, $err );
if ( ! $allowance ) {
Debug2::debug( '[CCSS] ❌ No credit: ' . $err );
$err && Admin_Display::error( Error::msg( $err ) );
return 'out_of_quota';
}
set_time_limit( 120 );
// Update css request status
$this->_summary[ 'curr_request_' . $type ] = time();
self::save_summary();
// Gather guest HTML to send
$html = $this->prepare_html( $request_url, $user_agent, $uid );
if ( ! $html ) {
return false;
}
// Parse HTML to gather all CSS content before requesting
list( $css, $html ) = $this->prepare_css( $html, $is_webp );
if ( ! $css ) {
$type_tag = strtoupper( $type );
Debug2::debug( '[' . $type_tag . '] ❌ No combined css' );
return false;
}
// Generate critical css
$data = [
'url' => $request_url,
'queue_k' => $queue_k,
'user_agent' => $user_agent,
'is_mobile' => $is_mobile ? 1 : 0, // todo:compatible w/ tablet
'is_webp' => $is_webp ? 1 : 0,
'html' => $html,
'css' => $css,
];
if ( ! isset( $this->_ccss_whitelist ) ) {
$this->_ccss_whitelist = $this->_filter_whitelist();
}
$data['whitelist'] = $this->_ccss_whitelist;
self::debug( 'Generating: ', $data );
$json = Cloud::post( Cloud::SVC_CCSS, $data, 30 );
if ( ! is_array( $json ) ) {
return $json;
}
// Old version compatibility
if ( empty( $json['status'] ) ) {
if ( ! empty( $json[ $type ] ) ) {
$this->_save_con( $type, $json[ $type ], $queue_k, $is_mobile, $is_webp );
}
// Delete the row
return false;
}
// Unknown status, remove this line
if ( 'queued' !== $json['status'] ) {
return false;
}
// Save summary data
$this->_summary[ 'last_spent_' . $type ] = time() - (int) $this->_summary[ 'curr_request_' . $type ];
$this->_summary[ 'last_request_' . $type ] = $this->_summary[ 'curr_request_' . $type ];
$this->_summary[ 'curr_request_' . $type ] = 0;
self::save_summary();
return true;
}
/**
* Save CCSS/UCSS content.
*
* @since 4.2
*
* @param string $type Type: 'ccss' or 'ucss'.
* @param string $css CSS content.
* @param string $queue_k Queue key.
* @param bool $mobile Is mobile.
* @param bool $webp Has webp support.
* @return void
*/
private function _save_con( $type, $css, $queue_k, $mobile, $webp ) {
// Add filters
$css = apply_filters( 'litespeed_' . $type, $css, $queue_k );
// Sanitize: CSS must not contain HTML tags
$css = wp_strip_all_tags( $css );
Debug2::debug2( '[CSS] con: ' . $css );
if ( substr( $css, 0, 2 ) === '/*' && substr( $css, -2 ) === '*/' ) {
self::debug( '❌ empty ' . $type . ' [content] ' . $css );
// continue; // Save the error info too
}
// Write to file
$filecon_md5 = md5( $css );
$filepath_prefix = $this->_build_filepath_prefix( $type );
$static_file = LITESPEED_STATIC_DIR . $filepath_prefix . $filecon_md5 . '.css';
File::save( $static_file, $css, true );
$url_tag = $this->_queue[ $queue_k ]['url_tag'];
$vary = $this->_queue[ $queue_k ]['vary'];
Debug2::debug2( "[CSS] Save URL to file [file] $static_file [vary] $vary" );
$this->cls( 'Data' )->save_url( $url_tag, $vary, $type, $filecon_md5, dirname( $static_file ), $mobile, $webp );
Purge::add( strtoupper( $type ) . '.' . md5( $queue_k ) );
}
/**
* Play for fun.
*
* @since 3.4.3
*
* @param string $request_url URL to test.
* @return void
*/
public function test_url( $request_url ) {
$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
$html = $this->prepare_html( $request_url, $user_agent );
list( $css, $html ) = $this->prepare_css( $html, true, true );
$data = [
'url' => $request_url,
'ccss_type' => 'test',
'user_agent' => $user_agent,
'is_mobile' => 0,
'html' => $html,
'css' => $css,
'type' => 'CCSS',
];
$json = Cloud::post( Cloud::SVC_CCSS, $data, 180 );
Debug2::debug2( '[CSS][test_url] response', $json );
}
/**
* Prepare HTML from URL.
*
* @since 3.4.3
*
* @param string $request_url URL to fetch.
* @param string $user_agent User agent to use.
* @param int|bool $uid Optional user ID for simulation.
* @return string|false
*/
public function prepare_html( $request_url, $user_agent, $uid = false ) {
$html = $this->cls( 'Crawler' )->self_curl( add_query_arg( 'LSCWP_CTRL', 'before_optm', $request_url ), $user_agent, $uid );
Debug2::debug2( '[CSS] self_curl result....', $html );
if ( ! $html ) {
return false;
}
$html = $this->cls( 'Optimizer' )->html_min( $html, true );
// Drop <noscript>xxx</noscript>
$html = preg_replace( '#<noscript>.*</noscript>#isU', '', $html );
return $html;
}
/**
* Prepare CSS from HTML for CCSS generation only. UCSS will use combined CSS directly.
* Prepare refined HTML for both CCSS and UCSS.
*
* @since 3.4.3
*
* @param string $html HTML content.
* @param bool $is_webp Convert backgrounds to WebP when supported.
* @param bool $dryrun If true, do not fetch external CSS files.
* @return array{0:string,1:string} [combined CSS, refined HTML]
*/
public function prepare_css( $html, $is_webp = false, $dryrun = false ) {
$css = '';
preg_match_all( '#<link ([^>]+)/?>|<style([^>]*)>([^<]+)</style>#isU', $html, $matches, PREG_SET_ORDER );
foreach ( $matches as $match ) {
$debug_info = '';
if ( strpos( $match[0], '<link' ) === 0 ) {
$attrs = Utility::parse_attr( $match[1] );
if ( empty( $attrs['rel'] ) ) {
continue;
}
if ( 'stylesheet' !== $attrs['rel'] ) {
if ( 'preload' !== $attrs['rel'] || empty( $attrs['as'] ) || 'style' !== $attrs['as'] ) {
continue;
}
}
if ( ! empty( $attrs['media'] ) && false !== strpos( $attrs['media'], 'print' ) ) {
continue;
}
if ( empty( $attrs['href'] ) ) {
continue;
}
// Check Google fonts hit
if ( false !== strpos( $attrs['href'], 'fonts.googleapis.com' ) ) {
$html = str_replace( $match[0], '', $html );
continue;
}
$debug_info = $attrs['href'];
// Load CSS content
if ( ! $dryrun ) {
// Dryrun will not load CSS but just drop them
$con = $this->cls( 'Optimizer' )->load_file( $attrs['href'] );
if ( ! $con ) {
continue;
}
} else {
$con = '';
}
} else {
// Inline style
$attrs = Utility::parse_attr( $match[2] );
if ( ! empty( $attrs['media'] ) && false !== strpos( $attrs['media'], 'print' ) ) {
continue;
}
Debug2::debug2( '[CSS] Load inline CSS ' . substr( $match[3], 0, 100 ) . '...', $attrs );
$con = $match[3];
$debug_info = '__INLINE__';
}
$con = Optimizer::minify_css( $con );
if ( $is_webp && $this->cls( 'Media' )->webp_support() ) {
$con = $this->cls( 'Media' )->replace_background_webp( $con );
}
if ( ! empty( $attrs['media'] ) && 'all' !== $attrs['media'] ) {
$con = '@media ' . $attrs['media'] . '{' . $con . "}\n";
} else {
$con = $con . "\n";
}
$con = '/* ' . $debug_info . ' */' . $con;
$css .= $con;
$html = str_replace( $match[0], '', $html );
}
return [ $css, $html ];
}
/**
* Filter the comment content, add quotes to selector from whitelist. Return the json.
*
* @since 7.1
* @return array
*/
private function _filter_whitelist() {
$whitelist = [];
$list = apply_filters( 'litespeed_ccss_whitelist', $this->conf( self::O_OPTM_CCSS_SELECTOR_WHITELIST ) );
foreach ( $list as $v ) {
if ( substr( $v, 0, 2 ) === '//' ) {
continue;
}
$whitelist[] = $v;
}
return $whitelist;
}
/**
* Notify finished from server.
*
* @since 7.1
* @return array
*/
public function notify() {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$post_data = \json_decode( file_get_contents( 'php://input' ), true );
if ( is_null( $post_data ) ) {
// Fallback for form-encoded payloads
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$post_data = $_POST;
}
self::debug( 'notify() data', $post_data );
$this->_queue = $this->load_queue( 'ccss' );
list( $post_data ) = $this->cls( 'Cloud' )->extract_msg( $post_data, 'ccss' );
$notified_data = $post_data['data'];
if ( empty( $notified_data ) || ! is_array( $notified_data ) ) {
self::debug( '❌ notify exit: no notified data' );
return Cloud::err( 'no notified data' );
}
// Check if its in queue or not
$valid_i = 0;
foreach ( $notified_data as $v ) {
if ( empty( $v['request_url'] ) ) {
self::debug( '❌ notify bypass: no request_url', $v );
continue;
}
if ( empty( $v['queue_k'] ) ) {
self::debug( '❌ notify bypass: no queue_k', $v );
continue;
}
if ( empty( $this->_queue[ $v['queue_k'] ] ) ) {
self::debug( '❌ notify bypass: no this queue [q_k]' . $v['queue_k'] );
continue;
}
// Save data
if ( ! empty( $v['data_ccss'] ) ) {
$is_mobile = $this->_queue[ $v['queue_k'] ]['is_mobile'];
$is_webp = $this->_queue[ $v['queue_k'] ]['is_webp'];
$this->_save_con( 'ccss', $v['data_ccss'], $v['queue_k'], $is_mobile, $is_webp );
++$valid_i;
}
unset( $this->_queue[ $v['queue_k'] ] );
self::debug( 'notify data handled, unset queue [q_k] ' . $v['queue_k'] );
}
$this->save_queue( 'ccss', $this->_queue );
self::debug( 'notified' );
return Cloud::ok( [ 'count' => $valid_i ] );
}
/**
* Handle all request actions from main cls.
*
* @since 2.3
* @access public
* @return void
*/
public function handler() {
$type = Router::verify_type();
switch ( $type ) {
case self::TYPE_GEN_CCSS:
self::cron_ccss( true );
break;
case self::TYPE_CLEAR_Q_CCSS:
$this->clear_q( 'ccss' );
break;
default:
break;
}
Admin::redirect();
}
}