File "class-cache-engine.php"

Full Path: /home/theinspectionboy/public_html/suffolk/includes/class-cache-engine.php
File size: 14.73 KB
MIME-type: text/x-php
Charset: utf-8

<?php
/**
 * SWIS Cache Engine
 *
 * @link https://ewww.io/swis/
 * @package SWIS_Performance
 */

namespace SWIS;

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

/**
 * Caching Engine for both delivering and storing cache files.
 */
final class Cache_Engine {

	/**
	 * Engine status.
	 *
	 * @var bool $started
	 */
	public static $started = false;

	/**
	 * Specific HTTP request headers from current request.
	 *
	 * @var array $request_headers
	 */
	public static $request_headers = array();

	/**
	 * Caching settings.
	 *
	 * @var array $settings
	 */
	public static $settings = array();

	/**
	 * Request URI.
	 *
	 * @var string $request_uri
	 */
	public static $request_uri = '';

	/**
	 * Start the cache engine.
	 *
	 * @return bool True if engine started, false otherwise.
	 */
	public static function start() {
		self::get_request_uri();
		if ( self::should_start() ) {
			new self();
		}
		return self::$started;
	}

	/**
	 * Constructor, does the actual work of firing things up, called by self::start().
	 */
	public function __construct() {
		// Get the request headers.
		self::$request_headers = self::get_request_headers();

		// Get settings from disk if core WP index file.
		if ( self::is_index() ) {
			self::$settings = Disk_Cache::get_settings();
			// Get settings from database otherwise (in late start).
		} elseif ( class_exists( '\SWIS\Cache' ) ) {
			self::$settings = swis()->cache->get_settings();
		}
		self::get_request_uri();

		// Set engine status.
		if ( ! empty( self::$settings ) ) {
			self::debug_message( 'cache engine started' );
			self::$started = true;
		} else {
			self::debug_message( 'cache settings missing' );
		}
	}

	/**
	 * Adds information to the in-memory debug log (wrapper for static class).
	 *
	 * @param string $message Debug information to add to the log.
	 */
	public static function debug_message( $message ) {
		if ( function_exists( 'swis' ) && class_exists( '\SWIS\Cache' ) ) {
			swis()->cache->debug_message( $message );
		}
	}

	/**
	 * Check if engine should start.
	 *
	 * @return bool True if engine should start, false otherwise.
	 */
	public static function should_start() {
		self::debug_message( __METHOD__ );
		// Check if engine is running already.
		if ( self::$started ) {
			self::debug_message( 'cache engine already running' );
			return false;
		}

		// Check if AJAX request in early start.
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX && ! class_exists( '\SWIS\Cache' ) ) {
			self::debug_message( 'AJAX request, and cache class does not exist yet' );
			return false;
		}

