CVE-2023-4372 (PHP, published 2024-01-11T09:15Z)
Description: value: The LiteSpeed Cache plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'esi' shortcode in versions up to, and including, 5.6 due to insufficient input sanitization and output escaping on user supplied attributes. This makes it possible for authenticated attackers with contributor-level and above permissions to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
Severity: nan (CVSS v3 base score: 5.4)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/litespeedtech/lscache_wp
Files changed:
litespeed-cache.php
readme.txt
src/esi.cls.php
Commit message: v5.6.1-a10: * 🐞**ESI** Patched a vulnerability issue. (István Márton@Wordfence #841011)


Vulnerable code:
 * Version:           5.6.1-a9
!defined('LSCWP_V') && define('LSCWP_V', '5.6.1-a9');
defined( 'WPINC' ) || exit;
class ESI extends Root {
	private $_nonce_actions = array( -1 => '' ); // val is cache control
	public function init() {
		if ( Router::is_ajax() || ! $this->cls( 'Router' )->esi_enabled() ) {
		if ( defined( 'LITESPEED_GUEST' ) && LITESPEED_GUEST ) {
		if ( defined( 'LITESPEED_ESI_OFF' ) ) {
		! defined( 'LITESPEED_ESI_INITED' ) && define( 'LITESPEED_ESI_INITED', true );
	private function _hooks() {
		add_filter( 'template_include', array( $this, 'esi_template' ), 99999 );
		add_action( 'load-widgets.php', __NAMESPACE__ . '\Purge::purge_widget' );
		add_action( 'wp_update_comment_count', __NAMESPACE__ . '\Purge::purge_comment_widget' );
		if ( ! empty( $_GET[ self::QS_ACTION ] ) ) {
		if ( ! is_admin() ) {
			add_shortcode( 'esi', array( $this, 'shortcode' ) );

	private function _transform_nonce() {
		if ( is_admin() ) {
		$nonces = $this->conf( Base::O_ESI_NONCE );
		add_filter( 'litespeed_esi_nonces', array( $this->cls( 'Data' ), 'load_esi_nonces' ) );
		if ( $nonces = apply_filters( 'litespeed_esi_nonces', $nonces ) ) {
			foreach ( $nonces as $action ) {
				$this->nonce_action( $action );
		add_action( 'litespeed_nonce', array( $this, 'nonce_action' ) );
	public function nonce_action( $action ) {
		$action = explode( ' ', $action );
		$control = ! empty( $action[ 1 ] ) ? $action[ 1 ] : '';
		$action = $action[ 0 ];
		$action = Utility::wildcard2regex( $action );
		if ( array_key_exists( $action, $this->_nonce_actions ) ) {
		$this->_nonce_actions[ $action ] = $control;
		Debug2::debug( '[ESI] Appended nonce action to nonce list [action] ' . $action );
	public function is_nonce_action( $action ) {
		if ( ! defined( 'LITESPEED_ESI_INITED' ) ) {
		if ( is_admin() ) {
		if ( defined( 'LITESPEED_ESI_OFF' ) ) {
		foreach ( $this->_nonce_actions as $k => $v ) {
			if ( strpos( $k, '*' ) !== false ) {
				if( preg_match( '#' . $k . '#iU', $action ) ) {
			}
			else {
				if ( $k == $action ) {
	public function shortcode( $atts ) {
		if ( empty( $atts[ 0 ] ) ) {
			Debug2::debug( '[ESI] ===shortcode wrong format', $atts );
		if ( ! empty( $atts[ 'cache' ] ) ) {
			$cache = $atts[ 'cache' ];
			unset( $atts[ 'cache' ] );
		if ( ! empty( $atts[ '_ls_silence' ] ) ) {
		do_action( 'litespeed_esi_shortcode-' . $atts[ 0 ] );
		return $this->sub_esi_block( 'esi', 'esi-shortcode', $atts, $cache, $silence );
	public static function has_esi() {
	public static function set_has_esi() {
	private function _register_esi_actions() {
		! defined( 'LSCACHE_IS_ESI' ) && define( 'LSCACHE_IS_ESI', $_GET[ self::QS_ACTION ] );// Reused this to ESI block ID
		! empty( $_SERVER[ 'ESI_REFERER' ] ) && defined( 'LSCWP_LOG' ) && Debug2::debug( '[ESI] ESI_REFERER: ' . $_SERVER[ 'ESI_REFERER' ] );
		if ( ! empty( $_SERVER[ 'ESI_REFERER' ] ) && ! $this->cls( 'REST' )->is_rest( $_SERVER[ 'ESI_REFERER' ] ) ) {
			$_SERVER[ 'REQUEST_URI' ] = $_SERVER[ 'ESI_REFERER' ];
		if ( ! empty( $_SERVER[ 'ESI_CONTENT_TYPE' ] ) && strpos( $_SERVER[ 'ESI_CONTENT_TYPE' ], 'application/json' ) === 0 ) {
			add_filter( 'litespeed_is_json', '__return_true' );
		add_action( 'rest_api_init', array( $this, 'load_esi_block' ), 101 );
		add_action('litespeed_esi_load-nonce', array( $this, 'load_nonce_block' ) );
		add_action('litespeed_esi_load-esi', array( $this, 'load_esi_shortcode' ) );
		add_action('litespeed_esi_load-' . self::COMBO, array( $this, 'load_combo' ) );
	public function esi_template( $template ) {
		if ( defined( 'LSCACHE_IS_ESI' ) ) {
			Debug2::debug( '[ESI] calling template' );
	private function _register_not_esi_actions() {
		do_action( 'litespeed_tpl_normal' );
		if ( ! Control::is_cacheable() ) {
		if ( Router::is_ajax() ) {
		add_filter('widget_display_callback', array( $this, 'sub_widget_block' ), 0, 3);
		if ( Router::is_logged_in() ) {
		if ( ! Router::is_ajax() && Vary::has_vary() ) {
			add_filter( 'comment_form_defaults', array( $this, 'register_comment_form_actions' ) );

	public static function combine( $block_id ) {
		if ( ! isset( $_SERVER[ 'X-LSCACHE' ] ) || strpos( $_SERVER[ 'X-LSCACHE' ], 'combine' ) === false ) {
		if ( in_array( $block_id, self::$_combine_ids ) ) {
	public function load_combo() {
		Control::set_nocache( 'ESI combine request' );
		if ( empty( $_POST[ 'esi_include' ] ) ) {
		Debug2::debug( '[ESI] 🍔 Load combo', $_POST[ 'esi_include' ] );
		foreach ( $_POST[ 'esi_include' ] as $url ) {
			$qs = parse_url( htmlspecialchars_decode( $url ), PHP_URL_QUERY );
			parse_str( $qs, $qs );
			if ( empty( $qs[ self::QS_ACTION ] ) ) {
			$esi_id = $qs[ self::QS_ACTION ];
			$esi_param = ! empty( $qs[ self::QS_PARAMS ] ) ? $this->_parse_esi_param( $qs[ self::QS_PARAMS ] ) : false;
			$inline_param = apply_filters( 'litespeed_esi_inline-' . $esi_id, array(), $esi_param ); // Returned array need to be [ val, control, tag ]
			if ( $inline_param ) {
				$output .= self::_build_inline( $url, $inline_param );
	private static function _build_inline( $url, $inline_param ) {
		if ( ! $url || empty( $inline_param[ 'val' ] ) || empty( $inline_param[ 'control' ] ) || empty( $inline_param[ 'tag' ] ) ) {
		return "<esi:inline name='$url' cache-control='" . $inline_param[ 'control' ] . "' cache-tag='" . $inline_param[ 'tag' ] . "'>" . $inline_param[ 'val' ] . "</esi:inline>";
	public function sub_esi_block( $block_id, $wrapper, $params = array(), $control = 'private,no-vary', $silence = false, $preserved = false, $svar = false, $inline_param = array() ) {
		if ( empty($block_id) || ! is_array($params) || preg_match('/[^\w-]/', $block_id) ) {
		if ( defined( 'LITESPEED_ESI_OFF' ) ) {
			Debug2::debug( '[ESI] ESI OFF so force loading [block_id] ' . $block_id );
			do_action( 'litespeed_esi_load-' . $block_id, $params );
		if ( $silence ) {
			$params[ '_ls_silence' ] = true;
		if ( $this->cls( 'REST' )->is_rest() || $this->cls( 'REST' )->is_internal_rest() ) {
			$params[ 'is_json' ] = 1;
		$params = apply_filters( 'litespeed_esi_params', $params, $block_id );
		$control = apply_filters('litespeed_esi_control', $control, $block_id );
		if ( !is_array($params) || !is_string($control) ) {
			defined( 'LSCWP_LOG' ) && Debug2::debug( "[ESI] 🛑 Sub hooks returned Params: \n" . var_export($params, true) . "\ncache control: \n" . var_export($control, true) );
		if ( ! empty( $control ) ) {
			$appended_params[ '_control' ] = $control;
		if ( $params ) {
			$appended_params[ self::QS_PARAMS ] = base64_encode( json_encode( $params ) );
			Debug2::debug2( '[ESI] param ', $params );
		$appended_params[ '_hash' ] = $this->_gen_esi_md5( $appended_params );
		$appended_params = array_map( 'urlencode', $appended_params );
		$url = add_query_arg( $appended_params, trailingslashit( wp_make_link_relative( home_url() ) ) );
		if ( $inline_param ) {
			$output .= self::_build_inline( $url, $inline_param );
		if ( ! empty( $control ) ) {
		if ( $svar ) {
		if ( in_array( $block_id, self::$_combine_ids ) ) {
		if ( $block_id == self::COMBO && isset( $_SERVER[ 'X-LSCACHE' ] ) && strpos( $_SERVER[ 'X-LSCACHE' ], 'combine' ) !== false ) {
		if ( ! $silence ) {
		Debug2::debug( "[ESI] 💕  [BLock_ID] $block_id \t[wrapper] $wrapper \t\t[Control] $control" );
		Debug2::debug2( $output );
		if ( $preserved ) {
			$hash = md5( $output );
			$this->_esi_preserve_list[ $hash ] = $output;
			Debug2::debug( "[ESI] Preserved to $hash" );
	private function _gen_esi_md5( $params ) {
		foreach ( $keys as $v ) {
			if ( isset( $params[ $v ] ) && is_string( $params[ $v ] ) ) {
				$str .= $params[ $v ];
		Debug2::debug2( '[ESI] md5_string=' . $str );
		return md5( $this->conf( Base::HASH ) . $str );
	private function _parse_esi_param( $qs_params = false ) {
		if ( $qs_params ) {
		}
		elseif ( isset( $_REQUEST[ self::QS_PARAMS ] ) ) {
			$req_params = $_REQUEST[ self::QS_PARAMS ];
		if ( ! $req_params ) {
		$unencrypted = base64_decode( $req_params );
		if ( $unencrypted === false ) {
		Debug2::debug2( '[ESI] parms', $unencrypted );
		$params = json_decode( $unencrypted, true );
	public function load_esi_block() {
		if ( empty( $_GET[ '_hash' ] ) || $this->_gen_esi_md5( $_GET ) != $_GET[ '_hash' ] ) {
			Debug2::debug( '[ESI] ❌ Failed to validate _hash' );
		if ( defined( 'LSCWP_LOG' ) ) {
			if( ! empty( $params[ self::PARAM_NAME ] ) ) {
				$logInfo .= ' Name: ' . $params[ self::PARAM_NAME ] . ' ----- ';
			Debug2::debug( $logInfo );
		if ( ! empty( $params[ '_ls_silence' ] ) ) {
			! defined( 'LSCACHE_ESI_SILENCE' ) && define( 'LSCACHE_ESI_SILENCE', true );
		if ( ! empty( $params[ 'is_json' ] ) ) {
			add_filter( 'litespeed_is_json', '__return_true' );
		Tag::add( rtrim( Tag::TYPE_ESI, '.' ) );
		Tag::add( Tag::TYPE_ESI . LSCACHE_IS_ESI );
		if ( ! empty( $_GET[ '_control' ] ) ) {
			$control = explode( ',', $_GET[ '_control' ] );
			if ( in_array( 'private', $control ) ) {
			if ( in_array( 'no-vary', $control ) ) {
// The *_sub_* functions are helpers for the sub_* functions.
// The *_load_* functions are helpers for the load_* functions.
	public static function widget_default_options($options, $widget) {
		if ( ! is_array($options) ) {
			case 'WP_Widget_Recent_Posts' :
			case 'WP_Widget_Recent_Comments' :
			default :
	public function sub_widget_block( $instance, $widget, $args ) {
		if ( ! is_array( $instance ) ) {
		$name = get_class( $widget );
		if ( ! isset( $instance[ Base::OPTION_NAME ] ) ) {
		$options = $instance[ Base::OPTION_NAME ];
		if ( ! isset( $options ) || ! $options[ self::WIDGET_O_ESIENABLE ] ) {
			defined( 'LSCWP_LOG' ) && Debug2::debug( 'ESI 0 ' . $name . ': '. ( ! isset( $options ) ? 'not set' : 'set off' ) );
		$esi_private = $options[ self::WIDGET_O_ESIENABLE ] == Base::VAL_ON2 ? 'private,' : '';
		echo $this->sub_esi_block( 'widget', 'widget ' . $name, $params, $esi_private . 'no-vary' );
	public function sub_admin_bar_block() {
		if ( ! is_admin_bar_showing() || ! is_object($wp_admin_bar) ) {
			'ref' => $_SERVER[ 'REQUEST_URI' ],
		echo $this->sub_esi_block( 'admin-bar', 'adminbar', $params );
	public function load_widget_block( $params ) {
		$option = $params[ self::PARAM_INSTANCE ];
		$option = $option[ Base::OPTION_NAME ];
		$ttl = $option[ self::WIDGET_O_TTL ];
		defined( 'LSCWP_LOG' ) && Debug2::debug( 'ESI widget render: name ' . $params[ self::PARAM_NAME ] . ', id ' . $params[ self::PARAM_ID ] . ', ttl ' . $ttl );
		if ( $ttl == 0 ) {
			Control::set_nocache( 'ESI Widget time to live set to 0' );
		}
		else {
			Control::set_custom_ttl( $ttl );

			if ( $option[ self::WIDGET_O_ESIENABLE ] == Base::VAL_ON2 ) {
			Tag::add( Tag::TYPE_WIDGET . $params[ self::PARAM_ID ] );
		the_widget( $params[ self::PARAM_NAME ], $params[ self::PARAM_INSTANCE ], $params[ self::PARAM_ARGS ] );
	public function load_admin_bar_block( $params ) {
		if ( ! empty( $params[ 'ref' ] ) ) {
			$ref_qs = parse_url( $params[ 'ref' ], PHP_URL_QUERY );
			if ( ! empty( $ref_qs ) ) {
				parse_str( $ref_qs, $ref_qs_arr );
				if ( ! empty( $ref_qs_arr ) ) {
					foreach ( $ref_qs_arr as $k => $v ) {
						$_GET[ $k ] = $v;
		if ( ! $this->conf( Base::O_ESI_CACHE_ADMBAR ) ) {
			Control::set_nocache( 'build-in set to not cacheable' );
		}
		else {
		defined( 'LSCWP_LOG' ) && Debug2::debug( 'ESI: adminbar ref: ' . $_SERVER[ 'REQUEST_URI' ] );
	public function load_comment_form_block( $params ) {
		comment_form( $params[ self::PARAM_ARGS ], $params[ self::PARAM_ID ] );
		if ( ! $this->conf( Base::O_ESI_CACHE_COMMFORM ) ) {
			Control::set_nocache( 'build-in set to not cacheable' );
		}
		else {
			if ( Vary::has_vary() ) {
	public function load_nonce_block( $params ) {
		$action = $params[ 'action' ];
		Debug2::debug( '[ESI] load_nonce_block [action] ' . $action );
		Control::set_custom_ttl( 43200 );
		if ( Router::is_logged_in() ) {
		if ( function_exists( 'wp_create_nonce_litespeed_esi' ) ) {
			echo wp_create_nonce_litespeed_esi( $action );
		}
		else {
			echo wp_create_nonce( $action );
	public function load_esi_shortcode( $params ) {
		if ( isset( $params[ 'ttl' ] ) ) {
			if ( ! $params[ 'ttl' ] ) {
				Control::set_nocache( 'ESI shortcode att ttl=0' );
			}
			else {
				Control::set_custom_ttl( $params[ 'ttl' ] );
			unset( $params[ 'ttl' ] );
		$shortcode = $params[ 0 ];
		foreach ( $params as $k => $v ) {
			if ( $k === 0 ) {
			$atts_ori[] = is_string( $k ) ? "$k='" . addslashes( $v ) . "'" : $v;
		Tag::add( Tag::TYPE_ESI . "esi.$shortcode" );
		echo do_shortcode( "[$shortcode " . implode( ' ', $atts_ori ) . " ]" );
	public function register_comment_form_actions( $defaults ) {
		add_filter( 'comment_form_submit_button', array( $this, 'sub_comment_form_btn' ), 1000, 2 ); // To save the params passed in
		add_action( 'comment_form', array( $this, 'sub_comment_form_block' ), 1000 );
	public function sub_comment_form_btn( $unused, $args ) {
		if ( empty( $args ) || empty( $this->esi_args ) ) {
			Debug2::debug( 'comment form args empty?' );
		foreach ( $args as $k => $v ) {
			if ( ! isset( $this->esi_args[ $k ] ) ) {
				$esi_args[ $k ] = $v;
			}
			elseif ( is_array( $v ) ) {
				$diff = array_diff_assoc( $v, $this->esi_args[ $k ] );
				if ( ! empty( $diff ) ) {
					$esi_args[ $k ] = $diff;
			}
			elseif ( $v !== $this->esi_args[ $k ] ) {
				$esi_args[ $k ] = $v;
	public function sub_comment_form_block( $post_id ) {
		echo $this->sub_esi_block( 'comment-form', 'comment form', $params );
		add_action( 'comment_form_after', array( $this, 'comment_form_sub_clean' ) );
	public function comment_form_sub_clean() {
	public function finalize( $buffer ) {
		if ( self::$_combine_ids ) {
			Debug2::debug( '[ESI] 🍔 Enabled combo' );
			$esi_block = $this->sub_esi_block( self::COMBO, '__COMBINE_MAIN__', array(), 'no-cache', true );
		if ( ! $this->_esi_preserve_list ) {
		$keys = array_keys( $this->_esi_preserve_list );
		Debug2::debug( '[ESI] replacing preserved blocks', $keys );
		$buffer = str_replace( $keys, $this->_esi_preserve_list, $buffer );
	public function contain_preserve_esi( $content ) {
		foreach ( $this->_esi_preserve_list as $k => $v ) {
			if ( strpos( $content, '"' . $k . '"' ) !== false ) {
			if ( strpos( $content, "'" . $k . "'" ) !== false ) {


Fixed code:
 * Version:           5.6.1-a10
!defined('LSCWP_V') && define('LSCWP_V', '5.6.1-a10');
* 🐞**ESI** Patched a possible vulnerability issue. (István Márton@Wordfence #841011)


defined('WPINC') || exit;
class ESI extends Root
{
	private $_nonce_actions = array(-1 => ''); // val is cache control
	public function init()
	{
		if (Router::is_ajax() || !$this->cls('Router')->esi_enabled()) {
		if (defined('LITESPEED_GUEST') && LITESPEED_GUEST) {
		if (defined('LITESPEED_ESI_OFF')) {
		!defined('LITESPEED_ESI_INITED') && define('LITESPEED_ESI_INITED', true);
	private function _hooks()
	{
		add_filter('template_include', array($this, 'esi_template'), 99999);
		add_action('load-widgets.php', __NAMESPACE__ . '\Purge::purge_widget');
		add_action('wp_update_comment_count', __NAMESPACE__ . '\Purge::purge_comment_widget');
		if (!empty($_GET[self::QS_ACTION])) {
		if (!is_admin()) {
			add_shortcode('esi', array($this, 'shortcode'));
	private function _transform_nonce()
	{
		if (is_admin()) {
		$nonces = $this->conf(Base::O_ESI_NONCE);
		add_filter('litespeed_esi_nonces', array($this->cls('Data'), 'load_esi_nonces'));
		if ($nonces = apply_filters('litespeed_esi_nonces', $nonces)) {
			foreach ($nonces as $action) {
				$this->nonce_action($action);
		add_action('litespeed_nonce', array($this, 'nonce_action'));
	public function nonce_action($action)
	{
		$action = explode(' ', $action);
		$control = !empty($action[1]) ? $action[1] : '';
		$action = $action[0];
		$action = Utility::wildcard2regex($action);
		if (array_key_exists($action, $this->_nonce_actions)) {
		$this->_nonce_actions[$action] = $control;
		Debug2::debug('[ESI] Appended nonce action to nonce list [action] ' . $action);
	public function is_nonce_action($action)
	{
		if (!defined('LITESPEED_ESI_INITED')) {
		if (is_admin()) {
		if (defined('LITESPEED_ESI_OFF')) {
		foreach ($this->_nonce_actions as $k => $v) {
			if (strpos($k, '*') !== false) {
				if (preg_match('#' . $k . '#iU', $action)) {
			} else {
				if ($k == $action) {
	public function shortcode($atts)
	{
		if (empty($atts[0])) {
			Debug2::debug('[ESI] ===shortcode wrong format', $atts);
		if (!empty($atts['cache'])) {
			$cache = $atts['cache'];
			unset($atts['cache']);
		if (!empty($atts['_ls_silence'])) {
		do_action('litespeed_esi_shortcode-' . $atts[0]);
		return $this->sub_esi_block('esi', 'esi-shortcode', $atts, $cache, $silence);
	public static function has_esi()
	{
	public static function set_has_esi()
	{
	private function _register_esi_actions()
	{
		!defined('LSCACHE_IS_ESI') && define('LSCACHE_IS_ESI', $_GET[self::QS_ACTION]); // Reused this to ESI block ID
		!empty($_SERVER['ESI_REFERER']) && defined('LSCWP_LOG') && Debug2::debug('[ESI] ESI_REFERER: ' . $_SERVER['ESI_REFERER']);
		if (!empty($_SERVER['ESI_REFERER']) && !$this->cls('REST')->is_rest($_SERVER['ESI_REFERER'])) {
			$_SERVER['REQUEST_URI'] = $_SERVER['ESI_REFERER'];
		if (!empty($_SERVER['ESI_CONTENT_TYPE']) && strpos($_SERVER['ESI_CONTENT_TYPE'], 'application/json') === 0) {
			add_filter('litespeed_is_json', '__return_true');
		add_action('rest_api_init', array($this, 'load_esi_block'), 101);
		add_action('litespeed_esi_load-nonce', array($this, 'load_nonce_block'));
		add_action('litespeed_esi_load-esi', array($this, 'load_esi_shortcode'));
		add_action('litespeed_esi_load-' . self::COMBO, array($this, 'load_combo'));
	public function esi_template($template)
	{
		if (defined('LSCACHE_IS_ESI')) {
			Debug2::debug('[ESI] calling template');
	private function _register_not_esi_actions()
	{
		do_action('litespeed_tpl_normal');
		if (!Control::is_cacheable()) {
		if (Router::is_ajax()) {
		add_filter('widget_display_callback', array($this, 'sub_widget_block'), 0, 3);
		if (Router::is_logged_in()) {
		if (!Router::is_ajax() && Vary::has_vary()) {
			add_filter('comment_form_defaults', array($this, 'register_comment_form_actions'));
	public static function combine($block_id)
	{
		if (!isset($_SERVER['X-LSCACHE']) || strpos($_SERVER['X-LSCACHE'], 'combine') === false) {
		if (in_array($block_id, self::$_combine_ids)) {
	public function load_combo()
	{
		Control::set_nocache('ESI combine request');
		if (empty($_POST['esi_include'])) {
		Debug2::debug('[ESI] 🍔 Load combo', $_POST['esi_include']);
		foreach ($_POST['esi_include'] as $url) {
			$qs = parse_url(htmlspecialchars_decode($url), PHP_URL_QUERY);
			parse_str($qs, $qs);
			if (empty($qs[self::QS_ACTION])) {
			$esi_id = $qs[self::QS_ACTION];
			$esi_param = !empty($qs[self::QS_PARAMS]) ? $this->_parse_esi_param($qs[self::QS_PARAMS]) : false;
			$inline_param = apply_filters('litespeed_esi_inline-' . $esi_id, array(), $esi_param); // Returned array need to be [ val, control, tag ]
			if ($inline_param) {
				$output .= self::_build_inline($url, $inline_param);
	private static function _build_inline($url, $inline_param)
	{
		if (!$url || empty($inline_param['val']) || empty($inline_param['control']) || empty($inline_param['tag'])) {
		return "<esi:inline name='$url' cache-control='" . $inline_param['control'] . "' cache-tag='" . $inline_param['tag'] . "'>" . $inline_param['val'] . "</esi:inline>";
	public function sub_esi_block($block_id, $wrapper, $params = array(), $control = 'private,no-vary', $silence = false, $preserved = false, $svar = false, $inline_param = array())
	{
		if (empty($block_id) || !is_array($params) || preg_match('/[^\w-]/', $block_id)) {
		if (defined('LITESPEED_ESI_OFF')) {
			Debug2::debug('[ESI] ESI OFF so force loading [block_id] ' . $block_id);
			do_action('litespeed_esi_load-' . $block_id, $params);
		if ($silence) {
			$params['_ls_silence'] = true;
		if ($this->cls('REST')->is_rest() || $this->cls('REST')->is_internal_rest()) {
			$params['is_json'] = 1;
		$params = apply_filters('litespeed_esi_params', $params, $block_id);
		$control = apply_filters('litespeed_esi_control', $control, $block_id);
		if (!is_array($params) || !is_string($control)) {
			defined('LSCWP_LOG') && Debug2::debug("[ESI] 🛑 Sub hooks returned Params: \n" . var_export($params, true) . "\ncache control: \n" . var_export($control, true));
		if (!empty($control)) {
			$appended_params['_control'] = $control;
		if ($params) {
			$appended_params[self::QS_PARAMS] = base64_encode(json_encode($params));
			Debug2::debug2('[ESI] param ', $params);
		$appended_params['_hash'] = $this->_gen_esi_md5($appended_params);
		$appended_params = array_map('urlencode', $appended_params);
		$url = add_query_arg($appended_params, trailingslashit(wp_make_link_relative(home_url())));
		if ($inline_param) {
			$output .= self::_build_inline($url, $inline_param);
		if (!empty($control)) {
			$control = esc_attr($control);
		if ($svar) {
		if (in_array($block_id, self::$_combine_ids)) {
		if ($block_id == self::COMBO && isset($_SERVER['X-LSCACHE']) && strpos($_SERVER['X-LSCACHE'], 'combine') !== false) {
		if (!$silence) {
		Debug2::debug("[ESI] 💕  [BLock_ID] $block_id \t[wrapper] $wrapper \t\t[Control] $control");
		Debug2::debug2($output);
		if ($preserved) {
			$hash = md5($output);
			$this->_esi_preserve_list[$hash] = $output;
			Debug2::debug("[ESI] Preserved to $hash");
	private function _gen_esi_md5($params)
	{
		foreach ($keys as $v) {
			if (isset($params[$v]) && is_string($params[$v])) {
				$str .= $params[$v];
		Debug2::debug2('[ESI] md5_string=' . $str);
		return md5($this->conf(Base::HASH) . $str);
	private function _parse_esi_param($qs_params = false)
	{
		if ($qs_params) {
		} elseif (isset($_REQUEST[self::QS_PARAMS])) {
			$req_params = $_REQUEST[self::QS_PARAMS];
		if (!$req_params) {
		$unencrypted = base64_decode($req_params);
		if ($unencrypted === false) {
		Debug2::debug2('[ESI] parms', $unencrypted);
		$params = json_decode($unencrypted, true);
	public function load_esi_block()
	{
		if (empty($_GET['_hash']) || $this->_gen_esi_md5($_GET) != $_GET['_hash']) {
			Debug2::debug('[ESI] ❌ Failed to validate _hash');
		if (defined('LSCWP_LOG')) {
			if (!empty($params[self::PARAM_NAME])) {
				$logInfo .= ' Name: ' . $params[self::PARAM_NAME] . ' ----- ';
			Debug2::debug($logInfo);
		if (!empty($params['_ls_silence'])) {
			!defined('LSCACHE_ESI_SILENCE') && define('LSCACHE_ESI_SILENCE', true);
		if (!empty($params['is_json'])) {
			add_filter('litespeed_is_json', '__return_true');
		Tag::add(rtrim(Tag::TYPE_ESI, '.'));
		Tag::add(Tag::TYPE_ESI . LSCACHE_IS_ESI);
		if (!empty($_GET['_control'])) {
			$control = explode(',', $_GET['_control']);
			if (in_array('private', $control)) {
			if (in_array('no-vary', $control)) {
	// The *_sub_* functions are helpers for the sub_* functions.
	// The *_load_* functions are helpers for the load_* functions.
	public static function widget_default_options($options, $widget)
	{
		if (!is_array($options)) {
			case 'WP_Widget_Recent_Posts':
			case 'WP_Widget_Recent_Comments':
			default:
	public function sub_widget_block($instance, $widget, $args)
	{
		if (!is_array($instance)) {
		$name = get_class($widget);
		if (!isset($instance[Base::OPTION_NAME])) {
		$options = $instance[Base::OPTION_NAME];
		if (!isset($options) || !$options[self::WIDGET_O_ESIENABLE]) {
			defined('LSCWP_LOG') && Debug2::debug('ESI 0 ' . $name . ': ' . (!isset($options) ? 'not set' : 'set off'));
		$esi_private = $options[self::WIDGET_O_ESIENABLE] == Base::VAL_ON2 ? 'private,' : '';
		echo $this->sub_esi_block('widget', 'widget ' . $name, $params, $esi_private . 'no-vary');
	public function sub_admin_bar_block()
	{
		if (!is_admin_bar_showing() || !is_object($wp_admin_bar)) {
			'ref' => $_SERVER['REQUEST_URI'],
		echo $this->sub_esi_block('admin-bar', 'adminbar', $params);
	public function load_widget_block($params)
	{
		$option = $params[self::PARAM_INSTANCE];
		$option = $option[Base::OPTION_NAME];
		$ttl = $option[self::WIDGET_O_TTL];
		defined('LSCWP_LOG') && Debug2::debug('ESI widget render: name ' . $params[self::PARAM_NAME] . ', id ' . $params[self::PARAM_ID] . ', ttl ' . $ttl);
		if ($ttl == 0) {
			Control::set_nocache('ESI Widget time to live set to 0');
		} else {
			Control::set_custom_ttl($ttl);

			if ($option[self::WIDGET_O_ESIENABLE] == Base::VAL_ON2) {
			Tag::add(Tag::TYPE_WIDGET . $params[self::PARAM_ID]);
		the_widget($params[self::PARAM_NAME], $params[self::PARAM_INSTANCE], $params[self::PARAM_ARGS]);
	public function load_admin_bar_block($params)
	{
		if (!empty($params['ref'])) {
			$ref_qs = parse_url($params['ref'], PHP_URL_QUERY);
			if (!empty($ref_qs)) {
				parse_str($ref_qs, $ref_qs_arr);
				if (!empty($ref_qs_arr)) {
					foreach ($ref_qs_arr as $k => $v) {
						$_GET[$k] = $v;
		if (!$this->conf(Base::O_ESI_CACHE_ADMBAR)) {
			Control::set_nocache('build-in set to not cacheable');
		} else {
		defined('LSCWP_LOG') && Debug2::debug('ESI: adminbar ref: ' . $_SERVER['REQUEST_URI']);
	public function load_comment_form_block($params)
	{
		comment_form($params[self::PARAM_ARGS], $params[self::PARAM_ID]);
		if (!$this->conf(Base::O_ESI_CACHE_COMMFORM)) {
			Control::set_nocache('build-in set to not cacheable');
		} else {
			if (Vary::has_vary()) {
	public function load_nonce_block($params)
	{
		$action = $params['action'];
		Debug2::debug('[ESI] load_nonce_block [action] ' . $action);
		Control::set_custom_ttl(43200);
		if (Router::is_logged_in()) {
		if (function_exists('wp_create_nonce_litespeed_esi')) {
			echo wp_create_nonce_litespeed_esi($action);
		} else {
			echo wp_create_nonce($action);
	public function load_esi_shortcode($params)
	{
		if (isset($params['ttl'])) {
			if (!$params['ttl']) {
				Control::set_nocache('ESI shortcode att ttl=0');
			} else {
				Control::set_custom_ttl($params['ttl']);
			unset($params['ttl']);
		$shortcode = $params[0];
		foreach ($params as $k => $v) {
			if ($k === 0) {
			$atts_ori[] = is_string($k) ? "$k='" . addslashes($v) . "'" : $v;
		Tag::add(Tag::TYPE_ESI . "esi.$shortcode");
		echo do_shortcode("[$shortcode " . implode(' ', $atts_ori) . " ]");
	public function register_comment_form_actions($defaults)
	{
		add_filter('comment_form_submit_button', array($this, 'sub_comment_form_btn'), 1000, 2); // To save the params passed in
		add_action('comment_form', array($this, 'sub_comment_form_block'), 1000);
	public function sub_comment_form_btn($unused, $args)
	{
		if (empty($args) || empty($this->esi_args)) {
			Debug2::debug('comment form args empty?');
		foreach ($args as $k => $v) {
			if (!isset($this->esi_args[$k])) {
				$esi_args[$k] = $v;
			} elseif (is_array($v)) {
				$diff = array_diff_assoc($v, $this->esi_args[$k]);
				if (!empty($diff)) {
					$esi_args[$k] = $diff;
			} elseif ($v !== $this->esi_args[$k]) {
				$esi_args[$k] = $v;
	public function sub_comment_form_block($post_id)
	{
		echo $this->sub_esi_block('comment-form', 'comment form', $params);
		add_action('comment_form_after', array($this, 'comment_form_sub_clean'));
	public function comment_form_sub_clean()
	{
	public function finalize($buffer)
	{
		if (self::$_combine_ids) {
			Debug2::debug('[ESI] 🍔 Enabled combo');
			$esi_block = $this->sub_esi_block(self::COMBO, '__COMBINE_MAIN__', array(), 'no-cache', true);
		if (!$this->_esi_preserve_list) {
		$keys = array_keys($this->_esi_preserve_list);
		Debug2::debug('[ESI] replacing preserved blocks', $keys);
		$buffer = str_replace($keys, $this->_esi_preserve_list, $buffer);
	public function contain_preserve_esi($content)
	{
		foreach ($this->_esi_preserve_list as $k => $v) {
			if (strpos($content, '"' . $k . '"') !== false) {
			if (strpos($content, "'" . $k . "'") !== false) {