defined( 'WPINC' ) || exit();
* Optimize CSS handler class.
const TYPE_GEN_CCSS = 'gen_ccss';
const TYPE_CLEAR_Q_CCSS = 'clear_q_ccss';
private $_ccss_whitelist;
public function __construct() {
$this->_summary = self::get_summary();
add_filter( 'litespeed_ccss_whitelist', [ $this->cls( 'Data' ), 'load_ccss_whitelist' ] );
public function prepare_html_lazy() {
return '<style>' . implode( ',', $this->conf( self::O_OPTM_HTML_LAZY ) ) . '{content-visibility:auto;contain-intrinsic-size:1px 1000px;}</style>';
public function prepare_ccss() {
// Get critical css for current page
// Note: need to consider mobile
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>';
* @param string $request_url Current request URL.
private function _gen_ccss_file_tag( $request_url ) {
if ( $this->conf( self::O_OPTM_CCSS_PER_URL ) ) {
$sep_uri = $this->conf( self::O_OPTM_CCSS_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 );
$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 );
* The critical css content of the current page.
private function _ccss() {
// get current request url
$permalink_structure = get_option( 'permalink_structure' );
if ( ! empty( $permalink_structure ) ) {
$request_url = trailingslashit( home_url( $wp->request ) );
$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' );
$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' );
$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,
]; // 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 ) );
* @param bool $should_continue Continue processing multiple items.
public static function cron_ccss( $should_continue = false ) {
$_instance = self::cls();
return $_instance->_cron_handler( 'ccss', $should_continue );
* @param string $type Job type: 'ccss' or 'ucss'.
* @param bool $should_continue Continue processing multiple items.
private function _cron_handler( $type, $should_continue ) {
$this->_queue = $this->load_queue( $type );
if ( empty( $this->_queue ) ) {
$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' );
foreach ( $this->_queue as $k => $v ) {
if ( ! empty( $v['_status'] ) ) {
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' );
if ( ! isset( $v['is_webp'] ) ) {
$res = $this->_send_req( $v['url'], $k, $v['uid'], $v['user_agent'], $v['vary'], $v['url_tag'], $type, $v['is_mobile'], $v['is_webp'] );
// Status is wrong, drop this this->_queue
unset( $this->_queue[ $k ] );
$this->save_queue( $type, $this->_queue );
if ( ! $should_continue ) {
GUI::print_loading( count( $this->_queue ), $type_tag );
return Router::self_redirect( Router::ACTION_CSS, self::TYPE_GEN_CCSS );
// Exit queue if out of quota or service is hot
if ( 'out_of_quota' === $res || 'svc_hot' === $res ) {
$this->_queue[ $k ]['_status'] = 'requested';
$this->save_queue( $type, $this->_queue );
// only request first one
if ( ! $should_continue ) {
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.
* @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
$allowance = $this->cls( 'Cloud' )->allowance( Cloud::SVC_CCSS, $err );
Debug2::debug( '[CCSS] ❌ No credit: ' . $err );
$err && Admin_Display::error( Error::msg( $err ) );
// Update css request status
$this->_summary[ 'curr_request_' . $type ] = time();
// Gather guest HTML to send
$html = $this->prepare_html( $request_url, $user_agent, $uid );
// Parse HTML to gather all CSS content before requesting
list( $css, $html ) = $this->prepare_css( $html, $is_webp );
$type_tag = strtoupper( $type );
Debug2::debug( '[' . $type_tag . '] ❌ No combined css' );
'user_agent' => $user_agent,
'is_mobile' => $is_mobile ? 1 : 0, // todo:compatible w/ tablet
'is_webp' => $is_webp ? 1 : 0,
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 ) ) {
// Old version compatibility
if ( empty( $json['status'] ) ) {
if ( ! empty( $json[ $type ] ) ) {
$this->_save_con( $type, $json[ $type ], $queue_k, $is_mobile, $is_webp );
// Unknown status, remove this line
if ( 'queued' !== $json['status'] ) {
$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;
* Save CCSS/UCSS content.
* @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.
private function _save_con( $type, $css, $queue_k, $mobile, $webp ) {
$css = apply_filters( 'litespeed_' . $type, $css, $queue_k );
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
$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 ) );
* @param string $request_url URL to test.
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 );
'user_agent' => $user_agent,
$json = Cloud::post( Cloud::SVC_CCSS, $data, 180 );
Debug2::debug2( '[CSS][test_url] response', $json );
* @param string $request_url URL to fetch.
* @param string $user_agent User agent to use.
* @param int|bool $uid Optional user ID for simulation.
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 );
$html = $this->cls( 'Optimizer' )->html_min( $html, true );
// Drop <noscript>xxx</noscript>
$html = preg_replace( '#<noscript>.*</noscript>#isU', '', $html );
* Prepare CSS from HTML for CCSS generation only. UCSS will use combined CSS directly.
* Prepare refined HTML for both CCSS and UCSS.
* @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 ) {
preg_match_all( '#<link ([^>]+)/?>|<style([^>]*)>([^<]+)</style>#isU', $html, $matches, PREG_SET_ORDER );
foreach ( $matches as $match ) {
if ( strpos( $match[0], '<link' ) === 0 ) {
$attrs = Utility::parse_attr( $match[1] );
if ( empty( $attrs['rel'] ) ) {