		// Check if REST API request.
		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			self::debug_message( 'rest request, not starting' );
			return false;
		}

		// Check if XMLRPC request.
		if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
			self::debug_message( 'xmlrpc request, not starting' );
			return false;
		}

		// Check request URI.
		if ( self::$request_uri && str_replace( array( '.ico', '.txt', '.xml', '.xsl', '.map' ), '', self::$request_uri ) !== self::$request_uri ) {
			self::debug_message( 'disallowed file extension, not starting' );
			return false;
		}

		return true;
	}

	/**
	 * Start output buffering.
	 */
	public static function start_buffering() {
		self::debug_message( 'cache buffer starting' );
		ob_start( 'self::end_buffering' );
	}

	/**
	 * End output buffering and cache page if applicable.
	 *
	 * @param string $contents  Contents of a page from the output buffer.
	 * @param int    $phase Bitmask of PHP_OUTPUT_HANDLER_* constants.
	 * @return string Content of a page from the output buffer.
	 */
	private static function end_buffering( $contents, $phase ) {
		if ( $phase & PHP_OUTPUT_HANDLER_FINAL || $phase & PHP_OUTPUT_HANDLER_END ) {
			if ( self::is_cacheable( $contents ) && ! self::bypass_cache() ) {
				Disk_Cache::cache_page( $contents );
			}
		}
		return $contents;
	}

	/**
	 * Remove any unexpected characters from a given HTTP request header.
	 *
	 * @param string $header The value of an HTTP request header.
	 * @return string The sanitized and unslashed header value.
	 */
	public static function sanitize_header( $header ) {
		$header = trim( preg_replace( '#[^\w\s/=;+,:\*\.\(\)-]#', '', stripslashes( $header ) ) );
		return $header;
	}

	/**
	 * Get needed HTTP request headers from current request.
	 *
	 * @return array A list of HTTP request headers from this request.
	 */
	private static function get_request_headers() {
		$request_headers = ( function_exists( 'apache_request_headers' ) ) ? apache_request_headers() : array();

		$request_headers = array(
			'Accept'             => ( isset( $request_headers['Accept'] ) ) ? $request_headers['Accept'] : ( ( isset( $_SERVER['HTTP_ACCEPT'] ) ) ? $_SERVER['HTTP_ACCEPT'] : '' ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
			'Accept-Encoding'    => ( isset( $request_headers['Accept-Encoding'] ) ) ? $request_headers['Accept-Encoding'] : ( ( isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) ? $_SERVER['HTTP_ACCEPT_ENCODING'] : '' ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
			'Host'               => ( isset( $request_headers['Host'] ) ) ? $request_headers['Host'] : ( ( isset( $_SERVER['HTTP_HOST'] ) ) ? $_SERVER['HTTP_HOST'] : '' ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
			'If-Modified-Since'  => ( isset( $request_headers['If-Modified-Since'] ) ) ? $request_headers['If-Modified-Since'] : ( ( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] : '' ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
			'User-Agent'         => ( isset( $request_headers['User-Agent'] ) ) ? $request_headers['User-Agent'] : ( ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) ? $_SERVER['HTTP_USER_AGENT'] : '' ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
			'X-Forwarded-Proto'  => ( isset( $request_headers['X-Forwarded-Proto'] ) ) ? $request_headers['X-Forwarded-Proto'] : ( ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) ? $_SERVER['HTTP_X_FORWARDED_PROTO'] : '' ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
			'X-Forwarded-Scheme' => ( isset( $request_headers['X-Forwarded-Scheme'] ) ) ? $request_headers['X-Forwarded-Scheme'] : ( ( isset( $_SERVER['HTTP_X_FORWARDED_SCHEME'] ) ) ? $_SERVER['HTTP_X_FORWARDED_SCHEME'] : '' ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
		);

		// Clean things up just to be safe, runs the headers through stripslashes and a very restrictive whitelist of characters.
		array_walk( $request_headers, array( 'self', 'sanitize_header' ) );
		return $request_headers;
	}
	/**
	 * Get request URI.
	 */
	public static function get_request_uri() {
		if ( self::$request_uri ) {
			return;
		}
		if ( isset( $_SERVER['REQUEST_URI'] ) ) {
			self::$request_uri = trim( stripslashes( $_SERVER['REQUEST_URI'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
		}
	}

	/**
	 * Check if installation directory index.
	 *
	 * @return  bool  true if installation directory index, false otherwise
	 */
	private static function is_index() {
		if ( isset( $_SERVER['SCRIPT_NAME'] ) && strtolower( basename( $_SERVER['SCRIPT_NAME'] ) ) === 'index.php' ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
			return true;
		}
		self::debug_message( 'not from index.php' );
		// NOTE: Only keeping this here if we need to debug something.
		if ( false && isset( $_SERVER['SCRIPT_NAME'] ) ) {
			self::debug_message( 'from ' . stripslashes( $_SERVER['SCRIPT_NAME'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
		}
		return false;
	}

	/**
	 * Check if page can be cached.
	 *
	 * @param string $contents Contents of a page from the output buffer.
	 * @return bool True if page contents are cacheable, false otherwise.
	 */
	private static function is_cacheable( $contents ) {
		$has_html_tag       = ( stripos( $contents, '<html' ) !== false );
		$has_html5_doctype  = preg_match( '/^<!DOCTYPE.+html\s*>/i', ltrim( $contents ) );
		$has_xsl_stylesheet = ( stripos( $contents, '<xsl:stylesheet' ) !== false || stripos( $contents, '<?xml-stylesheet' ) !== false );

		if ( $has_html_tag ) {
			self::debug_message( 'has html tag' );
		}
		if ( $has_html5_doctype ) {
			self::debug_message( 'has html DOCTYPE' );
		}
		if ( $has_xsl_stylesheet ) {
			self::debug_message( 'has xsl/xml stylesheet' );
		}
		if ( $has_html_tag && $has_html5_doctype && ! $has_xsl_stylesheet ) {
			return true;
		}

		return false;
	}

	/**
	 * Check permalink structure.
	 *
	 * @return bool True if request URI does not match permalink structure or if plain, false otherwise.
	 */
	private static function is_wrong_permalink_structure() {
		if ( empty( self::$settings ) ) {
			self::debug_message( 'could not check permalink structure, no settings' );
			return false;
		}
		// Check if trailing slash is set and missing (ignoring root index and file extensions).
		if ( 'has_trailing_slash' === self::$settings['permalink_structure'] ) {
			if ( self::$request_uri && preg_match( '/\/[^\.\/\?]+(\?.*)?$/', self::$request_uri ) ) {
				self::debug_message( 'should have trailing slash, but does not' );
				return true;
			}
		}

		// Check if trailing slash is not set and appended (ignoring root index and file extensions).
		if ( 'no_trailing_slash' === self::$settings['permalink_structure'] ) {
			if ( self::$request_uri && preg_match( '/\/[^\.\/\?]+\/(\?.*)?$/', self::$request_uri ) ) {
				self::debug_message( 'should not have trailing slash, but does' );
				return true;
			}
		}

		// Check if custom permalink structure is not set.
		if ( 'plain' === self::$settings['permalink_structure'] ) {
			self::debug_message( 'plain permalinks, no good' );
			return true;
		}

		return false;
	}

	/**
	 * Check if page is excluded from cache.
	 *
	 * @return bool True if page is excluded from the cache, false otherwise.
	 */
	private static function is_excluded() {
		self::debug_message( __METHOD__ );
		// If page path is excluded.
		if ( ! empty( self::$settings['exclusions'] ) && is_array( self::$settings['exclusions'] ) ) {
			$uri = parse_url( self::$request_uri, PHP_URL_PATH );
			foreach ( self::$settings['exclusions'] as $exclusion ) {
				if ( false !== strpos( $uri, $exclusion ) ) {
					self::debug_message( "$uri is excluded by $exclusion" );
					return true;
				}
			}
		}

		// If query string excluded.
		if ( ! empty( $_GET ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			// Set regex matching query strings that should bypass the cache.
			if ( ! empty( self::$settings['excluded_query_strings'] ) ) {
				$query_string_regex = self::$settings['excluded_query_strings'];
			} else {
				$query_string_regex = '/^(?!(fbclid|ref|mc_(cid|eid)|utm_(source|medium|campaign|term|content|expid)|gclid|fb_(action_ids|action_types|source)|age-verified|usqp|cn-reloaded|_ga|_ke)).+$/';
			}

			$query_string = parse_url( self::$request_uri, PHP_URL_QUERY );

			if ( preg_match( $query_string_regex, $query_string ) ) {
				self::debug_message( 'excluded by query string regex' );
				return true;
			}
		}

		// If cookie excluded.
		if ( ! empty( $_COOKIE ) ) {
			// Set regex matching cookies that should bypass the cache.
			if ( ! empty( self::$settings['excluded_cookies'] ) ) {
				$cookies_regex = self::$settings['excluded_cookies'];
			} else {
				$cookies_regex = '/^(wp-postpass|wordpress_logged_in|comment_author|woocommerce_items_in_cart|wp_woocommerce_session)/';
			}
			// Bypass cache if an excluded cookie is found.
			foreach ( $_COOKIE as $key => $value ) {
				if ( preg_match( $cookies_regex, $key ) ) {
					self::debug_message( 'excluded by cookie regex' );
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Check if this is a search page.
	 *
	 * @return bool True if it is, false if it ain't.
	 */
	private static function is_search() {
		if ( apply_filters( 'swis_cache_exclude_search', is_search() ) ) {
			return true;
		}
		return false;
	}

	/**
	 * Check if cache should be bypassed.
	 *
	 * @return bool True if cache should be bypassed, false otherwise.
	 */
	private static function bypass_cache() {
		// Bypass cache hook.
		if ( apply_filters( 'bypass_cache', false ) || apply_filters( 'swis_bypass_cache', false ) ) {
			self::debug_message( 'bypassed by filter' );
			return true;
		}

		// Check request method.
		if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'GET' !== $_SERVER['REQUEST_METHOD'] ) {
			self::debug_message( 'bypassed because request_method !== GET' );
			return true;
		}

		// Check HTTP status code.
		if ( http_response_code() !== 200 ) {
			self::debug_message( 'bypassed because http response code is ' . http_response_code() );
			return true;
		}

		// Check DONOTCACHEPAGE constant.
		if ( defined( 'DONOTCACHEPAGE' ) && DONOTCACHEPAGE ) {
			self::debug_message( 'bypassed via DONOTCACHEPAGE' );
			return true;
		}

		// Check conditional tags.
		if ( self::is_wrong_permalink_structure() || self::is_excluded() ) {
			self::debug_message( 'bypassed by permalink check or exclusion' );
			return true;
		}

		global $wp_query;
		// Check conditional tags when output buffering has ended.
		if ( class_exists( 'WP' ) ) {
			if ( is_admin() || ! isset( $wp_query ) || self::is_search() || is_feed() || is_trackback() || is_robots() || is_preview() || is_customize_preview() || post_password_required() ) {
				if ( is_admin() ) {
					self::debug_message( 'bypassed for is_admin()' );
				}
				if ( self::is_search() ) {
					self::debug_message( 'bypassed for is_search()' );
				}
				if ( is_feed() ) {
					self::debug_message( 'bypassed for is_feed()' );
				}
				if ( is_trackback() ) {
					self::debug_message( 'bypassed for is_trackback()' );
				}
				if ( is_robots() ) {
					self::debug_message( 'bypassed for is_robots()' );
				}
				if ( is_preview() ) {
					self::debug_message( 'bypassed for is_preview()' );
				}
				if ( is_customize_preview() ) {
					self::debug_message( 'bypassed for is_customize_preview()' );
				}
				if ( post_password_required() ) {
					self::debug_message( 'bypassed for post_password_required()' );
				}
				return true;
			}
		}

		return false;
	}


	/**
	 * Deliver cache.
	 *
	 * @return bool False if cached page was not delivered. Dies otherwise.
	 */
	public static function deliver_cache() {
		$cache_file = Disk_Cache::get_cache_file();
		if ( Disk_Cache::cache_exists( $cache_file ) && ! Disk_Cache::cache_expired( $cache_file ) && ! self::bypass_cache() ) {
			header( 'X-Cache-Handler: swis-cache-engine' );

			// Check modified since with cached file and return 304 if no difference.
			if ( ! empty( self::$request_headers['If-Modified-Since'] ) && strtotime( self::$request_headers['If-Modified-Since'] >= filemtime( $cache_file ) ) ) {
				header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? stripslashes( $_SERVER['SERVER_PROTOCOL'] ) : 'HTTP/1.1' ) . ' 304 Not Modified', true, 304 ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
				exit;
			}

			readfile( $cache_file );
			exit;
		}
		return false;
	}
}