oont-contents/plugins/wp-rocket/inc/classes/Buffer/class-cache.php
2025-02-08 15:10:23 +01:00

690 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace WP_Rocket\Buffer;
/**
* Handle page cache.
*
* @since 3.3
*/
class Cache extends Abstract_Buffer {
/**
* Process identifier used by the logger.
*
* @var string
* @since 3.3
*/
protected $process_id = 'caching process';
/**
* Tests instance
*
* @var Tests
*/
protected $tests;
/**
* Config instance
*
* @var Config
*/
private $config;
/**
* Path to the directory containing the cache files.
*
* @var string
* @since 3.3
*/
private $cache_dir_path;
/**
* Constructor.
*
* @since 3.3
*
* @param Tests $tests Tests instance.
* @param Config $config Config instance.
* @param array $args {
* An array of arguments.
*
* @type string $cache_dir_path Path to the directory containing the cache files.
* }
*/
public function __construct( Tests $tests, Config $config, array $args ) {
$this->config = $config;
$this->cache_dir_path = rtrim( $args['cache_dir_path'], '/' ) . '/';
parent::__construct( $tests );
$this->log( 'CACHING PROCESS STARTED.', [], 'info' );
}
/** ----------------------------------------------------------------------------------------- */
/** CACHE =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Serve the cache file if it exists. If not, init the buffer.
*
* @since 3.3
*/
public function maybe_init_process() {
if ( ! $this->tests->can_init_process() ) {
$this->define_donotoptimize_true();
$this->log_last_test_error();
return;
}
/**
* Serve the cache file if it exists.
*/
$cache_filepath = $this->get_cache_path();
$this->log(
'Looking for cache file.',
[
'path' => $cache_filepath,
]
);
$cache_filepath_gzip = $cache_filepath . '_gzip';
$accept_encoding = $this->config->get_server_input( 'HTTP_ACCEPT_ENCODING' );
$accept_gzip = $accept_encoding && false !== strpos( $accept_encoding, 'gzip' );
// Check if cache file exist.
if ( $accept_gzip && is_readable( $cache_filepath_gzip ) ) {
$this->serve_gzip_cache_file( $cache_filepath_gzip );
}
if ( is_readable( $cache_filepath ) ) {
$this->serve_cache_file( $cache_filepath );
}
// Maybe we're looking for a webp file.
$cache_filename = basename( $cache_filepath );
if ( strpos( $cache_filename, '-webp' ) !== false ) {
// We're looking for a webp file that doesn't exist: try to locate any `.no-webp` file.
$cache_dir_path = rtrim( dirname( $cache_filepath ), '/\\' ) . DIRECTORY_SEPARATOR;
if ( file_exists( $cache_dir_path . '.no-webp' ) ) {
// We have a `.no-webp` file: try to deliver a non-webp cache file.
$cache_filepath = $cache_dir_path . str_replace( '-webp', '', $cache_filename );
$cache_filepath_gzip = $cache_filepath . '_gzip';
$this->log(
'Looking for non-webp cache file.',
[
'path' => $cache_filepath,
]
);
// Try to deliver the non-webp version instead.
if ( $accept_gzip && is_readable( $cache_filepath_gzip ) ) {
$this->serve_gzip_cache_file( $cache_filepath_gzip );
}
if ( is_readable( $cache_filepath ) ) {
$this->serve_cache_file( $cache_filepath );
}
}
}
/**
* No cache file yet: launch caching process.
*/
$this->log(
'Start buffer.',
[
'path' => $cache_filepath,
]
);
ob_start( [ $this, 'maybe_process_buffer' ] );
}
/**
* Serve a cache file.
*
* @since 3.3
*
* @param string $cache_filepath Path to the cache file.
*/
private function serve_cache_file( $cache_filepath ) {
header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', filemtime( $cache_filepath ) ) . ' GMT' );
$if_modified_since = $this->get_if_modified_since();
// Checking if the client is validating his cache and if it is current.
if ( $if_modified_since && ( strtotime( $if_modified_since ) === @filemtime( $cache_filepath ) ) ) {
// Client's cache is current, so we just respond '304 Not Modified'.
header( $this->config->get_server_input( 'SERVER_PROTOCOL', '' ) . ' 304 Not Modified', true, 304 );
header( 'Expires: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' );
header( 'Cache-Control: no-cache, must-revalidate' );
$this->log(
'Serving `304` cache file.',
[
'path' => $cache_filepath,
'modified' => $if_modified_since,
],
'info'
);
exit;
}
// Serve the cache if file isn't store in the client browser cache.
readfile( $cache_filepath );
$this->log(
'Serving cache file.',
[
'path' => $cache_filepath,
'modified' => $if_modified_since,
],
'info'
);
exit;
}
/**
* Serve a gzipped cache file.
*
* @since 3.3
*
* @param string $cache_filepath Path to the gzip cache file.
*/
private function serve_gzip_cache_file( $cache_filepath ) {
header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', filemtime( $cache_filepath ) ) . ' GMT' );
$if_modified_since = $this->get_if_modified_since();
// Checking if the client is validating his cache and if it is current.
if ( $if_modified_since && ( strtotime( $if_modified_since ) === @filemtime( $cache_filepath ) ) ) {
// Client's cache is current, so we just respond '304 Not Modified'.
header( $this->config->get_server_input( 'SERVER_PROTOCOL', '' ) . ' 304 Not Modified', true, 304 );
header( 'Expires: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' );
header( 'Cache-Control: no-cache, must-revalidate' );
$this->log(
'Serving `304` gzip cache file.',
[
'path' => $cache_filepath,
'modified' => $if_modified_since,
],
'info'
);
exit;
}
// Serve the cache if file isn't store in the client browser cache.
readgzfile( $cache_filepath );
$this->log(
'Serving gzip cache file.',
[
'path' => $cache_filepath,
'modified' => $if_modified_since,
],
'info'
);
exit;
}
/**
* Maybe cache the page content.
*
* @since 3.3
*
* @param string $buffer The buffer content.
* @return string The buffered content.
*/
public function maybe_process_buffer( $buffer ) {
if ( ! $this->tests->can_process_buffer( $buffer ) ) {
$this->log_last_test_error();
return $buffer;
}
$footprint = '';
$is_html = $this->is_html( $buffer );
if ( ! static::can_generate_caching_files() ) {
// Not allowed to generate cache files.
if ( $is_html ) {
$footprint = $this->get_rocket_footprint();
}
$this->log(
'Page not cached by filter.',
[
'filter' => 'do_rocket_generate_caching_files',
]
);
return $buffer . $footprint;
}
$webp_enabled = preg_match( '@<!-- Rocket (has|no) webp -->@', $buffer, $webp_tag );
$has_webp = ! empty( $webp_tag ) ? 'has' === $webp_tag[1] : false;
$cache_filepath = $this->get_cache_path( [ 'webp' => $has_webp ] );
$cache_dir_path = dirname( $cache_filepath );
// Create cache folders.
rocket_mkdir_p( $cache_dir_path );
if ( $is_html ) {
$footprint = $this->get_rocket_footprint( time() );
}
// Webp request.
if ( $webp_enabled ) {
$buffer = str_replace( $webp_tag[0], '', $buffer );
if ( ! $has_webp ) {
// The buffer doesnt contain webp files.
$cache_dir_path = rtrim( dirname( $cache_filepath ), '/\\' );
$this->maybe_create_nowebp_file( $cache_dir_path );
}
}
$this->write_cache_file( $cache_filepath, $buffer . $footprint );
$this->maybe_create_nginx_mobile_file( $cache_dir_path );
// Send headers with the last modified time of the cache file.
if ( file_exists( $cache_filepath ) ) {
header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', filemtime( $cache_filepath ) ) . ' GMT' );
}
if ( $is_html ) {
$footprint = $this->get_rocket_footprint();
}
$this->log(
'Page cached.',
[
'path' => $cache_filepath,
],
'info'
);
return $buffer . $footprint;
}
/**
* Writes the cache file(s)
*
* @since 3.5
*
* @param string $cache_filepath Absolute path to the cache file.
* @param string $content Content to write in the cache file.
* @return void
*/
private function write_cache_file( $cache_filepath, $content ) {
$gzip_filepath = $cache_filepath . '_gzip';
$temp_filepath = $cache_filepath . '_temp';
$temp_gzip_filepath = $gzip_filepath . '_temp';
if ( rocket_direct_filesystem()->exists( $temp_filepath ) ) {
return;
}
// Save the cache file.
if ( ! rocket_put_content( $temp_filepath, $content ) ) {
return;
}
rocket_direct_filesystem()->move( $temp_filepath, $cache_filepath, true );
if ( function_exists( 'gzencode' ) ) {
/**
* Filters the Gzip compression level to use for the cache file
*
* @param int $compression_level Compression level between 0 and 9.
*/
$compression_level = apply_filters( 'rocket_gzencode_level_compression', 6 );
if ( ! rocket_put_content( $temp_gzip_filepath, gzencode( $content, $compression_level ) ) ) {
return;
}
rocket_direct_filesystem()->move( $temp_gzip_filepath, $gzip_filepath, true );
}
}
/**
* Get the path to the cache file.
*
* @since 3.3
*
* @param array $args {
* A list of arguments.
*
* @type bool $webp Set to false to prevent adding the part related to webp.
* }
* @return string
*/
public function get_cache_path( $args = [] ) {
$args = array_merge(
[
'webp' => true,
],
$args
);
$cookies = $this->tests->get_cookies();
$request_uri_path = $this->get_request_cache_path( $cookies );
$filename = 'index';
$filename = $this->maybe_mobile_filename( $filename );
// Rename the caching filename for SSL URLs.
if ( is_ssl() && $this->config->get_config( 'cache_ssl' ) ) {
$filename .= '-https';
}
if ( $args['webp'] ) {
$filename = $this->maybe_webp_filename( $filename );
}
$filename = $this->maybe_dynamic_cookies_filename( $filename, $cookies );
// Ensure proper formatting of the path.
$request_uri_path = preg_replace_callback( '/%[0-9A-F]{2}/', [ $this, 'reset_lowercase' ], $request_uri_path );
// Directories in Windows can't contain question marks.
$request_uri_path = str_replace( '?', '#', $request_uri_path );
// Limit filename max length to 255 characters.
$request_uri_path .= '/' . substr( $filename, 0, 250 ) . '.html';
return $request_uri_path;
}
/** ----------------------------------------------------------------------------------------- */
/** VARIOUS TOOLS =========================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Declares and sets value of constant preventing Optimizations.
*
* @since 3.3
*/
private function define_donotoptimize_true() {
if ( ! defined( 'DONOTROCKETOPTIMIZE' ) ) {
define( 'DONOTROCKETOPTIMIZE', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals
}
}
/**
* Gets If-modified-since header value
*
* @since 3.3
* @return string
*/
private function get_if_modified_since() {
if ( function_exists( 'apache_request_headers' ) ) {
$headers = apache_request_headers();
return isset( $headers['If-Modified-Since'] ) ? $headers['If-Modified-Since'] : '';
}
return $this->config->get_server_input( 'HTTP_IF_MODIFIED_SINCE', '' );
}
/**
* Get WP Rocket footprint
*
* @since 3.0.5 White label footprint if WP_ROCKET_WHITE_LABEL_FOOTPRINT is defined.
* @since 2.0
*
* @param int $time UNIX timestamp when the cache file was saved.
* @return string The footprint that will be printed
*/
private function get_rocket_footprint( $time = '' ) {
$footprint = defined( 'WP_ROCKET_WHITE_LABEL_FOOTPRINT' ) ?
"\n" . '<!-- Cached for great performance' :
"\n" . '<!-- This website is like a Rocket, isn\'t it? Performance optimized by ' . WP_ROCKET_PLUGIN_NAME . '. Learn more: https://wp-rocket.me';
if ( ! empty( $time ) ) {
$footprint .= ' - Debug: cached@' . $time;
}
$footprint .= ' -->';
return $footprint;
}
/**
* Create a hidden empty file for mobile detection on NGINX with the Rocket NGINX configuration.
*
* @param string $cache_dir_path Path to the current cache directory.
* @return void
*/
private function maybe_create_nginx_mobile_file( $cache_dir_path ) {
global $is_nginx;
if ( ! $this->config->get_config( 'do_caching_mobile_files' ) ) {
return;
}
if ( ! $is_nginx ) {
return;
}
$nginx_mobile_detect = $cache_dir_path . '/.mobile-active';
if ( rocket_direct_filesystem()->exists( $nginx_mobile_detect ) ) {
return;
}
rocket_direct_filesystem()->touch( $nginx_mobile_detect );
}
/**
* Create a hidden empty file when webp is enabled but the buffer doesnt contain webp files.
*
* @since 3.4
*
* @param string $cache_dir_path Path to the current cache directory (without trailing slah).
*/
private function maybe_create_nowebp_file( $cache_dir_path ) {
$nowebp_filepath = $cache_dir_path . DIRECTORY_SEPARATOR . '.no-webp';
if ( rocket_direct_filesystem()->exists( $nowebp_filepath ) ) {
return;
}
rocket_direct_filesystem()->touch( $nowebp_filepath );
}
/**
* Tell if generating cache files is allowed.
*
* @since 3.3
*
* @return bool
*/
public static function can_generate_caching_files() {
/**
* Allow to the generate the caching file.
*
* @since 2.5
*
* @param bool True will force the cache file generation.
*/
return (bool) apply_filters( 'do_rocket_generate_caching_files', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals
}
/**
* Gets the base cache path for the current request
*
* @since 3.3
*
* @param array $cookies Cookies for the current request.
* @return string
*/
private function get_request_cache_path( $cookies ) {
$host = $this->config->get_host();
if ( $this->config->get_config( 'url_no_dots' ) ) {
$host = str_replace( '.', '_', $host );
}
$request_uri = $this->tests->get_clean_request_uri();
$cookie_hash = $this->config->get_config( 'cookie_hash' );
$logged_in_cookie = $this->config->get_config( 'logged_in_cookie' );
$logged_in_cookie_no_hash = str_replace( $cookie_hash, '', $logged_in_cookie );
// Get cache folder of host name.
if ( $logged_in_cookie && isset( $cookies[ $logged_in_cookie ] ) && ! $this->tests->has_rejected_cookie( $logged_in_cookie_no_hash ) ) {
if ( $this->config->get_config( 'common_cache_logged_users' ) ) {
return $this->cache_dir_path . $host . '-loggedin-' . $this->config->get_config( 'secret_cache_key' ) . rtrim( $request_uri, '/' );
}
$user_key = explode( '|', $cookies[ $logged_in_cookie ] );
$user_key = reset( $user_key );
$user_key = $this->sanitize_key( $user_key . '-' . $this->config->get_config( 'secret_cache_key' ) );
// Get cache folder of host name.
return $this->cache_dir_path . $host . '-' . $user_key . rtrim( $request_uri, '/' );
}
return $this->cache_dir_path . $host . rtrim( $request_uri, '/' );
}
/**
* Modifies the filename if the request is from a mobile device.
*
* @since 3.3
*
* @param string $filename Cache filename.
* @return string
*/
private function maybe_mobile_filename( $filename ) {
$cache_mobile_files_tablet = $this->config->get_config( 'cache_mobile_files_tablet' );
if ( ! ( $this->config->get_config( 'cache_mobile' ) && $this->config->get_config( 'do_caching_mobile_files' ) ) ) {
return $filename;
}
if ( ! $cache_mobile_files_tablet ) {
return $filename;
}
if ( ! class_exists( 'WP_Rocket_Mobile_Detect' ) ) {
return $filename;
}
$detect = new \WP_Rocket_Mobile_Detect();
if ( $detect->isMobile() && ! $detect->isTablet() && 'desktop' === $cache_mobile_files_tablet || ( $detect->isMobile() || $detect->isTablet() ) && 'mobile' === $cache_mobile_files_tablet ) {
return $filename .= '-mobile';
}
return $filename;
}
/**
* Modifies the filename if the request is WebP compatible
*
* @since 3.4
*
* @param string $filename Cache filename.
* @return string
*/
private function maybe_webp_filename( $filename ) {
if ( ! $this->config->get_config( 'cache_webp' ) ) {
return $filename;
}
/**
* Force WP Rocket to disable its webp cache.
*
* @since 3.4
*
* @param bool $disable_webp_cache Set to true to disable the webp cache.
*/
$disable_webp_cache = apply_filters( 'rocket_disable_webp_cache', false );
if ( $disable_webp_cache ) {
return $filename;
}
$http_accept = $this->config->get_server_input( 'HTTP_ACCEPT', '' );
if ( ! $http_accept && function_exists( 'apache_request_headers' ) ) {
$headers = apache_request_headers();
$http_accept = isset( $headers['Accept'] ) ? $headers['Accept'] : '';
}
if ( ! $http_accept || false === strpos( $http_accept, 'webp' ) ) {
if ( preg_match( '#Firefox/(?<version>[0-9]{2})#i', $this->config->get_server_input( 'HTTP_USER_AGENT' ), $matches ) ) {
if ( 66 <= (int) $matches['version'] ) {
return $filename . '-webp';
}
}
return $filename;
}
return $filename . '-webp';
}
/**
* Modifies the filename if dynamic cookies are set
*
* @param string $filename Cache filename.
* @param array $cookies Cookies for the request.
* @return string
*/
private function maybe_dynamic_cookies_filename( $filename, $cookies ) {
$cache_dynamic_cookies = $this->config->get_config( 'cache_dynamic_cookies' );
if ( ! $cache_dynamic_cookies ) {
return $filename;
}
foreach ( $cache_dynamic_cookies as $key => $cookie_name ) {
if ( is_array( $cookie_name ) ) {
if ( isset( $_COOKIE[ $key ] ) ) {
foreach ( $cookie_name as $cookie_key ) {
if ( '' !== $cookies[ $key ][ $cookie_key ] ) {
$cache_key = $cookies[ $key ][ $cookie_key ];
$cache_key = preg_replace( '/[^a-z0-9_\-]/i', '-', $cache_key );
$filename .= '-' . $cache_key;
}
}
}
continue;
}
if ( isset( $cookies[ $cookie_name ] ) && '' !== $cookies[ $cookie_name ] ) {
$cache_key = $cookies[ $cookie_name ];
$cache_key = preg_replace( '/[^a-z0-9_\-]/i', '-', $cache_key );
$filename .= '-' . $cache_key;
}
}
return $filename;
}
/**
* Force lowercase on encoded url strings from different alphabets to prevent issues on some hostings.
*
* @since 3.3
*
* @param array $matches Cache path.
* @return string Cache path in lowercase.
*/
protected function reset_lowercase( $matches ) {
return strtolower( $matches[0] );
}
/**
* Sanitizes a string key.
*
* @param string $key String key.
*
* @return string
*/
private function sanitize_key( string $key ): string {
$sanitized_key = '';
$sanitized_key = strtolower( $key );
return preg_replace( '/[^a-z0-9_\-]/', '', $sanitized_key );
}
}