$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())));
$output .= self::_build_inline($url, $inline_param);
$output .= "<esi:include src='$url'";
$control = esc_attr($control);
$output .= " cache-control='$control'";
$output .= " as-var='1'";
if (in_array($block_id, self::$_combine_ids)) {
$output .= " combine='sub'";
if ($block_id == self::COMBO && isset($_SERVER['X-LSCACHE']) && strpos($_SERVER['X-LSCACHE'], 'combine') !== false) {
$output .= " combine='main'";
$output = "<!-- lscwp $wrapper -->$output<!-- lscwp $wrapper esi end -->";
self::debug("💕 [BLock_ID] $block_id \t[wrapper] $wrapper \t\t[Control] $control");
// Convert to string to avoid html chars filter when using
// Will reverse the buffer when output in self::finalize()
$this->_esi_preserve_list[$hash] = $output;
self::debug("Preserved to $hash");
private function _gen_esi_md5( $params ) {
$keys = array( self::QS_ACTION, '_control', self::QS_PARAMS );
if (isset($params[$v]) && is_string($params[$v])) {
Debug2::debug2('[ESI] md5_string=' . $str);
return md5($this->conf(Base::HASH) . $str);
* Parses the request parameters on an ESI request
private function _parse_esi_param( $qs_params = false ) {
$req_params = $qs_params;
} elseif (isset($_REQUEST[self::QS_PARAMS])) {
$req_params = $_REQUEST[self::QS_PARAMS];
$unencrypted = base64_decode($req_params);
if ($unencrypted === false) {
Debug2::debug2('[ESI] params', $unencrypted);
// $unencoded = urldecode($unencrypted); no need to do this as $_GET is already parsed
$params = \json_decode($unencrypted, true);
* Select the correct esi output based on the parameters in an ESI request.
public function load_esi_block() {
* Validate if is a legal ESI req
if (empty($_GET['_hash']) || $this->_gen_esi_md5($_GET) != $_GET['_hash']) {
Debug2::debug('[ESI] ❌ Failed to validate _hash');
$params = $this->_parse_esi_param();
if (defined('LSCWP_LOG')) {
if (!empty($params[self::PARAM_NAME])) {
$logInfo .= ' Name: ' . $params[self::PARAM_NAME] . ' ----- ';
$logInfo .= ' [ID] ' . LSCACHE_IS_ESI;
if (!empty($params['_ls_silence'])) {
!defined('LSCACHE_ESI_SILENCE') && define('LSCACHE_ESI_SILENCE', true);
* Buffer needs to be JSON format
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);
// Debug2::debug(var_export($params, true ));
* Handle default cache control 'private,no-vary' for sub_esi_block() @ticket #923505
if (!empty($_GET['_control'])) {
$control = explode(',', $_GET['_control']);
if (in_array('private', $control)) {
if (in_array('no-vary', $control)) {
do_action('litespeed_esi_load-' . LSCACHE_IS_ESI, $params);
// The *_sub_* functions are helpers for the sub_* functions.
// The *_load_* functions are helpers for the load_* functions.
* Loads the default options for default WordPress widgets.
public static function widget_default_options( $options, $widget ) {
if (!is_array($options)) {
$widget_name = get_class($widget);
case 'WP_Widget_Recent_Posts':
case 'WP_Widget_Recent_Comments':
$options[self::WIDGET_O_ESIENABLE] = Base::VAL_OFF;
$options[self::WIDGET_O_TTL] = 86400;
* Hooked to the widget_display_callback filter.
* If the admin configured the widget to display via esi, this function
* will set up the esi request and cancel the widget display.
* @param array $instance Parameter used to build the widget.
* @param \WP_Widget $widget The widget to build.
* @param array $args Parameter used to build the widget.
* @return mixed Return false if display through esi, instance otherwise.
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,' : '';
self::PARAM_NAME => $name,
self::PARAM_ID => $widget->id,
self::PARAM_INSTANCE => $instance,
self::PARAM_ARGS => $args,
echo $this->sub_esi_block('widget', 'widget ' . $name, $params, $esi_private . 'no-vary');
* Hooked to the wp_footer action.
* Sets up the ESI request for the admin bar.
* @global type $wp_admin_bar
public function sub_admin_bar_block() {
if (!is_admin_bar_showing() || !is_object($wp_admin_bar)) {
// To make each admin bar ESI request different for `Edit` button different link
'ref' => $_SERVER['REQUEST_URI'],
echo $this->sub_esi_block('admin-bar', 'adminbar', $params);
* Parses the esi input parameters and generates the widget for esi display.
* @global $wp_widget_factory
* @param array $params Input parameters needed to correctly display widget
public function load_widget_block( $params ) {
// global $wp_widget_factory;
// $widget = $wp_widget_factory->widgets[ $params[ self::PARAM_NAME ] ];
$option = $params[self::PARAM_INSTANCE];
$option = $option[Base::OPTION_NAME];
// Since we only reach here via esi, safe to assume setting exists.
$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);
Control::set_nocache('ESI Widget time to live set to 0');
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]);
* Generates the admin bar for esi display.
public function load_admin_bar_block( $params ) {
if (!empty($params['ref'])) {
$ref_qs = parse_url($params['ref'], PHP_URL_QUERY);
parse_str($ref_qs, $ref_qs_arr);
if (!empty($ref_qs_arr)) {
foreach ($ref_qs_arr as $k => $v) {
// Needed when permalink structure is "Plain"
if (!isset($wp_the_query)) {
if (!$this->conf(Base::O_ESI_CACHE_ADMBAR)) {
Control::set_nocache('build-in set to not cacheable');
defined('LSCWP_LOG') && Debug2::debug('ESI: adminbar ref: ' . $_SERVER['REQUEST_URI']);
* Parses the esi input parameters and generates the comment form for esi display.
* @param array $params Input parameters needed to correctly display comment form
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');
// by default comment form is public
* Generate nonce for certain action
public function load_nonce_block( $params ) {
$action = $params['action'];
Debug2::debug('[ESI] load_nonce_block [action] ' . $action);
// set nonce TTL to half day
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);
echo wp_create_nonce($action);
* Show original shortcode
public function load_esi_shortcode( $params ) {
if (isset($params['ttl'])) {
Control::set_nocache('ESI shortcode att ttl=0');
Control::set_custom_ttl($params['ttl']);
// Replace to original shortcode
foreach ($params as $k => $v) {
$atts_ori[] = is_string($k) ? "$k='" . addslashes($v) . "'" : $v;
Tag::add(Tag::TYPE_ESI . "esi.$shortcode");
// Output original shortcode final content
echo do_shortcode("[$shortcode " . implode(' ', $atts_ori) . ' ]');
* Hooked to the comment_form_defaults filter.
* Stores the default comment form settings.
* If sub_comment_form_block is triggered, the output buffer is cleared and an esi block is added. The remaining comment form is also buffered and cleared.
* Else there is no need to make the comment form ESI.
public function register_comment_form_actions( $defaults ) {
$this->esi_args = $defaults;
echo GUI::clean_wrapper_begin();
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);
* Store the args passed in comment_form for the ESI comment param usage in `$this->sub_comment_form_block()`
public function sub_comment_form_btn( $unused, $args ) {
if (empty($args) || empty($this->esi_args)) {
Debug2::debug('comment form args empty?');
// compare current args with default ones
foreach ($args as $k => $v) {
if (!isset($this->esi_args[$k])) {
} elseif (is_array($v)) {
$diff = array_diff_assoc($v, $this->esi_args[$k]);
} elseif ($v !== $this->esi_args[$k]) {
$this->esi_args = $esi_args;
* Hooked to the comment_form_submit_button filter.
* This method will compare the used comment form args against the default args. The difference will be passed to the esi request.
public function sub_comment_form_block( $post_id ) {
echo GUI::clean_wrapper_end();
self::PARAM_ID => $post_id,
self::PARAM_ARGS => $this->esi_args,
echo $this->sub_esi_block('comment-form', 'comment form', $params);
echo GUI::clean_wrapper_begin();
add_action('comment_form_after', array( $this, 'comment_form_sub_clean' ));
* Hooked to the comment_form_after action.
* Cleans up the remaining comment form output.
public function comment_form_sub_clean() {
echo GUI::clean_wrapper_end();
* Replace preserved blocks