* The viewport image class.
defined('WPINC') || exit();
const TYPE_CLEAR_Q = 'clear_q';
public function __construct()
$this->_summary = self::get_summary();
* The VPI content of the current page
public function add_to_queue()
$is_mobile = $this->_separate_mobile();
$request_url = home_url($wp->request);
$ua = !empty($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
// Store it to prepare for cron
$this->_queue = $this->load_queue('vpi');
if (count($this->_queue) > 500) {
self::debug('Queue is full - 500');
$home_id = get_option('page_for_posts');
if (!is_singular() && !($home_id > 0 && is_home())) {
self::debug('not single post ID');
$post_id = is_home() ? $home_id : get_the_ID();
$queue_k = ($is_mobile ? 'mobile' : '') . ' ' . $request_url;
if (!empty($this->_queue[$queue_k])) {
self::debug('queue k existed ' . $queue_k);
$this->_queue[$queue_k] = array(
'url' => apply_filters('litespeed_vpi_url', $request_url),
'user_agent' => substr($ua, 0, 200),
'is_mobile' => $this->_separate_mobile(),
); // Current UA will be used to request
$this->save_queue('vpi', $this->_queue);
self::debug('Added queue_vpi [url] ' . $queue_k . ' [UA] ' . $ua);
// Prepare cache tag for later purge
Tag::add('VPI.' . md5($queue_k));
* Notify finished from server
$post_data = \json_decode(file_get_contents('php://input'), true);
if (is_null($post_data)) {
self::debug('notify() data', $post_data);
$this->_queue = $this->load_queue('vpi');
if (empty($post_data['domain_key']) || $post_data['domain_key'] !== md5($this->conf(self::O_API_KEY))) {
self::debug('❌ notify wrong key');
self::save_summary(array('notify_ts_err' => time()));
return Cloud::err('wrong_key');
list($post_data) = $this->cls('Cloud')->extract_msg($post_data, 'vpi');
$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
foreach ($notified_data as $v) {
if (empty($v['request_url'])) {
self::debug('❌ notify bypass: no request_url', $v);
if (empty($v['queue_k'])) {
self::debug('❌ notify bypass: no queue_k', $v);
// $queue_k = ( $is_mobile ? 'mobile' : '' ) . ' ' . $v[ 'request_url' ];
$queue_k = $v['queue_k'];
if (empty($this->_queue[$queue_k])) {
self::debug('❌ notify bypass: no this queue [q_k]' . $queue_k);
if (!empty($v['data_vpi'])) {
$post_id = $this->_queue[$queue_k]['post_id'];
$name = !empty($v['is_mobile']) ? 'litespeed_vpi_list_mobile' : 'litespeed_vpi_list';
$urldecode = is_array($v['data_vpi']) ? array_map('urldecode', $v['data_vpi']) : urldecode($v['data_vpi']);
self::debug('save data_vpi', $urldecode);
$this->cls('Metabox')->save($post_id, $name, $urldecode);
unset($this->_queue[$queue_k]);
self::debug('notify data handled, unset queue [q_k] ' . $queue_k);
$this->save_queue('vpi', $this->_queue);
return Cloud::ok(array('count' => $valid_i));
public static function cron($continue = false)
$_instance = self::cls();
return $_instance->_cron_handler($continue);
private function _cron_handler($continue = false)
self::debug('cron start');
$this->_queue = $this->load_queue('vpi');
if (empty($this->_queue)) {
// For cron, need to check request interval too
if (!empty($this->_summary['curr_request_vpi']) && time() - $this->_summary['curr_request_vpi'] < 300 && !$this->conf(self::O_DEBUG)) {
self::debug('Last request not done');
foreach ($this->_queue as $k => $v) {
if (!empty($v['_status'])) {
self::debug('cron job [tag] ' . $k . ' [url] ' . $v['url'] . ($v['is_mobile'] ? ' 📱 ' : '') . ' [UA] ' . $v['user_agent']);
$res = $this->_send_req($v['url'], $k, $v['user_agent'], $v['is_mobile']);
// Status is wrong, drop this this->_queue
$this->_queue = $this->load_queue('vpi');
unset($this->_queue[$k]);
$this->save_queue('vpi', $this->_queue);
GUI::print_loading(count($this->_queue), 'VPI');
return Router::self_redirect(Router::ACTION_VPI, self::TYPE_GEN);
// Exit queue if out of quota
if ($res === 'out_of_quota') {
$this->_queue = $this->load_queue('vpi');
$this->_queue[$k]['_status'] = 'requested';
$this->save_queue('vpi', $this->_queue);
self::debug('Saved to queue [k] ' . $k);
// only request first one
GUI::print_loading(count($this->_queue), 'VPI');
return Router::self_redirect(Router::ACTION_VPI, self::TYPE_GEN);
* Send to QC API to generate VPI
private function _send_req($request_url, $queue_k, $user_agent, $is_mobile)
// Check if has credit to push or not
$allowance = $this->cls('Cloud')->allowance($svc, $err);
self::debug('❌ No credit: ' . $err);
$err && Admin_Display::error(Error::msg($err));
// Update css request status
self::save_summary(array('curr_request_vpi' => time()), true);
// Gather guest HTML to send
$html = $this->cls('CSS')->prepare_html($request_url, $user_agent);
// Parse HTML to gather all CSS content before requesting
list($css, $html) = $this->cls('CSS')->prepare_css($html);
'user_agent' => $user_agent,
'is_mobile' => $is_mobile ? 1 : 0, // todo:compatible w/ tablet
self::debug('Generating: ', $data);
$json = Cloud::post($svc, $data, 30);
// Unknown status, remove this line
if ($json['status'] != 'queued') {
$this->_summary['last_spent_vpi'] = time() - $this->_summary['curr_request_vpi'];
$this->_summary['last_request_vpi'] = $this->_summary['curr_request_vpi'];
$this->_summary['curr_request_vpi'] = 0;
* Handle all request actions from main cls
public function handler()
$type = Router::verify_type();