<?php
/**
 * Page caching class.
 *
 * For easier code maintenance and support, class is split into sections:
 * I.   INIT FUNCTIONS
 * II.  HELPER FUNCTIONS
 * III. FILESYSTEM FUNCTIONS
 * IV.  CACHE CONTROL FUNCTIONS
 * V.   ACTIONS AND FILTERS
 *
 * @package Hummingbird
 */

namespace Hummingbird\Core\Modules;

use Hummingbird\Core\Filesystem;
use Hummingbird\Core\Installer;
use Hummingbird\Core\Module;
use Hummingbird\Core\Modules\Caching\Preload;
use Hummingbird\Core\Settings;
use Hummingbird\Core\Utils;
use Hummingbird\Core\Modules\Caching\Fast_CGI;
use Hummingbird\Core\SafeMode;
use stdClass;
use WP_Error;
use WP_Post;
use WP_Term;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * This is a compatibility check to support pre 2.5 versions upgrading to 2.5+.
 * Because in 2.5 we've added traits, but the advanced-cache.php was flawed and did not allow for a proper upgrade.
 * Remove once the upgrade_2_5_0() is removed (approximately in version 2.9/3.0).
 *
 * @since 2.5.0
 * @see Installer::upgrade_2_5_0()
 */
if ( ! trait_exists( '\Hummingbird\Core\Traits\WPConfig' ) && isset( $plugin_path ) ) {
	$trait = $plugin_path . 'core/traits/trait-wpconfig.php';
	if ( file_exists( $trait ) ) {
		include_once $plugin_path . 'core/traits/trait-wpconfig.php';
	}
}

/**
 * Class Page_Cache
 *
 * @since 1.7.0
 */
class Page_Cache extends Module {

	use \Hummingbird\Core\Traits\WPConfig;

	/**
	 * Last error.
	 *
	 * @since 1.7.0
	 * @var   WP_Error $error
	 */
	public $error = false;

	/**
	 * Start time when caching a file.
	 * Used for calculating the amount of time it takes to build the cached file.
	 *
	 * @since 1.7.0
	 * @var   int $start_time
	 */
	private $start_time;

	/**
	 * List of mobile user agents.
	 *
	 * Props to WP Super Cache
	 *
	 * @since 2.1.0
	 * @var array $mobile_agents
	 */
	private static $mobile_agents = array( 'Android', '2.0 MMP', '240x320', 'AvantGo', 'BlackBerry', 'Blazer', 'Cellphone', 'Danger', 'DoCoMo', 'Elaine/3.0', 'EudoraWeb', 'hiptop', 'IEMobile', 'iPhone', 'iPod', 'KYOCERA/WX310K', 'LG/U990', 'MIDP-2.0', 'MMEF20', 'MOT-V', 'NetFront', 'Newt', 'Nintendo Wii', 'Nitro', 'Nokia', 'Opera Mini', 'Palm', 'Playstation Portable', 'portalmmm', 'Proxinet', 'ProxiNet', 'SHARP-TQ-GX10', 'Small', 'SonyEricsson', 'Symbian OS', 'SymbianOS', 'TS21i-10', 'UP.Browser', 'UP.Link', 'Windows CE', 'WinWAP' );

	/**
	 * Execute module actions.
	 *
	 * @since 1.7.0
	 */
	public function run() {
		// Init modules and perform pre-run checks.
		$this->init_filesystem();
		$this->check_plugin_compatibility();
		$this->check_fast_cgi_cache();

		add_action( 'admin_init', array( $this, 'maybe_update_advanced_cache' ) );

		add_action( 'init', array( $this, 'init_preloader' ) );
		// Preload page cache on post/page update.
		add_action( 'wphb_page_cache_preload_page', array( new Preload(), 'preload_page_on_purge' ) );

		/**
		 * Trigger a cache clear.
		 *
		 * If post ID is set, will try to clear cache for that page or post with all the related
		 * taxonomies (tags, category and author pages).
		 *
		 * @since 1.9.2
		 *
		 * @param int $post_id  Post ID.
		 */
		add_action( 'wphb_clear_page_cache', array( $this, 'clear_cache_action' ) );
		add_action( 'wp_update_nav_menu', array( $this, 'clear_cache_after_menu_update' ), 10, 2 );
		add_action( 'wphb_clear_cache_url', array( $this, 'clear_external_cache' ), 50 );

		// Post status transitions.
		add_action( 'edit_post', array( $this, 'post_edit' ), 0 );
		add_action( 'transition_post_status', array( $this, 'post_status_change' ), 10, 3 );

		// Clear cache button on edit post page.
		add_action( 'post_submitbox_misc_actions', array( $this, 'clear_cache_button' ) );
		add_filter( 'post_updated_messages', array( $this, 'clear_cache_message' ) );

		// Clear cache on new comment.
		add_action( 'comment_post', array( $this, 'clear_on_comment_post' ), 10, 3 );

		// Clear cache on home display changed.
		add_action( 'update_option_page_on_front', array( $this, 'clear_on_home_page_on_front_change' ), 10, 2 );
		add_action( 'update_option_page_for_posts', array( $this, 'clear_on_home_page_for_posts' ), 10, 2 );

		// Clear cache when defender updating security headers settings.
		add_action( 'wd_save_setting_security_headers', array( $this, 'clear_cache' ) );

		// Clear cache when menu updated through Block editor.
		add_action( 'save_post_wp_navigation', array( $this, 'wphb_save_post_wp_navigation_menu' ), 10, 3 );

		// Clear cache on WordPress appearance changes.
		add_action( 'customize_save_after', array( $this, 'clear_cache_after_customizer_save' ) );
		add_filter( 'widget_update_callback', array( $this, 'clear_cache_on_widget_change' ), 10, 4 );
		add_action( 'update_option_theme_mods_' . get_option( 'stylesheet' ), array( $this, 'clear_cache_after_theme_mods_update' ), 10, 2 );
		add_action( 'save_post_wp_global_styles', array( $this, 'clear_cache_after_global_styles_save' ), 10, 3 );

		// Only cache pages when there are no errors.
		if ( ! is_wp_error( $this->error ) ) {
			$this->init_caching();
		}
	}

	/**
	 * Initialize module.
	 *
	 * @since 1.7.0
	 */
	public function init() {
		add_filter( 'wp_hummingbird_is_active_module_page_cache', array( $this, 'module_status' ) );
		add_action( 'template_redirect', array( $this, 'wphb_maybe_start_buffer' ), 2 );
	}

	/**
	 * Activate page cache.
	 *
	 * @since   1.7.0
	 * @aince   1.8.0  Changed access to private
	 * @access  private
	 * @used-by Page_Cache::toggle_service()
	 */
	private function activate() {
		if ( $this->check_wp_settings( true ) ) {
			$this->init_filesystem();
			$this->wpconfig_add( 'WP_CACHE', true );
		}
	}

	/**
	 * Enable page cache module.
	 *
	 * @since 1.9.0
	 *
	 * @used-by \Hummingbird\Admin\Pages\Caching::trigger_load_action()
	 * @used-by \Hummingbird\Core\Api\Hub::action_enable()
	 *
	 * @param bool $page_cache Enable page cache only.
	 */
	public function enable( $page_cache = false ) {
		if ( Fast_CGI::is_fast_cgi_supported() && ! Utils::is_subsite() && ! $page_cache ) {
			Utils::get_api()->hosting->toggle_fast_cgi( true );
		} else {
			$this->toggle_service( true, true );
		}
	}

	/**
	 * Disable page cache module.
	 *
	 * @since 1.9.0
	 *
	 * @used-by \Hummingbird\Admin\Pages\Caching::trigger_load_action()
	 * @used-by \Hummingbird\Core\Api\Hub::action_disable()
	 */
	public function disable() {
		$this->toggle_service( false, true );
		$this->clear_cache();
	}

	/**
	 * *************************
	 * I. INIT FUNCTIONS
	 *
	 * Available methods:
	 * check_plugin_compatibility()
	 * check_minification_queue()
	 * init_filesystem()
	 * init_preloader()
	 * maybe_update_advanced_cache()
	 ***************************/

	/**
	 * Check for other caching plugins.
	 * Add error if incompatible plugin detected.
	 *
	 * @since   1.7.0
	 * @access  private
	 * @used-by Page_Cache::init()
	 */
	private function check_plugin_compatibility() {
		if ( is_wp_error( $this->error ) || ! $this->is_active() ) {
			return;
		}

		$caching_plugins = array(
			'autoptimize/autoptimize.php'               => 'Autoptimize',
			'litespeed-cache/litespeed-cache.php'       => 'LiteSpeed Cache',
			'speed-booster-pack/speed-booster-pack.php' => 'Speed Booster Pack',
			'swift-performance-lite/performance.php'    => 'Swift Performance Lite',
			'w3-total-cache/w3-total-cache.php'         => 'W3 Total Cache',
			'wp-fastest-cache/wpFastestCache.php'       => 'WP Fastest Cache',
			'wp-optimize/wp-optimize.php'               => 'WP-Optimize',
			'wp-performance-score-booster'              => 'WP Performance Score Booster',
			'wp-performance/wp-performance.php'         => 'WP Performance',
			'wp-super-cache/wp-cache.php'               => 'WP Super Cache',
		);

		foreach ( $caching_plugins as $plugin => $plugin_name ) {
			if ( in_array( $plugin, get_option( 'active_plugins', array() ), true ) ) {
				$this->error = new WP_Error(
					'caching-plugin-detected',
					/* translators: %s: plugin name. */
					sprintf( __( '%s plugin detected. Please disable it to use Hummingbird page caching functionality.', 'wphb' ), $plugin_name )
				);
				break;
			}
		}

		// See if there's already an advanced-cache.php file in place.
		$adv_cache_file = dirname( get_theme_root() ) . '/advanced-cache.php';
		if ( file_exists( $adv_cache_file ) && false === strpos( file_get_contents( $adv_cache_file ), 'WPHB_ADVANCED_CACHE' ) ) {
			$this->error = new WP_Error(
				'advanced-cache-detected',
				sprintf( /* translators: %1$s - opening a tag, %2$s - closing a tag, %3$s - button tag, %4$s - closing button tag */
					__( 'Hummingbird has detected an advanced-cache.php file in your site’s wp-content directory. %1$sManage your plugins%2$s and disable any other active caching plugins to ensure Hummingbird’s page caching works properly.<br>If no other caching plugins are active, the advanced-cache.php may have been left by a previously used caching plugin. You can remove the file from the wp-content directory, or remove it via your file manager or FTP.%3$sRemove file%4$s', 'wphb' ),
					'<a href="' . esc_url( network_admin_url( 'plugins.php' ) ) . '">',
					'</a>',
					'<br><button id="wphb-remove-advanced-cache" style="margin-top: 10px" class="sui-button sui-button-blue" role="button">',
					'</button>'
				)
			);
		}
	}

