_summary = self::get_summary(); } /** * HTML lazyload CSS * @since 4.0 */ public function prepare_html_lazy() { return ''; } /** * Output critical css * * @since 1.3 * @access public */ 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 ''; } /** * Generate CCSS url tag * * @since 4.0 */ 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); if ($sep_uri && ($hit = Utility::str_hit_array($request_url, $sep_uri))) { 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)) { 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 */ private function _ccss() { global $wp; $request_url = home_url($wp->request); $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 = !empty($_SERVER['HTTP_USER_AGENT']) ? $_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] = array( '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)); // For v4.1- clean up if (isset($this->_summary['ccss_type_history']) || isset($this->_summary['ccss_history']) || isset($this->_summary['queue_ccss'])) { if (isset($this->_summary['ccss_type_history'])) { unset($this->_summary['ccss_type_history']); } if (isset($this->_summary['ccss_history'])) { unset($this->_summary['ccss_history']); } if (isset($this->_summary['queue_ccss'])) { unset($this->_summary['queue_ccss']); } self::save_summary(); } return null; } /** * Cron ccss generation * * @since 2.3 * @access private */ public static function cron_ccss($continue = false) { $_instance = self::cls(); return $_instance->_cron_handler('ccss', $continue); } /** * Handle UCSS/CCSS cron * * @since 4.2 */ private function _cron_handler($type, $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 (!$continue) { if (!empty($this->_summary['curr_request_' . $type]) && time() - $this->_summary['curr_request_' . $type] < 300 && !$this->conf(self::O_DEBUG)) { Debug2::debug('[' . $type_tag . '] Last request not done'); return; } } $i = 0; $timeoutLimit = ini_get('max_execution_time'); $this->_endts = time() + $timeoutLimit; foreach ($this->_queue as $k => $v) { if (!empty($v['_status'])) { continue; } if (function_exists('set_time_limit')) { $this->_endts += 120; set_time_limit(120); } if ($this->_endts - time() < 10) { // self::debug("🚨 End loop due to timeout limit reached " . $timeoutLimit . "s"); // return; } Debug2::debug('[' . $type_tag . '] cron job [tag] ' . $k . ' [url] ' . $v['url'] . ($v['is_mobile'] ? ' 📱 ' : '') . ' [UA] ' . $v['user_agent']); if ($type == 'ccss' && 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 (!$continue) { return; } if ($i > 3) { GUI::print_loading(count($this->_queue), $type_tag); return Router::self_redirect(Router::ACTION_CSS, CSS::TYPE_GEN_CCSS); } continue; } // Exit queue if out of quota if ($res === 'out_of_quota') { return; } $this->_queue[$k]['_status'] = 'requested'; $this->save_queue($type, $this->_queue); // only request first one if (!$continue) { return; } if ($i > 3) { GUI::print_loading(count($this->_queue), $type_tag); return Router::self_redirect(Router::ACTION_CSS, CSS::TYPE_GEN_CCSS); } } } /** * Send to QC API to generate CCSS/UCSS * * @since 2.3 * @access private */ 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'; } // 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) { Debug2::debug('[UCSS] ❌ No combined css'); return false; } // Generate critical css $data = array( '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, ); self::debug('Generating: ', $data); $json = Cloud::post(Cloud::SVC_CCSS, $data, 30); if (!is_array($json)) { return false; } // 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 ($json['status'] != 'queued') { return false; } // Save summary data $this->_summary['last_spent_' . $type] = time() - $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 */ private function _save_con($type, $css, $queue_k, $mobile, $webp) { // Add filters $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 } // 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 */ public function test_url($request_url) { $user_agent = $_SERVER['HTTP_USER_AGENT']; $html = $this->prepare_html($request_url, $user_agent); list($css, $html) = $this->prepare_css($html, true, true); // var_dump( $css ); // $html = << $request_url, 'ccss_type' => 'test', 'user_agent' => $user_agent, 'is_mobile' => 0, 'html' => $html, 'css' => $css, 'type' => 'CCSS', ); // self::debug( 'Generating: ', $data ); $json = Cloud::post(Cloud::SVC_CCSS, $data, 180); var_dump($json); } /** * Prepare HTML from URL * * @since 3.4.3 */ 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 $html = preg_replace('##isU', '', $html); return $html; } /** * Prepare CSS from HTML for CCSS generation only. UCSS will used combined CSS directly. * Prepare refined HTML for both CCSS and UCSS. * * @since 3.4.3 */ public function prepare_css($html, $is_webp = false, $dryrun = false) { $css = ''; preg_match_all('#]+)/?>|]*)>([^<]+)#isU', $html, $matches, PREG_SET_ORDER); foreach ($matches as $match) { $debug_info = ''; if (strpos($match[0], 'cls('Optimizer')->load_file($attrs['href']); if (!$con) { continue; } } else { $con = ''; } } else { // Inline style $attrs = Utility::parse_attr($match[2]); if (!empty($attrs['media']) && strpos($attrs['media'], 'print') !== false) { 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']) && $attrs['media'] !== 'all') { $con = '@media ' . $attrs['media'] . '{' . $con . "}\n"; } else { $con = $con . "\n"; } $con = '/* ' . $debug_info . ' */' . $con; $css .= $con; $html = str_replace($match[0], '', $html); } return array($css, $html); } /** * Handle all request actions from main cls * * @since 2.3 * @access public */ 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(); } }