* The plugin cache-control class for X-Litespeed-Cache-Control
* @subpackage LiteSpeed/inc
* @author LiteSpeed Technologies <info@litespeedtech.com>
defined('WPINC') || exit();
class Control extends Root
const BM_FORCED_CACHEABLE = 32;
const BM_PUBLIC_FORCED = 64;
const BM_NOTCACHEABLE = 256;
const X_HEADER = 'X-LiteSpeed-Cache-Control';
protected static $_control = 0;
protected static $_custom_ttl = 0;
private $_response_header_ttls = array();
* Add vary filter for Role Excludes
add_filter('litespeed_vary', array($this, 'vary_add_role_exclude'));
add_filter('wp_redirect', array($this, 'check_redirect'), 10, 2);
// Load response header conf
$this->_response_header_ttls = $this->conf(Base::O_CACHE_TTL_STATUS);
foreach ($this->_response_header_ttls as $k => $v) {
if (empty($v[0]) || empty($v[1])) {
$this->_response_header_ttls[$v[0]] = $v[1];
if ($this->conf(Base::O_PURGE_STALE)) {
* Exclude role from optimization filter
public function vary_add_role_exclude($vary)
if ($this->in_cache_exc_roles()) {
$vary['role_exclude_cache'] = 1;
* Check if one user role is in exclude cache 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_cache_exc_roles($role = null)
$role = Router::get_role();
$roles = explode(',', $role);
$found = array_intersect($roles, $this->conf(Base::O_CACHE_EXC_ROLES));
return $found ? implode(',', $found) : false;
* 1. Initialize cacheable status for `wp` hook
* 2. Hook error page tags for cacheable pages
public function init_cacheable()
// Hook `wp` to mark default cacheable status
// NOTE: Any process that does NOT run into `wp` hook will not get cacheable by default
add_action('wp', array($this, 'set_cacheable'), 5);
// Hook WP REST to be cacheable
if ($this->conf(Base::O_CACHE_REST)) {
add_action('rest_api_init', array($this, 'set_cacheable'), 5);
// NOTE: If any strange resource doesn't use normal WP logic `wp_loaded` hook, rewrite rule can handle it
$cache_res = $this->conf(Base::O_CACHE_RES);
$uri = esc_url($_SERVER['REQUEST_URI']); // todo: check if need esc_url()
$pattern = '!' . LSCWP_CONTENT_FOLDER . Htaccess::RW_PATTERN_RES . '!';
if (preg_match($pattern, $uri)) {
add_action('wp_loaded', array($this, 'set_cacheable'), 5);
$ajax_cache = $this->conf(Base::O_CACHE_AJAX_TTL);
foreach ($ajax_cache as $v) {
if (empty($v[0]) || empty($v[1])) {
// self::debug("Initializing cacheable status for wp_ajax_nopriv_" . $v[0]);
'wp_ajax_nopriv_' . $v[0],
self::set_custom_ttl($v[1]);
self::force_cacheable('ajax Cache setting for action ' . $v[0]);
add_filter('status_header', array($this, 'check_error_codes'), 10, 2);
* Check if the page returns any error code.
public function check_error_codes($status_header, $code)
if (array_key_exists($code, $this->_response_header_ttls)) {
if (self::is_cacheable() && !$this->_response_header_ttls[$code]) {
self::set_nocache('[Ctrl] TTL is set to no cache [status_header] ' . $code);
self::set_custom_ttl($this->_response_header_ttls[$code]);
} elseif (self::is_cacheable()) {
if (substr($code, 0, 1) == 4 || substr($code, 0, 1) == 5) {
self::set_nocache('[Ctrl] 4xx/5xx default to no cache [status_header] ' . $code);
Tag::add(Tag::TYPE_HTTP . $code);
// Give the default status_header back
public static function set_no_vary()
if (self::is_no_vary()) {
self::$_control |= self::BM_NO_VARY;
Debug2::debug('[Ctrl] X Cache_control -> no-vary', 3);
public static function is_no_vary()
return self::$_control & self::BM_NO_VARY;
public function set_stale()
self::$_control |= self::BM_STALE;
Debug2::debug('[Ctrl] X Cache_control -> stale');
public static function is_stale()
return self::$_control & self::BM_STALE;
* Set cache control to shared private
* @param string $reason The reason to no cache
public static function set_shared($reason = false)
self::$_control |= self::BM_SHARED;
if (!is_string($reason)) {
Debug2::debug('[Ctrl] X Cache_control -> shared ' . $reason);
* Check if is shared private
public static function is_shared()
return self::$_control & self::BM_SHARED && self::is_private();
* Set cache control to forced public
public static function set_public_forced($reason = false)
if (self::is_public_forced()) {
self::$_control |= self::BM_PUBLIC_FORCED;
if (!is_string($reason)) {
Debug2::debug('[Ctrl] X Cache_control -> public forced ' . $reason);
* Check if is public forced
public static function is_public_forced()
return self::$_control & self::BM_PUBLIC_FORCED;
* Set cache control to private
* @param string $reason The reason to no cache
public static function set_private($reason = false)
if (self::is_private()) {
self::$_control |= self::BM_PRIVATE;
if (!is_string($reason)) {
Debug2::debug('[Ctrl] X Cache_control -> private ' . $reason);
public static function is_private()
if (defined('LITESPEED_GUEST') && LITESPEED_GUEST) {
return self::$_control & self::BM_PRIVATE && !self::is_public_forced();
* Initialize cacheable status in `wp` hook, if not call this, by default it will be non-cacheable
public function set_cacheable($reason = false)
self::$_control |= self::BM_CACHEABLE;
if (!is_string($reason)) {
$reason = ' [reason] ' . $reason;
Debug2::debug('[Ctrl] X Cache_control init on' . $reason);
* This will disable non-cacheable BM
public static function force_cacheable($reason = false)
self::$_control |= self::BM_FORCED_CACHEABLE;
if (!is_string($reason)) {
$reason = ' [reason] ' . $reason;
Debug2::debug('[Ctrl] Forced cacheable' . $reason);
* Switch to nocacheable status
* @param string $reason The reason to no cache
public static function set_nocache($reason = false)
self::$_control |= self::BM_NOTCACHEABLE;
if (!is_string($reason)) {
Debug2::debug('[Ctrl] X Cache_control -> no Cache ' . $reason, 5);
* Check current notcacheable bit set
* @return bool True if notcacheable bit is set, otherwise false.
public static function isset_notcacheable()
return self::$_control & self::BM_NOTCACHEABLE;
* Check current force cacheable bit set
public static function is_forced_cacheable()
return self::$_control & self::BM_FORCED_CACHEABLE;
* Check current cacheable status
* @return bool True if is still cacheable, otherwise false.
public static function is_cacheable()
if (defined('LSCACHE_NO_CACHE') && LSCACHE_NO_CACHE) {
Debug2::debug('[Ctrl] LSCACHE_NO_CACHE constant defined');
// Guest mode always cacheable
if (defined('LITESPEED_GUEST') && LITESPEED_GUEST) {
// If its forced public cacheable
if (self::is_public_forced()) {
// If its forced cacheable
if (self::is_forced_cacheable()) {
return !self::isset_notcacheable() && self::$_control & self::BM_CACHEABLE;
* Set a custom TTL to use with the request if needed.
* @param mixed $ttl An integer or string to use as the TTL. Must be numeric.
public static function set_custom_ttl($ttl, $reason = false)
self::$_custom_ttl = $ttl;
Debug2::debug('[Ctrl] X Cache_control TTL -> ' . $ttl . ($reason ? ' [reason] ' . $ttl : ''));
public function get_ttl()
if (self::$_custom_ttl != 0) {
return self::$_custom_ttl;
// Check if is in timed url list or not
$timed_urls = Utility::wildcard2regex($this->conf(Base::O_PURGE_TIMED_URLS));
$timed_urls_time = $this->conf(Base::O_PURGE_TIMED_URLS_TIME);
if ($timed_urls && $timed_urls_time) {
$current_url = Tag::build_uri_tag(true);
$scheduled_time = strtotime($timed_urls_time);