	/**
	 * Check for FastCGI cache.
	 *
	 * @since   3.4.0
	 * @access  private
	 * @used-by Page_Cache::init()
	 */
	private function check_fast_cgi_cache() {
		if ( is_wp_error( $this->error ) || ! $this->is_active() ) {
			return;
		}

		if ( Fast_CGI::is_fast_cgi_enabled() ) {
			$this->error = new WP_Error(
				'fast-cgi-cache-active',
				__( 'Hummingbird has halted page caching to prevent any issues with FastCGI cache', 'wphb' )
			);
		}
	}

	/**
	 * Init filesystem.
	 *
	 * @since   1.7.0
	 * @access  private
	 * @used-by Page_Cache::init()
	 */
	private function init_filesystem() {
		// If module not active - return.
		if ( ! $this->is_active() ) {
			return;
		}

		// If there's an error (except not found WP_CACHE constant) - return.
		if ( is_wp_error( $this->error ) && 'no-wp-cache-constant' !== $this->error->get_error_code() ) {
			return;
		}

		// Init filesystem.
		global $wphb_fs;

		if ( ! $wphb_fs ) {
			$wphb_fs = Filesystem::instance();
		}

		if ( is_wp_error( $wphb_fs->status ) ) {
			$this->error = $wphb_fs->status;
			return;
		}

		if ( ! file_exists( WP_CONTENT_DIR . '/advanced-cache.php' ) ) {
			// Try to add advanced-cache.php file.
			$adv_cache_file_src = dirname( plugin_dir_path( __FILE__ ) ) . '/advanced-cache.php';

			if ( ! file_exists( $adv_cache_file_src ) ) {
				return;
			}

			$contents = file_get_contents( $adv_cache_file_src );
			$wphb_fs->write( WP_CONTENT_DIR . '/advanced-cache.php', $contents );
		}

		// Try to define WP_CACHE in wp-config.php file.
		$this->check_wp_settings();
	}

	/**
	 * Init page caching preloader.
	 *
	 * @since 2.1.0
	 */
	public function init_preloader() {
		$options = $this->get_options();
		if ( ! isset( $options['preload'] ) || ! $options['preload'] ) {
			return;
		}

		new Preload();
	}

	/**
	 * Make sure advanced-cache.php file is always up-to-date in between updates.
	 *
	 * @since 2.7.1
	 */
	public function maybe_update_advanced_cache() {
		// No advanced-cache.php, probably cache is disabled - exit.
		if ( ! file_exists( WP_CONTENT_DIR . '/advanced-cache.php' ) ) {
			return;
		}

		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
			return;
		}

		// Can't find original, won't be able to replace - exit.
		if ( ! file_exists( dirname( plugin_dir_path( __FILE__ ) ) . '/advanced-cache.php' ) ) {
			return;
		}

		// Files are identical - exit.
		if ( filesize( WP_CONTENT_DIR . '/advanced-cache.php' ) === filesize( dirname( plugin_dir_path( __FILE__ ) ) . '/advanced-cache.php' ) ) {
			return;
		}

		// Check if this is an advanced-cache.php file from Hummingbird.
		$adv_cache_content = file_get_contents( WP_CONTENT_DIR . '/advanced-cache.php' );
		if ( false === strpos( $adv_cache_content, 'WPHB_ADVANCED_CACHE' ) ) {
			// File is not from Hummingbird - exit.
			return;
		}

		unlink( WP_CONTENT_DIR . '/advanced-cache.php' );

