* The frontend GUI class.
* Provides front-end and admin-bar UI helpers for LiteSpeed Cache.
defined( 'WPINC' ) || exit();
* GUI helpers for LiteSpeed Cache.
* Counter for temporary HTML wrappers.
* @var int Counter for temporary HTML wrappers to remove from the buffer.
private static $_clean_counter = 0;
* @var bool Internal flag used by promo templates to decide whether to display.
private $_promo_true = false;
* Promo list configuration.
* Format: [ file_tag => [ days, litespeed_only ], ... ]
* @var array<string, array{0:int,1:bool}>
'new_version' => [ 7, false ],
'score' => [ 14, false ],
// 'slack' => [ 3, false ],
/** Path to guest JavaScript file. */
const LIB_GUEST_JS = 'assets/js/guest.min.js';
/** Path to guest document.referrer JavaScript file. */
const LIB_GUEST_DOCREF_JS = 'assets/js/guest.docref.min.js';
/** Path to guest vary endpoint. */
const PHP_GUEST = 'guest.vary.php';
/** Dismiss type: WHM. */
const TYPE_DISMISS_WHM = 'whm';
/** Dismiss type: ExpiresDefault. */
const TYPE_DISMISS_EXPIRESDEFAULT = 'ExpiresDefault';
/** Dismiss type: Promo. */
const TYPE_DISMISS_PROMO = 'promo';
/** Dismiss type: PIN. */
const TYPE_DISMISS_PIN = 'pin';
/** WHM message option name. */
const WHM_MSG = 'lscwp_whm_install';
/** WHM message option value. */
const WHM_MSG_VAL = 'whm_install';
* @var array<string,mixed> Summary/options cache.
public function __construct() {
$this->_summary = self::get_summary();
if ( is_admin_bar_showing() && current_user_can( 'manage_options' ) ) {
add_action( 'wp_enqueue_scripts', [ $this, 'frontend_enqueue_style' ] );
add_action( 'admin_bar_menu', [ $this, 'frontend_shortcut' ], 95 );
if ( $this->conf( self::O_UTIL_INSTANT_CLICK ) ) {
add_action( 'wp_enqueue_scripts', [ $this, 'frontend_enqueue_style_public' ] );
// NOTE: this needs to be before optimizer to avoid wrapper being removed.
add_filter( 'litespeed_buffer_finalize', [ $this, 'finalize' ], 8 );
* Print a loading message when redirecting CCSS/UCSS page to avoid blank page confusion.
* @param int $counter Files left in queue.
* @param string $type Queue type label.
public static function print_loading( $counter, $type ) {
echo '<div style="font-size:25px;text-align:center;padding-top:150px;width:100%;position:absolute;">';
echo "<img width='35' src='" . esc_url( LSWCP_PLUGIN_URL . 'assets/img/Litespeed.icon.svg' ) . "' alt='' /> ";
/* translators: 1: number, 2: text */
esc_html__( '%1$s %2$s files left in queue', 'litespeed-cache' ),
esc_html( number_format_i18n( $counter ) ),
echo '<p><a href="' . esc_url( admin_url( 'admin.php?page=litespeed-page_optm' ) ) . '">' . esc_html__( 'Cancel', 'litespeed-cache' ) . '</a></p>';
* @param array<string,string> $tabs Key => Label pairs.
public static function display_tab_list( $tabs ) {
foreach ( $tabs as $k => $val ) {
$accesskey = $i <= 9 ? $i : '';
'<a class="litespeed-tab nav-tab" href="#%1$s" data-litespeed-tab="%1$s" litespeed-accesskey="%2$s">%3$s</a>',
* Render a pie chart SVG string.
* @param int $percent Percentage 0-100.
* @param int $width Width/height in pixels.
* @param bool $finished_tick Show a tick when 100%.
* @param bool $without_percentage Hide the % label.
* @param string|bool $append_cls Extra CSS class.
* @return string SVG markup.
public static function pie( $percent, $width = 50, $finished_tick = false, $without_percentage = false, $append_cls = false ) {
$label = $without_percentage ? $percent : ( $percent . '%' );
$percentage = '<text x="50%" y="50%">' . esc_html( $label ) . '</text>';
if ( 100 === $percent && $finished_tick ) {
$percentage = '<text x="50%" y="50%" class="litespeed-pie-done">✓</text>';
"<svg class='litespeed-pie %1\$s' viewbox='0 0 33.83098862 33.83098862' width='%2\$d' height='%2\$d' xmlns='http://www.w3.org/2000/svg'>
<circle class='litespeed-pie_bg' cx='16.91549431' cy='16.91549431' r='15.91549431' />
<circle class='litespeed-pie_circle' cx='16.91549431' cy='16.91549431' r='15.91549431' stroke-dasharray='%3\$d,100' />
<g class='litespeed-pie_info'>%4\$s</g>
* Allowed SVG tags/attributes for kses.
* @return array<string,array<string,bool>> Allowed tags/attributes.
public static function allowed_svg_tags() {
'viewbox' => true, // Note: SVG standard uses 'viewBox', but wp_kses normalizes to lowercase.
'stroke-dasharray' => true,
'data-balloon-break' => true,
'data-balloon-pos' => true,
* Display a tiny pie with a tooltip.
* @param int $percent Percentage 0-100.
* @param int $width Width/height in pixels.
* @param string $tooltip Tooltip text.
* @param string $tooltip_pos Tooltip position (e.g., 'up').
* @param string|bool $append_cls Extra CSS class.
* @return string HTML/SVG.
public static function pie_tiny( $percent, $width = 50, $tooltip = '', $tooltip_pos = 'up', $append_cls = false ) {
$dasharray = 2 * 3.1416 * 9 * ( $percent / 100 );
<button type='button' data-balloon-break data-balloon-pos='%1\$s' aria-label='%2\$s' class='litespeed-btn-pie'>
<svg class='litespeed-pie litespeed-pie-tiny %3\$s' viewbox='0 0 30 30' width='%4\$d' height='%4\$d' xmlns='http://www.w3.org/2000/svg'>
<circle class='litespeed-pie_bg' cx='15' cy='15' r='9' />
<circle class='litespeed-pie_circle' cx='15' cy='15' r='9' stroke-dasharray='%5\$s,100' />
<g class='litespeed-pie_info'><text x='50%%' y='50%%'>i</text></g>
esc_attr( $tooltip_pos ),
* Get CSS class name for PageSpeed score.
* @param int $score Score 0-100.
* @return string Class name: success|warning|danger.
public function get_cls_of_pagescore( $score ) {
* Handle dismiss actions for banners and notices.
public static function dismiss() {
$_instance = self::cls();
switch ( Router::verify_type() ) {
case self::TYPE_DISMISS_WHM:
case self::TYPE_DISMISS_EXPIRESDEFAULT:
self::update_option( Admin_Display::DB_DISMISS_MSG, Admin_Display::RULECONFLICT_DISMISSED );
case self::TYPE_DISMISS_PIN:
Admin_Display::dismiss_pin();
case self::TYPE_DISMISS_PROMO:
if ( empty( $_GET['promo_tag'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$promo_tag = sanitize_key( wp_unslash( $_GET['promo_tag'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( empty( $_instance->_promo_list[ $promo_tag ] ) ) {
defined( 'LSCWP_LOG' ) && self::debug( 'Dismiss promo ' . $promo_tag );
if ( ! empty( $_GET['done'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$_instance->_summary[ $promo_tag ] = 'done';
} elseif ( ! empty( $_GET['later'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
// Delay the banner to half year later.
$_instance->_summary[ $promo_tag ] = time() + ( 86400 * 180 );
// Update welcome banner to 30 days after.
$_instance->_summary[ $promo_tag ] = time() + ( 86400 * 30 );
if ( Router::is_ajax() ) {
// All dismiss actions are considered as ajax call, so just exit.
exit( wp_json_encode( [ 'success' => 1 ] ) );
// Plain click link, redirect to referral url.
* Check if has rule conflict notice.
* @return bool True if message should be shown.
public static function has_msg_ruleconflict() {
$db_dismiss_msg = self::get_option( Admin_Display::DB_DISMISS_MSG );
if ( ! $db_dismiss_msg ) {
self::update_option( Admin_Display::DB_DISMISS_MSG, -1 );
return Admin_Display::RULECONFLICT_ON === $db_dismiss_msg;
* Check if has WHM notice.
* @return bool True if message should be shown.
public static function has_whm_msg() {
$val = self::get_option( self::WHM_MSG );
return self::WHM_MSG_VAL === $val;
* Delete WHM message tag.
public static function dismiss_whm() {
self::update_option( self::WHM_MSG, -1 );
* Whether current request is a LiteSpeed admin page.
* @return bool True if LiteSpeed page.
private function _is_litespeed_page() {
! empty( $_GET['page'] ) && // phpcs:ignore WordPress.Security.NonceVerification.Recommended
(string) $_GET['page'], // phpcs:ignore WordPress.Security.NonceVerification.Recommended
Admin::PAGE_EDIT_HTACCESS,
'litespeed-optimization',
* Display promo banner (or check-only mode to know which promo would display).
* @param bool $check_only If true, only return the promo tag that would be shown.
* @return false|string False if none, or the promo tag string.
public function show_promo( $check_only = false ) {
$is_litespeed_page = $this->_is_litespeed_page();
// Bypass showing info banner if disabled all in debug.
if ( defined( 'LITESPEED_DISABLE_ALL' ) && LITESPEED_DISABLE_ALL ) {
if ( file_exists( ABSPATH . '.litespeed_no_banner' ) ) {
defined( 'LSCWP_LOG' ) && self::debug( 'Bypass banners due to silence file' );
foreach ( $this->_promo_list as $promo_tag => $v ) {
list( $delay_days, $litespeed_page_only ) = $v;
if ( $litespeed_page_only && ! $is_litespeed_page ) {
if ( empty( $this->_summary[ $promo_tag ] ) ) {
$this->_summary[ $promo_tag ] = time() + 86400 * $delay_days;
$promo_timestamp = $this->_summary[ $promo_tag ];
if ( 'done' === $promo_timestamp ) {
// Not reach the dateline yet.
if ( time() < $promo_timestamp ) {