initial
This commit is contained in:
+235
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache;
|
||||
|
||||
use Automattic\Jetpack_Boost\Contracts\Has_Activate;
|
||||
use Automattic\Jetpack_Boost\Contracts\Has_Deactivate;
|
||||
use Automattic\Jetpack_Boost\Contracts\Is_Always_On;
|
||||
use Automattic\Jetpack_Boost\Contracts\Sub_Feature;
|
||||
use Automattic\Jetpack_Boost\Lib\Cornerstone\Cornerstone_Utils;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Logger;
|
||||
|
||||
/**
|
||||
* Class Cache_Preload
|
||||
*
|
||||
* Handles the rebuilding/preloading of cache for pages, currently only for Cornerstone Pages.
|
||||
* This module automagically preloads the cache after cache invalidation events such as rebuilds, or when
|
||||
* Cornerstone Pages are updated, to ensure that important pages always have a cache.
|
||||
*
|
||||
* @since 3.11.0
|
||||
* @package Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache
|
||||
*/
|
||||
class Cache_Preload implements Sub_Feature, Has_Activate, Has_Deactivate, Is_Always_On {
|
||||
|
||||
/**
|
||||
* @since 3.11.0
|
||||
*/
|
||||
public function setup() {
|
||||
add_action( 'update_option_jetpack_boost_ds_cornerstone_pages_list', array( $this, 'schedule_cornerstone' ) );
|
||||
add_action( 'jetpack_boost_preload_cornerstone', array( $this, 'preload_cornerstone' ) );
|
||||
add_action( 'jetpack_boost_preload', array( $this, 'preload' ) );
|
||||
|
||||
add_action( 'post_updated', array( $this, 'handle_post_update' ) );
|
||||
add_action( 'jetpack_boost_invalidate_cache_success', array( $this, 'handle_cache_invalidation' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.11.0
|
||||
*/
|
||||
public static function get_slug() {
|
||||
return 'cache_preload';
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.11.0
|
||||
*/
|
||||
public static function is_available() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* As this is a submodule, this activate is triggered when the parent module is activated,
|
||||
* despite the module having Is_Always_On.
|
||||
*
|
||||
* @since 3.12.0
|
||||
*/
|
||||
public static function activate() {
|
||||
$instance = new self();
|
||||
$instance->schedule_cornerstone_cronjob();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @since 3.12.0
|
||||
*/
|
||||
public static function deactivate() {
|
||||
wp_unschedule_hook( 'jetpack_boost_preload_cornerstone' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule the cronjob to preload the cache for Cornerstone Pages.
|
||||
*
|
||||
* @since 3.12.0
|
||||
* @return void
|
||||
*/
|
||||
public function schedule_cornerstone_cronjob() {
|
||||
if ( ! wp_next_scheduled( 'jetpack_boost_preload_cornerstone' ) ) {
|
||||
wp_schedule_event( time(), 'twicehourly', 'jetpack_boost_preload_cornerstone' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule Preload for all Cornerstone Pages.
|
||||
*
|
||||
* This method is triggered when the Cornerstone Pages list is updated,
|
||||
* ensuring all Cornerstone Pages have their cache rebuilt.
|
||||
*
|
||||
* @since 3.12.0
|
||||
* @return void
|
||||
*/
|
||||
public function schedule_cornerstone() {
|
||||
wp_schedule_single_event( time(), 'jetpack_boost_preload_cornerstone' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a rebuild for the given URLs.
|
||||
*
|
||||
* @since 3.12.0
|
||||
* @param array $urls The URLs of the Cornerstone Pages to rebuild.
|
||||
* @return void
|
||||
*/
|
||||
public function schedule( array $urls ) {
|
||||
Logger::debug( sprintf( 'Scheduling preload for %d pages', count( $urls ) ) );
|
||||
wp_schedule_single_event( time(), 'jetpack_boost_preload', array( $urls ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the cache for all Cornerstone Pages.
|
||||
*
|
||||
* @since 3.12.0
|
||||
* @return void
|
||||
*/
|
||||
public function preload_cornerstone() {
|
||||
$urls = Cornerstone_Utils::get_list();
|
||||
$this->preload( $urls );
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the cache for the given URLs.
|
||||
*
|
||||
* @since 3.12.0
|
||||
* @param array $urls The URLs of the pages to preload.
|
||||
* @return void
|
||||
*/
|
||||
public function preload( array $urls ) {
|
||||
Logger::debug( sprintf( 'Preload started for %d pages', count( $urls ) ) );
|
||||
|
||||
// Pause the hook here to avoid an infinite loop of: invalidation → preload → invalidation.
|
||||
remove_action( 'jetpack_boost_invalidate_cache_success', array( $this, 'handle_cache_invalidation' ) );
|
||||
$boost_cache = new Boost_Cache();
|
||||
foreach ( $urls as $url ) {
|
||||
$boost_cache->rebuild_page( $url );
|
||||
$this->request_page( $url );
|
||||
}
|
||||
add_action( 'jetpack_boost_invalidate_cache_success', array( $this, 'handle_cache_invalidation' ), 10, 3 );
|
||||
|
||||
Logger::debug( sprintf( 'Preload completed for %d pages', count( $urls ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the pages scheduled for preload.
|
||||
*
|
||||
* @since 3.11.0
|
||||
* @param array $posts The posts to preload.
|
||||
* @return void
|
||||
*/
|
||||
public function request_pages( $posts ) {
|
||||
if ( empty( $posts ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $posts as $url ) {
|
||||
$this->request_page( $url );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP request to the specified URL to generate a fresh cache entry.
|
||||
*
|
||||
* @since 3.11.0
|
||||
* @param string $page The URL of the page to preload.
|
||||
* @return void
|
||||
*/
|
||||
private function request_page( string $page ) {
|
||||
// Add a cache-busting header to ensure our response is fresh.
|
||||
$args = array(
|
||||
'headers' => array(
|
||||
'Cache-Control' => 'no-cache, no-store, must-revalidate, max-age=0',
|
||||
'Pragma' => 'no-cache',
|
||||
'Expires' => '0',
|
||||
),
|
||||
);
|
||||
|
||||
$response = wp_remote_get( $page, $args );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
Logger::debug( 'Error preloading page: ' . $response->get_error_message() );
|
||||
return;
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code( $response );
|
||||
if ( $status_code !== 200 ) {
|
||||
Logger::debug( sprintf( 'Error preloading page %s: HTTP status code %d', $page, $status_code ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle post updates to check if the post is a cornerstone page and schedule preload if needed.
|
||||
*
|
||||
* @since 3.11.0
|
||||
* @param int $post_id The ID of the post being updated.
|
||||
* @return void
|
||||
*/
|
||||
public function handle_post_update( int $post_id ) {
|
||||
if ( Cornerstone_Utils::is_cornerstone_page( $post_id ) ) {
|
||||
$this->schedule( array( get_permalink( $post_id ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle cache invalidation events to schedule preloading for affected pages.
|
||||
*
|
||||
* If cache for Cornerstone Pages is invalidated, this method schedules those pages
|
||||
* for preloading to ensure they have fresh cache.
|
||||
*
|
||||
* @since 3.11.0
|
||||
* @param string $path The path that was invalidated.
|
||||
* @param string $type The type of invalidation that occurred (rebuild or delete).
|
||||
* @param string $scope The scope of the invalidation (page or recursive).
|
||||
* @return void
|
||||
*/
|
||||
public function handle_cache_invalidation( string $path, string $type, string $scope ) {
|
||||
if ( $path === home_url() && $scope === 'recursive' ) {
|
||||
Logger::debug( 'Invalidating all files, scheduling preload for all Cornerstone Pages.' );
|
||||
// If the cache is invalidated for all files, schedule preload for all Cornerstone Pages.
|
||||
$this->schedule_cornerstone();
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise identify if a Cornerstone Page cache file is being deleted and schedule preload that page if it is.
|
||||
Logger::debug( 'Invalidating a specific page, scheduling preload for that page if it is a Cornerstone Page.' );
|
||||
$cornerstone_pages = Cornerstone_Utils::get_list();
|
||||
$cornerstone_pages = array_map( 'untrailingslashit', $cornerstone_pages );
|
||||
// If the $path is in the Cornerstone Page list, add it to the preload list.
|
||||
if ( in_array( untrailingslashit( $path ), $cornerstone_pages, true ) ) {
|
||||
$this->schedule( array( $path ) );
|
||||
}
|
||||
}
|
||||
|
||||
public static function get_parent_features(): array {
|
||||
return array(
|
||||
Page_Cache::class,
|
||||
);
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache;
|
||||
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Logger;
|
||||
|
||||
class Garbage_Collection {
|
||||
const ACTION = 'jetpack_boost_cache_garbage_collection';
|
||||
|
||||
/**
|
||||
* Register hooks.
|
||||
*/
|
||||
public static function setup() {
|
||||
add_action( self::ACTION, array( self::class, 'garbage_collect' ) );
|
||||
|
||||
// Clear old log files when garbage collection is run. Do not pass the $older_than parameter to the method as it's not supported.
|
||||
add_action( self::ACTION, array( Logger::class, 'delete_old_logs' ), 10, 0 );
|
||||
}
|
||||
|
||||
public static function schedule_single_garbage_collection() {
|
||||
$older_than = time();
|
||||
wp_schedule_single_event( $older_than, self::ACTION, array( 'older_than' => $older_than ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Garbage collect expired files.
|
||||
*
|
||||
* @param int|null $older_than The timestamp before which files should be deleted. If not provided, the files older than default cache duration will be deleted.
|
||||
*/
|
||||
public static function garbage_collect( $older_than = null ) {
|
||||
$cache_ttl = JETPACK_BOOST_CACHE_DURATION;
|
||||
|
||||
/*
|
||||
* If an $older_than value is specified, use it to calculate the cache TTL.
|
||||
* By specifying $older_than, you can instruct garbage collection to apply on files created before a certain point in time.
|
||||
* This ensures garbage collection is not clearing files that were created after the request was made. Useful to avoid race conditions.
|
||||
*/
|
||||
if ( $older_than !== null ) {
|
||||
$cache_ttl = time() - $older_than;
|
||||
}
|
||||
|
||||
$cache = new Boost_Cache();
|
||||
$cache->get_storage()->garbage_collect( $cache_ttl );
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the garbage collection cron job.
|
||||
*/
|
||||
public static function activate() {
|
||||
self::setup();
|
||||
|
||||
if ( ! wp_next_scheduled( self::ACTION ) ) {
|
||||
wp_schedule_event( time(), 'hourly', self::ACTION );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the garbage collection cron job.
|
||||
*/
|
||||
public static function deactivate() {
|
||||
wp_clear_scheduled_hook( self::ACTION );
|
||||
}
|
||||
}
|
||||
+449
@@ -0,0 +1,449 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache;
|
||||
|
||||
use Automattic\Jetpack_Boost\Lib\Analytics;
|
||||
use Automattic\Jetpack_Boost\Lib\Super_Cache_Config_Compatibility;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache_Error;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache_Settings;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Filesystem_Utils;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Logger;
|
||||
|
||||
class Page_Cache_Setup {
|
||||
|
||||
private static $notices = array();
|
||||
|
||||
/**
|
||||
* Runs setup steps and returns whether setup was successful or not.
|
||||
*
|
||||
* @return bool|\WP_Error
|
||||
*/
|
||||
public static function run_setup() {
|
||||
// Steps that are only for cache system verification. They don't change anything.
|
||||
$verification_steps = array(
|
||||
'verify_wp_content_writable',
|
||||
'verify_permalink_setting',
|
||||
);
|
||||
|
||||
foreach ( $verification_steps as $step ) {
|
||||
$result = self::run_step( $step );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Steps that may change something to setup the cache system.
|
||||
* Each of them should return the result in following format:
|
||||
* - true if the step was successful and changes were made
|
||||
* - false if the step was successful but no changes were made
|
||||
* - WP_Error if the step failed
|
||||
*/
|
||||
$setup_steps = array(
|
||||
'create_settings_file',
|
||||
'create_advanced_cache',
|
||||
'add_wp_cache_define',
|
||||
'enable_caching',
|
||||
);
|
||||
|
||||
$changes_made = false;
|
||||
foreach ( $setup_steps as $step ) {
|
||||
$result = self::run_step( $step );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ( $result === true ) {
|
||||
$changes_made = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $changes_made ) {
|
||||
Analytics::record_user_event( 'page_cache_setup_succeeded' );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function get_notices() {
|
||||
return self::$notices;
|
||||
}
|
||||
|
||||
private static function add_notice( $title, $message ) {
|
||||
self::$notices[] = array(
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
);
|
||||
}
|
||||
|
||||
private static function run_step( $step ) {
|
||||
$result = self::$step();
|
||||
|
||||
if ( $result instanceof Boost_Cache_Error ) {
|
||||
Analytics::record_user_event( 'page_cache_setup_failed', array( 'error_code' => $result->get_error_code() ) );
|
||||
return $result->to_wp_error();
|
||||
}
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
Analytics::record_user_event( 'page_cache_setup_failed', array( 'error_code' => $result->get_error_code() ) );
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable caching step of setup.
|
||||
*
|
||||
* @return Boost_Cache_Error|bool - True on success, false if it was already enabled, error otherwise.
|
||||
*/
|
||||
private static function enable_caching() {
|
||||
$settings = Boost_Cache_Settings::get_instance();
|
||||
$previous_value = $settings->get_enabled();
|
||||
|
||||
if ( $previous_value === true ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$enabled_result = $settings->set( array( 'enabled' => true ) );
|
||||
|
||||
if ( $enabled_result === true ) {
|
||||
Logger::debug( 'Caching enabled in cache config' );
|
||||
}
|
||||
|
||||
return $enabled_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the wp-content directory is writeable.
|
||||
*/
|
||||
private static function verify_wp_content_writable() {
|
||||
$filename = WP_CONTENT_DIR . '/' . uniqid() . '.txt';
|
||||
$result = @file_put_contents( $filename, 'test' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents, WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
wp_delete_file( $filename );
|
||||
|
||||
if ( $result === false ) {
|
||||
return new \WP_Error( 'wp-content-not-writable' );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if WordPress is using a proper permalink setup. WP_Error if not.
|
||||
*/
|
||||
private static function verify_permalink_setting() {
|
||||
global $wp_rewrite;
|
||||
|
||||
if ( ! $wp_rewrite || ! $wp_rewrite->using_permalinks() ) {
|
||||
return new \WP_Error( 'not-using-permalinks' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a settings file, if one does not already exist.
|
||||
*
|
||||
* @return bool|\WP_Error - True if the file was created, WP_Error if there was a problem, or false if the file already exists.
|
||||
*/
|
||||
private static function create_settings_file() {
|
||||
$result = Boost_Cache_Settings::get_instance()->create_settings_file();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the advanced-cache.php file.
|
||||
*
|
||||
* @return string The full path to the advanced-cache.php file.
|
||||
*/
|
||||
public static function get_advanced_cache_path() {
|
||||
return WP_CONTENT_DIR . '/advanced-cache.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the advanced-cache.php file.
|
||||
*
|
||||
* Returns true if the files were setup correctly, or WP_Error if there was a problem.
|
||||
*
|
||||
* @return bool|\WP_Error
|
||||
*/
|
||||
private static function create_advanced_cache() {
|
||||
$advanced_cache_filename = self::get_advanced_cache_path();
|
||||
|
||||
if ( file_exists( $advanced_cache_filename ) ) {
|
||||
$content = file_get_contents( $advanced_cache_filename ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
|
||||
if ( strpos( $content, 'WP SUPER CACHE' ) !== false ) {
|
||||
// advanced-cache.php is already in use by WP Super Cache.
|
||||
|
||||
if ( Super_Cache_Config_Compatibility::is_compatible() ) {
|
||||
$deactivation = new Data_Sync_Actions\Deactivate_WPSC();
|
||||
$deactivation->handle();
|
||||
self::add_notice(
|
||||
__( 'WP Super Cache Has Been Deactivated', 'jetpack-boost' ),
|
||||
__( 'To ensure optimal performance, WP Super Cache has been automatically deactivated because Jetpack Boost\'s Cache is now active. Only one caching system can be used at a time.', 'jetpack-boost' )
|
||||
);
|
||||
|
||||
Analytics::record_user_event(
|
||||
'switch_to_boost_cache',
|
||||
array(
|
||||
'type' => 'silent',
|
||||
'reason' => 'super_cache_compatible',
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return new \WP_Error( 'advanced-cache-for-super-cache' );
|
||||
}
|
||||
} elseif ( strpos( $content, Page_Cache::ADVANCED_CACHE_SIGNATURE ) === false ) {
|
||||
// advanced-cache.php is in use by another plugin.
|
||||
return new \WP_Error( 'advanced-cache-incompatible' );
|
||||
}
|
||||
|
||||
if ( strpos( $content, Page_Cache::ADVANCED_CACHE_VERSION ) !== false ) {
|
||||
// The advanced-cache.php file belongs to current version of Boost Cache.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$plugin_dir_name = untrailingslashit( str_replace( JETPACK_BOOST_PLUGIN_FILENAME, '', JETPACK_BOOST_PLUGIN_BASE ) );
|
||||
$boost_cache_filename = WP_CONTENT_DIR . '/plugins/' . $plugin_dir_name . '/app/modules/optimizations/page-cache/pre-wordpress/class-boost-cache.php';
|
||||
if ( ! file_exists( $boost_cache_filename ) ) {
|
||||
return new \WP_Error( 'boost-cache-file-not-found' );
|
||||
}
|
||||
$contents = '<?php
|
||||
// ' . Page_Cache::ADVANCED_CACHE_SIGNATURE . ' - ' . Page_Cache::ADVANCED_CACHE_VERSION . '
|
||||
if ( ! file_exists( \'' . $boost_cache_filename . '\' ) ) {
|
||||
return;
|
||||
}
|
||||
require_once( \'' . $boost_cache_filename . '\');
|
||||
$boost_cache = new Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache();
|
||||
$boost_cache->init_actions();
|
||||
$boost_cache->serve();
|
||||
';
|
||||
|
||||
$write_advanced_cache = Filesystem_Utils::write_to_file( $advanced_cache_filename, $contents );
|
||||
if ( $write_advanced_cache instanceof Boost_Cache_Error ) {
|
||||
return new \WP_Error( 'unable-to-write-to-advanced-cache', $write_advanced_cache->get_error_code() );
|
||||
}
|
||||
self::clear_opcache( $advanced_cache_filename );
|
||||
|
||||
Logger::debug( 'Advanced cache file created' );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the WP_CACHE define to wp-config.php
|
||||
*/
|
||||
private static function add_wp_cache_define() {
|
||||
$config_file = self::find_wp_config();
|
||||
if ( $config_file === false ) {
|
||||
return new \WP_Error( 'wp-config-not-found' );
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
$content = file_get_contents( $config_file );
|
||||
if ( preg_match( '#^\s*(define\s*\(\s*[\'"]WP_CACHE[\'"]|const\s+WP_CACHE\s*=)#m', $content ) === 1 ) {
|
||||
/*
|
||||
* wp-settings.php checks "if ( WP_CACHE )" so it may be truthy and
|
||||
* not === true to pass that check.
|
||||
* Later, it is defined as false in default-constants.php, but
|
||||
* it may have been defined manually as true using "true", 1, or "1"
|
||||
* in wp-config.php.
|
||||
*/
|
||||
if ( defined( 'WP_CACHE' ) && ! WP_CACHE ) {
|
||||
return new \WP_Error( 'wp-cache-defined-not-true' );
|
||||
}
|
||||
|
||||
return false; // WP_CACHE already added.
|
||||
}
|
||||
$content = preg_replace(
|
||||
'#^<\?php#',
|
||||
'<?php
|
||||
define( \'WP_CACHE\', true ); // ' . Page_Cache::ADVANCED_CACHE_SIGNATURE,
|
||||
$content
|
||||
);
|
||||
|
||||
$result = self::write_to_file_direct( $config_file, $content );
|
||||
if ( $result === false ) {
|
||||
return new \WP_Error( 'wp-config-not-writable' );
|
||||
}
|
||||
self::clear_opcache( $config_file );
|
||||
|
||||
Logger::debug( 'WP_CACHE constant added to wp-config.php' );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if page cache can be run or not.
|
||||
*
|
||||
* @return bool True if the advanced-cache.php file doesn't exist or belongs to Boost, false otherwise.
|
||||
*/
|
||||
public static function can_run_cache() {
|
||||
$advanced_cache_path = self::get_advanced_cache_path();
|
||||
if ( ! file_exists( $advanced_cache_path ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$content = file_get_contents( $advanced_cache_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
return strpos( $content, Page_Cache::ADVANCED_CACHE_SIGNATURE ) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the advanced-cache.php file and the WP_CACHE define from wp-config.php
|
||||
* Fired when the plugin is deactivated.
|
||||
*/
|
||||
public static function deactivate() {
|
||||
$advanced_cache_deleted = self::delete_advanced_cache();
|
||||
// Only remove constant if Boost was the last to run caching.
|
||||
// This is to avoid breaking caching for other plugins.
|
||||
if ( $advanced_cache_deleted ) {
|
||||
self::delete_wp_cache_constant();
|
||||
} else {
|
||||
self::cleanup_wp_cache_constant();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the boost-cache directory, removing all cached files and the config file.
|
||||
* Fired when the plugin is uninstalled.
|
||||
*/
|
||||
public static function uninstall() {
|
||||
self::deactivate();
|
||||
// Call the Cache Preload module deactivation here to ensure it's cleaned up properly.
|
||||
Cache_Preload::deactivate();
|
||||
$result = Filesystem_Utils::delete_directory( WP_CONTENT_DIR . '/boost-cache' );
|
||||
if ( $result instanceof Boost_Cache_Error ) {
|
||||
return $result->to_wp_error();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the file advanced-cache.php if it exists.
|
||||
*/
|
||||
public static function delete_advanced_cache() {
|
||||
$advanced_cache_filename = self::get_advanced_cache_path();
|
||||
|
||||
if ( ! file_exists( $advanced_cache_filename ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$deleted_file = false;
|
||||
$content = file_get_contents( $advanced_cache_filename ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
if ( strpos( $content, Page_Cache::ADVANCED_CACHE_SIGNATURE ) !== false ) {
|
||||
wp_delete_file( $advanced_cache_filename );
|
||||
|
||||
// wp_delete_file doesn't return anything
|
||||
// so we manually check if the file was deleted.
|
||||
$deleted_file = ! file_exists( $advanced_cache_filename );
|
||||
}
|
||||
|
||||
self::clear_opcache( $advanced_cache_filename );
|
||||
|
||||
return $deleted_file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the WP_CACHE define from wp-config.php
|
||||
*
|
||||
* @return \WP_Error if an error occurred.
|
||||
*/
|
||||
public static function delete_wp_cache_constant() {
|
||||
$config_file = self::find_wp_config();
|
||||
if ( $config_file === false ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lines = file( $config_file );
|
||||
$found = false;
|
||||
foreach ( $lines as $key => $line ) {
|
||||
if ( preg_match( '#define\s*\(\s*[\'"]WP_CACHE[\'"]#', $line ) === 1 && strpos( $line, Page_Cache::ADVANCED_CACHE_SIGNATURE ) !== false ) {
|
||||
unset( $lines[ $key ] );
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
if ( ! $found ) {
|
||||
return;
|
||||
}
|
||||
$content = implode( '', $lines );
|
||||
Filesystem_Utils::write_to_file( $config_file, $content );
|
||||
self::clear_opcache( $config_file );
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the comment after WP_CACHE defined in wp-config.php (if any).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function cleanup_wp_cache_constant() {
|
||||
$config_file = self::find_wp_config();
|
||||
if ( $config_file === false ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lines = file( $config_file );
|
||||
$found = false;
|
||||
foreach ( $lines as $key => $line ) {
|
||||
if ( preg_match( '#define\s*\(\s*[\'"]WP_CACHE[\'"]#', $line ) === 1 && strpos( $line, Page_Cache::ADVANCED_CACHE_SIGNATURE ) !== false ) {
|
||||
$lines[ $key ] = preg_replace( '#\s*?\/\/.*$#', '', $line );
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
if ( ! $found ) {
|
||||
return;
|
||||
}
|
||||
$content = implode( '', $lines );
|
||||
Filesystem_Utils::write_to_file( $config_file, $content );
|
||||
self::clear_opcache( $config_file );
|
||||
}
|
||||
|
||||
/**
|
||||
* Find location of wp-config.php file.
|
||||
*
|
||||
* @return string|false - The path to the wp-config.php file, or false if it was not found.
|
||||
*/
|
||||
private static function find_wp_config() {
|
||||
if ( file_exists( ABSPATH . 'wp-config.php' ) ) {
|
||||
return ABSPATH . 'wp-config.php';
|
||||
} elseif ( file_exists( dirname( ABSPATH ) . '/wp-config.php' ) && ! file_exists( dirname( ABSPATH ) . '/wp-settings.php' ) ) {
|
||||
// While checking one directory up, check for wp-settings.php as well similar to WordPress core, to avoid nested WordPress installations.
|
||||
return dirname( ABSPATH ) . '/wp-config.php';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear opcache for a file.
|
||||
*/
|
||||
private static function clear_opcache( $file ) {
|
||||
// If API functions are restricted, we can't do anything.
|
||||
if ( ini_get( 'opcache.restrict_api' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( function_exists( 'opcache_invalidate' ) ) {
|
||||
opcache_invalidate( $file, true );
|
||||
}
|
||||
}
|
||||
|
||||
private static function write_to_file_direct( $file, $content ) {
|
||||
$filesystem = self::get_wp_filesystem();
|
||||
$chmod = $filesystem->getchmod( $file );
|
||||
if ( $chmod === false ) {
|
||||
$chmod = 0644; // Default to a common permission for files
|
||||
} else {
|
||||
$chmod = intval( '0' . $chmod, 8 ); // Ensure leading zero
|
||||
}
|
||||
return $filesystem->put_contents( $file, $content, $chmod );
|
||||
}
|
||||
|
||||
private static function get_wp_filesystem() {
|
||||
require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
|
||||
return new \WP_Filesystem_Direct( null );
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache;
|
||||
|
||||
use Automattic\Jetpack\Schema\Schema;
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Data_Sync;
|
||||
use Automattic\Jetpack_Boost\Contracts\Feature;
|
||||
use Automattic\Jetpack_Boost\Contracts\Has_Data_Sync;
|
||||
use Automattic\Jetpack_Boost\Contracts\Has_Deactivate;
|
||||
use Automattic\Jetpack_Boost\Contracts\Needs_To_Be_Ready;
|
||||
use Automattic\Jetpack_Boost\Contracts\Optimization;
|
||||
use Automattic\Jetpack_Boost\Lib\Cache_Compatibility;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Data_Sync\Page_Cache_Entry;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Data_Sync_Actions\Clear_Page_Cache;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Data_Sync_Actions\Deactivate_WPSC;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Data_Sync_Actions\Run_Setup;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache_Settings;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Logger;
|
||||
|
||||
class Page_Cache implements Feature, Has_Deactivate, Has_Data_Sync, Optimization, Needs_To_Be_Ready {
|
||||
/**
|
||||
* @var array - The errors that occurred when removing the cache.
|
||||
*/
|
||||
private $removal_errors = array();
|
||||
|
||||
/**
|
||||
* The signature used to identify the advanced-cache.php file owned by Jetpack Boost.
|
||||
*/
|
||||
const ADVANCED_CACHE_SIGNATURE = 'Boost Cache Plugin';
|
||||
|
||||
/**
|
||||
* The full signature including the current version, to verify the Advanced-cache file is current.
|
||||
*/
|
||||
const ADVANCED_CACHE_VERSION = 'v0.0.3';
|
||||
|
||||
/**
|
||||
* @var Boost_Cache_Settings - The settings for the page cache.
|
||||
*/
|
||||
private $settings;
|
||||
|
||||
public function __construct() {
|
||||
$this->settings = Boost_Cache_Settings::get_instance();
|
||||
}
|
||||
|
||||
public function setup() {
|
||||
Garbage_Collection::setup();
|
||||
|
||||
add_action( 'jetpack_boost_page_output_changed', array( $this, 'handle_page_output_change' ) );
|
||||
}
|
||||
|
||||
public function register_data_sync( Data_Sync $instance ) {
|
||||
$page_cache_schema = Schema::as_assoc_array(
|
||||
array(
|
||||
'bypass_patterns' => Schema::as_array( Schema::as_string() ),
|
||||
'logging' => Schema::as_boolean(),
|
||||
)
|
||||
);
|
||||
$page_cache_error_schema = Schema::as_assoc_array(
|
||||
array(
|
||||
'code' => Schema::as_string(),
|
||||
'message' => Schema::as_string(),
|
||||
'dismissed' => Schema::as_boolean()->fallback( false ),
|
||||
)
|
||||
)->nullable();
|
||||
|
||||
$instance->register_readonly( 'cache_debug_log', Schema::as_unsafe_any(), array( Logger::class, 'read' ) );
|
||||
$instance->register_readonly( 'cache_engine_loading', Schema::as_unsafe_any(), array( Boost_Cache::class, 'is_loaded' ) );
|
||||
|
||||
$instance->register( 'page_cache', $page_cache_schema, new Page_Cache_Entry() );
|
||||
// Page Cache error
|
||||
$instance->register( 'page_cache_error', $page_cache_error_schema );
|
||||
|
||||
$instance->register_action( 'page_cache', 'run-setup', Schema::as_void(), new Run_Setup() );
|
||||
|
||||
$instance->register_action( 'page_cache', 'clear-page-cache', Schema::as_void(), new Clear_Page_Cache() );
|
||||
$instance->register_action( 'page_cache', 'deactivate-wpsc', Schema::as_void(), new Deactivate_WPSC() );
|
||||
}
|
||||
|
||||
public function handle_page_output_change() {
|
||||
Garbage_Collection::schedule_single_garbage_collection();
|
||||
|
||||
// Remove the action so it doesn't run again during the same request.
|
||||
remove_action( 'jetpack_boost_page_output_changed', array( $this, 'handle_page_output_change' ) );
|
||||
}
|
||||
/**
|
||||
* Runs cleanup when the feature is deactivated.
|
||||
*/
|
||||
public static function deactivate() {
|
||||
Garbage_Collection::deactivate();
|
||||
Boost_Cache_Settings::get_instance()->set( array( 'enabled' => false ) );
|
||||
Page_Cache_Setup::delete_advanced_cache();
|
||||
}
|
||||
|
||||
/**
|
||||
* The module is active if cache engine is loaded.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_ready() {
|
||||
return Boost_Cache::is_loaded();
|
||||
}
|
||||
|
||||
public static function is_available() {
|
||||
// Disable Page Cache on WoA and WP Cloud clients.
|
||||
// They already have caching enabled.
|
||||
if ( ( new Host() )->is_woa_site() || ( new Host() )->is_atomic_platform() ) {
|
||||
if ( Page_Cache_Setup::can_run_cache() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Disable Page Cache on sites that have their own caching service.
|
||||
if ( Cache_Compatibility::has_cache() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function get_slug() {
|
||||
return 'page_cache';
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Data_Sync_Actions;
|
||||
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Data_Sync_Action;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache;
|
||||
|
||||
/**
|
||||
* Page Cache: Clear page cache
|
||||
*/
|
||||
class Clear_Page_Cache implements Data_Sync_Action {
|
||||
|
||||
/**
|
||||
* Handles the action logic.
|
||||
*
|
||||
* @param mixed $_data JSON Data passed to the action.
|
||||
* @param \WP_REST_Request $_request The request object.
|
||||
*/
|
||||
public function handle( $_data, $_request ) {
|
||||
$cache = new Boost_Cache();
|
||||
$delete = $cache->delete_recursive( home_url() );
|
||||
|
||||
if ( $delete === null || is_wp_error( $delete ) ) {
|
||||
return array(
|
||||
'message' => __( 'Cache already cleared.', 'jetpack-boost' ),
|
||||
);
|
||||
}
|
||||
|
||||
if ( $delete ) {
|
||||
return array(
|
||||
'message' => __( 'Cache cleared.', 'jetpack-boost' ),
|
||||
);
|
||||
}
|
||||
|
||||
return new \WP_Error( 'unable-to-clear-cache', __( 'Unable to clear cache.', 'jetpack-boost' ) );
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Data_Sync_Actions;
|
||||
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Data_Sync_Action;
|
||||
|
||||
/**
|
||||
* Cache Action: Disable Super Cache
|
||||
*/
|
||||
class Deactivate_WPSC implements Data_Sync_Action {
|
||||
|
||||
/**
|
||||
* Handles the action logic.
|
||||
*
|
||||
* @param mixed $_data JSON Data passed to the action.
|
||||
* @param null|\WP_REST_Request $_request The request object.
|
||||
*/
|
||||
public function handle( $_data = null, $_request = null ) {
|
||||
// Super Cache will define WPCACHEHOME if it's active.
|
||||
if ( ! defined( 'WPCACHEHOME' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Find the plugin base path for super cache.
|
||||
$super_cache_dir = basename( WPCACHEHOME );
|
||||
$plugin = $super_cache_dir . '/wp-cache.php';
|
||||
|
||||
// Load the necessary files to deactivate the plugin.
|
||||
require_once ABSPATH . 'wp-admin/includes/misc.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
deactivate_plugins( $plugin );
|
||||
|
||||
$success = ! is_plugin_active( $plugin );
|
||||
|
||||
return $success;
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Data_Sync_Actions;
|
||||
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Data_Sync_Action;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Garbage_Collection;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Page_Cache_Setup;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache_Settings;
|
||||
|
||||
/**
|
||||
* Critical CSS Action: request regeneration.
|
||||
*/
|
||||
class Run_Setup implements Data_Sync_Action {
|
||||
|
||||
/**
|
||||
* Handles the action logic.
|
||||
*
|
||||
* @param mixed $_data JSON Data passed to the action.
|
||||
* @param \WP_REST_Request $_request The request object.
|
||||
*/
|
||||
public function handle( $_data, $_request ) {
|
||||
$setup_result = Page_Cache_Setup::run_setup();
|
||||
|
||||
if ( is_wp_error( $setup_result ) ) {
|
||||
return $setup_result;
|
||||
}
|
||||
|
||||
Garbage_Collection::activate();
|
||||
Boost_Cache_Settings::get_instance()->set( array( 'enabled' => true ) );
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'notices' => Page_Cache_Setup::get_notices(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Data_Sync;
|
||||
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Get;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Set;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache_Settings;
|
||||
|
||||
class Page_Cache_Entry implements Entry_Can_Get, Entry_Can_Set {
|
||||
public function get( $_fallback = false ) {
|
||||
$cache_settings = Boost_Cache_Settings::get_instance();
|
||||
|
||||
$settings = array(
|
||||
'bypass_patterns' => $cache_settings->get_bypass_patterns(),
|
||||
'logging' => $cache_settings->get_logging(),
|
||||
);
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
public function set( $value ) {
|
||||
$cache_settings = Boost_Cache_Settings::get_instance();
|
||||
|
||||
$value['bypass_patterns'] = $this->sanitize_value( $value['bypass_patterns'] );
|
||||
|
||||
$cache_settings->set( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the given value, ensuring that it is list of valid patterns.
|
||||
*
|
||||
* @param mixed $value The value to sanitize.
|
||||
*
|
||||
* @return array The sanitized value.
|
||||
*/
|
||||
private function sanitize_value( $value ) {
|
||||
if ( is_array( $value ) ) {
|
||||
$value = array_values( array_unique( array_filter( array_map( 'trim', array_map( 'strtolower', $value ) ) ) ) );
|
||||
|
||||
$home_url = home_url( '/' );
|
||||
|
||||
foreach ( $value as &$path ) {
|
||||
// Strip home URL (both secure and non-secure).
|
||||
$path = str_ireplace(
|
||||
array(
|
||||
$home_url,
|
||||
str_replace( 'http:', 'https:', $home_url ),
|
||||
),
|
||||
array(
|
||||
'/',
|
||||
'/',
|
||||
),
|
||||
$path
|
||||
);
|
||||
|
||||
// Remove double shashes.
|
||||
$path = str_replace( '//', '/', $path );
|
||||
|
||||
// Remove symbols, as they are included in the regex check.
|
||||
$path = ltrim( $path, '^' );
|
||||
$path = rtrim( $path, '$' );
|
||||
$path = preg_replace( '/\/\?$/', '', $path );
|
||||
|
||||
// Make sure there's a leading slash.
|
||||
$path = '/' . ltrim( $path, '/' );
|
||||
|
||||
// Fix up any wildcards.
|
||||
$path = $this->sanitize_wildcards( $path );
|
||||
}
|
||||
|
||||
$value = array_values( array_unique( array_filter( $value ) ) );
|
||||
} else {
|
||||
$value = array();
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize wildcards in a given path.
|
||||
*
|
||||
* @param string $path The path to sanitize.
|
||||
* @return string The sanitized path.
|
||||
*/
|
||||
private function sanitize_wildcards( $path ) {
|
||||
if ( ! $path ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$path_components = explode( '/', $path );
|
||||
$arr = array(
|
||||
'.*' => '(.*)',
|
||||
'*' => '(.*)',
|
||||
'(*)' => '(.*)',
|
||||
'(.*)' => '(.*)',
|
||||
);
|
||||
|
||||
foreach ( $path_components as &$path_component ) {
|
||||
$path_component = strtr( $path_component, $arr );
|
||||
}
|
||||
$path = implode( '/', $path_components );
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
### Pre_WordPress namespace
|
||||
|
||||
Everything in this directory / namespace contains code which can execute before WordPress is fully
|
||||
initialized. It can be called from `advanced-cache.php`, but it can also be called directly from
|
||||
the main Boost code-base.
|
||||
|
||||
Nothing in the `Pre_WordPress` namespace may rely on autolaoding to load things; you must include
|
||||
an explicit `require_once` instruction in the entrypoint file `Boost_Cache.php`. It also must not rely on any WordPress functionality that is
|
||||
unavailable at the time that `advanced-cache.php` is executed.
|
||||
|
||||
You can use this code from elsewhere in Boost, and you can autoload it from outside this namespace, though.
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/**
|
||||
* This file contains all the public actions for the Page Cache module.
|
||||
* This file is loaded before WordPress is fully initialized.
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Logger;
|
||||
|
||||
/**
|
||||
* Delete all cache.
|
||||
*
|
||||
* Allow third-party plugins to clear all cache.
|
||||
*/
|
||||
add_action( 'jetpack_boost_clear_page_cache_all', 'jetpack_boost_delete_cache' );
|
||||
|
||||
/**
|
||||
* Delete cache for homepage and paged archives.
|
||||
*
|
||||
* Allow third-party plugins to clear front-page cache.
|
||||
*/
|
||||
add_action( 'jetpack_boost_clear_page_cache_home', 'jetpack_boost_delete_cache_for_home' );
|
||||
|
||||
/**
|
||||
* Delete cache for a specific URL.
|
||||
*
|
||||
* Allow third-party plugins to clear the cache for a specific URL.
|
||||
*
|
||||
* @param string $url - The URL to delete the cache for.
|
||||
*/
|
||||
add_action( 'jetpack_boost_clear_page_cache_url', 'jetpack_boost_delete_cache_for_url' );
|
||||
|
||||
/**
|
||||
* Delete cache for a specific post.
|
||||
*
|
||||
* Allow third-party plugins to clear the cache for a specific post.
|
||||
*
|
||||
* @param int $post_id - The ID of the post to delete the cache for.
|
||||
*/
|
||||
add_action( 'jetpack_boost_clear_page_cache_post', 'jetpack_boost_delete_cache_by_post_id' );
|
||||
|
||||
/**
|
||||
* Delete all cache files.
|
||||
*/
|
||||
function jetpack_boost_delete_cache() {
|
||||
$boost_cache = new Boost_Cache();
|
||||
$boost_cache->delete_recursive( home_url() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete cache for homepage and paged archives.
|
||||
*/
|
||||
function jetpack_boost_delete_cache_for_home() {
|
||||
$boost_cache = new Boost_Cache();
|
||||
$boost_cache->delete_page( home_url() );
|
||||
|
||||
Logger::debug( 'jetpack_boost_delete_cache_for_home: deleting front page cache' );
|
||||
if ( get_option( 'show_on_front' ) === 'page' ) {
|
||||
$posts_page_id = get_option( 'page_for_posts' ); // posts page
|
||||
if ( $posts_page_id ) {
|
||||
$boost_cache->delete_recursive( get_permalink( $posts_page_id ) );
|
||||
Logger::debug( 'jetpack_boost_delete_cache_for_home: deleting posts page cache' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete cache for a specific URL.
|
||||
*
|
||||
* @param string $url - The URL to delete the cache for.
|
||||
*/
|
||||
function jetpack_boost_delete_cache_for_url( $url ) {
|
||||
$boost_cache = new Boost_Cache();
|
||||
$boost_cache->delete_recursive( $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete cache for a specific post.
|
||||
*
|
||||
* @param int $post_id - The ID of the post to delete the cache for.
|
||||
*/
|
||||
function jetpack_boost_delete_cache_by_post_id( $post_id ) {
|
||||
$post = get_post( (int) $post_id );
|
||||
|
||||
Logger::debug( 'invalidate_cache_for_post: ' . $post->ID );
|
||||
|
||||
$boost_cache = new Boost_Cache();
|
||||
$boost_cache->delete_post_cache( $post );
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
/*
|
||||
* This file may be called before WordPress is fully initialized. See the README file for info.
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress;
|
||||
|
||||
/**
|
||||
* A replacement for WP_Error when working in a Pre_WordPress setting.
|
||||
*
|
||||
* This class deliberately offers a similar API to WP_Error for familiarity. All Pre_WordPress functions
|
||||
* which may return an error object use this class to represent an Error state.
|
||||
*
|
||||
* If you call a Pre_WordPress function after loading WordPress, use to_wp_error to convert these
|
||||
* objects to a standard WP_Error object.
|
||||
*/
|
||||
class Boost_Cache_Error {
|
||||
|
||||
/**
|
||||
* @var string - The error code.
|
||||
*/
|
||||
private $code;
|
||||
|
||||
/**
|
||||
* @var string - The error message.
|
||||
*/
|
||||
private $message;
|
||||
|
||||
/**
|
||||
* Create a Boost_Cache_Error object, with a code and a message.
|
||||
*/
|
||||
public function __construct( $code, $message ) {
|
||||
$this->code = $code;
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the error message.
|
||||
*/
|
||||
public function get_error_message() {
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the error code.
|
||||
*/
|
||||
public function get_error_code() {
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to a WP_Error.
|
||||
*
|
||||
* When calling a Pre_WordPress function from a WordPress context, use this method to convert
|
||||
* any resultant errors to WP_Errors for interfacing with other WordPress APIs.
|
||||
*
|
||||
* **Warning** - this function should only be called if WordPress has been loaded!
|
||||
*/
|
||||
public function to_wp_error() {
|
||||
if ( ! class_exists( '\WP_Error' ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( 'Warning: Boost_Cache_Error::to_wp_error called from a Pre-WordPress context' );
|
||||
}
|
||||
}
|
||||
|
||||
return new \WP_Error( $this->get_error_code(), $this->get_error_message() );
|
||||
}
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
/*
|
||||
* This file may be called before WordPress is fully initialized. See the README file for info.
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress;
|
||||
|
||||
/**
|
||||
* Cache settings class.
|
||||
* Settings are stored in a file in the boost-cache directory.
|
||||
*/
|
||||
class Boost_Cache_Settings {
|
||||
private static $instance = null;
|
||||
private $settings = array();
|
||||
private $config_file_path;
|
||||
private $config_file;
|
||||
|
||||
/**
|
||||
* An uninitialized config holds these settings.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $default_settings = array(
|
||||
'enabled' => false,
|
||||
'bypass_patterns' => array(),
|
||||
'logging' => false,
|
||||
);
|
||||
|
||||
private function __construct() {
|
||||
$this->config_file_path = WP_CONTENT_DIR . '/boost-cache/';
|
||||
$this->config_file = $this->config_file_path . 'config.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the instance of the class.
|
||||
*
|
||||
* @return Boost_Cache_Settings The instance of the class.
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( self::$instance === null ) {
|
||||
self::$instance = new Boost_Cache_Settings();
|
||||
self::$instance->init_settings();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a settings file exists, if one isn't there already.
|
||||
*
|
||||
* @return Boost_Cache_Error|bool - True if it was changed, or a Boost_Cache_Error on failure, false if it was already created.
|
||||
*/
|
||||
public function create_settings_file() {
|
||||
if ( file_exists( $this->config_file ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! file_exists( $this->config_file_path ) ) {
|
||||
if ( ! Filesystem_Utils::create_directory( $this->config_file_path ) ) {
|
||||
return new Boost_Cache_Error( 'failed-settings-write', 'Failed to create settings directory at ' . $this->config_file_path );
|
||||
}
|
||||
}
|
||||
|
||||
$write_result = $this->set( $this->default_settings );
|
||||
if ( $write_result instanceof Boost_Cache_Error ) {
|
||||
return $write_result;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If an error occurs while reading the options, it will be impossible to ever log this to the Boost Cache logs.
|
||||
* So, if WP_DEBUG is enabled write it to the error_log instead.
|
||||
*
|
||||
* @param string $message - The message to log.
|
||||
*/
|
||||
private function log_init_error( $message ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
error_log( $message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the settings from the config file, if available. Falls back to defaults if not.
|
||||
*/
|
||||
private function init_settings() {
|
||||
$this->settings = $this->default_settings;
|
||||
|
||||
// If no settings file exists yet, don't try to create one until we are writing a value.
|
||||
if ( ! file_exists( $this->config_file ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lines = @file( $this->config_file ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
if ( empty( $lines ) || count( $lines ) < 4 ) {
|
||||
$this->log_init_error( 'Invalid config file at ' . $this->config_file );
|
||||
return;
|
||||
}
|
||||
|
||||
$file_settings = null;
|
||||
foreach ( $lines as $line ) {
|
||||
if ( strpos( $line, '{' ) !== false ) {
|
||||
$file_settings = json_decode( $line, true );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! is_array( $file_settings ) ) {
|
||||
$this->log_init_error( 'Invalid config file at ' . $this->config_file );
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->settings = $file_settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the given setting.
|
||||
*
|
||||
* @param string $setting - The setting to get.
|
||||
* @return mixed - The value of the setting, or the default if the setting does not exist.
|
||||
*/
|
||||
public function get( $setting, $default = false ) {
|
||||
if ( ! isset( $this->settings[ $setting ] ) ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $this->settings[ $setting ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the cache is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function get_enabled() {
|
||||
return $this->get( 'enabled', false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of URLs that should not be cached.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_bypass_patterns() {
|
||||
return $this->get( 'bypass_patterns', array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether logging is enabled or not.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function get_logging() {
|
||||
return $this->get( 'logging', false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given settings, and saves them to the config file.
|
||||
*
|
||||
* @param array $settings - The settings to set in a key => value associative
|
||||
* array. This will be merged with the existing settings.
|
||||
* Example:
|
||||
* $result = $this->set( array( 'enabled' => true ) );
|
||||
*
|
||||
* @return Boost_Cache_Error|true - true if the settings were saved, Boost_Cache_Error otherwise.
|
||||
*/
|
||||
public function set( $settings ) {
|
||||
// If the settings file does not exist, attempt to create one.
|
||||
if ( ! file_exists( $this->config_file_path ) ) {
|
||||
$result = $this->create_settings_file();
|
||||
if ( $result instanceof Boost_Cache_Error ) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! is_writable( $this->config_file_path ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable
|
||||
$error = new Boost_Cache_Error( 'failed-settings-write', 'Could not write to the config file at ' . $this->config_file_path );
|
||||
Logger::debug( $error->get_error_message() );
|
||||
return $error;
|
||||
}
|
||||
|
||||
$this->settings = array_merge( $this->settings, $settings );
|
||||
|
||||
$contents = "<?php die();\n/*\n * Configuration data for Jetpack Boost Cache. Do not edit.\n" . json_encode( // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
|
||||
$this->settings,
|
||||
JSON_HEX_TAG // Need to escape slashes because this is, for some reason, going into a PHP comment and we need to guard against `*/`.
|
||||
) . "\n */";
|
||||
$result = Filesystem_Utils::write_to_file( $this->config_file, $contents );
|
||||
if ( $result instanceof Boost_Cache_Error ) {
|
||||
Logger::debug( $result->get_error_message() );
|
||||
return new Boost_Cache_Error( 'failed-settings-write', 'Failed to write settings file: ' . $result->get_error_message() );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/*
|
||||
* This file may be called before WordPress is fully initialized. See the README file for info.
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress;
|
||||
|
||||
use WP_Post;
|
||||
|
||||
class Boost_Cache_Utils {
|
||||
|
||||
/**
|
||||
* Performs a deep string replace operation to ensure the values in $search are no longer present.
|
||||
* Copied from wp-includes/formatting.php
|
||||
*
|
||||
* Repeats the replacement operation until it no longer replaces anything to remove "nested" values
|
||||
* e.g. $subject = '%0%0%0DDD', $search ='%0D', $result ='' rather than the '%0%0DD' that
|
||||
* str_replace would return
|
||||
*
|
||||
* @param string|array $search The value being searched for, otherwise known as the needle.
|
||||
* An array may be used to designate multiple needles.
|
||||
* @param string $subject The string being searched and replaced on, otherwise known as the haystack.
|
||||
* @return string The string with the replaced values.
|
||||
*/
|
||||
public static function deep_replace( $search, $subject ) {
|
||||
$subject = (string) $subject;
|
||||
|
||||
$count = 1;
|
||||
while ( $count ) {
|
||||
$subject = str_replace( $search, '', $subject, $count );
|
||||
}
|
||||
|
||||
return $subject;
|
||||
}
|
||||
|
||||
public static function trailingslashit( $string ) {
|
||||
return rtrim( $string, '/' ) . '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a sanitized directory path.
|
||||
*
|
||||
* @param string $path - The path to sanitize.
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitize_file_path( $path ) {
|
||||
$path = self::trailingslashit( $path );
|
||||
$path = self::deep_replace(
|
||||
array( '..', '\\' ),
|
||||
preg_replace(
|
||||
'/[ <>\'\"\r\n\t\(\)]/',
|
||||
'',
|
||||
preg_replace(
|
||||
'/(\?.*)?(#.*)?$/',
|
||||
'',
|
||||
$path
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the request uri so it can be used for caching purposes.
|
||||
* It removes the query string and the trailing slash, and characters
|
||||
* that might cause problems with the filesystem.
|
||||
*
|
||||
* **THIS DOES NOT SANITIZE THE VARIABLE IN ANY WAY.**
|
||||
* Only use it for comparison purposes or to generate an MD5 hash.
|
||||
*
|
||||
* @param string $request_uri - The request uri to normalize.
|
||||
* @return string - The normalized request uri.
|
||||
*/
|
||||
public static function normalize_request_uri( $request_uri ) {
|
||||
// get path from request uri
|
||||
$request_uri = parse_url( $request_uri, PHP_URL_PATH ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
|
||||
if ( empty( $request_uri ) ) {
|
||||
$request_uri = '/';
|
||||
} elseif ( substr( $request_uri, -1 ) !== '/' && ! is_file( ABSPATH . $request_uri ) ) {
|
||||
$request_uri .= '/';
|
||||
}
|
||||
|
||||
return $request_uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the post type is public.
|
||||
*
|
||||
* @param WP_Post $post - The post to check.
|
||||
* @return bool - True if the post type is public.
|
||||
*/
|
||||
public static function is_visible_post_type( $post ) {
|
||||
$post_type = is_a( $post, 'WP_Post' ) ? get_post_type_object( $post->post_type ) : null;
|
||||
if ( empty( $post_type ) || ! $post_type->public ) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+728
@@ -0,0 +1,728 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is loaded by advanced-cache.php, and so cannot rely on autoloading.
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress;
|
||||
|
||||
use WP_Comment;
|
||||
use WP_Post;
|
||||
|
||||
/*
|
||||
* Require all pre-wordpress files here. These files aren't autoloaded as they are loaded before WordPress is fully initialized.
|
||||
* pre-wordpress files assume all other pre-wordpress files are loaded here.
|
||||
*/
|
||||
require_once __DIR__ . '/path-actions/interface-path-action.php';
|
||||
require_once __DIR__ . '/path-actions/class-filter-older.php';
|
||||
require_once __DIR__ . '/path-actions/class-rebuild-file.php';
|
||||
require_once __DIR__ . '/path-actions/class-simple-delete.php';
|
||||
require_once __DIR__ . '/boost-cache-actions.php';
|
||||
require_once __DIR__ . '/class-boost-cache-error.php';
|
||||
require_once __DIR__ . '/class-boost-cache-settings.php';
|
||||
require_once __DIR__ . '/class-boost-cache-utils.php';
|
||||
require_once __DIR__ . '/class-filesystem-utils.php';
|
||||
require_once __DIR__ . '/class-logger.php';
|
||||
require_once __DIR__ . '/class-request.php';
|
||||
require_once __DIR__ . '/storage/interface-storage.php';
|
||||
require_once __DIR__ . '/storage/class-file-storage.php';
|
||||
|
||||
// Define how many seconds the cache should last for each cached page.
|
||||
if ( ! defined( 'JETPACK_BOOST_CACHE_DURATION' ) ) {
|
||||
define( 'JETPACK_BOOST_CACHE_DURATION', HOUR_IN_SECONDS );
|
||||
}
|
||||
|
||||
// Define how many seconds the rebuild cache should be considered stale, but usable, for each cached page.
|
||||
if ( ! defined( 'JETPACK_BOOST_CACHE_REBUILD_DURATION' ) ) {
|
||||
define( 'JETPACK_BOOST_CACHE_REBUILD_DURATION', 10 );
|
||||
}
|
||||
|
||||
class Boost_Cache {
|
||||
/**
|
||||
* @var Boost_Cache_Settings - The settings for the page cache.
|
||||
*/
|
||||
private $settings;
|
||||
|
||||
/**
|
||||
* @var Storage\Storage - The storage system used by Boost Cache.
|
||||
*/
|
||||
private $storage;
|
||||
|
||||
/**
|
||||
* @var Request - The request object that provides utility for the current request.
|
||||
*/
|
||||
private $request = null;
|
||||
|
||||
/**
|
||||
* @var bool - Indicates whether the cache engine has been loaded.
|
||||
*/
|
||||
private static $cache_engine_loaded = false;
|
||||
|
||||
/**
|
||||
* @var bool - Indicates whether WordPress initialized correctly and we can cache the page.
|
||||
*/
|
||||
private $do_cache = false;
|
||||
|
||||
/**
|
||||
* @var string - The ignored cookies that were removed from the cache parameters.
|
||||
*/
|
||||
private $ignored_cookies = '';
|
||||
|
||||
/**
|
||||
* @var string - The ignored GET parameters that were removed from the cache parameters.
|
||||
*/
|
||||
private $ignored_get_parameters = '';
|
||||
|
||||
/**
|
||||
* @param ?Storage\Storage $storage - Optionally provide a Storage subclass to handle actually storing and retrieving cached content. Defaults to a new instance of File_Storage.
|
||||
*/
|
||||
public function __construct( $storage = null ) {
|
||||
$this->settings = Boost_Cache_Settings::get_instance();
|
||||
$home = isset( $_SERVER['HTTP_HOST'] ) ? strtolower( $_SERVER['HTTP_HOST'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$this->storage = $storage ?? new Storage\File_Storage( $home );
|
||||
$this->request = Request::current();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the actions for the cache.
|
||||
*/
|
||||
public function init_actions() {
|
||||
add_action( 'transition_post_status', array( $this, 'invalidate_on_post_transition' ), 10, 3 );
|
||||
add_action( 'transition_comment_status', array( $this, 'invalidate_on_comment_transition' ), 10, 3 );
|
||||
add_action( 'comment_post', array( $this, 'rebuild_on_comment_post' ), 10, 3 );
|
||||
add_action( 'edit_comment', array( $this, 'rebuild_on_comment_edit' ), 10, 2 );
|
||||
add_action( 'switch_theme', array( $this, 'rebuild_all' ) );
|
||||
add_action( 'wp_trash_post', array( $this, 'delete_on_post_trash' ), 10, 2 );
|
||||
add_filter( 'wp_php_error_message', array( $this, 'disable_caching_on_error' ) );
|
||||
add_filter( 'init', array( $this, 'init_do_cache' ) );
|
||||
add_filter( 'jetpack_boost_cache_parameters', array( $this, 'ignore_cookies' ) );
|
||||
add_filter( 'jetpack_boost_cache_parameters', array( $this, 'ignore_get_parameters' ) );
|
||||
$this->load_extra();
|
||||
}
|
||||
|
||||
private function load_extra() {
|
||||
if ( file_exists( WP_CONTENT_DIR . '/boost-cache-extra.php' ) ) {
|
||||
include_once WP_CONTENT_DIR . '/boost-cache-extra.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve the cached page if it exists, otherwise start output buffering.
|
||||
*/
|
||||
public function serve() {
|
||||
if ( ! $this->settings->get_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Indicate that the cache engine has been loaded.
|
||||
self::$cache_engine_loaded = true;
|
||||
|
||||
if ( ! $this->request->is_cacheable() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->serve_cached() ) {
|
||||
$this->ob_start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the cache engine has been loaded.
|
||||
*
|
||||
* @return bool - True if the cache engine has been loaded, false otherwise.
|
||||
*/
|
||||
public static function is_loaded() {
|
||||
return self::$cache_engine_loaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the storage instance used by Boost Cache.
|
||||
*
|
||||
* @return Storage\Storage
|
||||
*/
|
||||
public function get_storage() {
|
||||
return $this->storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve cached content, if any is available for the current request. Will terminate if it does so.
|
||||
* Otherwise, returns false.
|
||||
*/
|
||||
public function serve_cached() {
|
||||
if ( ! $this->request->is_cacheable() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if rebuild file exists and rename it to the correct file
|
||||
$rebuild_found = $this->storage->reset_rebuild_file( $this->request->get_uri(), $this->request->get_parameters() );
|
||||
if ( $rebuild_found ) {
|
||||
Logger::debug( 'Rebuild file found. Will be used for cache until new file created.' );
|
||||
$cached = false;
|
||||
} else {
|
||||
$cached = $this->storage->read( $this->request->get_uri(), $this->request->get_parameters() );
|
||||
}
|
||||
|
||||
if ( is_string( $cached ) ) {
|
||||
$this->send_header( 'X-Jetpack-Boost-Cache: hit' );
|
||||
$ignored_cookies_message = $this->ignored_cookies === '' ? '' : " and ignored cookies: {$this->ignored_cookies}";
|
||||
$ignored_get_message = $this->ignored_get_parameters === '' ? '' : " and ignored GET parameters: {$this->ignored_get_parameters}";
|
||||
Logger::debug( 'Serving cached page' . $ignored_cookies_message . $ignored_get_message );
|
||||
echo $cached; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
die( 0 );
|
||||
}
|
||||
|
||||
$cache_status = $rebuild_found ? 'rebuild' : 'miss';
|
||||
$this->send_header( 'X-Jetpack-Boost-Cache: ' . $cache_status );
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function send_header( $header ) {
|
||||
if ( ! headers_sent() ) {
|
||||
header( $header );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts output buffering and sets the callback to save the cache file.
|
||||
*
|
||||
* @return bool - false if page is not cacheable.
|
||||
*/
|
||||
public function ob_start() {
|
||||
if ( ! $this->request->is_cacheable() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ob_start( array( $this, 'ob_callback' ) );
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function from output buffer. This function saves the output
|
||||
* buffer to a cache file and then returns the buffer so PHP will send it
|
||||
* to the browser.
|
||||
*
|
||||
* @param string $buffer - The output buffer to save to the cache file.
|
||||
* @return string - The output buffer.
|
||||
*/
|
||||
public function ob_callback( $buffer ) {
|
||||
if ( strlen( $buffer ) > 0 && $this->request->is_cacheable() ) {
|
||||
|
||||
// Do not cache the page as WordPress did not initialize correctly.
|
||||
if ( ! $this->do_cache ) {
|
||||
Logger::debug( 'Page exited early. Do not cache.' );
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
if ( false === stripos( $buffer, '</html>' ) ) {
|
||||
Logger::debug( 'Closing HTML tag not found, not caching' );
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
$result = $this->storage->write( $this->request->get_uri(), $this->request->get_parameters(), $buffer );
|
||||
|
||||
if ( $result instanceof Boost_Cache_Error ) {
|
||||
Logger::debug( 'Error writing cache file: ' . $result->get_error_message() );
|
||||
} else {
|
||||
$ignored_cookies_message = $this->ignored_cookies === '' ? '' : " and ignored cookies: {$this->ignored_cookies}";
|
||||
$ignored_get_message = $this->ignored_get_parameters === '' ? '' : " and ignored GET parameters: {$this->ignored_get_parameters}";
|
||||
Logger::debug( 'Cache file created' . $ignored_cookies_message . $ignored_get_message );
|
||||
}
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete/rebuild the cache for the front page and paged archives.
|
||||
* This is called when a post is edited, deleted, or published.
|
||||
*/
|
||||
public function rebuild_front_page() {
|
||||
if ( get_option( 'show_on_front' ) === 'page' ) {
|
||||
$this->rebuild_page( home_url() );
|
||||
$posts_page_id = get_option( 'page_for_posts' ); // posts page
|
||||
if ( $posts_page_id ) {
|
||||
Logger::debug( 'rebuild_front_page: deleting posts page cache' );
|
||||
$this->rebuild_post_cache( get_post( $posts_page_id ) );
|
||||
}
|
||||
} else {
|
||||
$this->rebuild_page( home_url() );
|
||||
Logger::debug( 'delete front page cache ' . Boost_Cache_Utils::normalize_request_uri( home_url() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the cache for the post if the comment transitioned from one state to another.
|
||||
*
|
||||
* @param string $new_status - The new status of the comment.
|
||||
* @param string $old_status - The old status of the comment.
|
||||
* @param WP_Comment $comment - The comment that transitioned.
|
||||
*/
|
||||
public function invalidate_on_comment_transition( $new_status, $old_status, $comment ) {
|
||||
if ( $new_status === $old_status ) {
|
||||
return;
|
||||
}
|
||||
Logger::debug( "invalidate_on_comment_transition: $new_status, $old_status" );
|
||||
|
||||
if ( $new_status !== 'approved' && $old_status !== 'approved' ) {
|
||||
Logger::debug( 'invalidate_on_comment_transition: comment not approved' );
|
||||
return;
|
||||
}
|
||||
|
||||
$post = get_post( (int) $comment->comment_post_ID );
|
||||
$this->rebuild_post_cache( $post );
|
||||
}
|
||||
|
||||
/**
|
||||
* After editing a comment, rebuild the cache for the post if the comment is approved.
|
||||
* If changing state and editing, both actions will be called, but the cache will only be rebuilt once.
|
||||
*
|
||||
* @param int $comment_id - The id of the comment.
|
||||
* @param array $commentdata - The comment data.
|
||||
*/
|
||||
public function rebuild_on_comment_edit( $comment_id, $commentdata ) {
|
||||
$post = get_post( $commentdata['comment_post_ID'] );
|
||||
|
||||
if ( (int) $commentdata['comment_approved'] === 1 ) {
|
||||
$this->rebuild_post_cache( $post );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* After a comment is posted, rebuild the cache for the post if the comment is approved.
|
||||
* If the comment is not approved, only rebuild the cache for this post for this visitor.
|
||||
*
|
||||
* @param int $comment_id - The id of the comment.
|
||||
* @param int $comment_approved - The approval status of the comment.
|
||||
* @param array $commentdata - The comment data.
|
||||
*/
|
||||
public function rebuild_on_comment_post( $comment_id, $comment_approved, $commentdata ) {
|
||||
$post = get_post( $commentdata['comment_post_ID'] );
|
||||
Logger::debug( "rebuild_on_comment_post: $comment_id, $comment_approved, {$post->ID}" );
|
||||
/**
|
||||
* If a comment is not approved, we only need to delete the cache for
|
||||
* this post for this visitor so the unmoderated comment is shown to them.
|
||||
*/
|
||||
if ( $comment_approved !== 1 ) {
|
||||
$parameters = $this->request->get_parameters();
|
||||
|
||||
/*
|
||||
* If there are no cookies, then visitor did not click "remember me".
|
||||
* They'll be redirected to a page with a hash in the URL for the
|
||||
* moderation message.
|
||||
* Only delete the cache for visitors who clicked "remember me".
|
||||
*/
|
||||
if ( isset( $parameters['cookies'] ) && ! empty( $parameters['cookies'] ) ) {
|
||||
$this->delete_page( get_permalink( $post->ID ), $parameters );
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$this->rebuild_post_cache( $post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the post is published or private.
|
||||
*
|
||||
* @param string $status - The status of the post.
|
||||
* @return bool
|
||||
*/
|
||||
private function is_published( $status ) {
|
||||
return $status === 'publish' || $status === 'private';
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the cached post if it transitioned from one state to another.
|
||||
*
|
||||
* @param string $new_status - The new status of the post.
|
||||
* @param string $old_status - The old status of the post.
|
||||
* @param WP_Post $post - The post that transitioned.
|
||||
*/
|
||||
public function invalidate_on_post_transition( $new_status, $old_status, $post ) {
|
||||
// Special case: Delete cache if the post type can effect the whole site.
|
||||
$special_post_types = array( 'wp_template', 'wp_template_part', 'wp_global_styles' );
|
||||
if ( in_array( $post->post_type, $special_post_types, true ) ) {
|
||||
Logger::debug( 'invalidate_on_post_transition: special post type ' . $post->post_type );
|
||||
$this->rebuild_all();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! Boost_Cache_Utils::is_visible_post_type( $post ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $new_status === 'trash' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger::debug( "invalidate_on_post_transition: $new_status, $old_status, {$post->ID}" );
|
||||
|
||||
// Don't delete the cache for posts that weren't published and aren't published now
|
||||
if ( ! $this->is_published( $new_status ) && ! $this->is_published( $old_status ) ) {
|
||||
Logger::debug( 'invalidate_on_post_transition: not published' );
|
||||
return;
|
||||
}
|
||||
|
||||
// delete the cache files entirely if the post was unpublished
|
||||
if ( 'publish' === $old_status && 'publish' !== $new_status ) {
|
||||
Logger::debug( 'invalidate_on_post_transition: delete cache on new private page' );
|
||||
$this->delete_on_post_trash( $post->ID, $old_status );
|
||||
return;
|
||||
}
|
||||
Logger::debug( "invalidate_on_post_transition: rebuilding post {$post->ID}" );
|
||||
|
||||
$this->rebuild_post_cache( $post );
|
||||
$this->rebuild_post_terms_cache( $post );
|
||||
$this->rebuild_front_page();
|
||||
$this->rebuild_author_page( (int) $post->post_author );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the cache for the post if it was trashed.
|
||||
*
|
||||
* @param int $post_id - The id of the post.
|
||||
* @param string $old_status - The old status of the post.
|
||||
*/
|
||||
public function delete_on_post_trash( $post_id, $old_status ) {
|
||||
if ( $this->is_published( $old_status ) ) {
|
||||
$post = get_post( $post_id );
|
||||
$post_path = $this->get_post_path_for_invalidation( $post );
|
||||
if ( $post_path ) {
|
||||
$this->delete_recursive( $post_path );
|
||||
}
|
||||
$this->rebuild_post_terms_cache( $post );
|
||||
$this->rebuild_front_page();
|
||||
$this->rebuild_author_page( (int) $post->post_author );
|
||||
}
|
||||
}
|
||||
|
||||
private function get_post_path_for_invalidation( $post ) {
|
||||
static $already_deleted = -1;
|
||||
if ( $already_deleted === $post->ID ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't invalidate the cache for post types that are not public.
|
||||
*/
|
||||
if ( ! Boost_Cache_Utils::is_visible_post_type( $post ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$already_deleted = $post->ID;
|
||||
|
||||
/**
|
||||
* If a post is unpublished, the permalink will be deleted. In that case,
|
||||
* get_sample_permalink() will return a permalink with ?p=123 instead of
|
||||
* the post name. We need to get the post name from the post object.
|
||||
*/
|
||||
$permalink = get_permalink( $post->ID );
|
||||
if ( strpos( $permalink, '?p=' ) !== false || strpos( $permalink, '?page_id=' ) !== false ) {
|
||||
if ( $post->post_type === 'page' ) {
|
||||
$permalink = get_page_link( $post->ID, false, true );
|
||||
} else {
|
||||
if ( ! function_exists( 'get_sample_permalink' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/post.php';
|
||||
}
|
||||
list( $permalink, $post_name ) = get_sample_permalink( $post->ID );
|
||||
$permalink = str_replace( '%postname%', $post_name, $permalink );
|
||||
}
|
||||
}
|
||||
return $permalink;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the cache for terms associated with this post.
|
||||
*
|
||||
* @param WP_Post $post - The post to delete the cache for.
|
||||
*/
|
||||
public function rebuild_post_terms_cache( $post ) {
|
||||
$categories = get_the_category( $post->ID );
|
||||
if ( is_array( $categories ) ) {
|
||||
foreach ( $categories as $category ) {
|
||||
$link = trailingslashit( get_category_link( $category->term_id ) );
|
||||
$this->rebuild_recursive( $link );
|
||||
}
|
||||
}
|
||||
|
||||
$tags = get_the_tags( $post->ID );
|
||||
if ( is_array( $tags ) ) {
|
||||
foreach ( $tags as $tag ) {
|
||||
$link = trailingslashit( get_tag_link( $tag->term_id ) );
|
||||
$this->rebuild_recursive( $link );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the entire cache for the author's archive page.
|
||||
*
|
||||
* @param int $author_id - The id of the author.
|
||||
*/
|
||||
public function rebuild_author_page( $author_id ) {
|
||||
$author = get_userdata( $author_id );
|
||||
if ( ! $author ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$author_link = get_author_posts_url( $author_id, $author->user_nicename );
|
||||
$this->rebuild_recursive( $author_link );
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entire cache.
|
||||
*/
|
||||
public function rebuild_all() {
|
||||
$this->rebuild_recursive( home_url() );
|
||||
}
|
||||
|
||||
public function delete_post_cache( $post ) {
|
||||
$post_path = $this->get_post_path_for_invalidation( $post );
|
||||
if ( null === $post_path ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( Boost_Cache_Utils::trailingslashit( $post_path ) !== Boost_Cache_Utils::trailingslashit( home_url() ) ) {
|
||||
$this->delete_recursive( $post_path );
|
||||
} else {
|
||||
$this->delete_page( $post_path );
|
||||
}
|
||||
}
|
||||
|
||||
public function rebuild_post_cache( $post ) {
|
||||
$post_path = $this->get_post_path_for_invalidation( $post );
|
||||
if ( null === $post_path ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( Boost_Cache_Utils::trailingslashit( $post_path ) !== Boost_Cache_Utils::trailingslashit( home_url() ) ) {
|
||||
$this->rebuild_recursive( $post_path );
|
||||
} else {
|
||||
$this->rebuild_page( $post_path );
|
||||
}
|
||||
}
|
||||
|
||||
public function rebuild_page( $path, $parameters = false ) {
|
||||
$this->storage->clear(
|
||||
$path,
|
||||
array(
|
||||
'rebuild' => true,
|
||||
'parameters' => $parameters,
|
||||
)
|
||||
);
|
||||
$this->invalidate_cache_success( $path, 'rebuild', 'page' );
|
||||
}
|
||||
|
||||
public function delete_page( $path, $parameters = false ) {
|
||||
$this->storage->clear(
|
||||
$path,
|
||||
array(
|
||||
'rebuild' => false,
|
||||
'parameters' => $parameters,
|
||||
)
|
||||
);
|
||||
$this->invalidate_cache_success( $path, 'delete', 'page' );
|
||||
}
|
||||
|
||||
public function rebuild_recursive( $path ) {
|
||||
$this->storage->clear(
|
||||
$path,
|
||||
array(
|
||||
'rebuild' => true,
|
||||
'recursive' => true,
|
||||
)
|
||||
);
|
||||
$this->invalidate_cache_success( $path, 'rebuild', 'recursive' );
|
||||
}
|
||||
|
||||
public function delete_recursive( $path ) {
|
||||
$this->storage->clear(
|
||||
$path,
|
||||
array(
|
||||
'rebuild' => false,
|
||||
'recursive' => true,
|
||||
)
|
||||
);
|
||||
$this->invalidate_cache_success( $path, 'delete', 'recursive' );
|
||||
}
|
||||
|
||||
private function invalidate_cache_success( $path, $type, $scope ) {
|
||||
do_action( 'jetpack_boost_invalidate_cache_success', $path, $type, $scope );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore certain GET parameters in the cache parameters so cached pages can be served to these visitors.
|
||||
*
|
||||
* @param array $parameters - The parameters with the GET array to filter.
|
||||
* @return array - The parameters with GET parameters removed.
|
||||
*/
|
||||
public function ignore_get_parameters( $parameters ) {
|
||||
static $params = false;
|
||||
|
||||
// Only run this once as it may be called multiple times on uncached pages.
|
||||
if ( $params ) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the GET parameters so cached pages can be served to these visitors.
|
||||
* The list is an array of regex patterns. The default list contains the
|
||||
* most common GET parameters used by analytics services.
|
||||
*
|
||||
* @since 3.8.0
|
||||
*
|
||||
* @param array $get_parameters An array of regexes to remove items from the GET parameter list.
|
||||
*/
|
||||
$get_parameters = apply_filters(
|
||||
'jetpack_boost_ignore_get_parameters',
|
||||
array( 'utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'ysclid', 'srsltid', 'yclid' )
|
||||
);
|
||||
|
||||
$get_parameters = array_unique(
|
||||
array_map(
|
||||
'trim',
|
||||
$get_parameters
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $get_parameters as $get_parameter ) {
|
||||
foreach ( array_keys( $parameters['get'] ) as $get_parameter_name ) {
|
||||
if ( preg_match( '/^' . $get_parameter . '$/', $get_parameter_name ) ) {
|
||||
unset( $parameters['get'][ $get_parameter_name ] );
|
||||
$this->ignored_get_parameters .= $get_parameter_name . ',';
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( $this->ignored_get_parameters !== '' ) {
|
||||
$this->ignored_get_parameters = rtrim( $this->ignored_get_parameters, ',' );
|
||||
}
|
||||
|
||||
$params = $parameters;
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore certain cookies in the cache parameters so cached pages can be served to these visitors.
|
||||
*
|
||||
* @param array $parameters - The parameters with the cookies array to filter.
|
||||
* @return array - The parameters with cookies removed.
|
||||
*/
|
||||
public function ignore_cookies( $parameters ) {
|
||||
static $params = false;
|
||||
|
||||
// Only run this once as it may be called multiple times on uncached pages.
|
||||
if ( $params ) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
$default_cookies = array(
|
||||
'cf_clearance',
|
||||
'cf_chl_rc_i',
|
||||
'cf_chl_rc_ni',
|
||||
'cf_chl_rc_m',
|
||||
'_cfuvid',
|
||||
'__cfruid',
|
||||
'__cfwaitingroom',
|
||||
'cf_ob_info',
|
||||
'cf_use_ob',
|
||||
'__cfseq',
|
||||
'__cf_bm',
|
||||
'__cflb',
|
||||
|
||||
// Sourcebuster
|
||||
'sbjs_(.*)',
|
||||
|
||||
// Google Analytics
|
||||
'_ga(?:_[A-Z0-9]*)?',
|
||||
|
||||
// AWS Load Balancer
|
||||
'AWSELB',
|
||||
'AWSELBCORS',
|
||||
'AWSALB',
|
||||
'AWSALBCORS',
|
||||
);
|
||||
$jetpack_cookies = array( 'tk_ai', 'tk_qs' );
|
||||
$cookies = array_merge( $default_cookies, $jetpack_cookies );
|
||||
|
||||
/**
|
||||
* Filters the browser cookies so cached pages can be served to these visitors.
|
||||
* The list is an array of regex patterns. The default list contains the
|
||||
* cookies used by Cloudflare, and the regex pattern for the sbjs_ cookies
|
||||
* used by sourcebuster.js
|
||||
*
|
||||
* @since 3.8.0
|
||||
*
|
||||
* @param array $cookies An array of regexes to remove items from the cookie list.
|
||||
*/
|
||||
$cookies = apply_filters(
|
||||
'jetpack_boost_ignore_cookies',
|
||||
$cookies
|
||||
);
|
||||
|
||||
$cookies = array_unique(
|
||||
array_map(
|
||||
'trim',
|
||||
$cookies
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* The Jetpack Cookie Banner plugin sets a cookie to indicate that the
|
||||
* user has accepted the cookie policy.
|
||||
* The value of the cookie is the expiry date of the cookie, which means
|
||||
* that everyone who has accepted the cookie policy will use a different
|
||||
* cache file.
|
||||
* Set it to 1 here so those visitors will use the same cache file.
|
||||
*/
|
||||
if ( isset( $parameters['cookies']['eucookielaw'] ) ) {
|
||||
$parameters['cookies']['eucookielaw'] = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is for the personalized ads consent cookie.
|
||||
*/
|
||||
if ( isset( $parameters['cookies']['personalized-ads-consent'] ) ) {
|
||||
$parameters['cookies']['personalized-ads-consent'] = 1;
|
||||
}
|
||||
|
||||
$cookie_keys = array();
|
||||
if ( isset( $parameters['cookies'] ) && is_array( $parameters['cookies'] ) ) {
|
||||
$cookie_keys = array_keys( $parameters['cookies'] );
|
||||
} else {
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
foreach ( $cookies as $cookie ) {
|
||||
foreach ( $cookie_keys as $cookie_name ) {
|
||||
if ( preg_match( '/^' . $cookie . '$/', $cookie_name ) ) {
|
||||
unset( $parameters['cookies'][ $cookie_name ] );
|
||||
$this->ignored_cookies .= $cookie_name . ',';
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( $this->ignored_cookies !== '' ) {
|
||||
$this->ignored_cookies = rtrim( $this->ignored_cookies, ',' );
|
||||
}
|
||||
|
||||
$params = $parameters;
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
public function disable_caching_on_error( $message ) {
|
||||
if ( ! defined( 'DONOTCACHEPAGE' ) ) {
|
||||
define( 'DONOTCACHEPAGE', true );
|
||||
}
|
||||
Logger::debug( 'Fatal error detected, caching disabled' );
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called after WordPress is loaded, on "init".
|
||||
* It is used to indicate that it is safe to cache and that no
|
||||
* fatal errors occurred.
|
||||
*/
|
||||
public function init_do_cache() {
|
||||
$this->do_cache = true;
|
||||
}
|
||||
}
|
||||
+382
@@ -0,0 +1,382 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress;
|
||||
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Path_Actions\Path_Action;
|
||||
use SplFileInfo;
|
||||
|
||||
class Filesystem_Utils {
|
||||
|
||||
const DELETE_ALL = 'delete-all'; // delete all files and directories in a given directory, recursively.
|
||||
const DELETE_FILE = 'delete-single'; // delete a single file or recursively delete a single directory in a given directory.
|
||||
const DELETE_FILES = 'delete-files'; // delete all files in a given directory.
|
||||
const REBUILD_ALL = 'rebuild-all'; // rebuild all files and directories in a given directory, recursively.
|
||||
const REBUILD_FILE = 'rebuild-single'; // rebuild a single file or recursively rebuild a single directory in a given directory.
|
||||
const REBUILD_FILES = 'rebuild-files'; // rebuild all files in a given directory.
|
||||
const REBUILD = 'rebuild'; // rebuild mode for managing expired files
|
||||
const DELETE = 'delete'; // delete mode for managing expired files
|
||||
const REBUILD_FILE_EXTENSION = '.rebuild.html'; // The extension used for rebuilt files.
|
||||
|
||||
/**
|
||||
* Iterate over a directory and apply an action to each file.
|
||||
*
|
||||
* This applies the action to all files and subdirectories in the given directory.
|
||||
*
|
||||
* @param string $path - The directory to iterate over.
|
||||
* @param Path_Action $action - The action to apply to each file.
|
||||
* @return int|Boost_Cache_Error - The number of files processed, or Boost_Cache_Error on failure.
|
||||
*/
|
||||
public static function iterate_directory( $path, Path_Action $action ) {
|
||||
clearstatcache();
|
||||
$validation_error = self::validate_path( $path );
|
||||
if ( $validation_error instanceof Boost_Cache_Error ) {
|
||||
return $validation_error;
|
||||
}
|
||||
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator( $path, \RecursiveDirectoryIterator::SKIP_DOTS ),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
$count = 0;
|
||||
foreach ( $iterator as $file ) {
|
||||
$count += $action->apply_to_path( new SplFileInfo( $file ) );
|
||||
}
|
||||
|
||||
$count += $action->apply_to_path( new SplFileInfo( $path ) );
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over a directory and apply an action to each file.
|
||||
*
|
||||
* This applies the action to all files in the directory, except index.html. And doesn't go into subdirectories.
|
||||
*
|
||||
* @param string $path - The directory to iterate over.
|
||||
* @param Path_Action $action - The action to apply to each file.
|
||||
* @return int|Boost_Cache_Error - The number of files processed, or Boost_Cache_Error on failure.
|
||||
*/
|
||||
public static function iterate_files( $path, Path_Action $action ) {
|
||||
clearstatcache();
|
||||
$validation_error = self::validate_path( $path );
|
||||
if ( $validation_error instanceof Boost_Cache_Error ) {
|
||||
return $validation_error;
|
||||
}
|
||||
|
||||
$path = Boost_Cache_Utils::trailingslashit( $path );
|
||||
// Files to delete are all files in the given directory, except index.html. index.html is used to prevent directory listing.
|
||||
$files = array_diff( scandir( $path ), array( '.', '..', 'index.html' ) );
|
||||
$count = 0;
|
||||
foreach ( $files as $file ) {
|
||||
$fileinfo = new SplFileInfo( $path . $file );
|
||||
$count += (int) $action->apply_to_path( $fileinfo );
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively delete a directory and everything in it, including cache files,
|
||||
* index.html placeholder files, subdirectories and the directory itself.
|
||||
*
|
||||
* Unlike iterate_directory() with a Simple_Delete action, this does not keep
|
||||
* index.html placeholder files, does not log each deletion, and removes each
|
||||
* entry as the iterator visits it instead of building a file list in memory,
|
||||
* so it stays time- and memory-efficient even for very large caches. Used to
|
||||
* completely remove the boost-cache directory when the plugin is uninstalled.
|
||||
*
|
||||
* @param string $path - The directory to delete.
|
||||
* @return bool|Boost_Cache_Error - True on success (or if the directory is already gone), Boost_Cache_Error on failure.
|
||||
*/
|
||||
public static function delete_directory( $path ) {
|
||||
clearstatcache();
|
||||
|
||||
// Strip a trailing slash so the is_link() guard below sees the link itself.
|
||||
// is_link( 'foo/' ) is false on POSIX, which would let a trailing-slash path
|
||||
// slip past the symlink-root check; rtrim() closes that for this public,
|
||||
// destructive primitive even though the current caller passes no slash.
|
||||
$path = rtrim( $path, '/' );
|
||||
|
||||
// Refuse to follow a symlinked cache root. realpath() resolves a symlink
|
||||
// to its target, so a boost-cache symlink pointing outside wp-content would
|
||||
// resolve identically to $cache_root below and pass the containment check,
|
||||
// causing the target tree to be deleted. Boost never creates boost-cache as
|
||||
// a symlink, so a symlinked root is unexpected and we refuse it outright.
|
||||
// This is checked on the literal $path, not the resolved target, and only
|
||||
// guards the root itself; symlinks encountered inside the tree are unlinked
|
||||
// (never followed) by the deletion loop below.
|
||||
if ( is_link( $path ) ) {
|
||||
return new Boost_Cache_Error( 'invalid-directory', 'Refusing to delete a symlinked directory: ' . $path );
|
||||
}
|
||||
|
||||
$resolved = realpath( $path );
|
||||
if ( false === $resolved ) {
|
||||
// Nothing to delete if the directory is already gone.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Strict containment check. is_boost_cache_directory() only does a substring
|
||||
// match, which would also accept sibling paths like boost-cache-old; since
|
||||
// this helper deletes whole trees during uninstall, only the cache root
|
||||
// itself or paths inside it are accepted, compared on resolved paths.
|
||||
$cache_root = realpath( WP_CONTENT_DIR . '/boost-cache' );
|
||||
if ( false === $cache_root || ( $resolved !== $cache_root && strpos( $resolved, $cache_root . '/' ) !== 0 ) ) {
|
||||
return new Boost_Cache_Error( 'invalid-directory', 'Invalid directory ' . $path );
|
||||
}
|
||||
|
||||
if ( ! is_dir( $resolved ) ) {
|
||||
return new Boost_Cache_Error( 'not-a-directory', 'Not a directory' );
|
||||
}
|
||||
|
||||
// Deleting a large cache can take a while; try not to time out half-way through.
|
||||
if ( function_exists( 'set_time_limit' ) ) {
|
||||
@set_time_limit( 0 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
}
|
||||
|
||||
try {
|
||||
// CATCH_GET_CHILD keeps the walk best-effort: an unreadable subdirectory
|
||||
// is skipped instead of throwing and aborting the whole cleanup, so the
|
||||
// rest of the tree is still deleted. Anything left behind is reported by
|
||||
// the final is_dir() re-check below.
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator( $resolved, \RecursiveDirectoryIterator::SKIP_DOTS ),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST,
|
||||
\RecursiveIteratorIterator::CATCH_GET_CHILD
|
||||
);
|
||||
|
||||
// Errors for individual entries are suppressed so a single failure doesn't abort the cleanup.
|
||||
foreach ( $iterator as $file ) {
|
||||
if ( $file->isDir() && ! $file->isLink() ) {
|
||||
@rmdir( $file->getPathname() ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir, WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
} else {
|
||||
@unlink( $file->getPathname() ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink, WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
}
|
||||
}
|
||||
} catch ( \Throwable $e ) {
|
||||
// The iterator itself can throw (e.g. an unreadable subdirectory).
|
||||
// Uninstall cleanup must fail with a controlled error, not an
|
||||
// uncaught exception.
|
||||
return new Boost_Cache_Error( 'could-not-delete-directory', 'Could not completely delete directory: ' . $e->getMessage() );
|
||||
}
|
||||
|
||||
@rmdir( $resolved ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir, WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
|
||||
// Re-check against the filesystem, not a stale stat cache, so a successful
|
||||
// removal is not misreported as a failure.
|
||||
clearstatcache();
|
||||
if ( is_dir( $resolved ) ) {
|
||||
return new Boost_Cache_Error( 'could-not-delete-directory', 'Could not completely delete directory: ' . $path );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function validate_path( $path ) {
|
||||
$path = realpath( $path );
|
||||
if ( ! $path ) {
|
||||
// translators: %s is the directory that does not exist.
|
||||
return new Boost_Cache_Error( 'directory-missing', 'Directory does not exist: ' . $path ); // realpath returns false if a file does not exist.
|
||||
}
|
||||
|
||||
// make sure that $dir is a directory inside WP_CONTENT . '/boost-cache/';
|
||||
if ( self::is_boost_cache_directory( $path ) === false ) {
|
||||
// translators: %s is the directory that is invalid.
|
||||
return new Boost_Cache_Error( 'invalid-directory', 'Invalid directory %s' . $path );
|
||||
}
|
||||
|
||||
if ( ! is_dir( $path ) ) {
|
||||
return new Boost_Cache_Error( 'not-a-directory', 'Not a directory' );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given directory is inside the boost-cache directory.
|
||||
*
|
||||
* @param string $dir - The directory to check.
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_boost_cache_directory( $dir ) {
|
||||
$dir = Boost_Cache_Utils::sanitize_file_path( $dir );
|
||||
return strpos( $dir, WP_CONTENT_DIR . '/boost-cache' ) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a request_uri and its parameters, return the filename to use for this cached data. Does not include the file path.
|
||||
*
|
||||
* @param array $parameters - An associative array of all the things that make this request special/different. Includes GET parameters and COOKIEs normally.
|
||||
*/
|
||||
public static function get_request_filename( $parameters ) {
|
||||
|
||||
/**
|
||||
* Filters the components used to generate the cache key.
|
||||
*
|
||||
* @param array $parameters The array of components, url, cookies, get parameters, etc.
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @deprecated 3.8.0
|
||||
*/
|
||||
$key_components = apply_filters_deprecated( 'boost_cache_key_components', array( $parameters ), '3.8.0', 'jetpack_boost_cache_parameters' );
|
||||
|
||||
return md5(
|
||||
json_encode( // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
|
||||
$key_components,
|
||||
0 // phpcs:ignore Jetpack.Functions.JsonEncodeFlags.ZeroFound -- No `json_encode()` flags because this needs to match whatever is calculating the hash on the other end.
|
||||
)
|
||||
) . '.html';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file is a rebuild file.
|
||||
*
|
||||
* @param string $file - The file to check.
|
||||
* @return bool - True if the file is a rebuild file, false otherwise.
|
||||
*/
|
||||
public static function is_rebuild_file( $file ) {
|
||||
return substr( $file, -strlen( self::REBUILD_FILE_EXTENSION ) ) === self::REBUILD_FILE_EXTENSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the directory if it doesn't exist.
|
||||
*
|
||||
* @param string $path - The path to the directory to create.
|
||||
*/
|
||||
public static function create_directory( $path ) {
|
||||
if ( ! is_dir( $path ) ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.dir_mkdir_dirname, WordPress.WP.AlternativeFunctions.file_system_operations_mkdir, WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
$dir_created = @mkdir( $path, 0755, true );
|
||||
|
||||
if ( $dir_created ) {
|
||||
self::create_empty_index_files( $path );
|
||||
}
|
||||
|
||||
return $dir_created;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty index.html file in the given directory.
|
||||
* This is done to prevent directory listing.
|
||||
*/
|
||||
private static function create_empty_index_files( $path ) {
|
||||
if ( self::is_boost_cache_directory( $path ) ) {
|
||||
self::write_to_file( $path . '/index.html', '' );
|
||||
|
||||
// Create an empty index.html file in the parent directory as well.
|
||||
self::create_empty_index_files( dirname( $path ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild a file. Make a copy of the file with a different extension instead of deleting it.
|
||||
*
|
||||
* @param string $file_path - The file to rebuild.
|
||||
* @return bool - True if the file was rebuilt, false otherwise.
|
||||
*/
|
||||
public static function rebuild_file( $file_path ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable
|
||||
if ( is_writable( $file_path ) ) {
|
||||
// only rename the file if it is not already a rebuild file.
|
||||
if ( ! self::is_rebuild_file( $file_path ) ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename, WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
@rename( $file_path, $file_path . self::REBUILD_FILE_EXTENSION );
|
||||
@touch( $file_path . self::REBUILD_FILE_EXTENSION ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_touch, WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a file that was rebuilt so the cache file can be used for other visitors.
|
||||
*
|
||||
* @param string $file_path - The rebuilt file
|
||||
* @return bool - True if the file was restored, false otherwise.
|
||||
*/
|
||||
public static function restore_file( $file_path ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable
|
||||
if ( is_writable( $file_path ) ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename, WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
return @rename( $file_path, str_replace( self::REBUILD_FILE_EXTENSION, '', $file_path ) );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file.
|
||||
*
|
||||
* @param string $file_path - The file to delete.
|
||||
* @return bool - True if the file was deleted, false otherwise.
|
||||
*/
|
||||
public static function delete_file( $file_path ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable
|
||||
$deletable = is_writable( $file_path );
|
||||
|
||||
if ( $deletable ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink, WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
return @unlink( $file_path );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an empty cache directory.
|
||||
*
|
||||
* @param string $dir - The directory to delete.
|
||||
* @return int - 1 if the directory was deleted, 0 otherwise.
|
||||
*
|
||||
* This function will delete the index.html file and the directory itself.
|
||||
*/
|
||||
public static function delete_empty_dir( $dir ) {
|
||||
if ( self::is_dir_empty( $dir ) ) {
|
||||
@unlink( $dir . '/index.html' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink, WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
@rmdir( $dir ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir, WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a directory is empty.
|
||||
*
|
||||
* @param string $dir - The directory to check.
|
||||
*/
|
||||
public static function is_dir_empty( $dir ) {
|
||||
if ( ! is_readable( $dir ) ) {
|
||||
return new Boost_Cache_Error( 'directory_not_readable', 'Directory is not readable' );
|
||||
}
|
||||
|
||||
$files = array_diff( scandir( $dir ), array( '.', '..', 'index.html' ) );
|
||||
return empty( $files );
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes data to a file.
|
||||
* This creates a temporary file first, then renames the file to the final filename.
|
||||
* This is done to prevent the file from being read while it is being written to.
|
||||
*
|
||||
* @param string $filename - The filename to write to.
|
||||
* @param string $data - The data to write to the file.
|
||||
* @return bool|Boost_Cache_Error - true on sucess or Boost_Cache_Error on failure.
|
||||
*/
|
||||
public static function write_to_file( $filename, $data ) {
|
||||
$tmp_filename = $filename . uniqid( uniqid(), true ) . '.tmp';
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents, WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
if ( false === @file_put_contents( $tmp_filename, $data ) ) {
|
||||
return new Boost_Cache_Error( 'could-not-write', 'Could not write to tmp file: ' . $tmp_filename );
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename
|
||||
if ( ! rename( $tmp_filename, $filename ) ) {
|
||||
return new Boost_Cache_Error( 'could-not-rename', 'Could not rename tmp file to final file: ' . $filename );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
/*
|
||||
* This file may be called before WordPress is fully initialized. See the README file for info.
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress;
|
||||
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Path_Actions\Filter_Older;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Path_Actions\Simple_Delete;
|
||||
|
||||
/**
|
||||
* A utility that manages logging for the boost cache.
|
||||
*/
|
||||
class Logger {
|
||||
/**
|
||||
* The singleton instance of the logger.
|
||||
*
|
||||
* @var self
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* The header to place on top of every log file.
|
||||
*/
|
||||
const LOG_HEADER = "<?php die(); // This file is not intended to be accessed directly. ?>\n\n";
|
||||
|
||||
/**
|
||||
* The directory where log files are stored.
|
||||
*/
|
||||
const LOG_DIRECTORY = WP_CONTENT_DIR . '/boost-cache/logs';
|
||||
|
||||
/**
|
||||
* The Process Identifier used by this Logger instance.
|
||||
*
|
||||
* @var int|float
|
||||
*/
|
||||
private $pid = null;
|
||||
|
||||
/**
|
||||
* Get the singleton instance of the logger.
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( self::$instance !== null ) {
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
$instance = new Logger();
|
||||
$prepared_log_file = $instance->prepare_file();
|
||||
if ( $prepared_log_file instanceof Boost_Cache_Error ) {
|
||||
return $prepared_log_file;
|
||||
}
|
||||
|
||||
self::$instance = $instance;
|
||||
return $instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
if ( function_exists( 'getmypid' ) ) {
|
||||
$this->pid = getmypid();
|
||||
} else {
|
||||
// Where PID is not available, use the microtime of the first log of the session.
|
||||
$this->pid = microtime( true );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the log file exists, and if not, create it.
|
||||
*/
|
||||
private function prepare_file() {
|
||||
$log_file = $this->get_log_file();
|
||||
if ( file_exists( $log_file ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$directory = dirname( $log_file );
|
||||
if ( ! Filesystem_Utils::create_directory( $directory ) ) {
|
||||
return new Boost_Cache_Error( 'could-not-create-log-dir', 'Could not create boost cache log directory' );
|
||||
}
|
||||
|
||||
return Filesystem_Utils::write_to_file( $log_file, self::LOG_HEADER );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a debug message to the log file after doing necessary checks.
|
||||
*/
|
||||
public static function debug( $message ) {
|
||||
$settings = Boost_Cache_Settings::get_instance();
|
||||
if ( ! $settings->get_logging() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$logger = self::get_instance();
|
||||
|
||||
// TODO: Check to make sure that current request IP is allowed to create logs.
|
||||
|
||||
if ( $logger instanceof Boost_Cache_Error ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$logger->log( $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a message to the log file.
|
||||
*
|
||||
* @param string $message - The message to write to the log file.
|
||||
*/
|
||||
public function log( $message ) {
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$request_uri = htmlspecialchars( $_SERVER['REQUEST_URI'] ?? '<unknown request uri>', ENT_QUOTES, 'UTF-8' );
|
||||
|
||||
// don't log the ABSPATH constant. Logs may be copied to a public forum.
|
||||
$message = str_replace( ABSPATH, '[...]/', $message );
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
|
||||
$line = json_encode(
|
||||
array(
|
||||
'time' => gmdate( 'Y-m-d H:i:s' ),
|
||||
'pid' => $this->pid,
|
||||
'uri' => $request_uri,
|
||||
'msg' => $message,
|
||||
'uid' => uniqid(), // Uniquely identify this log line.
|
||||
),
|
||||
JSON_UNESCAPED_SLASHES
|
||||
);
|
||||
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( $line . PHP_EOL, 3, $this->get_log_file() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the log file and returns the contents.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function read() {
|
||||
$instance = self::get_instance();
|
||||
|
||||
// If we failed to set up a Logger instance (e.g.: unwriteable directory), return the error as log content.
|
||||
if ( $instance instanceof Boost_Cache_Error ) {
|
||||
return $instance->get_error_message();
|
||||
}
|
||||
|
||||
$log_file = $instance->get_log_file();
|
||||
if ( ! file_exists( $log_file ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Get the content after skipping the LOG_HEADER.
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
$logs = file_get_contents( $log_file, false, null, strlen( self::LOG_HEADER ) ) ?? '';
|
||||
$logs = explode( PHP_EOL, $logs );
|
||||
$lines = array();
|
||||
|
||||
foreach ( $logs as $log ) {
|
||||
$line = json_decode( $log, true );
|
||||
if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $line ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The current log format requires time, pid, uri, and msg.
|
||||
if ( ! isset( $line['time'] ) || ! isset( $line['pid'] ) || ! isset( $line['uri'] ) || ! isset( $line['msg'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$info = sprintf(
|
||||
'[%s] [%s] ',
|
||||
$line['time'],
|
||||
$line['pid']
|
||||
);
|
||||
|
||||
$formatted = $info . $line['uri'];
|
||||
// Add msg to the next line offset by the length of the info string.
|
||||
$formatted .= PHP_EOL . str_repeat( ' ', strlen( $info ) ) . $line['msg'];
|
||||
|
||||
$lines[] = $formatted;
|
||||
}
|
||||
|
||||
return implode( PHP_EOL, $lines );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the log file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_log_file() {
|
||||
$today = gmdate( 'Y-m-d' );
|
||||
return self::LOG_DIRECTORY . "/log-{$today}.log.php";
|
||||
}
|
||||
|
||||
public static function delete_old_logs() {
|
||||
Filesystem_Utils::iterate_directory( self::LOG_DIRECTORY, new Filter_Older( time() - 24 * 60 * 60, new Simple_Delete() ) );
|
||||
}
|
||||
}
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
/*
|
||||
* This file may be called before WordPress is fully initialized. See the README file for info.
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress;
|
||||
|
||||
class Request {
|
||||
/**
|
||||
* @var Request - The request instance for current request.
|
||||
*/
|
||||
private static $current_request = null;
|
||||
|
||||
/**
|
||||
* @var string - The normalized path for the current request. This is not sanitized. Only to be used for comparison purposes.
|
||||
*/
|
||||
private $request_uri = false;
|
||||
|
||||
/**
|
||||
* @var array - The GET parameters and cookies for the current request. Everything considered in the cache key.
|
||||
*/
|
||||
private $request_parameters;
|
||||
|
||||
/**
|
||||
* Gets the singleton request instance.
|
||||
*
|
||||
* @return Request The instance of the class.
|
||||
*/
|
||||
public static function current() {
|
||||
if ( self::$current_request === null ) {
|
||||
self::$current_request = new self(
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
isset( $_SERVER['REQUEST_URI'] ) ? Boost_Cache_Utils::normalize_request_uri( $_SERVER['REQUEST_URI'] ) : false,
|
||||
// Set the cookies and get parameters for the current request. Sometimes these arrays are modified by WordPress or other plugins.
|
||||
// We need to cache them here so they can be used for the cache key later. We don't need to sanitize them, as they are only used for comparison.
|
||||
array(
|
||||
'cookies' => $_COOKIE,
|
||||
'get' => $_GET, // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return self::$current_request;
|
||||
}
|
||||
|
||||
public function __construct( $uri, $parameters ) {
|
||||
$this->request_uri = $uri;
|
||||
$this->request_parameters = $parameters;
|
||||
}
|
||||
|
||||
public function get_uri() {
|
||||
return $this->request_uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parameters for the current request.
|
||||
*
|
||||
* @return array The parameters for the current request, made up of cookies and get parameters.
|
||||
*/
|
||||
public function get_parameters() {
|
||||
/**
|
||||
* Filters the parameters for the current request to identify the cache key.
|
||||
*
|
||||
* @since 3.8.0
|
||||
*
|
||||
* @param array $parameters The parameters for the current request, made up of cookies and get parameters.
|
||||
*/
|
||||
return apply_filters( 'jetpack_boost_cache_parameters', $this->request_parameters );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current request has a fatal error.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_fatal_error() {
|
||||
$error = error_get_last();
|
||||
if ( $error === null ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fatal_errors = array(
|
||||
E_ERROR,
|
||||
E_PARSE,
|
||||
E_CORE_ERROR,
|
||||
E_COMPILE_ERROR,
|
||||
E_USER_ERROR,
|
||||
);
|
||||
|
||||
return in_array( $error['type'], $fatal_errors, true );
|
||||
}
|
||||
|
||||
public function is_url_excluded( $request_uri = '' ) {
|
||||
if ( $request_uri === '' ) {
|
||||
$request_uri = $this->request_uri;
|
||||
}
|
||||
|
||||
// Check if the query parameters `jb-disable-modules` or `jb-generate-critical-css` exist.
|
||||
$request_parameters = $this->get_parameters();
|
||||
$query_params = $request_parameters['get'] ?? array();
|
||||
if ( isset( $query_params['jb-disable-modules'] ) || isset( $query_params['jb-generate-critical-css'] ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$bypass_patterns = Boost_Cache_Settings::get_instance()->get_bypass_patterns();
|
||||
|
||||
/**
|
||||
* Filters the bypass patterns for the page cache.
|
||||
* If you need to sanitize them, do it before passing them to this filter,
|
||||
* as there's no sanitization done after this filter.
|
||||
*
|
||||
* @since 3.2.0
|
||||
*
|
||||
* @param array $bypass_patterns An array of regex patterns that define URLs that bypass caching.
|
||||
*/
|
||||
$bypass_patterns = apply_filters( 'jetpack_boost_cache_bypass_patterns', $bypass_patterns );
|
||||
|
||||
$bypass_patterns[] = 'wp-.*\.php';
|
||||
foreach ( $bypass_patterns as $expr ) {
|
||||
if ( ! empty( $expr ) && preg_match( "~^$expr/?$~", $request_uri ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the request is cacheable.
|
||||
*
|
||||
* If a request is in the backend, or is a POST request, or is not an
|
||||
* html request, it is not cacheable.
|
||||
* The filter boost_cache_cacheable can be used to override this.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_cacheable() {
|
||||
/**
|
||||
* Determines if the request is considered cacheable.
|
||||
*
|
||||
* Can be used to prevent a request from being cached.
|
||||
*
|
||||
* @since 3.2.0
|
||||
*
|
||||
* @param bool $default_status The default cacheability status (true for cacheable).
|
||||
* @param string $request_uri The request URI to be evaluated for cacheability.
|
||||
*/
|
||||
if ( ! apply_filters( 'jetpack_boost_cache_request_cacheable', true, $this->request_uri ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( defined( 'DONOTCACHEPAGE' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// do not cache post previews or customizer previews
|
||||
if ( ! empty( $_GET ) && ( isset( $_GET['preview'] ) || isset( $_GET['customize_changeset_uuid'] ) ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->is_fatal_error() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( function_exists( 'is_user_logged_in' ) && is_user_logged_in() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->is_404() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->is_feed() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->is_backend() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->is_bypassed_extension() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( isset( $_SERVER['REQUEST_METHOD'] ) && $_SERVER['REQUEST_METHOD'] !== 'GET' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->is_url_excluded() ) {
|
||||
Logger::debug( 'Url excluded, not cached!' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->is_module_disabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the accept headers to determine if the request should be cached.
|
||||
*
|
||||
* This filter allows modification of the content types that browsers send
|
||||
* to the server during a request. If the acceptable browser content type header (HTTP_ACCEPT)
|
||||
* matches one of these content types the request will not be cached,
|
||||
* or a cached file served to this visitor.
|
||||
*
|
||||
* @since 3.2.0
|
||||
*
|
||||
* @param array $accept_headers An array of header values that should prevent a request from being cached.
|
||||
*/
|
||||
$accept_headers = apply_filters( 'jetpack_boost_cache_accept_headers', array( 'application/json', 'application/activity+json', 'application/ld+json' ) );
|
||||
$accept_headers = array_map( 'strtolower', $accept_headers );
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- $accept is checked and set below.
|
||||
$accept = isset( $_SERVER['HTTP_ACCEPT'] ) ? strtolower( filter_var( $_SERVER['HTTP_ACCEPT'] ) ) : '';
|
||||
|
||||
if ( $accept !== '' ) {
|
||||
foreach ( $accept_headers as $header ) {
|
||||
if ( str_contains( $accept, $header ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the request appears to be for something with a known file extension that is not
|
||||
* usually HTML. e.g.:
|
||||
* - *.txt (including robots.txt, license.txt)
|
||||
* - *.ico (favicon.ico)
|
||||
* - *.jpg, *.png, *.webm (image files).
|
||||
*/
|
||||
public function is_bypassed_extension() {
|
||||
$file_extension = pathinfo( $this->request_uri, PATHINFO_EXTENSION );
|
||||
|
||||
return in_array(
|
||||
$file_extension,
|
||||
array(
|
||||
'txt',
|
||||
'ico',
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'png',
|
||||
'webp',
|
||||
'gif',
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current request is one of the following:
|
||||
* 1. wp-admin
|
||||
* 2. wp-login.php, xmlrpc.php or wp-cron.php/cron request
|
||||
* 3. WP_CLI
|
||||
* 4. REST request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_backend() {
|
||||
|
||||
$is_backend = is_admin();
|
||||
if ( $is_backend ) {
|
||||
return $is_backend;
|
||||
}
|
||||
|
||||
$script = isset( $_SERVER['PHP_SELF'] ) ? basename( $_SERVER['PHP_SELF'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
if ( $script !== 'index.php' ) {
|
||||
if ( in_array( $script, array( 'wp-login.php', 'xmlrpc.php', 'wp-cron.php' ), true ) ) {
|
||||
$is_backend = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
|
||||
$is_backend = true;
|
||||
}
|
||||
|
||||
if ( PHP_SAPI === 'cli' || ( defined( 'WP_CLI' ) && constant( 'WP_CLI' ) ) ) {
|
||||
$is_backend = true;
|
||||
}
|
||||
|
||||
if ( defined( 'REST_REQUEST' ) ) {
|
||||
$is_backend = true;
|
||||
}
|
||||
|
||||
return $is_backend;
|
||||
}
|
||||
|
||||
/**
|
||||
* "Safe" version of WordPress' is_404 method. When called before WordPress' query is run, returns
|
||||
* `null` (a falsey value) instead of outputting a _doing_it_wrong warning.
|
||||
*/
|
||||
public function is_404() {
|
||||
global $wp_query;
|
||||
|
||||
if ( ! isset( $wp_query ) || ! function_exists( '\is_404' ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return \is_404();
|
||||
}
|
||||
|
||||
/**
|
||||
* "Safe" version of WordPress' is_feed method. When called before WordPress' query is run, returns
|
||||
* `null` (a falsey value) instead of outputting a _doing_it_wrong warning.
|
||||
*/
|
||||
public function is_feed() {
|
||||
global $wp_query;
|
||||
|
||||
if ( ! isset( $wp_query ) || ! function_exists( '\is_feed' ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return \is_feed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the Page Cache module is disabled, or null if we don't know yet.
|
||||
*
|
||||
* If Status and Page_Cache are not available, it means the plugin is not loaded.
|
||||
* This function will be called later when writing a cache file to disk.
|
||||
* It's then that we can check if the module is active.
|
||||
*
|
||||
* @return null|bool
|
||||
*/
|
||||
public function is_module_disabled() {
|
||||
|
||||
// A simple check to make sure we're in the output buffer callback.
|
||||
if ( ! function_exists( '\is_feed' ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
class_exists( '\Automattic\Jetpack_Boost\Lib\Status' ) &&
|
||||
class_exists( '\Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Page_Cache' )
|
||||
) {
|
||||
$page_cache_status = new \Automattic\Jetpack_Boost\Lib\Status(
|
||||
\Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Page_Cache::get_slug()
|
||||
);
|
||||
return ! $page_cache_status->get();
|
||||
} else {
|
||||
return true; // if the classes aren't available, the plugin isn't loaded.
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Path_Actions;
|
||||
|
||||
use SplFileInfo;
|
||||
|
||||
/**
|
||||
* Apply a given sub-action to all files in the path that are older than a given timestamp.
|
||||
*/
|
||||
class Filter_Older implements Path_Action {
|
||||
private $timestamp;
|
||||
private $sub_action;
|
||||
|
||||
public function __construct( $timestamp, Path_Action $action ) {
|
||||
$this->timestamp = $timestamp;
|
||||
$this->sub_action = $action;
|
||||
}
|
||||
|
||||
public function apply_to_path( SplFileInfo $file ) {
|
||||
$file_path = $file->getPathname();
|
||||
$filemtime = @filemtime( $file_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
|
||||
if ( ! $filemtime ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* if the file is a directory, then we process it, regardless of age.
|
||||
* Any modification of items in the directory will update the filemtime of the directory.
|
||||
* That's why we always process directories.
|
||||
*/
|
||||
if ( $file->isDir() || $filemtime <= $this->timestamp ) {
|
||||
return $this->sub_action->apply_to_path( $file );
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Path_Actions;
|
||||
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Filesystem_Utils;
|
||||
use SplFileInfo;
|
||||
|
||||
/**
|
||||
* Rebuild a file.
|
||||
*/
|
||||
class Rebuild_File implements Path_Action {
|
||||
public function apply_to_path( SplFileInfo $file ) {
|
||||
if ( $file->isDir() && Filesystem_Utils::is_dir_empty( $file->getPathname() ) ) {
|
||||
Filesystem_Utils::delete_empty_dir( $file->getPathname() );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $file->isDir() || $file->getFilename() === 'index.html' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If it's already a rebuild file, delete it because it expired long ago.
|
||||
if ( Filesystem_Utils::is_rebuild_file( $file->getFilename() ) ) {
|
||||
$action = new Simple_Delete();
|
||||
return $action->apply_to_path( $file );
|
||||
}
|
||||
|
||||
$rebuilt = Filesystem_Utils::rebuild_file( $file->getPathname() );
|
||||
return $rebuilt ? 1 : false;
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Path_Actions;
|
||||
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Filesystem_Utils;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Logger;
|
||||
use SplFileInfo;
|
||||
|
||||
/**
|
||||
* Delete a file or directory, non-recursively.
|
||||
*/
|
||||
class Simple_Delete implements Path_Action {
|
||||
/**
|
||||
* Delete a file or directory.
|
||||
*
|
||||
* @param SplFileInfo $file The file or directory to delete.
|
||||
* @return int The number of files or directories deleted.
|
||||
*/
|
||||
public function apply_to_path( SplFileInfo $file ) {
|
||||
if ( $file->isDir() && Filesystem_Utils::is_dir_empty( $file->getPathname() ) ) {
|
||||
Logger::debug( 'rmdir: ' . $file->getPathname() );
|
||||
return $this->delete_dir( $file );
|
||||
} elseif ( $file->isFile() ) {
|
||||
// Do not delete index.html files independently. We will only delete them when the directory is empty.
|
||||
if ( $file->getFilename() === 'index.html' ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Delete a file in the directory
|
||||
Logger::debug( 'unlink: ' . $file->getPathname() );
|
||||
$this->delete_file( $file );
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function delete_dir( SplFileInfo $file ) {
|
||||
$count = 0;
|
||||
if ( Filesystem_Utils::is_dir_empty( $file->getPathname() ) ) {
|
||||
// An empty directory will still have an index.html file, which we will delete with the directory.
|
||||
$count += Filesystem_Utils::delete_empty_dir( $file->getPathname() );
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
private function delete_file( SplFileInfo $file ) {
|
||||
@unlink( $file->getPathname() ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink, WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Path_Actions;
|
||||
|
||||
use SplFileInfo;
|
||||
|
||||
interface Path_Action {
|
||||
/**
|
||||
* Apply the action to the path.
|
||||
*
|
||||
* @param SplFileInfo $path The path to apply the action to.
|
||||
* @return false|int False if nothing was done, or the number of files deleted.
|
||||
*/
|
||||
public function apply_to_path( SplFileInfo $path );
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
/*
|
||||
* This file may be called before WordPress is fully initialized. See the README file for info.
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Storage;
|
||||
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache_Error;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache_Utils;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Filesystem_Utils;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Logger;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Path_Actions\Filter_Older;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Path_Actions\Rebuild_File;
|
||||
use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Path_Actions\Simple_Delete;
|
||||
use SplFileInfo;
|
||||
|
||||
/**
|
||||
* File Storage - handles writing to disk, reading from disk, purging and pruning old content.
|
||||
*/
|
||||
class File_Storage implements Storage {
|
||||
|
||||
/**
|
||||
* @var string - The root path where all cached files go.
|
||||
*/
|
||||
private $root_path;
|
||||
|
||||
public function __construct( $root_path ) {
|
||||
$this->root_path = WP_CONTENT_DIR . '/boost-cache/cache/' . Boost_Cache_Utils::sanitize_file_path( Boost_Cache_Utils::trailingslashit( $root_path ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a request_uri and its parameters, store the given data in the cache.
|
||||
*
|
||||
* @param string $request_uri - The URI of this request (excluding GET parameters)
|
||||
* @param array $parameters - An associative array of all the things that make this request special/different. Includes GET parameters and COOKIEs normally.
|
||||
* @param string $data - The data to write to disk.
|
||||
*/
|
||||
public function write( $request_uri, $parameters, $data ) {
|
||||
$directory = self::get_uri_directory( $request_uri );
|
||||
$filename = Filesystem_Utils::get_request_filename( $parameters );
|
||||
|
||||
if ( ! Filesystem_Utils::create_directory( $directory ) ) {
|
||||
return new Boost_Cache_Error( 'cannot-create-cache-dir', 'Could not create cache directory' );
|
||||
}
|
||||
|
||||
return Filesystem_Utils::write_to_file( $directory . $filename, $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a request_uri and its parameters, reset the filename of a rebuild
|
||||
* cache file and return true, or false otherwise.
|
||||
* If a rebuild file is too old, it will be deleted and false will be returned.
|
||||
*
|
||||
* @param string $request_uri - The URI of this request (excluding GET parameters)
|
||||
* @param array $parameters - An associative array of all the things that make this request special/different. Includes GET parameters and COOKIEs normally.
|
||||
*/
|
||||
public function reset_rebuild_file( $request_uri, $parameters ) {
|
||||
$directory = self::get_uri_directory( $request_uri );
|
||||
$filename = Filesystem_Utils::get_request_filename( $parameters ) . Filesystem_Utils::REBUILD_FILE_EXTENSION;
|
||||
$hash_path = $directory . $filename;
|
||||
|
||||
if ( file_exists( $hash_path ) ) {
|
||||
$expired = ( @filemtime( $hash_path ) + JETPACK_BOOST_CACHE_REBUILD_DURATION ) <= time(); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
|
||||
if ( $expired ) {
|
||||
if ( Filesystem_Utils::delete_file( $hash_path ) ) {
|
||||
Logger::debug( "Deleted expired rebuilt file: $hash_path" );
|
||||
} else {
|
||||
Logger::debug( "Could not delete expired rebuilt file: $hash_path" );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( Filesystem_Utils::restore_file( $hash_path ) ) {
|
||||
Logger::debug( "Restored rebuilt file: $hash_path" );
|
||||
return true;
|
||||
} else {
|
||||
Logger::debug( "Could not restore rebuilt file: $hash_path" );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a request_uri and its parameters, return any stored data from the cache, or false otherwise.
|
||||
*
|
||||
* @param string $request_uri - The URI of this request (excluding GET parameters)
|
||||
* @param array $parameters - An associative array of all the things that make this request special/different. Includes GET parameters and COOKIEs normally.
|
||||
*/
|
||||
public function read( $request_uri, $parameters ) {
|
||||
$directory = self::get_uri_directory( $request_uri );
|
||||
$filename = Filesystem_Utils::get_request_filename( $parameters );
|
||||
$hash_path = $directory . $filename;
|
||||
|
||||
if ( file_exists( $hash_path ) ) {
|
||||
$filemtime = @filemtime( $hash_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
$expired = ( $filemtime + JETPACK_BOOST_CACHE_DURATION ) <= time();
|
||||
|
||||
// If file exists and is not expired, return the file contents.
|
||||
if ( ! $expired ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
return file_get_contents( $hash_path );
|
||||
}
|
||||
|
||||
// If file exists but is expired, delete it.
|
||||
if ( Filesystem_Utils::delete_file( $hash_path ) ) {
|
||||
Logger::debug( "Deleted expired file: $hash_path" );
|
||||
} else {
|
||||
Logger::debug( "Could not delete expired file: $hash_path" );
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Garbage collect expired files.
|
||||
*
|
||||
* @param int $cache_ttl If file is is created before this specified number of seconds, it will be deleted.
|
||||
*/
|
||||
public function garbage_collect( $cache_ttl = JETPACK_BOOST_CACHE_DURATION ) {
|
||||
if ( $cache_ttl === -1 ) {
|
||||
// Garbage collection is disabled.
|
||||
return false;
|
||||
}
|
||||
|
||||
$created_before = time() - $cache_ttl;
|
||||
|
||||
$count = Filesystem_Utils::iterate_directory( $this->root_path, new Filter_Older( $created_before, new Rebuild_File() ) );
|
||||
if ( $count instanceof Boost_Cache_Error ) {
|
||||
Logger::debug( 'Garbage collection failed: ' . $count->get_error_message() );
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger::debug( "Garbage collected $count files" );
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a request_uri, return the filesystem path where it should get stored. Handles sanitization.
|
||||
* Note that the directory path does not take things like GET parameters or cookies into account, for easy cache purging.
|
||||
*
|
||||
* @param string $request_uri - The URI of this request (excluding GET parameters)
|
||||
*/
|
||||
private function get_uri_directory( $request_uri ) {
|
||||
return Boost_Cache_Utils::trailingslashit( $this->root_path . self::sanitize_path( $request_uri ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a path for safe usage on the local filesystem.
|
||||
*
|
||||
* @param string $path - The path to sanitize.
|
||||
*/
|
||||
private function sanitize_path( $path ) {
|
||||
static $_cache = array();
|
||||
if ( isset( $_cache[ $path ] ) ) {
|
||||
return $_cache[ $path ];
|
||||
}
|
||||
|
||||
$path = Boost_Cache_Utils::sanitize_file_path( $path );
|
||||
|
||||
$_cache[ $path ] = $path;
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete cache based on given parameters.
|
||||
*
|
||||
* @param string $path - The path to delete the cache for.
|
||||
* @param array $args - The parameters defining the cache filename.
|
||||
* Example:
|
||||
* array(
|
||||
* 'rebuild' => (boolean) default true - If true, cache files will be rebuilt instead of being deleted.
|
||||
* 'parameters' => false | array, default false - If array is provided, the files created for that specific request matching the parameter will be effected. If parameter is provided, recursive is ignored and considered as false.
|
||||
* 'recursive' => (boolean) default false - If true, the cache will be deleted recursively in subdirectories.
|
||||
* )
|
||||
*/
|
||||
public function clear( $path, $args = array() ) {
|
||||
$normalized_path = Boost_Cache_Utils::normalize_request_uri( $this->sanitize_path( $path ) );
|
||||
$normalized_path = Boost_Cache_Utils::trailingslashit( $this->root_path . $normalized_path );
|
||||
|
||||
// Ensure the path is within the cache directory
|
||||
if ( strpos( $normalized_path, $this->root_path ) !== 0 ) {
|
||||
Logger::debug( 'Attempted to delete cache for path outside of cache directory: ' . $path );
|
||||
return;
|
||||
}
|
||||
|
||||
$recursive = $args['recursive'] ?? false;
|
||||
$rebuild = $args['rebuild'] ?? true;
|
||||
$parameters = $args['parameters'] ?? false;
|
||||
|
||||
if ( $rebuild ) {
|
||||
$action = new Rebuild_File();
|
||||
} else {
|
||||
$action = new Simple_Delete();
|
||||
}
|
||||
|
||||
// If parameters are provided, delete the specific file and skip any iteration.
|
||||
if ( $parameters ) {
|
||||
$action->apply_to_path( new SplFileInfo( $normalized_path . Filesystem_Utils::get_request_filename( $parameters ) ) );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $recursive ) {
|
||||
Filesystem_Utils::iterate_directory( $normalized_path, $action );
|
||||
} else {
|
||||
Filesystem_Utils::iterate_files( $normalized_path, $action );
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
/*
|
||||
* This file may be called before WordPress is fully initialized. See the README file for info.
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Storage;
|
||||
|
||||
/**
|
||||
* Interface for Cache storage - a system for storing and purging caches.
|
||||
*/
|
||||
interface Storage {
|
||||
|
||||
public function write( $request_uri, $parameters, $data );
|
||||
public function read( $request_uri, $parameters );
|
||||
public function reset_rebuild_file( $request_uri, $parameters );
|
||||
|
||||
public function clear( $path, $args = array() );
|
||||
public function garbage_collect( $cache_ttl = JETPACK_BOOST_CACHE_DURATION );
|
||||
}
|
||||
Reference in New Issue
Block a user