		// Create the file.
		$this->init_filesystem();
	}

	/**
	 * *************************
	 * II. HELPER FUNCTIONS
	 * Most of the methods here are private and static because they are internal.
	 *
	 * Available methods:
	 * load_config()
	 * get_settings()
	 * get_default_settings()
	 * check_wp_settings()
	 * get_page_types()
	 * get_file_cache_path()
	 * get_cookies()
	 * skip_url()
	 * skip_user_agent()
	 * skip_page_type()
	 * logged_in_user()
	 * skip_subsite()
	 * can_serve_compressed()
	 * skip_mobile_agent()
	 * skip_custom_cookie()
	 * get_request_uri()
	 * get_mapped_domain()
	 ***************************/

	/**
	 * Get config from file and prepare for use.
	 *
	 * @since   1.7.0
	 * @access  private
	 * @used-by Page_Cache::should_cache_request()
	 * @used-by Page_Cache::post_edit()
	 * @used-by Page_Cache::post_status_change()
	 */
	private static function load_config() {
		global $wphb_cache_config;

		self::log_msg( 'Loading config file.' );

		$config_file = WP_CONTENT_DIR . '/wphb-cache/wphb-cache.php';
		if ( ! file_exists( $config_file ) ) {
			self::log_msg( 'Config file does not exist. Loading defaults.' );
			// This is only a fallback, so we don't error out. Config file will be written as soon as user logs in.
			$settings = self::get_default_settings();
		} else {
			$settings = json_decode( file_get_contents( $config_file ), true );
		}

		$wphb_cache_config            = new stdClass();
		$wphb_cache_config->cache_dir = WP_CONTENT_DIR . '/wphb-cache/cache/';
		// Cache selected page types.
		$wphb_cache_config->page_types = $settings['page_types'];

		// Clear cache interval.
		$wphb_cache_config->clear_interval = isset( $settings['clear_interval'] ) ? $settings['clear_interval'] : false;

		// Custom post types.
		$wphb_cache_config->custom_post_types = isset( $settings['custom_post_types'] ) ? $settings['custom_post_types'] : array();
		// Cache if user is logged in.
		$wphb_cache_config->cache_logged_in = (bool) $settings['settings']['logged_in'];
		// Cache if the URL has $_GET params or not.
		$wphb_cache_config->cache_with_get_params = (bool) $settings['settings']['url_queries'];
		// Cache 404 pages.
		$wphb_cache_config->cache_404 = (bool) $settings['settings']['cache_404'];
		// Clear cache on update.
		$wphb_cache_config->clear_on_update = (bool) $settings['settings']['clear_update'];
		// Enable debug log.
		$wphb_cache_config->debug_log = (bool) $settings['settings']['debug_log'];
		// Show cache identifier.
		$wphb_cache_config->cache_identifier = ! isset( $settings['settings']['cache_identifier'] ) || $settings['settings']['cache_identifier'];
		// Gzip compression of cached files.
		$wphb_cache_config->compress = isset( $settings['settings']['compress'] ) && $settings['settings']['compress'];
		// Cache on mobile devices.
		$wphb_cache_config->mobile = ! isset( $settings['settings']['mobile'] ) || $settings['settings']['mobile'];
		// Clear cache on comment post.
		$wphb_cache_config->comment_clear = ! isset( $settings['settings']['comment_clear'] ) || $settings['settings']['comment_clear'];
		// Cache Headers.
		$wphb_cache_config->cache_headers = isset( $settings['settings']['cache_headers'] ) && $settings['settings']['cache_headers'];

		$wphb_cache_config->exclude_url     = isset( $settings['exclude']['url_strings'] ) && is_array( $settings['exclude']['url_strings'] ) ? $settings['exclude']['url_strings'] : array();
		$wphb_cache_config->exclude_agents  = isset( $settings['exclude']['user_agents'] ) && is_array( $settings['exclude']['user_agents'] ) ? $settings['exclude']['user_agents'] : array();
		$wphb_cache_config->exclude_cookies = isset( $settings['exclude']['cookies'] ) && is_array( $settings['exclude']['cookies'] ) ? $settings['exclude']['cookies'] : array();
		$wphb_cache_config->safemode_status = isset( $settings['safemode_status'] ) ? (bool) $settings['safemode_status'] : false;
	}

	/**
	 * Check if the config file is in place and get the settings.
	 *
	 * TODO: refactor this. Now only used to get settings in page caching page. We need to create a file if it doesn't exist for the method above
	 *
	 * @since   1.7.0
	 * @used-by \Hummingbird\Admin\Pages\Caching::page_caching_metabox()
	 */
	public function get_settings() {
		global $wphb_fs;

		if ( ! $wphb_fs ) {
			$wphb_fs = Filesystem::instance();
		}

		$config_file = $wphb_fs->basedir . 'wphb-cache.php';

		$defaults = self::get_default_settings();
		$settings = $defaults;

		if ( file_exists( $config_file ) ) {
			$settings             = json_decode( file_get_contents( $config_file ), true );
			$settings             = wp_parse_args( $settings, $defaults );
			$settings['settings'] = wp_parse_args( $settings['settings'], $defaults['settings'] );
		} else {
			self::log_msg( 'Config file not found at: ' . $config_file );
			$this->write_file( $config_file, wp_json_encode( $defaults ) );
		}

		return $settings;
	}

	/**
	 * Get array of default settings.
	 *
	 * @since 1.7.2
	 * @since 2.1.0  Changed to public.
	 *
	 * @return array
	 */
	public static function get_default_settings() {
		return array(
			'page_types'        => self::get_page_types( true ),
			'custom_post_types' => array(),
			'clear_interval'    => array(
				'enabled'  => false,
				'interval' => 720, // 30 days in hours.
			),
			'settings'          => array(
				'logged_in'        => 0,
				'url_queries'      => 0,
				'cache_404'        => 0,
				'clear_update'     => 0,
				'debug_log'        => 0,
				'cache_identifier' => 1,
				'compress'         => 0,
				'mobile'           => 1,
				'comment_clear'    => 1,
				'cache_headers'    => 0,
			),
			'exclude'           => array(
				'url_strings' => array( 'wp-.*\.php', 'index\.php', 'xmlrpc\.php', 'sitemap[^\/.]*\.xml' ),
				'user_agents' => array( 'bot', 'is_archive', 'slurp', 'crawl', 'spider', 'Yandex' ),
				'cookies'     => array( 'wp_woocommerce_session_', 'wpdef_lockout_' ),
			),
		);
	}

	/**
	 * Check if WP_CACHE is set.
	 *
	 * @since 1.7.0
	 *
	 * @access  private
	 * @used-by Page_Cache::activate()
	 *
	 * @param bool $activate  Skip the WP_CACHE check on activation, will be checked in init_filesystem() method.
	 *
	 * @return bool
	 */
	public function check_wp_settings( $activate = false ) {
		// WP_CACHE is already defined.
		if ( $activate || ( defined( 'WP_CACHE' ) && WP_CACHE ) ) {
			$this->error = false;
			return true;
		}

		// Could not find the file.
		if ( ! file_exists( $this->wp_config_file ) ) {
			$this->error = new WP_Error(
				'no-wp-config-file',
				sprintf( /* translators: %1$s - code tag, %2$s - closing code tag, %3$s - button tag, %4$s - closing button tag */
					__( "Hummingbird could not locate your site’s wp-config.php file. Please ensure the following line has been added to the file:%1\$sdefine('WP_CACHE', true);%2\$sClick Retry to try again.%3\$sRetry%4\$s", 'wphb' ),
					'<br><code>',
					'</code><br><br>',
					'<br><a href="' . esc_url( network_admin_url( 'admin.php?page=wphb-caching' ) ) . '" style="margin-top: 10px" class="sui-button sui-button-blue">',
					'</a>'
				)
			);

			return false;
		}

		// wp-config.php is not writable.
		if ( ! is_writable( $this->wp_config_file ) || ! is_writable( dirname( $this->wp_config_file ) ) ) {
			$this->error = new WP_Error(
				'wp-config-not-writable',
				sprintf( /* translators: %1$s - code tag, %2$s - closing code tag, button tag, %3$s - closing button tag */
					__( "Hummingbird could not write to your site’s wp-config.php file. Click Retry to try again, or manually add the following line to the file:%1\$sdefine('WP_CACHE', true);%2\$sRetry%3\$s", 'wphb' ),
					'<br><code>',
					'</code><br><a href="' . esc_url( network_admin_url( 'admin.php?page=wphb-caching' ) ) . '" style="margin-top: 10px" class="sui-button sui-button-blue">',
					'</a>'
				)
			);

			return false;
		}

		return true;
	}

	/**
	 * Return the list of available page types.
	 *
	 * @since   1.7.0
	 * @used-by Page_Cache::get_settings()
	 * @used-by \Hummingbird\Admin\Pages\Caching::page_caching_metabox()
	 *
	 * @param bool $keys  Only array keys or with translations.
	 *
	 * @return array
	 */
	public static function get_page_types( $keys = false ) {
		if ( $keys ) {
			return array( 'frontpage', 'home', 'page', 'single', 'archive', 'category', 'tag' );
		}

		return array(
			'frontpage' => __( 'Frontpage', 'wphb' ),
			'home'      => __( 'Blog', 'wphb' ),
			'page'      => __( 'Pages', 'wphb' ),
			'single'    => __( 'Posts', 'wphb' ),
			'archive'   => __( 'Archives', 'wphb' ),
			'category'  => __( 'Categories', 'wphb' ),
			'tag'       => __( 'Tags', 'wphb' ),
		);
	}
	/**
	 * Skip custom post type added in settings.
	 *
	 * @since   1.9.0
	 * @access  private
	 * @param string $post_type  `Post` type to check in settings.
	 *
	 * @return bool
	 */
	private static function skip_custom_post_type( $post_type ) {
		global $wphb_cache_config;

		if ( in_array( $post_type, $wphb_cache_config->custom_post_types, true ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Return file path for the cached file.
	 *
	 * @since   1.7.0
	 * @access  private
	 * @used-by Page_Cache::serve_cache()
	 * @used-by Page_Cache::init_caching()
	 */
	private static function get_file_cache_path() {
		global $wphb_cache_config, $wphb_cache_file, $wphb_meta_file;

		$request_uri = self::get_request_uri();

		// Prepare some variables.
		$http_host = htmlentities( stripslashes( $_SERVER['HTTP_HOST'] ) ); // Input var ok.
		$port      = isset( $_SERVER['SERVER_PORT'] ) ? (int) $_SERVER['SERVER_PORT'] : 0; // Input var ok.

		/**
		 * Generate cache hash.
		 */
		// Convert dots to dashes for consistent cache file naming across URL variants.
		$request_uri = str_replace( '.', '-', $request_uri );
		// Remove index.php from query.
		$hash = str_replace( '/index.php', '/', $request_uri );
		// Remove any query hash from request URI.
		$hash    = preg_replace( '/#.*$/', '', $hash );
		$cookies = self::get_cookies();
		$hash    = md5( $http_host . $hash . $port . $cookies );

		// Remove get params.
		$request_uri = preg_replace( '/(\?.*)?$/', '', $request_uri );

		$ext = '.html';
		if ( $wphb_cache_config->cache_logged_in ) {
			/**
			 * If caching for logged-in users, we need to set the cache file extension to .php and
			 * add die(); in file header, to prevent phishing attacks.
			 */
			$ext = '.php';
		}

		$mobile        = self::is_mobile_agent() ? '/mobile/' : '';
		$directory     = $wphb_cache_config->cache_dir . $mobile . $http_host;
		$raw_file_path = $directory . $request_uri . $hash;
		if ( ! self::is_valid_cache_file_path( $raw_file_path ) ) {
			// Likely path traversal attack attempt: https://www.synopsys.com/glossary/what-is-path-traversal.html
			self::log_msg( 'Invalid cache file path: ' . $raw_file_path );

			$wphb_cache_file = false;
			$wphb_meta_file  = false;
		} else {
			$filename = str_replace( '//', '/', $raw_file_path );

			$wphb_cache_file = $filename . $ext;
			$wphb_meta_file  = $filename . '-meta.php';

			self::log_msg( 'Caching to file: ' . $wphb_cache_file );
		}
	}

	private static function is_valid_cache_file_path( $path ) {
		$path  = self::normalize_path( $path );
		$parts = array_filter( explode( '/', $path ), 'strlen' );
		foreach ( $parts as $part ) {
			if ( '.' == $part || '..' == $part ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Copy of wp_normalize_path. Added because wp_normalize_path is not available when the cached page is being served by wp-content/advanced-cache.php
	 *
	 * @param $path
	 *
	 * @return string
	 */
	private static function normalize_path( $path ) {
		// Standardize all paths to use '/'.
		$path = str_replace( '\\', '/', $path );

		// Replace multiple slashes down to a singular, allowing for network shares having two slashes.
		$path = preg_replace( '|(?<=.)/+|', '/', $path );

		// Windows paths should uppercase the drive letter.
		if ( ':' === substr( $path, 1, 1 ) ) {
			$path = ucfirst( $path );
		}

		return $path;
	}

	/**
	 * Get cookie keys for generating file hash.
	 *
	 * @since   1.7.0
	 * @used-by Page_Cache::prepare_file()
	 *
	 * @return string
	 */
	private static function get_cookies() {
		static $cookie_value = '';

		if ( ! empty( $cookie_value ) ) {
			self::log_msg( 'Cookie cached: ' . $cookie_value );
			return $cookie_value;
		}

		foreach ( $_COOKIE as $key => $value ) { // Input var ok.
			// Check password protected post, comment author, logged-in user.
			if ( preg_match( '/^wp-postpass_|^comment_author_|^wordpress_logged_in_|^wphb_cache_/', $key ) ) {
				self::log_msg( 'Found cookie: ' . $key );
				$cookie_value .= $_COOKIE[ $key ] . ','; // Input var ok.
			}
		}

		if ( ! empty( $cookie_value ) ) {
			$cookie_value = md5( $cookie_value );
			self::log_msg( 'Cookie hashed value: ' . $cookie_value );
		}

		return $cookie_value;
	}

	/**
	 * Check if the URL is in the exception list in the settings.
	 *
	 * @since   1.7.0
	 * @access  private
	 * @used-by Page_Cache::should_cache_request()
	 * @param   string $uri  URL to skip.
	 *
	 * @return bool
	 */
	private static function skip_url( $uri ) {
		global $wphb_cache_config;

		// Remove empty values.
		$uri_pattern = array_filter( $wphb_cache_config->exclude_url );
		if ( empty( $uri_pattern ) ) {
			return false;
		}

		$uri_pattern = implode( '|', $wphb_cache_config->exclude_url );
		if ( preg_match( "/$uri_pattern/i", $uri ) ) {
			return true;
		}

		// Now do the same, but test the URI as part of the full URL.
		$http_host = isset( $_SERVER['HTTP_HOST'] ) ? htmlentities( stripslashes( $_SERVER['HTTP_HOST'] ) ) : '';
		$http_port = isset( $_SERVER['SERVER_PORT'] ) && 443 === (int) $_SERVER['SERVER_PORT'] ? 'https://' : 'http://';
		if ( preg_match( "/$uri_pattern/i", $http_port . $http_host . $uri ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Check if the user agent is in the exception list in the settings.
	 *
	 * @since   1.7.0
	 * @access  private
	 * @used-by Page_Cache::should_cache_request()
	 *
	 * @return bool
	 */
	private static function skip_user_agent() {
		global $wphb_cache_config;

		// Remove empty values.
		$agent_pattern = array_filter( $wphb_cache_config->exclude_agents );
		if ( empty( $agent_pattern ) ) {
			return false;
		}

		$agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? stripslashes( $_SERVER['HTTP_USER_AGENT'] ) : ''; // Input var ok.

		// In case no user agent or agent is in exclude list, we do not cache the page.
		if ( empty( $agent ) || in_array( $agent, $agent_pattern, true ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Skip page type selected in settings.
	 *
	 * @since   1.7.0
	 * @access  private
	 * @used-by Page_Cache::cache_request()
	 *
	 * @return bool
	 */
	private static function skip_page_type() {
		global $wphb_cache_config;

		if ( ! is_array( $wphb_cache_config->page_types ) ) {
			return false;
		}
		$blog_is_frontpage = 'posts' === get_option( 'show_on_front' ) && ! is_multisite();

		if ( is_front_page() ) {
			return ! in_array( 'frontpage', $wphb_cache_config->page_types, true );
		} elseif ( is_home() && ! in_array( 'home', $wphb_cache_config->page_types, true ) && ! $blog_is_frontpage ) {
			return true;
		} elseif ( is_page() && ! in_array( 'page', $wphb_cache_config->page_types, true ) ) {
			return true;
		} elseif ( is_single() && 'post' === get_post_type() && ! in_array( 'single', $wphb_cache_config->page_types, true ) ) {
			return true;
		} elseif ( is_archive() ) {
			if ( in_array( 'archive', $wphb_cache_config->page_types, true ) ) {
				return false;
			} elseif ( is_category() && ! in_array( 'category', $wphb_cache_config->page_types, true ) ) {
				return true;
			} elseif ( is_tag() && ! in_array( 'tag', $wphb_cache_config->page_types, true ) ) {
				return true;
			}
		} elseif ( self::skip_custom_post_type( get_post_type() ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Check if the user is logged in.
	 *
	 * @since 1.7.0
	 * @access private
	 * @used-by Page_Cache::should_cache_request()
	 *
	 * @return bool
	 */
	private static function logged_in_user() {
		if ( function_exists( 'is_user_logged_in' ) ) {
			return is_user_logged_in();
		}

		foreach ( $_COOKIE as $key => $value ) { // Input var ok.
			// Check logged in user.
			if ( preg_match( '/^wordpress_logged_in_/', $key ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Skip subsite when administrator has turned off page caching.
	 *
	 * @since   1.8.0
	 * @access  private
	 * @used-by Page_Cache::cache_request()
	 *
	 * @return bool
	 */
	private static function skip_subsite() {
		if ( ! is_multisite() ) {
			return false;
		}

		$options = Utils::get_module( 'page_cache' )->get_options();

		return ! $options['enabled'];
	}

	/**
	 * Check if we should be compressing.
	 *
	 * @since 2.1
	 *
	 * @return bool
	 */
	private static function can_serve_compressed() {
		if ( 1 === ini_get( 'zlib.output_compression' ) || 'on' === strtolower( ini_get( 'zlib.output_compression' ) ) ) {
			return false;
		}

		if ( ! isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) || false === strpos( stripslashes( $_SERVER['HTTP_ACCEPT_ENCODING'] ), 'gzip' ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Check if mobile user agent.
	 *
	 * @since 2.2.0
	 * @access private
	 * @used-by Page_Cache::should_cache_request()
	 * @return bool
	 */
	public static function is_mobile_agent() {
		$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? strtolower( stripslashes( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; // Input var ok.

		$result = array_filter(
			self::$mobile_agents,
			function( $browser_agent ) use ( $user_agent ) {
				return false !== strstr( $user_agent, trim( strtolower( $browser_agent ) ) );
			}
		);

		return ! empty( $result );
	}

	/**
	 * Skip pages with predefined cookies in page caching settings.
	 *
	 * @since 2.1.0
	 *
	 * @return bool
	 */
	private static function skip_custom_cookies() {
		global $wphb_cache_config;

		// Remove empty values.
		$cookies = array_filter( $wphb_cache_config->exclude_cookies );
		if ( empty( $cookies ) ) {
			return false;
		}

		$uri_pattern = implode( '|', $wphb_cache_config->exclude_cookies );

		foreach ( $_COOKIE as $key => $value ) { // Input var ok.
			if ( preg_match( "/$uri_pattern/i", $key ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Get REQUEST_URI.
	 *
	 * @since 3.3.1
	 *
	 * @return string
	 */
	private static function get_request_uri() {
		$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? stripslashes( $_SERVER['REQUEST_URI'] ) : '';

		/**
		 * Filter the REQUEST_URI value.
		 *
		 * @param string $request_uri URI of the current page.
		 */
		return apply_filters( 'wphb_page_cache_request_uri', $request_uri );
	}

	/**
	 * Get mapped domain.
	 *
	 * @since 3.3.1
	 *
	 * @param string $http_host  HTTP host.
	 * @param string $cache_dir  Cache directory.
	 *
	 * @return string
	 */
	private function get_mapped_domain( $http_host = '', $cache_dir = '' ) {
		if ( class_exists( 'domain_map' ) ) {
			global $dm_map;
			$utils         = $dm_map->utils();
			$mapped_domain = $utils->get_mapped_domain();
			if ( $mapped_domain ) {
				return $mapped_domain;
			}
		} elseif ( class_exists( '\Mercator\Mapping' ) ) {
			$mapped_domain = false;

			if ( method_exists( '\Mercator\Mapping', 'get_by_site' ) ) {
				if ( isset( $GLOBALS['mercator_current_mapping'] ) ) {
					$mapped_domain = $GLOBALS['mercator_current_mapping']->get_domain();
				} else {
					$mappings = \Mercator\Mapping::get_by_site( get_current_blog_id() );
					if ( $mappings ) {
						foreach ( $mappings as $mapping ) {
							if ( $mapping->is_active() ) {
								$mapped_domain = $mapping->get_domain();
							}
						}
					}
				}
			} elseif ( function_exists( 'wu_get_site' ) ) {
				$mapping = wu_get_site( get_current_blog_id() );
				if ( $mapping && $mapping->is_active() ) {
					$mapped_domain = $mapping->get_domain();
				}
			}

			if ( $mapped_domain ) {
				return str_replace( $http_host, $mapped_domain, $cache_dir );
			}
		}

		return '';
	}

	/**
	 * *************************
	 * III. FILESYSTEM FUNCTIONS
	 *
	 * Available methods:
	 * write_file()
	 * add_index()
	 * save_settings()
	 * disable()
	 ***************************/

	/**
	 * Write page buffer to file.
	 *
	 * @since   1.7.0
	 * @used-by Page_Cache::get_settings()
	 * @used-by Page_Cache::cache_request()
	 * @param   string $file     File name.
	 * @param   string $content  File content.
	 */
	private function write_file( $file, $content ) {
		global $wphb_fs;

		if ( ! $wphb_fs ) {
			$wphb_fs = Filesystem::instance();
		}

		$wphb_fs->write( $file, $content );
		$this->add_index( dirname( $file ) );
	}

	/**
	 * Add empty index.html file for protection.
	 *
	 * @since   1.7.0
	 * @access  private
	 * @param   string $dir  Directory path.
	 * @used-by Page_Cache::write_file()
	 */
	private function add_index( $dir ) {
		if ( is_dir( $dir ) && is_file( "$dir/index.html" ) ) {
			return;
		}

		$file = fopen( "$dir/index.html", 'w' );
		if ( $file ) {
			fclose( $file );
		}
	}

	/**
	 * Save settings to file.
	 *
	 * @since   1.7.0
	 * @param   array $settings  Settings array.
	 * @used-by \Hummingbird\Admin\Pages\Caching::on_load()
	 */
	public function save_settings( $settings ) {
		if ( ! is_array( $settings ) ) {
			return;
		}

		// If non-member enable cache_identifier.
		if ( ! Utils::is_member() ) {
			$settings['settings']['cache_identifier'] = 1;
		}

		global $wphb_cache_config, $wphb_fs;

		if ( ! $wphb_fs ) {
			$wphb_fs = Filesystem::instance();
		}

		$wphb_cache_config            = new stdClass();
		$wphb_cache_config->cache_dir = $wphb_fs->cache_dir;

		$config_file = $wphb_fs->basedir . 'wphb-cache.php';

		self::log_msg( 'Writing configuration to: ' . $config_file );
		$settings['safemode_status'] = SafeMode::instance()->get_status();
		$this->write_file( $config_file, wp_json_encode( $settings ) );

		$this->clear_cache();
	}

	/**
	 * Disable page caching:
	 * - removes advanced-cache.php file
	 * - removes WP_CACHE from wp-config.php
	 * - purge cache folder
	 *
	 * @since   1.7.0
	 * @since   1.8.0  Changed access to private.
	 * @access  private
	 * @used-by Page_Cache::toggle_service()
	 */
	private function cleanup() {
		global $wphb_fs;

		if ( ! $wphb_fs ) {
			$wphb_fs = Filesystem::instance();
		}

		if ( $wphb_fs->purge() ) {
			self::log_msg( 'Page cache deactivation: successfully purged cache folder.' );
		} else {
			self::log_msg( 'Page cache deactivation: error purging cache folder.' );
		}

		// Do not disable page caching completely on MU if disabling only for subsite.
		if ( is_multisite() && ! is_network_admin() ) {
			return;
		}

		// Remove advanced-cache.php.
		$adv_cache_file = dirname( get_theme_root() ) . '/advanced-cache.php';

		// If no file or file not writable - exit.
		if ( ! file_exists( $adv_cache_file ) || ! is_writable( $adv_cache_file ) ) {
			self::log_msg( 'Page cache deactivation: unable to remove advanced-cache.php.' );
			return;
		}

		// Remove only Hummingbird file.
		if ( false !== strpos( file_get_contents( $adv_cache_file ), 'WPHB_ADVANCED_CACHE' ) ) {
			$msg = 'Page cache deactivation: error removing advanced-cache.php file.';
			if ( unlink( $adv_cache_file ) ) {
				self::log_msg( 'Page cache deactivation: advanced-cache.php file removed.' );
			}

			self::log_msg( $msg );
		}

		// Reset cached pages count to 0.
		Settings::update_setting( 'pages_cached', 0, 'page_cache' );
	}

	/**
	 * *************************
	 * IV. CACHE CONTROL FUNCTIONS
	 *
	 * Available methods:
	 * should_cache_request()
	 * cache_request()
	 * send_headers()
	 * purge_post_cache()
	 * clear_external_cache()
	 * clear_cache()
	 ***************************/

	/**
	 * Should we cache the request or not?
	 *
	 * @since   1.7.0
	 * @access  private
	 * @used-by Page_Cache::serve_cache()
	 * @used-by Page_Cache::init_caching()
	 * @param   string $request_uri  Request URI.
	 *
	 * @return bool
	 */
	private static function should_cache_request( $request_uri ) {
		global $wphb_cache_config;

		// In most cases the filter is used to disable caching on incompatible hosts.
		$state = apply_filters( 'wphb_should_cache_request_pre', true );

		if ( ! $state ) {
			self::log_msg( apply_filters( 'wphb_should_cache_request_pre', 'Do not cache, blocked by filter' ) );
			return false;
		}

		self::load_config();

		if ( ( defined( 'DOING_CRON' ) && DOING_CRON ) || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) {
			self::log_msg( 'Page not cached because of active cron or ajax request.' );
			return false;
		} elseif ( is_admin() ) {
			self::log_msg( 'Do not cache admin pages.' );
			return false;
		} elseif ( self::logged_in_user() && ! $wphb_cache_config->cache_logged_in ) {
			self::log_msg( 'Do not cache pages for logged in users.' );
			return false;
		} elseif ( isset( $_SERVER['REQUEST_METHOD'] ) && 'GET' !== $_SERVER['REQUEST_METHOD'] ) { // Input var okay.
			self::log_msg( "Skipping page. Used {$_SERVER['REQUEST_METHOD']} method. Only GET allowed." ); // Input var ok.
			return false;
		} elseif ( isset( $_GET['preview'] ) ) { // Input var okay.
			self::log_msg( 'Do not cache preview post pages.' );
			return false;
		} elseif ( false === empty( $_GET ) && ( ! $wphb_cache_config->cache_with_get_params && ! self::should_cache_safemode( $wphb_cache_config->safemode_status ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			self::log_msg( 'Skipping page with GET params.' );
			return false;
		} elseif ( preg_match( '/^\/wp.*php$/', strtok( $request_uri, '?' ) ) ) {
			// Remove string parameters and do not cache any /wp-login.php or /wp-admin/*.php pages.
			// TODO: Maybe improve regex, as it takes a bit more than needed.
			self::log_msg( 'Do not cache wp-admin pages.' );
			return false;
		} elseif ( self::skip_url( $request_uri ) ) {
			self::log_msg( 'Do not cache page. URL exclusion rule match: ' . $request_uri );
			return false;
		} elseif ( self::skip_user_agent() ) {
			self::log_msg( 'Do not cache page. User-Agent is empty or excluded in settings.' );
			return false;
		} elseif ( ! $wphb_cache_config->mobile && self::is_mobile_agent() ) {
			self::log_msg( 'Do not cache page. Mobile agents are excluded in settings.' );
			return false;
		} elseif ( self::skip_custom_cookies() ) {
			self::log_msg( 'Do not cache page. Found excluded cookie.' );
			return false;
		} elseif ( ! isset( $_SERVER['HTTP_HOST'] ) ) { // Input var ok.
			self::log_msg( 'Page can not be cached, no HTTP_HOST set.' );
			return false;
		} elseif ( $wphb_cache_config->safemode_status && ! isset( $_GET['wphb_preview_safe_mode'] ) && ( ! isset( $_SERVER['HTTP_USER_AGENT'] ) || strpos( $_SERVER['HTTP_USER_AGENT'], 'Lighthouse' ) === false ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			self::log_msg( 'Do not serve safemode cache for logged out users.' );
			return false;
		}

		self::log_msg( 'Request passed should_cache_request check. Ready to cache.' );

		return true;
	}

	/**
	 * Parse the buffer. Used in callback for ob_start in init_caching().
	 *
	 * @since   1.7.0
	 * @used-by Page_Cache::init_caching()
	 * @param   string $buffer  Page buffer.
	 *
	 * @return string
	 */
	public function cache_request( $buffer ) {
		global $wphb_cache_file, $wphb_cache_config, $wphb_meta_file;

		if ( Fast_CGI::is_fast_cgi_enabled() ) {
			self::log_msg( 'Page not cached. Static server cache is enabled.' );
			return $buffer;
		}

		if ( apply_filters( 'wphb_should_cache_exit', false ) ) {
			// Exit early.
			return $buffer;
		}

		$cache_page = true;
		$is_404     = false;

		if ( empty( $buffer ) ) {
			$cache_page = false;
			self::log_msg( 'Empty buffer. Exiting.' );
		}

		$http_response_code = http_response_code();
		if ( ! in_array( $http_response_code, array( 200, 404 ), true ) ) {
			$cache_page = false;
			self::log_msg( 'Page not cached because unsupported http response code: ' . $http_response_code );
		}

		if ( defined( 'DONOTCACHEPAGE' ) && DONOTCACHEPAGE ) {
			$cache_page = false;
			self::log_msg( 'Page not cached because DONOTCACHEPAGE is defined.' );
		} elseif ( is_feed() ) {
			$cache_page = false;
			self::log_msg( 'Do not cache feeds.' );
		} elseif ( self::skip_page_type() ) {
			$cache_page = false;
			self::log_msg( 'Do not cache page. Skipped in settings.' );
		} elseif ( ! preg_match( '/(<\/html>|<\/rss>|<\/feed>|<\/urlset|<\?xml)/i', $buffer ) ) {
			$cache_page = false;
			self::log_msg( 'HTML corrupt. Page not cached.' );
		} elseif ( self::skip_subsite() ) {
			$cache_page = false;
			self::log_msg( 'Do not cache page. Subsite caching disabled in settings.' );
		}

		// Handle 404 pages.
		if ( is_404() ) {
			if ( ! $wphb_cache_config->cache_404 ) {
				$cache_page = false;
				self::log_msg( 'Do not cache 404 pages.' );
			} else {
				$is_404 = true;
				self::log_msg( '404 page found. Caching for 404 enabled. Page will be cached.' );
			}
		}

		if ( ! $cache_page ) {
			self::log_msg( 'Page not cached. Sending buffer to user.' );
			return $buffer;
		}

		$content = '';
		if ( $wphb_cache_config->cache_identifier ) {
			$content = '<!-- This page is cached by the Hummingbird Performance plugin v' . WPHB_VERSION . ' - https://wordpress.org/plugins/hummingbird-performance/. -->';
		}
		$content       .= $buffer;
		$time_to_create = microtime( true ) - $this->start_time;

		if ( $wphb_cache_config->cache_identifier ) {
			$content .= '<!-- Hummingbird cache file was created in ' . $time_to_create . ' seconds, on ' . gmdate( 'd-m-y G:i:s', time() ) . ' -->';
		}

		$content = apply_filters( 'wphb_cache_content', $content );

		if ( $wphb_cache_file ) {
			// If this is php file and caching for logged-in users - add die() on top (except for when it's compressed, just to avoid that extra decoding step).
			if ( preg_match( '/\.php/', basename( $wphb_cache_file ) ) && ! $is_404 && ( ! isset( $wphb_cache_config->compress ) || ! $wphb_cache_config->compress ) ) {
				$content = '<?php die(); ?>' . $content;
			}

			// Maybe compress the content?
			if ( $wphb_cache_config->compress && self::can_serve_compressed() ) {
				self::log_msg( 'Applying gzip compression to cached file' );
				$content = gzencode( $content, 7 );
			}

			self::log_msg( 'Saving page to cache file: ' . $wphb_cache_file );
			$this->write_file( $wphb_cache_file, $content );

			// Cache Headers if enabled.
			if ( $wphb_cache_config->cache_headers ) {
				self::log_msg( 'Saving page headers to cache file: ' . $wphb_meta_file );
				$this->write_file( $wphb_meta_file, '<?php die(); ?>' . wp_json_encode( $this->get_page_headers() ) );
			}

			// Update cached pages count.
			$count = Settings::get_setting( 'pages_cached', 'page_cache' );
			Settings::update_setting( 'pages_cached', ++$count, 'page_cache' );
		}

		return $buffer;
	}

	/**
	 * Send headers to the browser.
	 *
	 * @since   1.7.0
	 * @access  private
	 * @used-by Page_Cache::init_caching()
	 * @used-by Page_Cache::start_cache()
	 */
	private static function send_headers() {
		global $wphb_cache_file, $wphb_cache_config;

		$headers_default = array(
			'Content-Type'  => 'Content-Type: text/html; charset=UTF-8',
			'Cache-Control' => 'Cache-Control: max-age=3600, must-revalidate',
		);

		$headers = array_merge( $headers_default, self::get_page_headers_cached() );

		// Check last modified time or file.
		$file_modified = filemtime( $wphb_cache_file );
		if ( $file_modified ) {
			$headers['Last-Modified'] = 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $file_modified ) . ' GMT';
		} else {
			$headers['Last-Modified'] = 'HTTP/1.0 304 Not Modified';
		}

		if ( $wphb_cache_config->compress && self::can_serve_compressed() ) {
			$headers['Content-Encoding'] = 'Content-Encoding: gzip';
			$headers['Content-Length']   = 'Content-Length: ' . filesize( $wphb_cache_file );
		}

		foreach ( $headers as $header ) {
			/*
			 * Godaddy fix, via http://blog.gneu.org/2008/05/wp-supercache-on-godaddy/ and
			 * http://www.littleredrails.com/blog/2007/09/08/using-wp-cache-on-godaddy-500-error/.
			 */
			if ( strpos( $header, 'Last-Modified:' ) === false ) {
				header( $header );
			}
		}

		header( 'Hummingbird-Cache: Served' );
	}

	/**
	 * Server cached file to user.
	 *
	 * @since   1.7.2
	 * @access  private
	 * @used-by Page_Cache::init_caching()
	 * @used-by Page_Cache::start_cache()
	 * @param   string $wphb_cache_file  File to cache.
	 */
	private static function send_file( $wphb_cache_file ) {
		// If this is php file (caching for logged-in users) - remove die().
		if ( preg_match( '/\.php/', basename( $wphb_cache_file ) ) ) {
			$content = file_get_contents( $wphb_cache_file );
			/* Remove <?php die(); ?> from file */
			if ( 0 === strpos( $content, '<?php die(); ?>' ) ) {
				$content = substr( $content, 15 );
			}
			echo $content;
			exit();
		}

		if ( defined( 'WPMU_ACCEL_REDIRECT' ) && WPMU_ACCEL_REDIRECT ) {
			header( 'X-Accel-Redirect: ' . str_replace( WP_CONTENT_DIR, '/wp-content/', $wphb_cache_file ) );
			exit;
		} elseif ( defined( 'WPMU_SENDFILE' ) && WPMU_SENDFILE ) {
			header( 'X-Sendfile: ' . $wphb_cache_file );
			exit;
		} else {
			@readfile( $wphb_cache_file );
			exit();
		}
	}

	/**
	 * Purge single post page cache and relative pages (tags, category and author pages).
	 *
	 * @since   1.7.0
	 * @param   int $post_id  Post ID.
	 * @used-by Page_Cache::post_status_change()
	 * @used-by Page_Cache::post_edit()
	 */
	private function purge_post_cache( $post_id ) {
		global $post_trashed, $wphb_cache_config;

		if ( apply_filters( 'wphb_post_cache_purged', false, $post_id ) ) {
			self::log_msg( 'Cache has been purged already for post id : ' . $post_id );

			return;
		}

		$replacement = preg_replace( '|https?://[^/]+|i', '', home_url() );
		$permalink   = trailingslashit( str_replace( home_url(), $replacement, get_permalink( $post_id ) ) );

		// If post is being trashed.
		if ( $post_trashed ) {
			$permalink = preg_replace( '/__trashed(-?)(\d*)\/$/', '/', $permalink );
		}

		// When we have a static page as a home directory, we need to make sure that we do not clear all the other sub-folders.
		$force_single_clear = '/' === $permalink;

		$this->clear_cache( $permalink, $force_single_clear );
		do_action( 'wphb_cloudflare_apo_clear_cache', $post_id );
		self::log_msg( 'Cache has been purged for post id: ' . $post_id );
		do_action( 'wphb_page_cache_preload_page', $permalink );

		// Clear categories and tags pages if cached.
		$meta_array = array(
			'category' => 'category',
			'tag'      => 'post_tag',
		);
		foreach ( $meta_array as $meta_name => $meta_key ) {
			// If page_types not array, skip early.
			if ( empty( $wphb_cache_config ) || ( ! is_array( $wphb_cache_config->page_types ) ) ) {
				continue;
			}

			// If not cached, skip meta.
			if ( ! in_array( $meta_name, $wphb_cache_config->page_types, true ) ) {
				continue;
			}

			$metas = get_the_terms( $post_id, $meta_key );

			if ( ! $metas ) {
				continue;
			}

			/**
			 * WP term meta.
			 *
			 * @var WP_Term $meta
			 */
			foreach ( $metas as $meta ) {
				$meta_link = trailingslashit( str_replace( get_option( 'home' ), $replacement, get_category_link( $meta->term_id ) ) );
				$this->clear_cache( $meta_link );
				self::log_msg( "Cache has been purged for $meta_name: $meta->name" );
			}
		}

		$post = get_post( $post_id );
		if ( ! is_object( $post ) ) {
			return;
		}

		// Author page.
		if ( isset( $post->post_author ) && 0 !== $post->post_author ) {
			$author_link = trailingslashit( str_replace( get_option( 'home' ), $replacement, get_author_posts_url( $post->post_author ) ) );
			if ( $author_link ) {
				$this->clear_cache( $author_link );
				self::log_msg( "Cache has been purged for author page: $author_link" );
			}
		}

		/**
		 * Support for custom terms.
		 *
		 * @since 2.0.0
		 */
		$custom_terms = apply_filters( 'wphb_page_cache_custom_terms', array() );
		foreach ( $custom_terms as $term ) {
			$metas = get_the_terms( $post_id, $term );

			if ( ! $metas && ! is_wp_error( $metas ) ) {
				continue;
			}

			foreach ( $metas as $meta ) {
				if ( ! isset( $meta->term_id ) && ! is_wp_error( $meta ) ) {
					continue;
				}

				$meta_link = str_replace( get_option( 'home' ), $replacement, get_term_link( $meta->term_id, $term ) );
				$this->clear_cache( $meta_link );
				self::log_msg( "Cache has been purged for $term: $meta->name" );

				if ( ( ! isset( $meta->parent ) || 0 === $meta->parent ) && ! is_wp_error( $meta ) ) {
					continue;
				}

				$meta_link = str_replace( get_option( 'home' ), $replacement, get_term_link( $meta->parent, $term ) );
				$this->clear_cache( $meta_link );
				self::log_msg( "Cache has been purged for $term: $meta->name" );
			}
		}
	}

	/**
	 * Purge cache for specific page in external cache service (varnish).
	 *
	 * @since 2.1.0
	 * @param string $path  Path for which to clear cache.
	 */
	public function clear_external_cache( $path ) {
		$options = $this->get_options();

		if ( isset( $options['integrations']['varnish'] ) && $options['integrations']['varnish'] && ! get_transient( 'wphb-processing' ) ) {
			Utils::get_api()->varnish->purge_cache( $path );
		}
	}

	/**
	 * Implement abstract parent method for clearing cache.
	 *
	 * Purge cache directory.
	 *
	 * @since   1.7.0
	 * @since   1.7.1 Renamed to clear_cache from purge_cache_dir
	 * @since   3.3.0 Added $domain_check parameter
	 *
	 * @used-by \Hummingbird\Admin\Pages\Caching::run_actions()
	 * @used-by Page_Cache::save_settings()
	 * @used-by Page_Cache::purge_post_cache()
	 * @used-by Page_Cache::post_edit()
	 * @used-by Page_Cache::post_status_change()
	 *
	 * @param string $directory     Directory to remove.
	 * @param bool   $single        Make sure we only clear out a single directory for posts that are set as a site homepage.
	 * @param bool   $domain_check  Attempt to detect host in multisite.
	 *
	 * @return bool
	 */
	public function clear_cache( $directory = '', $single = false, $domain_check = true ) {
		global $wphb_fs;

		if ( ! $wphb_fs ) {
			$wphb_fs = Filesystem::instance();
		}

		$skip_sub_dirs    = $domain_check;
		$directory_origin = $directory;

		// Remove notice for clearing page cache.
		delete_option( 'wphb-notice-cache-cleaned-show' );

		/**
		 * Function is_network_admin() does not work in ajax, so this is a hack.
		 *
		 * @see https://core.trac.wordpress.org/ticket/22589
		 */
		$is_network_admin = false;
		if ( is_multisite() && isset( $_SERVER['HTTP_REFERER'] ) ) {
			$is_network_admin = preg_match( '#^' . network_admin_url() . '#i', $_SERVER['HTTP_REFERER'] );
		}

		// For multisite we need to set this to null.
		if ( is_multisite() && ! $is_network_admin && ! $directory ) {
			$current_blog  = get_site( get_current_blog_id() );
			$directory     = $current_blog->path;
			$skip_sub_dirs = false; // We are clearing all cache.
		}

		// Purge whole cache directory.
		if ( ! $directory ) {
			// Reset cached pages count.
			Settings::update_setting( 'pages_cached', 0, 'page_cache' );

			self::log_msg( 'Cache directory purged' );
			$status = $wphb_fs->purge();

			$options = $this->get_options();

			do_action( 'wphb_clear_cache_url' ); // Clear integrations cache.
			if ( isset( $options['preload'] ) && $options['preload'] && isset( $options['preload_type'] ) && isset( $options['preload_type']['home_page'] ) && $options['preload_type']['home_page'] ) {
				$preload = new Preload();
				$preload->preload_home_page();
			}

			do_action( 'wphb_cache_directory_cleared' );

			return $status;
		}

		// Purge specific folder.
		$http_host = '';
		if ( ! empty( $_SERVER['HTTP_HOST'] ) ) {
			$http_host = htmlentities( wp_unslash( $_SERVER['HTTP_HOST'] ) ); // Input var ok.
		} elseif ( $domain_check && function_exists( 'get_option' ) ) {
			$http_host = preg_replace( '/https?:\/\//', '', get_option( 'siteurl' ) );
		}

		/**
		 * Filter the HTTP_HOST value.
		 *
		 * @param string $http_host  Current HTTP host value.
		 *
		 * @since 2.7.3
		 */
		$http_host = apply_filters( 'wphb_page_cache_http_host', $http_host );

		$cache_dir = $http_host . $directory;
		$full_path = $wphb_fs->cache_dir . $cache_dir;

		// Check if current blog is mapped and change directory to mapped domain.
		if ( class_exists( 'domain_map' ) || class_exists( '\Mercator\Mapping' ) ) {
			$cache_dir = $this->get_mapped_domain( $http_host, $cache_dir );
			$full_path = $wphb_fs->cache_dir . $cache_dir;
		}

		// If dir does not exist - return.
		if ( empty( $full_path ) || ! is_dir( $full_path ) ) {
			do_action( 'wphb_clear_cache_url', $directory_origin ); // Clear integrations cache.
			return true;
		}

		$count = Settings::get_setting( 'pages_cached', 'page_cache' );

		if ( is_dir( wp_normalize_path( $wphb_fs->cache_dir . '/mobile/' . $cache_dir ) ) ) {
			if ( ! $skip_sub_dirs ) {
				$files_purged = $wphb_fs->count_files( $wphb_fs->cache_dir . '/mobile/' . $cache_dir );
			}
			if ( $wphb_fs->purge( 'cache/mobile/' . $http_host . $directory, $skip_sub_dirs ) ) {
				self::log_msg( 'Mobile cache has been cleared.' );
				( ! $skip_sub_dirs ) ? $count -= $files_purged : $count--;
			}
		}

		if ( ! $skip_sub_dirs ) {
			$files_purged = $wphb_fs->count_files( $full_path );
		}
		$status = $wphb_fs->purge( 'cache/' . $cache_dir, $skip_sub_dirs );
		if ( $status ) {
			( ! $skip_sub_dirs ) ? $count -= $files_purged : $count--;
		}

		Settings::update_setting( 'pages_cached', $count, 'page_cache' );

		do_action( 'wphb_clear_cache_url', $directory_origin ); // Clear integrations cache.

		return $status;
	}

	/**
	 * *************************
	 * V. ACTIONS AND FILTERS
	 *
	 * Available methods:
	 * serve_cache()
	 * init_caching()
	 * post_status_change()
	 * post_edit()
	 * log()
	 * clear_cache_button()
	 * clear_cache_message()
	 * clear_cache_action()
	 * toggle_service()
	 * clear_on_comment_post()
	 * module_status()
	 ***************************/

	/**
	 * Server a cached file.
	 *
	 * @since 1.7.0
	 * @used-by advanced-cache.php
	 */
	public static function serve_cache() {
		global $wphb_cache_file, $wphb_cache_config, $wphb_meta_file;

		// Exit early if in admin.
		if ( is_admin() ) {
			return;
		}

		if ( ! self::should_cache_request( self::get_request_uri() ) ) {
			return;
		}

		/**
		 * 1. Get the file and header names.
		 * $wphb_cache_file available with path to cached file
		 * Generate file path where the cache will be saved.
		 */
		self::get_file_cache_path();

		/**
		 * 2. Check if the files are there?
		 */
		if ( file_exists( $wphb_cache_file ) ) {
			// Check expiry.
			if ( isset( $wphb_cache_config->clear_interval['enabled'] ) && $wphb_cache_config->clear_interval['enabled'] ) {
				self::log_msg(
					sprintf( /* translators: %s - Cache expiration time, %s - Cache old time */
						'Cache file found. Expiry set to %s hours, file is %s hours old.',
						$wphb_cache_config->clear_interval['interval'],
						round( ( time() - filemtime( $wphb_cache_file ) ) / HOUR_IN_SECONDS )
					)
				);

				if ( time() - filemtime( $wphb_cache_file ) >= $wphb_cache_config->clear_interval['interval'] * HOUR_IN_SECONDS ) {
					self::log_msg( 'Cache file found, but cache interval is defined and cache is expired. Removing file.' );
					unlink( $wphb_cache_file );
					if ( file_exists( $wphb_meta_file ) ) {
						unlink( $wphb_meta_file );
					}
					return;
				}
			}
			// Cache headers enabled: Check for header cache file, if it doesn't exist, unlink page cache file.
			if ( $wphb_cache_config->cache_headers && ! file_exists( $wphb_meta_file ) ) {
				self::log_msg( "Cache file found, but header cache file doesn't exists. Removing file." );
				unlink( $wphb_cache_file );
				return;
			}

			self::log_msg( 'Cached file found. Serving to user.' );

			self::send_headers();

			self::send_file( $wphb_cache_file );
		}
	}

	/**
	 * Try to avoid WP functions here (though we need to test).
	 *
	 * @since   1.7.0
	 * @used-by init action
	 */
	public function init_caching() {
		global $wphb_cache_file, $wphb_cache_config, $wphb_meta_file;

		if ( ! self::should_cache_request( self::get_request_uri() ) ) {
			return;
		}

		/**
		 * 1. Get the file and header names.
		 * $wphb_cache_file available with path to cached file
		 * Generate file path where the cache will be saved.
		 */
		self::get_file_cache_path();

		$is_cached = file_exists( $wphb_cache_file );

		if ( $is_cached && $wphb_cache_config->cache_headers ) {
			$is_cached = file_exists( $wphb_meta_file );
		}

		/**
		 * 2. Check if the files are there?
		 */
		if ( $is_cached ) {
			self::log_msg( 'Cached file found. Serving to user.' );

			self::send_headers();

			self::send_file( $wphb_cache_file );
		} else {
			self::log_msg( 'Cached file not found. Passing to ob_start.' );
			// Write the file and send headers.
			$this->start_time = microtime( true );
			ob_start( array( $this, 'cache_request' ) );
		}
	}

	/**
	 * Parse post status transitions.
	 *
	 * @since   1.7.0
	 * @param   string  $new_status  New post status.
	 * @param   string  $old_status  Old post status.
	 * @param   WP_Post $post        Post object.
	 * @used-by transition_post_status action
	 */
	public function post_status_change( $new_status, $old_status, $post ) {
		global $post_trashed, $wphb_cache_config;

		// Only trigger for public post types.
		if ( ! isset( $post->post_type ) || ! is_post_type_viewable( $post->post_type ) ) {
			return;
		}

		// Nothing changed or revision. Exit.
		if ( $new_status === $old_status || wp_is_post_revision( $post->ID ) ) {
			return;
		}

		// New post in draft mode. Exit.
		if ( 'auto-draft' === $new_status || 'draft' === $new_status ) {
			return;
		}

		$post_trashed = false;
		if ( 'trash' === $new_status ) {
			$post_trashed = true;
		}

		// Purge cache on post publish/un-publish/move to trash.
		if ( ( 'publish' === $new_status && 'publish' !== $old_status ) || 'trash' === $new_status ) {
			// If settings not loaded - load them.
			if ( ! isset( $wphb_cache_config ) ) {
				self::load_config();
			}

			// Clear all cache files and return.
			if ( $wphb_cache_config->clear_on_update ) {
				$this->clear_cache();

				// Reset cached pages count.
				Settings::update_setting( 'pages_cached', 0, 'page_cache' );

				return;
			}

			// Delete category and tag cache.
			// Delete page cache.
			$this->purge_post_cache( $post->ID );
		}
	}

	/**
	 * Fires on edit_post action.
	 *
	 * @since   1.7.0
	 * @param   int $post_id  Post ID.
	 * @used-by edit_post action
	 */
	public function post_edit( $post_id ) {
		global $wphb_cache_config;

		// Clear cache button on post edit pressed.
		if ( filter_input( INPUT_POST, 'wphb-clear-cache', FILTER_VALIDATE_BOOLEAN ) ) {
			// Delete page cache.
			$this->purge_post_cache( $post_id );

			// This variable doesn't really do anything... If you comment it out, nothing will change, except the
			// same variable below in apply_filters will be underlined in all code editors.
			$messages['post'][4] = __( 'Cache for post has been cleared.', 'wphb' );
			apply_filters( 'post_updated_messages', $messages );

			return;
		}

		if ( wp_is_post_revision( $post_id ) ) {
			return;
		}

		// Only trigger for public post types.
		$post = get_post( $post_id );
		if ( ! isset( $post->post_type ) || ! is_post_type_viewable( $post->post_type ) ) {
			return;
		}

		// If settings not loaded - load them.
		if ( ! isset( $wphb_cache_config ) ) {
			self::load_config();
		}

		// Clear all cache files and return.
		if ( $wphb_cache_config->clear_on_update ) {
			$this->clear_cache();

			// Reset cached pages count.
			Settings::update_setting( 'pages_cached', 0, 'page_cache' );
		} else {
			// Delete category and tag cache.
			// Delete page cache.
			$this->purge_post_cache( $post_id );
		}
	}

	/**
	 * Write notice or error to debug.log
	 *
	 * @since 1.7.0
	 * @param mixed $message  Error/notice message.
	 */
	public static function log_msg( $message ) {
		// Check that page caching logging is enabled.
		$config_file = WP_CONTENT_DIR . '/wphb-cache/wphb-cache.php';
		if ( ! file_exists( $config_file ) ) {
			return;
		}
		$settings = json_decode( file_get_contents( $config_file ), true );

		if ( ! (bool) $settings['settings']['debug_log'] ) {
			return;
		}

		if ( ! is_string( $message ) ) {
			$message = print_r( $message, true );
		}

		$message = '[' . date( 'c' ) . '] ' . $message . PHP_EOL;

		$file = WP_CONTENT_DIR . '/wphb-logs/page-caching-log.php';

		// If file does not exist, we need to create it and add the die() header.
		if ( ! file_exists( $file ) ) {
			global $wphb_fs;

			if ( ! $wphb_fs && class_exists( 'Filesystem' ) ) {
				$wphb_fs = Filesystem::instance();
			}

			if ( $wphb_fs ) {
				$wphb_fs->write( $file, '<?php die(); ?>' . PHP_EOL );
			}
		}

		error_log( $message, 3, $file );
	}

	/**
	 * Add a clear cache button to edit post screen (under published on field).
	 *
	 * @since 1.8
	 *
	 * @used-by Page_Cache::run() (post_submitbox_misc_actions action).
	 */
	public function clear_cache_button() {
		?>
		<div class="misc-pub-section wphb-clear-cache-button">
			<input type="submit" value="<?php esc_attr_e( 'Clear cache', 'wphb' ); ?>" class="button" id="wphb-clear-cache" name="wphb-clear-cache">
		</div>
		<?php
	}

	/**
	 * Overwrite message when the clear cache button has been pressed.
	 *
	 * @since 1.8
	 *
	 * @param array $messages  Messages.
	 * @used-by Page_Cache::run() (post_updated_messages filter)
	 *
	 * @return array
	 */
	public function clear_cache_message( $messages ) {
		$messages['post'][4] = __( 'Cache for post has been cleared.', 'wphb' );
		return $messages;
	}

	/**
	 * Trigger a cache clear.
	 *
	 * If post ID is set, will try to clear cache for that page or post with all the related
	 * taxonomies (tags, category and author pages).
	 *
	 * @since 1.9.2
	 *
	 * @used-by wphb_clear_page_cache action.
	 *
	 * @param int|bool $post_id  Post ID.
	 */
	public function clear_cache_action( $post_id = false ) {
		if ( $post_id ) {
			$this->purge_post_cache( (int) $post_id );
		} else {
			$this->clear_cache();
		}
	}

	/**
	 * Toggle page caching.
	 *
	 * @since 1.8
	 *
	 * @used-by Page_Cache::enable()
	 * @used-by Page_Cache::disable()
	 * @used-by Installer::deactivate()
	 *
	 * @param bool $value   Value for page caching. Accepts boolean value: true or false.
	 * @param bool $network Value for network. Default: false.
	 */
	public function toggle_service( $value, $network = false ) {
		$options = parent::get_options();

		if ( is_multisite() ) {
			// We need to use this `define` for calls from Hub.
			$is_network_admin = defined( 'WPHB_IS_NETWORK_ADMIN' ) && WPHB_IS_NETWORK_ADMIN;
			if ( $network && ( is_network_admin() || Utils::is_ajax_network_admin() || $is_network_admin ) ) {
				// Updating for the whole network.
				$options['enabled']    = $value;
				$options['cache_blog'] = $value;
			} else {
				// Updating on subsite.
				if ( ! $options['enabled'] ) {
					// Page caching is turned off for the whole network, do not activate it per site.
					$options['cache_blog'] = false;
				} else {
					$options['cache_blog'] = $value;
				}
			}
		} else {
			$options['enabled'] = $value;
		}

		$this->update_options( $options );

		// Do not disable global cache, when disabled on subsite.
		if ( is_multisite() && ! is_network_admin() ) {
			return;
		}

		// Run activate/deactivate module actions.
		if ( $value ) {
			$this->activate();
		} else {
			$this->wpconfig_remove( 'WP_CACHE' );
			$this->cleanup();
		}
	}

	/**
	 * Clear cache for a specific page, when a comment is posted.
	 *
	 * @since 2.1.0
	 *
	 * @param int        $comment_id        The comment ID.
	 * @param int|string $comment_approved  1 if the comment is approved, 0 if not, 'spam' if spam.
	 * @param array      $comment_data      Comment data.
	 */
	public function clear_on_comment_post( $comment_id, $comment_approved, $comment_data ) {
		global $wphb_cache_config;

		// Option to clear cache on comment post is not set.
		if ( ! isset( $wphb_cache_config->comment_clear ) || ! $wphb_cache_config->comment_clear ) {
			return;
		}

		// Comment hasn't been approved, so it won't appear on the page just yet - no need to clear the cache.
		if ( 1 !== $comment_approved ) {
			return;
		}

		// Post ID is not set, nothing to clear - return.
		if ( ! isset( $comment_data['comment_post_ID'] ) || 0 === $comment_data['comment_post_ID'] ) {
			return;
		}

		$this->purge_post_cache( $comment_data['comment_post_ID'] );
	}

	/**
	 * Clear cache when home page display is changed.
	 *
	 * @since 3.15.0
	 *
	 * @param string|int $old_value  Old value.
	 * @param string|int $new_value  New value.
	 */
	public function clear_on_home_page_on_front_change( $old_value, $new_value ) {
		$this->clear_cache( '/', true );
		$old_value = (int) $old_value;
		$new_value = (int) $new_value;

		$this->clear_on_home_page_for_posts( $old_value, $new_value );
	}

	/**
	 * Clear cache for pages settings changed.
	 *
	 * @since 3.15.0
	 *
	 * @param string|int $old_value  Old value.
	 * @param string|int $new_value  New value.
	 */
	public function clear_on_home_page_for_posts( $old_value, $new_value ) {
		if ( 0 === $old_value || 0 === $new_value ) {
			$page_id = 0 === $old_value ? $new_value : $old_value;
			$this->purge_post_cache( $page_id );
		} else {
			$this->purge_post_cache( $old_value );
			$this->purge_post_cache( $new_value );
		}
	}

	/**
	 * Get module status.
	 *
	 * @return bool
	 */
	public function module_status() {
		$options = Settings::get_settings( 'page_cache' );

		if ( Fast_CGI::is_fast_cgi_enabled() ) {
			return true;
		}

		if ( false === $options['enabled'] ) {
			return false;
		}

		// Additional check for ajax (is_network_admin() does not work in ajax calls).
		$network_admin = is_network_admin() || Utils::is_ajax_network_admin();

		// If blog admins can't control cache settings, use global settings.
		if ( is_multisite() && ! $network_admin && 'blog-admins' === $options['enabled'] ) {
			$current = $options['cache_blog'];
		} else {
			$current = $options['enabled'];
		}

		return $current;
	}

	/**
	 * Gets the list of page headers being sent.
	 *
	 * @since 2.6.0
	 *
	 * @return array Empty array or list of headers
	 */
	public function get_page_headers() {
		if ( ! function_exists( 'headers_list' ) ) {
			return array();
		}

		$headers_list = headers_list();
		if ( empty( $headers_list ) ) {
			return array();
		}

		$headers = array();
		foreach ( $headers_list as $header ) {
			$pos = strpos( $header, ':' );

			if ( empty( $pos ) ) {
				continue;
			}

			$key = rtrim( substr( $header, 0, $pos ) );
			$val = ltrim( substr( $header, $pos + 1 ) );

			if ( ! empty( $headers[ $key ] ) ) {
				$val = $headers[ $key ] . ', ' . $val;
			}

			$headers[ $key ] = $val;
		}

		return $headers;
	}

	/**
	 * Get the headers from cached meta file if header caching is enabled and the cached file exists already.
	 *
	 * @since 2.6.0
	 *
	 * @return array Array of headers with array( Header => Header: Value ) syntax or empty array.
	 */
	public static function get_page_headers_cached() {
		global $wphb_meta_file, $wphb_cache_config;

		// If headers caching is disabled or the headers file doesn't exist.
		if ( ! $wphb_cache_config->cache_headers || ! file_exists( $wphb_meta_file ) ) {
			return array();
		}

		$headers     = array();
		$headers_raw = file_get_contents( $wphb_meta_file );

		/* Remove <?php die(); ?> from file */
		if ( 0 === strpos( $headers_raw, '<?php die(); ?>' ) ) {
			$headers = substr( $headers_raw, 15 );
			$headers = (array) json_decode( $headers );
		}

		if ( empty( $headers ) ) {
			return array();
		}

		/**
		 * The only reason we do this is to please sir RIPS-a-Lot flagging this as code quality issue.
		 *
		 * Loop Iteration Change.
		 * If it is not intended to override the loop counter dynamically within the body, a new variable can be introduced and used.
		 */
		$new_headers = array();
		foreach ( $headers as $k => $v ) {
			$new_headers[ $k ] = "$k: $v";
		}
		unset( $headers );

		return $new_headers;
	}

	/**
	 * Maybe start the buffer process.
	 *
	 * This method is responsible for starting the buffer process.
	 *
	 * @since 3.9.0
	 *
	 * @return void
	 */
	public function wphb_maybe_start_buffer() {
		ob_start( array( $this, 'wphb_maybe_process_buffer' ) );
	}

	/**
	 * Maybe process the output buffer.
	 *
	 * @since 3.9.0
	 *
	 * @param string $buffer Buffer content.
	 *
	 * @return string
	 */
	public function wphb_maybe_process_buffer( $buffer ) {
		if ( is_admin() ) {
			return $buffer;
		}

		$buffer = (string) apply_filters( 'wphb_buffer', $buffer );

		return $buffer;
	}

	/**
	 * Call the clear page cache after menu update.
	 *
	 * @param int   $menu_id   ID of the updated menu.
	 * @param array $menu_data An array of menu data.
	 */
	public function clear_cache_after_menu_update( $menu_id, $menu_data = array() ) {
		$this->clear_cache();
	}

	/**
	 * Clear the page cache after menu item updated through block editor.
	 *
	 * @param int     $post_id Post ID.
	 * @param WP_Post $post    Post object.
	 * @param bool    $update  Whether this is an existing post being updated.
	 */
	public function wphb_save_post_wp_navigation_menu( $post_id, $post, $update ) {
		$this->clear_cache();
	}

	/**
	 * Clear page cache when widget settings change.
	 *
	 * This method is called when any widget is updated, ensuring that
	 * cached pages reflect the new widget configuration.
	 *
	 * @since 3.17.0
	 *
	 * @param array     $instance     The current widget instance's settings.
	 * @param array     $new_instance Array of new widget settings.
	 * @param array     $old_instance Array of old widget settings.
	 * @param WP_Widget $widget       The current widget instance.
	 *
	 * @return array The widget instance.
	 */
	public function clear_cache_on_widget_change( $instance, $new_instance, $old_instance, $widget ) {
		// Only clear cache if widget settings actually changed.
		if ( $new_instance !== $old_instance ) {
			$this->clear_cache();
		}

		return $instance;
	}

	/**
	 * Clear cache after customizer save with error handling.
	 *
	 * @since 3.17.0
	 *
	 * @param WP_Customize_Manager $wp_customize WP_Customize_Manager instance.
	 */
	public function clear_cache_after_customizer_save( $wp_customize ) {
		$this->clear_cache();
	}

	/**
	 * Clear cache after theme mods update.
	 *
	 * @since 3.17.0
	 *
	 * @param mixed $old_value The old option value.
	 * @param mixed $value     The new option value.
	 */
	public function clear_cache_after_theme_mods_update( $old_value, $value ) {
		$this->clear_cache();
	}

	/**
	 * Clear Hummingbird cache after block theme global styles are updated.
	 *
	 * @since 3.17.0
	 * @param int     $post_id Post ID.
	 * @param WP_Post $post    Post object.
	 * @param bool    $update  Whether this is an existing post being updated.
	 */
	public function clear_cache_after_global_styles_save( $post_id, $post, $update ) {
		if ( 'publish' !== $post->post_status ) {
			return;
		}

		$this->clear_cache();
	}

	/**
	 * Determine if we should cache the request for safemode is enabled.
	 *
	 * @since 3.18.0
	 *
	 * @param bool $safemode_status Current safemode status.
	 *
	 * @return bool
	 */
	private static function should_cache_safemode( $safemode_status ) :bool {
		$preview = filter_input( INPUT_GET, 'wphb_preview_safe_mode', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
		if ( $safemode_status && $preview ) {
			return true;
		}

		return false;
	}

}
