* The plugin vary class to manage X-LiteSpeed-Vary
defined('WPINC') || exit();
const X_HEADER = 'X-LiteSpeed-Vary';
private static $_vary_name = '_lscache_vary'; // this default vary cookie is used for logged in status check
private static $_can_change_vary = false; // Currently only AJAX used this
* Adds the actions used for setting up cookies on log in/out.
* Also checks if the database matches the rewrite rule.
$this->_update_vary_name();
* Update the default vary name if changed
private function _update_vary_name()
$db_cookie = $this->conf(Base::O_CACHE_LOGIN_COOKIE); // [3.0] todo: check if works in network's sites
// If no vary set in rewrite rule
if (!isset($_SERVER['LSCACHE_VARY_COOKIE'])) {
// Display cookie error msg to admin
if (is_multisite() ? is_network_admin() : is_admin()) {
Admin_Display::show_error_cookie();
Control::set_nocache('vary cookie setting error');
// If db setting does not exist, skip checking db value
// beyond this point, need to make sure db vary setting is in $_SERVER env.
$vary_arr = explode(',', $_SERVER['LSCACHE_VARY_COOKIE']);
if (in_array($db_cookie, $vary_arr)) {
self::$_vary_name = $db_cookie;
if (is_multisite() ? is_network_admin() : is_admin()) {
Admin_Display::show_error_cookie();
Control::set_nocache('vary cookie setting lost error');
public function after_user_init()
if (Router::is_logged_in()) {
// If not esi, check cache logged-in user setting
if (!$this->cls('Router')->esi_enabled()) {
// If cache logged-in, then init cacheable to private
if ($this->conf(Base::O_CACHE_PRIV)) {
add_action('wp_logout', __NAMESPACE__ . '\Purge::purge_on_logout');
$this->cls('Control')->init_cacheable();
Control::set_private('logged in user');
// No cache for logged-in user
Control::set_nocache('logged in user');
// ESI is on, can be public cache
// Need to make sure vary is using group id
$this->cls('Control')->init_cacheable();
// register logout hook to clear login status
add_action('clear_auth_cookie', array($this, 'remove_logged_in'));
// Only after vary init, can detect if is Guest mode or not
$this->_maybe_guest_mode();
// Set vary cookie for logging in user, otherwise the user will hit public with vary=0 (guest version)
add_action('set_logged_in_cookie', array($this, 'add_logged_in'), 10, 4);
add_action('wp_login', __NAMESPACE__ . '\Purge::purge_on_logout');
$this->cls('Control')->init_cacheable();
// Check `login page` cacheable setting because they don't go through main WP logic
add_action('login_init', array($this->cls('Tag'), 'check_login_cacheable'), 5);
if (!empty($_GET['litespeed_guest'])) {
add_action('wp_loaded', array($this, 'update_guest_vary'), 20);
add_filter('comments_array', array($this, 'check_commenter'));
// Set vary cookie for commenter.
add_action('set_comment_cookies', array($this, 'append_commenter'));
* Don't change for REST call because they don't carry on user info usually
add_action('rest_api_init', function () {
// this hook is fired in `init` hook
Debug2::debug('[Vary] Rest API init disabled vary change');
add_filter('litespeed_can_change_vary', '__return_false');
* Check if is Guest mode or not
private function _maybe_guest_mode()
if (defined('LITESPEED_GUEST')) {
Debug2::debug('[Vary] 👒👒 Guest mode ' . (LITESPEED_GUEST ? 'predefined' : 'turned off'));
if (!$this->conf(Base::O_GUEST)) {
// If vary is set, then not a guest
// If has admin QS, then no guest
if (!empty($_GET[Router::ACTION])) {
if (defined('DOING_AJAX')) {
if (defined('DOING_CRON')) {
// If is the request to update vary, then no guest
// Don't need anymore as it is always ajax call
// Still keep it in case some WP blocked the lightweight guest vary update script, WP can still update the vary
if (!empty($_GET['litespeed_guest'])) {
/* @ref https://wordpress.org/support/topic/checkout-add-to-cart-executed-twice/ */
if (!empty($_GET['litespeed_guest_off'])) {
Debug2::debug('[Vary] 👒👒 Guest mode');
!defined('LITESPEED_GUEST') && define('LITESPEED_GUEST', true);
if ($this->conf(Base::O_GUEST_OPTM)) {
!defined('LITESPEED_GUEST_OPTM') && define('LITESPEED_GUEST_OPTM', true);
* @deprecated 4.1 Use independent lightweight guest.vary.php as a replacement
public function update_guest_vary()
// This process must not be cached
!defined('LSCACHE_NO_CACHE') && define('LSCACHE_NO_CACHE', true);
$_guest = new Lib\Guest();
if ($_guest->always_guest() || self::has_vary()) {
// If contains vary already, don't reload to avoid infinite loop when parent page having browser cache
!defined('LITESPEED_GUEST') && define('LITESPEED_GUEST', true); // Reuse this const to bypass set vary in vary finalize
Debug2::debug('[Vary] 🤠🤠Guest');
Debug2::debug('[Vary] Will update guest vary in finalize');
echo \json_encode(array('reload' => 'yes'));
* Hooked to the comments_array filter.
* Check if the user accessing the page has the commenter cookie.
* If the user does not want to cache commenters, just check if user is commenter.
* Otherwise if the vary cookie is set, unset it. This is so that when the page is cached, the page will appear as if the user was a normal user.
* Normal user is defined as not a logged in user and not a commenter.
* @param array $comments The current comments to output
* @return array The comments to output.
public function check_commenter($comments)
* Hook to bypass pending comment check for comment related plugins compatibility
if (apply_filters('litespeed_vary_check_commenter_pending', true)) {
foreach ($comments as $comment) {
if (!$comment->comment_approved) {
// current user has pending comment
// No pending comments, don't need to add private cache
Debug2::debug('[Vary] No pending comment');
$this->remove_commenter();
// Remove commenter prefilled info if exists, for public cache
foreach ($_COOKIE as $cookie_name => $cookie_value) {
if (strlen($cookie_name) >= 15 && strpos($cookie_name, 'comment_author_') === 0) {
unset($_COOKIE[$cookie_name]);
// Current user/visitor has pending comments
// set vary=2 for next time vary lookup
if ($this->conf(Base::O_CACHE_COMMENTER)) {
Control::set_private('existing commenter');
Control::set_nocache('existing commenter');
* Check if default vary has a value
public static function has_vary()
if (empty($_COOKIE[self::$_vary_name])) {
return $_COOKIE[self::$_vary_name];
* Append user status with logged in
* @since 1.6.2 Removed static referral
public function add_logged_in($logged_in_cookie = false, $expire = false, $expiration = false, $uid = false)
Debug2::debug('[Vary] add_logged_in');
* NOTE: Run before `$this->_update_default_vary()` to make vary changeable
// If the cookie is lost somehow, set it
$this->_update_default_vary($uid, $expire);
* Remove user logged in status
* @since 1.6.2 Removed static referral
public function remove_logged_in()
Debug2::debug('[Vary] remove_logged_in');
* NOTE: Run before `$this->_update_default_vary()` to make vary changeable
// Force update vary to remove login status
$this->_update_default_vary(-1);
* Allow vary can be changed for ajax calls
* @since 2.6 Changed to static
public static function can_ajax_vary()
Debug2::debug('[Vary] _can_change_vary -> true');
self::$_can_change_vary = true;
* Check if can change default vary
private function can_change_vary()
// Don't change for ajax due to ajax not sending webp header
if (!self::$_can_change_vary) {
Debug2::debug('[Vary] can_change_vary bypassed due to ajax call');
* POST request can set vary to fix #820789 login "loop" guest cache issue
if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] !== 'GET' && $_SERVER['REQUEST_METHOD'] !== 'POST') {
Debug2::debug('[Vary] can_change_vary bypassed due to method not get/post');
* Disable vary change if is from crawler
* @since 2.9.8 To enable woocommerce cart not empty warm up (@Taba)
if (!empty($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], Crawler::FAST_USER_AGENT) === 0) {
Debug2::debug('[Vary] can_change_vary bypassed due to crawler');
if (!apply_filters('litespeed_can_change_vary', true)) {
Debug2::debug('[Vary] can_change_vary bypassed due to litespeed_can_change_vary hook');
* @since 1.6.6.1 Add ran check to make it only run once ( No run multiple times due to login process doesn't have valid uid )
private function _update_default_vary($uid = false, $expire = false)
// Make sure header output only run once
if (!defined('LITESPEED_DID_' . __FUNCTION__)) {
define('LITESPEED_DID_' . __FUNCTION__, true);
Debug2::debug2('[Vary] _update_default_vary bypassed due to run already');
// If the cookie is lost somehow, set it
$vary = $this->finalize_default_vary($uid);
$current_vary = self::has_vary();
if ($current_vary !== $vary && $current_vary !== 'commenter' && $this->can_change_vary()) {
// $_COOKIE[ self::$_vary_name ] = $vary; // not needed
$expire = time() + 2 * DAY_IN_SECONDS;
$this->_cookie($vary, $expire);
Debug2::debug("[Vary] set_cookie ---> $vary");
// Control::set_nocache( 'changing default vary' . " $current_vary => $vary" );
public function get_vary_name()
return self::$_vary_name;
* Check if one user role is in vary group settings
* @since 3.0 Moved here from conf.cls
* @param string $role The user role
* @return int The set value if already set
public function in_vary_group($role)
$vary_groups = $this->conf(Base::O_CACHE_VARY_GROUP);
$roles = explode(',', $role);
if ($found = array_intersect($roles, array_keys($vary_groups))) {
foreach ($found as $curr_role) {
$groups[] = $vary_groups[$curr_role];
$group = implode(',', array_unique($groups));
} elseif (in_array('administrator', $roles)) {
Debug2::debug2('[Vary] role in vary_group [group] ' . $group);
* Finalize default Vary Cookie
* Get user vary tag based on admin_bar & role
* NOTE: Login process will also call this because it does not call wp hook as normal page loading
public function finalize_default_vary($uid = false)
// Must check this to bypass vary generation for guests
// Must check this to avoid Guest page's CSS/JS/CCSS/UCSS get non-guest vary filename
if (defined('LITESPEED_GUEST') && LITESPEED_GUEST) {
if ($this->conf(Base::O_GUEST)) {
$uid = get_current_user_id();
Debug2::debug('[Vary] uid: ' . $uid);
$role = Router::get_role($uid);